From d608d584f0a7e78f45652414354858e1745a0c40 Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 27 May 2026 11:59:51 +1000 Subject: [PATCH 1/2] feat(status): distinguish skills vs plugins, hoist `status` to root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `workspace status` and `plugin list` labeled every installed entry as a "plugin" even when the source was a standalone skill (single-skill repo or deep URL into a `skills/` subpath). That made the output misleading once support for standalone-skill sources was added in #406/#408/#413. This change classifies each resolved source as either `skill` (root SKILL.md, no `skills/` subdir) or `plugin` (everything else) and surfaces the kind in both human and JSON output: workspace status (before): ✓ NousResearch/hermes-agent/skills/research/llm-wiki (cached) workspace status (after): ✓ NousResearch/hermes-agent/skills/research/llm-wiki (skill, cached) `plugin list` gains a `Type:` line per entry and a kind-broken-down total. Also hoists `status` to the root surface so `allagents status` works without `workspace`, mirroring how `update` is already exposed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cli/commands/plugin.ts | 33 ++++++++++++++++++++++-- src/cli/commands/workspace.ts | 14 +++++------ src/cli/index.ts | 3 ++- src/cli/metadata/plugin.ts | 8 +++--- src/cli/metadata/workspace.ts | 9 ++++--- src/core/status.ts | 42 +++++++++++++++++++++++++++++++ tests/unit/cli/agent-help.test.ts | 6 ++--- 7 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/cli/commands/plugin.ts b/src/cli/commands/plugin.ts index ff15f8b2..9e8bcc5c 100644 --- a/src/cli/commands/plugin.ts +++ b/src/cli/commands/plugin.ts @@ -34,6 +34,7 @@ import { } from '../../core/user-workspace.js'; import { updatePlugin, type InstalledPluginUpdateResult } from '../../core/plugin.js'; import { getAllSkillsFromPlugins } from '../../core/skills.js'; +import { getWorkspaceStatus } from '../../core/status.js'; import { parseMarketplaceManifest } from '../../utils/marketplace-manifest-parser.js'; import { isJsonMode, jsonOutput } from '../json-output.js'; import { buildDescription, conciseSubcommands } from '../help.js'; @@ -772,6 +773,23 @@ const pluginListCmd = command({ const projectPlugins = await getInstalledProjectPlugins(process.cwd()); const allInstalled = [...userPlugins, ...projectPlugins]; + // Build a source→kind map so each listed entry can be labelled + // 'skill' (root SKILL.md, no skills/ subdir) or 'plugin' (everything + // else, including not-yet-resolved sources). + const kindBySource = new Map(); + try { + const status = await getWorkspaceStatus(process.cwd()); + for (const p of [ + ...status.plugins, + ...(status.userPlugins ?? []), + ]) { + kindBySource.set(p.source, p.kind); + } + } catch { + // Best-effort: if status lookup fails, fall back to labelling + // everything as 'plugin' below. + } + // Load native plugins from sync state const userSyncState = await loadSyncState(getAllagentsDir()); const projectSyncState = cwdIsHome ? null : await loadSyncState(process.cwd()); @@ -782,6 +800,7 @@ const pluginListCmd = command({ name: string; marketplace: string; scope: 'user' | 'project'; + kind: 'skill' | 'plugin'; fileClients: string[]; nativeClients: string[]; } @@ -794,6 +813,7 @@ const pluginListCmd = command({ name: p.name, marketplace: p.marketplace, scope: p.scope, + kind: kindBySource.get(p.spec) ?? 'plugin', fileClients: pluginClients.get(`${p.spec}:${p.scope}`) ?? [], nativeClients: [], }); @@ -821,6 +841,7 @@ const pluginListCmd = command({ name: parsed?.plugin ?? spec, marketplace: parsed?.marketplaceName ?? '', scope, + kind: kindBySource.get(spec) ?? 'plugin', fileClients: [], nativeClients: [client], }); @@ -840,6 +861,7 @@ const pluginListCmd = command({ name: p.name, marketplace: p.marketplace, scope: p.scope, + kind: p.kind, ...(p.fileClients.length > 0 && { clients: p.fileClients }), ...(p.nativeClients.length > 0 && { nativeClients: p.nativeClients }), })), @@ -858,9 +880,13 @@ const pluginListCmd = command({ return; } - console.log('Installed plugins:\n'); + const skillCount = plugins.filter((p) => p.kind === 'skill').length; + const pluginCount = plugins.length - skillCount; + + console.log('Installed:\n'); for (const p of plugins) { console.log(` ❯ ${p.spec}`); + console.log(` Type: ${p.kind}`); console.log(` Scope: ${p.scope}`); const hasClients = p.fileClients.length > 0 || p.nativeClients.length > 0; @@ -874,7 +900,10 @@ const pluginListCmd = command({ console.log(''); } - console.log(`Total: ${plugins.length} installed`); + const summaryParts: string[] = []; + if (pluginCount > 0) summaryParts.push(`${pluginCount} plugin${pluginCount === 1 ? '' : 's'}`); + if (skillCount > 0) summaryParts.push(`${skillCount} skill${skillCount === 1 ? '' : 's'}`); + console.log(`Total: ${summaryParts.join(', ')}`); } catch (error) { if (error instanceof Error) { if (isJsonMode()) { diff --git a/src/cli/commands/workspace.ts b/src/cli/commands/workspace.ts index 6fa416e8..fe2bb459 100644 --- a/src/cli/commands/workspace.ts +++ b/src/cli/commands/workspace.ts @@ -467,19 +467,19 @@ function formatTimingMs(ms: number): string { function formatPluginStatusLine(plugin: { source: string; type: 'local' | 'github' | 'marketplace'; + kind: 'skill' | 'plugin'; available: boolean; }): string { const status = plugin.available ? '✓' : '✗'; - let typeLabel: string | undefined; + const labels: string[] = [plugin.kind]; if (plugin.type === 'marketplace') { - typeLabel = plugin.available ? undefined : 'not synced'; + if (!plugin.available) labels.push('not synced'); } else if (plugin.type === 'github') { - typeLabel = plugin.available ? 'cached' : 'not cached'; + labels.push(plugin.available ? 'cached' : 'not cached'); } else { - typeLabel = 'local'; + labels.push('local'); } - const suffix = typeLabel ? ` (${typeLabel})` : ''; - return `${status} ${formatPluginSource(plugin.source)}${suffix}`; + return `${status} ${formatPluginSource(plugin.source)} (${labels.join(', ')})`; } const statusCmd = command({ @@ -838,7 +838,7 @@ const repoCmd = conciseSubcommands({ // workspace subcommands group // ============================================================================= -export { syncCmd, initCmd }; +export { syncCmd, initCmd, statusCmd }; export const workspaceCmd = conciseSubcommands({ name: 'workspace', diff --git a/src/cli/index.ts b/src/cli/index.ts index 907b34fa..d84b4f84 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,7 +2,7 @@ import { run } from 'cmd-ts'; import { conciseSubcommands } from './help.js'; -import { workspaceCmd, syncCmd, initCmd } from './commands/workspace.js'; +import { workspaceCmd, syncCmd, initCmd, statusCmd } from './commands/workspace.js'; import { pluginCmd } from './commands/plugin.js'; import { mcpCmd } from './commands/mcp.js'; import { selfCmd } from './commands/self.js'; @@ -31,6 +31,7 @@ const app = conciseSubcommands({ cmds: { init: initCmd, update: syncCmd, + status: statusCmd, workspace: workspaceCmd, plugin: pluginCmd, mcp: mcpCmd, diff --git a/src/cli/metadata/plugin.ts b/src/cli/metadata/plugin.ts index c9c2ff1b..e3f0e0fe 100644 --- a/src/cli/metadata/plugin.ts +++ b/src/cli/metadata/plugin.ts @@ -102,15 +102,15 @@ export const marketplaceBrowseMeta: AgentCommandMeta = { export const pluginListMeta: AgentCommandMeta = { command: 'plugin list', - description: 'List installed plugins', - whenToUse: 'To see which plugins are currently installed in your workspace', + description: 'List installed plugins and standalone skills', + whenToUse: 'To see which plugins and skills are currently installed in your workspace', examples: [ 'allagents plugin list', ], expectedOutput: - 'Lists installed plugins with their marketplace and scope. If none installed, suggests using marketplace browse.', + 'Lists installed items with type (plugin or skill), marketplace, and scope. If none installed, suggests using marketplace browse.', outputSchema: { - plugins: [{ name: 'string', marketplace: 'string', scope: 'string' }], + plugins: [{ name: 'string', marketplace: 'string', scope: 'string', kind: 'string' }], total: 'number', }, }; diff --git a/src/cli/metadata/workspace.ts b/src/cli/metadata/workspace.ts index 7e847e4c..72d4bd21 100644 --- a/src/cli/metadata/workspace.ts +++ b/src/cli/metadata/workspace.ts @@ -73,16 +73,17 @@ export const pruneMeta: AgentCommandMeta = { }; export const statusMeta: AgentCommandMeta = { - command: 'workspace status', + command: 'status', description: 'Show sync status of plugins', - whenToUse: 'To check which plugins are configured and whether they are available locally', + whenToUse: 'To check which plugins and skills are configured and whether they are available locally', examples: [ + 'allagents status', 'allagents workspace status', ], expectedOutput: - 'Lists all configured plugins with availability status and configured clients. Exit 0 on success, exit 1 if workspace is not initialized.', + 'Lists all configured plugins/skills with availability status and configured clients. Exit 0 on success, exit 1 if workspace is not initialized.', outputSchema: { - plugins: [{ source: 'string', type: 'string', available: 'boolean' }], + plugins: [{ source: 'string', type: 'string', kind: 'string', available: 'boolean' }], clients: ['string'], }, }; diff --git a/src/core/status.ts b/src/core/status.ts index f99d1f39..83cd1f2c 100644 --- a/src/core/status.ts +++ b/src/core/status.ts @@ -5,6 +5,7 @@ import { parseWorkspaceConfig } from '../utils/workspace-parser.js'; import { getPluginSource, getClientTypes } from '../models/workspace-config.js'; import { parsePluginSource, + parseGitHubUrl, getPluginCachePath, type ParsedPluginSource, } from '../utils/plugin-path.js'; @@ -20,12 +21,39 @@ import { getUserWorkspaceConfig, isUserConfigPath } from './user-workspace.js'; export interface PluginStatus { source: string; type: 'local' | 'github' | 'marketplace'; + /** + * 'skill' when the resolved path looks like a single-skill source (root + * SKILL.md, no skills/ subdir — matches the auto-wrap layout from #232/#249). + * 'plugin' otherwise, including when the path can't be inspected (not cached, + * not synced, missing locally). + */ + kind: 'skill' | 'plugin'; available: boolean; path: string; owner?: string; repo?: string; } +/** + * Classify a resolved cache/local path as a standalone skill or a plugin + * bundle. A "skill" has a SKILL.md at its root and no skills/ subdir; anything + * else (including paths that don't exist) is treated as a plugin. + */ +function classifyKind(path: string): 'skill' | 'plugin' { + if (!path) return 'plugin'; + try { + if ( + existsSync(join(path, 'SKILL.md')) && + !existsSync(join(path, 'skills')) + ) { + return 'skill'; + } + } catch { + // ignore — default to plugin + } + return 'plugin'; +} + /** * Result of workspace status check */ @@ -106,9 +134,21 @@ function getPluginStatus(parsed: ParsedPluginSource): PluginStatus { : ''; const available = cachePath ? existsSync(cachePath) : false; + // For GitHub plugins with a subpath, classify the resolved subdir rather + // than the repo root — that's what users actually consume. ParsedPluginSource + // drops the subpath, so re-parse the original URL to recover it. + const subpath = parseGitHubUrl(parsed.original)?.subpath; + const classifyPath = + available && cachePath + ? subpath + ? join(cachePath, subpath) + : cachePath + : ''; + return { source: parsed.original, type: 'github', + kind: classifyKind(classifyPath), available, path: cachePath, ...(parsed.owner && { owner: parsed.owner }), @@ -122,6 +162,7 @@ function getPluginStatus(parsed: ParsedPluginSource): PluginStatus { return { source: parsed.original, type: 'local', + kind: classifyKind(available ? parsed.normalized : ''), available, path: parsed.normalized, }; @@ -156,6 +197,7 @@ async function getMarketplacePluginStatus(spec: string): Promise { return { source: spec, type: 'marketplace', + kind: classifyKind(resolved.success ? (resolved.path ?? '') : ''), available: resolved.success, path: resolved.path ?? '', }; diff --git a/tests/unit/cli/agent-help.test.ts b/tests/unit/cli/agent-help.test.ts index 13ce5624..db32c582 100644 --- a/tests/unit/cli/agent-help.test.ts +++ b/tests/unit/cli/agent-help.test.ts @@ -90,8 +90,8 @@ describe('agent command metadata', () => { 'skill list', 'skill remove', 'skill search', + 'status', 'update', - 'workspace status', ]); }); @@ -131,8 +131,8 @@ describe('agent command metadata', () => { expect(installCmd.positionals![0].required).toBe(true); }); - test('workspace status has no positionals or options', () => { - const statusCmd = allCommands.find((c) => c.command === 'workspace status')!; + test('status has no positionals or options', () => { + const statusCmd = allCommands.find((c) => c.command === 'status')!; expect(statusCmd.positionals).toBeUndefined(); expect(statusCmd.options).toBeUndefined(); }); From c822575cf159aafe6d6e9a484dcd8bf11fba3714 Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 27 May 2026 13:20:22 +1000 Subject: [PATCH 2/2] feat(tui): show skill vs plugin kind in TUI status and plugins picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the CLI labeling added in the previous commit so the interactive wizard surfaces the same skill/plugin distinction users see in `allagents status` and `allagents plugin list`. status panel (before): ✓ (github) status panel (after): ✓ (skill, github) plugins picker hint (before): github · project plugins picker hint (after): skill · github · project Verified end-to-end by driving the wizard through agent-tui against a temp workspace containing a deep-URL skill source. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cli/tui/actions/plugins.ts | 4 ++-- src/cli/tui/actions/status.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/tui/actions/plugins.ts b/src/cli/tui/actions/plugins.ts index ce80aa96..4b2dffc2 100644 --- a/src/cli/tui/actions/plugins.ts +++ b/src/cli/tui/actions/plugins.ts @@ -303,7 +303,7 @@ export async function runPlugins(context: TuiContext, cache?: TuiCache): Promise options.push({ label: plugin.source, value: key, - hint: `${plugin.type} · project`, + hint: `${plugin.kind} · ${plugin.type} · project`, }); } for (const plugin of status.userPlugins ?? []) { @@ -311,7 +311,7 @@ export async function runPlugins(context: TuiContext, cache?: TuiCache): Promise options.push({ label: plugin.source, value: key, - hint: `${plugin.type} · user`, + hint: `${plugin.kind} · ${plugin.type} · user`, }); } } diff --git a/src/cli/tui/actions/status.ts b/src/cli/tui/actions/status.ts index ac8538c8..b83c2964 100644 --- a/src/cli/tui/actions/status.ts +++ b/src/cli/tui/actions/status.ts @@ -47,7 +47,7 @@ export async function runStatus(context: TuiContext, cache?: TuiCache): Promise< lines.push('Project plugins:'); for (const plugin of status.plugins) { const icon = plugin.available ? '\u2713' : '\u2717'; - lines.push(` ${icon} ${plugin.source} (${plugin.type})`); + lines.push(` ${icon} ${plugin.source} (${plugin.kind}, ${plugin.type})`); } } @@ -56,7 +56,7 @@ export async function runStatus(context: TuiContext, cache?: TuiCache): Promise< lines.push('User plugins:'); for (const plugin of userPlugins) { const icon = plugin.available ? '\u2713' : '\u2717'; - lines.push(` ${icon} ${plugin.source} (${plugin.type})`); + lines.push(` ${icon} ${plugin.source} (${plugin.kind}, ${plugin.type})`); } }