terminal-pilot 0.0.47 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/cli.d.ts +5 -0
  2. package/dist/cli.js +706 -164
  3. package/dist/cli.js.map +4 -4
  4. package/dist/commands/close-session.js.map +1 -1
  5. package/dist/commands/create-session.js.map +1 -1
  6. package/dist/commands/daemon-runtime.js.map +1 -1
  7. package/dist/commands/fill.js.map +1 -1
  8. package/dist/commands/get-session.js.map +1 -1
  9. package/dist/commands/index.js +77 -10
  10. package/dist/commands/index.js.map +4 -4
  11. package/dist/commands/install.js +81 -14
  12. package/dist/commands/install.js.map +4 -4
  13. package/dist/commands/installer.js +37 -6
  14. package/dist/commands/installer.js.map +3 -3
  15. package/dist/commands/list-sessions.js.map +1 -1
  16. package/dist/commands/press-key.js.map +1 -1
  17. package/dist/commands/read-history.js.map +1 -1
  18. package/dist/commands/read-screen.js.map +1 -1
  19. package/dist/commands/resize.js.map +1 -1
  20. package/dist/commands/runtime.js.map +1 -1
  21. package/dist/commands/screenshot.js.map +1 -1
  22. package/dist/commands/send-signal.js.map +1 -1
  23. package/dist/commands/type.js.map +1 -1
  24. package/dist/commands/uninstall.js +37 -6
  25. package/dist/commands/uninstall.js.map +3 -3
  26. package/dist/commands/wait-for-exit.js.map +1 -1
  27. package/dist/commands/wait-for.js.map +1 -1
  28. package/dist/composition.json +6 -1
  29. package/dist/testing/cli-repl.js +704 -163
  30. package/dist/testing/cli-repl.js.map +4 -4
  31. package/dist/testing/qa-cli.js +704 -163
  32. package/dist/testing/qa-cli.js.map +4 -4
  33. package/node_modules/@poe-code/agent-defs/README.md +15 -2
  34. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +2 -1
  35. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +1 -0
  36. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +2 -1
  37. package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -0
  38. package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +2 -1
  39. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
  40. package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +1 -0
  41. package/node_modules/@poe-code/agent-defs/dist/agents/index.js +1 -0
  42. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +2 -1
  43. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +2 -1
  44. package/node_modules/@poe-code/agent-defs/dist/agents/pi.d.ts +2 -0
  45. package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +15 -0
  46. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +1 -0
  47. package/node_modules/@poe-code/agent-defs/dist/capabilities.d.ts +14 -0
  48. package/node_modules/@poe-code/agent-defs/dist/capabilities.js +63 -0
  49. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +3 -2
  50. package/node_modules/@poe-code/agent-defs/dist/index.js +2 -1
  51. package/node_modules/@poe-code/agent-defs/dist/registry.js +5 -1
  52. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +8 -1
  53. package/node_modules/@poe-code/agent-skill-config/dist/apply.d.ts +2 -0
  54. package/node_modules/@poe-code/agent-skill-config/dist/apply.js +18 -14
  55. package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.js +3 -1
  56. package/node_modules/@poe-code/agent-skill-config/dist/configs.d.ts +2 -0
  57. package/node_modules/@poe-code/agent-skill-config/dist/configs.js +2 -1
  58. package/node_modules/@poe-code/frontmatter/README.md +3 -0
  59. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +1 -1
  60. package/node_modules/@poe-code/frontmatter/dist/index.js +1 -1
  61. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +9 -0
  62. package/node_modules/@poe-code/frontmatter/dist/parse.js +16 -0
  63. package/node_modules/toolcraft-design/dist/acp/components.d.ts +8 -1
  64. package/node_modules/toolcraft-design/dist/acp/components.js +11 -6
  65. package/node_modules/toolcraft-design/dist/acp/index.d.ts +1 -0
  66. package/node_modules/toolcraft-design/dist/components/command-errors.d.ts +2 -0
  67. package/node_modules/toolcraft-design/dist/components/command-errors.js +8 -2
  68. package/node_modules/toolcraft-design/dist/components/help-formatter-plain.js +61 -25
  69. package/node_modules/toolcraft-design/dist/components/help-formatter.d.ts +17 -0
  70. package/node_modules/toolcraft-design/dist/components/help-formatter.js +128 -29
  71. package/node_modules/toolcraft-design/dist/components/index.d.ts +3 -3
  72. package/node_modules/toolcraft-design/dist/components/index.js +2 -2
  73. package/node_modules/toolcraft-design/dist/components/table.d.ts +1 -0
  74. package/node_modules/toolcraft-design/dist/components/table.js +52 -5
  75. package/node_modules/toolcraft-design/dist/components/template.d.ts +9 -0
  76. package/node_modules/toolcraft-design/dist/components/template.js +27 -2
  77. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -0
  78. package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
  79. package/node_modules/toolcraft-design/dist/explorer/reducer.js +1 -0
  80. package/node_modules/toolcraft-design/dist/explorer/runtime.js +21 -2
  81. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +6 -0
  82. package/node_modules/toolcraft-design/dist/explorer/state.js +9 -6
  83. package/node_modules/toolcraft-design/dist/help-formatter.d.ts +2 -2
  84. package/node_modules/toolcraft-design/dist/help-formatter.js +1 -1
  85. package/node_modules/toolcraft-design/dist/index.d.ts +4 -4
  86. package/node_modules/toolcraft-design/dist/index.js +3 -3
  87. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +5 -0
  88. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +15 -1
  89. package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +1 -1
  90. package/package.json +4 -3
@@ -4,7 +4,8 @@ Shared catalog of supported coding-agent definitions.
4
4
 
5
5
  This package owns the declarative agent metadata used by CLI, SDK, spawn,
6
6
  configuration, and UI surfaces: stable ids, labels, aliases, config paths,
7
- binary names, API-shape support, OTEL capture wiring, and brand colors.
7
+ binary names, API-shape support, OTEL capture wiring, brand colors, and the
8
+ capability matrix that says which commands accept each agent.
8
9
 
9
10
  ## Usage
10
11
 
@@ -23,7 +24,19 @@ const codex = allAgents.find((agent) => agent.id === "codex");
23
24
  - `parseAgentSpecifier(input)`: parses `agent` or `agent:model` input.
24
25
  - `formatAgentSpecifier(specifier)`: formats an agent specifier.
25
26
  - `normalizeAgentId(input)`: normalizes the agent part through the registry.
26
- - Agent definition exports such as `codexAgent`, `claudeCodeAgent`, and `geminiCliAgent`.
27
+ - Agent definition exports such as `codexAgent`, `claudeCodeAgent`, `geminiCliAgent`, and `piAgent`.
28
+ - `listAgentsWithCapability(capability, options?)`: ids supporting a capability, optionally including aliases.
29
+ - `agentSupportsCapability(input, capability)`: resolves aliases, then checks the matrix.
30
+ - `formatAgentCapabilityError({ agent, capability })`: the shared message for a rejected agent argument.
31
+
32
+ ## Capability Matrix
33
+
34
+ Each `AgentDefinition` declares its `capabilities` (`spawn`, `configure`,
35
+ `install`, `test`, `skill`, `mcp`). This is the single published source for every
36
+ command's allow-list, so `spawn`/`configure`/`install`/`test`/`skill` cannot
37
+ drift apart. `src/cli/commands/agent-capability-matrix.test.ts` pins each
38
+ capability to the registry that implements it, and fails if a registry gains or
39
+ loses an agent without the matrix being updated.
27
40
 
28
41
  ## Config Options
29
42
 
@@ -2,7 +2,7 @@ export const claudeCodeAgent = {
2
2
  id: "claude-code",
3
3
  name: "claude-code",
4
4
  label: "Claude Code",
5
- summary: "Configure Claude Code to route through Poe.",
5
+ summary: "Anthropic's agentic coding tool for the terminal.",
6
6
  aliases: ["claude"],
7
7
  binaryName: "claude",
8
8
  apiShapes: ["anthropic-messages"],
@@ -12,6 +12,7 @@ export const claudeCodeAgent = {
12
12
  }
13
13
  },
14
14
  configPath: "~/.claude.json",
15
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
15
16
  branding: {
16
17
  colors: {
17
18
  dark: "#C15F3C",
@@ -9,6 +9,7 @@ export const claudeDesktopAgent = {
9
9
  linux: "~/.config/Claude/claude_desktop_config.json",
10
10
  win32: "~/AppData/Roaming/Claude/claude_desktop_config.json"
11
11
  },
12
+ capabilities: ["mcp"],
12
13
  branding: {
13
14
  colors: {
14
15
  dark: "#D97757",
@@ -2,7 +2,7 @@ export const codexAgent = {
2
2
  id: "codex",
3
3
  name: "codex",
4
4
  label: "Codex",
5
- summary: "Configure Codex to use Poe as the model provider.",
5
+ summary: "OpenAI's coding agent for the terminal.",
6
6
  binaryName: "codex",
7
7
  apiShapes: ["openai-responses"],
8
8
  otelCapture: {
@@ -16,6 +16,7 @@ export const codexAgent = {
16
16
  ]
17
17
  },
18
18
  configPath: "~/.codex/config.toml",
19
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
19
20
  branding: {
20
21
  colors: {
21
22
  dark: "#D5D9DF",
@@ -6,6 +6,7 @@ export const cursorAgent = {
6
6
  summary: "Cursor's CLI coding agent.",
7
7
  binaryName: "cursor-agent",
8
8
  configPath: "~/.cursor/mcp.json",
9
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
9
10
  branding: {
10
11
  colors: {
11
12
  dark: "#FFFFFF",
@@ -3,10 +3,11 @@ export const geminiCliAgent = {
3
3
  name: "gemini-cli",
4
4
  aliases: ["gemini"],
5
5
  label: "Gemini CLI",
6
- summary: "Configure Google's Gemini CLI to use a compatible Google generations API.",
6
+ summary: "Google's open-source AI agent for the terminal.",
7
7
  binaryName: "gemini",
8
8
  configPath: "~/.gemini/settings.json",
9
9
  apiShapes: ["google-generations"],
10
+ capabilities: ["spawn", "configure", "install", "test", "skill"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#8AB4F8",
@@ -7,6 +7,7 @@ export const gooseAgent = {
7
7
  apiShapes: ["openai-chat-completions"],
8
8
  otelCapture: {},
9
9
  configPath: "~/.config/goose/config.yaml",
10
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#FF6B35",
@@ -6,4 +6,5 @@ export { geminiCliAgent } from "./gemini-cli.js";
6
6
  export { openCodeAgent } from "./opencode.js";
7
7
  export { kimiAgent } from "./kimi.js";
8
8
  export { gooseAgent } from "./goose.js";
9
+ export { piAgent } from "./pi.js";
9
10
  export { poeAgentAgent } from "./poe-agent.js";
@@ -6,4 +6,5 @@ export { geminiCliAgent } from "./gemini-cli.js";
6
6
  export { openCodeAgent } from "./opencode.js";
7
7
  export { kimiAgent } from "./kimi.js";
8
8
  export { gooseAgent } from "./goose.js";
9
+ export { piAgent } from "./pi.js";
9
10
  export { poeAgentAgent } from "./poe-agent.js";
@@ -2,11 +2,12 @@ export const kimiAgent = {
2
2
  id: "kimi",
3
3
  name: "kimi",
4
4
  label: "Kimi",
5
- summary: "Configure Kimi CLI to use Poe API",
5
+ summary: "Moonshot AI's coding agent for the terminal.",
6
6
  aliases: ["kimi-cli"],
7
7
  binaryName: "kimi",
8
8
  apiShapes: ["openai-chat-completions"],
9
9
  configPath: "~/.kimi/mcp.json",
10
+ capabilities: ["spawn", "configure", "install", "test", "mcp"],
10
11
  branding: {
11
12
  colors: {
12
13
  dark: "#7B68EE",
@@ -2,7 +2,7 @@ export const openCodeAgent = {
2
2
  id: "opencode",
3
3
  name: "opencode",
4
4
  label: "OpenCode CLI",
5
- summary: "Configure OpenCode CLI to use the Poe API.",
5
+ summary: "Open-source AI coding agent for the terminal.",
6
6
  binaryName: "opencode",
7
7
  apiShapes: ["openai-chat-completions"],
8
8
  otelCapture: {
@@ -11,6 +11,7 @@ export const openCodeAgent = {
11
11
  }
12
12
  },
13
13
  configPath: "~/.config/opencode/opencode.json",
14
+ capabilities: ["spawn", "configure", "install", "test", "skill", "mcp"],
14
15
  branding: {
15
16
  colors: {
16
17
  dark: "#4A4F55",
@@ -0,0 +1,2 @@
1
+ import type { AgentDefinition } from "../types.js";
2
+ export declare const piAgent: AgentDefinition;
@@ -0,0 +1,15 @@
1
+ export const piAgent = {
2
+ id: "pi",
3
+ name: "pi",
4
+ aliases: ["pi-agent"],
5
+ label: "Pi",
6
+ summary: "Minimal AI coding agent for the terminal.",
7
+ binaryName: "pi",
8
+ capabilities: ["spawn"],
9
+ branding: {
10
+ colors: {
11
+ dark: "#F2F2F2",
12
+ light: "#242424"
13
+ }
14
+ }
15
+ };
@@ -5,6 +5,7 @@ export const poeAgentAgent = {
5
5
  summary: "Run one-shot prompts with the built-in Poe agent runtime.",
6
6
  apiShapes: ["openai-responses", "openai-chat-completions"],
7
7
  configPath: "~/.poe-code/config.json",
8
+ capabilities: ["configure"],
8
9
  branding: {
9
10
  colors: {
10
11
  dark: "#A465F7",
@@ -0,0 +1,14 @@
1
+ import type { AgentCapability } from "./types.js";
2
+ export declare function listAgentsWithCapability(capability: AgentCapability, options?: {
3
+ includeAliases?: boolean;
4
+ }): readonly string[];
5
+ export declare function agentSupportsCapability(input: string, capability: AgentCapability): boolean;
6
+ /**
7
+ * The single message for every agent argument that misses. It distinguishes a
8
+ * typo (unknown id, plus a did-you-mean) from a real capability gap ("pi
9
+ * supports: spawn"), and always names the agents the command does accept.
10
+ */
11
+ export declare function formatAgentCapabilityError(input: {
12
+ agent: string;
13
+ capability: AgentCapability;
14
+ }): string;
@@ -0,0 +1,63 @@
1
+ import { allAgents, resolveAgentId } from "./registry.js";
2
+ export function listAgentsWithCapability(capability, options) {
3
+ const names = [];
4
+ for (const agent of allAgents) {
5
+ if (!agent.capabilities?.includes(capability)) {
6
+ continue;
7
+ }
8
+ names.push(agent.id);
9
+ if (options?.includeAliases) {
10
+ names.push(...(agent.aliases ?? []));
11
+ }
12
+ }
13
+ return names;
14
+ }
15
+ export function agentSupportsCapability(input, capability) {
16
+ const id = resolveAgentId(input);
17
+ return id !== undefined && listAgentsWithCapability(capability).includes(id);
18
+ }
19
+ const MAX_SUGGESTION_DISTANCE = 3;
20
+ function editDistance(a, b) {
21
+ let previous = Array.from({ length: b.length + 1 }, (_unused, index) => index);
22
+ for (let i = 1; i <= a.length; i += 1) {
23
+ const current = [i];
24
+ for (let j = 1; j <= b.length; j += 1) {
25
+ const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
26
+ current[j] = Math.min(substitution, previous[j] + 1, current[j - 1] + 1);
27
+ }
28
+ previous = current;
29
+ }
30
+ return previous[b.length];
31
+ }
32
+ /** Every candidate tied for the smallest edit distance, when one is close enough. */
33
+ function suggest(input, candidates) {
34
+ const needle = input.trim().toLowerCase();
35
+ const scored = candidates
36
+ .map((candidate) => ({ candidate, score: editDistance(needle, candidate.toLowerCase()) }))
37
+ .filter((entry) => entry.score <= MAX_SUGGESTION_DISTANCE);
38
+ if (scored.length === 0) {
39
+ return [];
40
+ }
41
+ const best = Math.min(...scored.map((entry) => entry.score));
42
+ return scored.filter((entry) => entry.score === best).map((entry) => entry.candidate);
43
+ }
44
+ /**
45
+ * The single message for every agent argument that misses. It distinguishes a
46
+ * typo (unknown id, plus a did-you-mean) from a real capability gap ("pi
47
+ * supports: spawn"), and always names the agents the command does accept.
48
+ */
49
+ export function formatAgentCapabilityError(input) {
50
+ const allowed = listAgentsWithCapability(input.capability, { includeAliases: true });
51
+ const allowList = `Agents supporting ${input.capability}: ${allowed.length > 0 ? allowed.join(", ") : "none"}.`;
52
+ const id = resolveAgentId(input.agent);
53
+ if (!id) {
54
+ const near = suggest(input.agent, allowed);
55
+ const hint = near.length > 0 ? ` Did you mean: ${near.join(", ")}?` : "";
56
+ return `Unknown agent "${input.agent}".${hint} ${allowList}`;
57
+ }
58
+ const supported = ["spawn", "configure", "install", "test", "skill", "mcp"].filter((capability) => listAgentsWithCapability(capability).includes(id));
59
+ const supports = supported.length > 0
60
+ ? `${id} supports: ${supported.join(", ")}.`
61
+ : `${id} is not supported by poe-code agent commands.`;
62
+ return `Agent "${id}" does not support ${input.capability}. ${supports} ${allowList}`;
63
+ }
@@ -1,5 +1,6 @@
1
- export type { AgentDefinition, ApiShapeId, OtelCaptureDefinition } from "./types.js";
1
+ export type { AgentCapability, AgentDefinition, ApiShapeId, OtelCaptureDefinition } from "./types.js";
2
2
  export type { AgentSpecifier } from "./specifier.js";
3
- export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
3
+ export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
4
4
  export { allAgents, resolveAgentId } from "./registry.js";
5
+ export { agentSupportsCapability, formatAgentCapabilityError, listAgentsWithCapability } from "./capabilities.js";
5
6
  export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
@@ -1,3 +1,4 @@
1
- export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
1
+ export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
2
2
  export { allAgents, resolveAgentId } from "./registry.js";
3
+ export { agentSupportsCapability, formatAgentCapabilityError, listAgentsWithCapability } from "./capabilities.js";
3
4
  export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
@@ -1,4 +1,4 @@
1
- import { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
1
+ import { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
2
2
  function freezeAgent(agent) {
3
3
  if (agent.aliases !== undefined) {
4
4
  Object.freeze(agent.aliases);
@@ -6,6 +6,9 @@ function freezeAgent(agent) {
6
6
  if (agent.apiShapes !== undefined) {
7
7
  Object.freeze(agent.apiShapes);
8
8
  }
9
+ if (agent.capabilities !== undefined) {
10
+ Object.freeze(agent.capabilities);
11
+ }
9
12
  if (agent.otelCapture?.env !== undefined) {
10
13
  Object.freeze(agent.otelCapture.env);
11
14
  }
@@ -25,6 +28,7 @@ export const allAgents = Object.freeze([
25
28
  freezeAgent(openCodeAgent),
26
29
  freezeAgent(kimiAgent),
27
30
  freezeAgent(gooseAgent),
31
+ freezeAgent(piAgent),
28
32
  freezeAgent(poeAgentAgent)
29
33
  ]);
30
34
  const lookup = new Map();
@@ -1,4 +1,10 @@
1
1
  export type ApiShapeId = "openai-chat-completions" | "openai-responses" | "anthropic-messages" | "google-generations";
2
+ /**
3
+ * The poe-code surfaces an agent can be used with. This is the single
4
+ * published source for each command's allow-list; `agent-capability-matrix.test.ts`
5
+ * pins every value to the registry that implements it, so the sets cannot drift.
6
+ */
7
+ export type AgentCapability = "spawn" | "configure" | "install" | "test" | "skill" | "mcp";
2
8
  export interface OtelCaptureDefinition {
3
9
  env?: Record<string, string>;
4
10
  args?: (endpoint: string, content: boolean) => string[];
@@ -11,9 +17,10 @@ export interface AgentDefinition {
11
17
  aliases?: string[];
12
18
  /** Binary name for CLI agents. Optional for GUI-only apps like Claude Desktop. */
13
19
  binaryName?: string;
20
+ readonly capabilities?: readonly AgentCapability[];
14
21
  readonly apiShapes?: readonly ApiShapeId[];
15
22
  readonly otelCapture?: OtelCaptureDefinition;
16
- configPath: string;
23
+ configPath?: string;
17
24
  readonly configPaths?: {
18
25
  readonly darwin: string;
19
26
  readonly linux: string;
@@ -13,6 +13,8 @@ export type InstallSkillOptions = {
13
13
  scope: ApplyOptions["scope"];
14
14
  dryRun?: boolean;
15
15
  observers?: ApplyOptions["observers"];
16
+ /** Overwrite an existing SKILL.md instead of refusing to install over it. */
17
+ force?: boolean;
16
18
  };
17
19
  export type InstallSkillResult = {
18
20
  skillPath: string;
@@ -1,4 +1,5 @@
1
1
  import { fileMutation, runMutations, templateMutation } from "@poe-code/config-mutations";
2
+ import { UserError } from "@poe-code/user-error";
2
3
  import { resolveAgentSupport } from "./configs.js";
3
4
  import { hasOwnErrorCode } from "./error-codes.js";
4
5
  import { createTemplateLoader } from "./templates.js";
@@ -45,7 +46,7 @@ export async function configure(agentId, options) {
45
46
  if (await pathExists(options.fs, targetPath)) {
46
47
  const existing = await options.fs.readFile(targetPath, "utf8");
47
48
  if (existing !== (await templateLoader(templateId))) {
48
- throw new Error(`Skill already exists: ${targetPath}`);
49
+ throw new UserError(`Skill already exists and differs from the bundled ${templateId}: ${targetPath}\nMove or delete it, then re-run this command to install the poe-code version.`);
49
50
  }
50
51
  }
51
52
  }
@@ -75,27 +76,30 @@ export async function unconfigure(agentId, options) {
75
76
  const scope = options.scope ?? "global";
76
77
  const config = support.config;
77
78
  const skillDir = scope === "global" ? config.globalSkillDir : toHomeRelative(config.localSkillDir);
79
+ const displayDir = scope === "global" ? config.globalSkillDir : config.localSkillDir;
78
80
  const homeDir = scope === "global" ? options.homeDir : options.cwd;
79
81
  const templateLoader = createTemplateLoader();
80
82
  const removeBundledSkills = [];
81
83
  for (const templateId of bundledSkillTemplateIds) {
82
- const targetPath = `${scope === "global" ? options.homeDir : options.cwd}/${skillDir.slice(2)}/${templateId}`;
83
- if (await pathExists(options.fs, targetPath)) {
84
- const existing = await options.fs.readFile(targetPath, "utf8");
85
- if (existing === (await templateLoader(templateId))) {
86
- removeBundledSkills.push(fileMutation.remove({
87
- target: `${skillDir}/${templateId}`,
88
- label: `Remove bundled skill ${templateId} from ${skillDir}`
89
- }));
90
- }
84
+ const targetPath = `${homeDir}/${skillDir.slice(2)}/${templateId}`;
85
+ if (!(await pathExists(options.fs, targetPath))) {
86
+ continue;
87
+ }
88
+ const existing = await options.fs.readFile(targetPath, "utf8");
89
+ if (options.force || existing === (await templateLoader(templateId))) {
90
+ removeBundledSkills.push(fileMutation.remove({
91
+ target: `${skillDir}/${templateId}`,
92
+ label: `Remove bundled skill ${templateId} from ${displayDir}`
93
+ }));
91
94
  }
92
95
  }
93
96
  await runMutations([
94
97
  ...removeBundledSkills,
98
+ // Never force-remove the shared skills root: it holds skills poe-code does not
99
+ // manage. It is only removed once poe-code's own skills left it empty.
95
100
  fileMutation.removeDirectory({
96
101
  path: skillDir,
97
- force: options.force,
98
- label: `Remove skills directory ${skillDir}`
102
+ label: `Remove empty skills directory ${displayDir}`
99
103
  })
100
104
  ], {
101
105
  fs: options.fs,
@@ -132,8 +136,8 @@ export async function installSkill(agentId, skill, options) {
132
136
  const skillFilePath = `${skillFolderPath}/SKILL.md`;
133
137
  const displayPath = `${scope === "global" ? config.globalSkillDir : config.localSkillDir}/${skill.name}/SKILL.md`;
134
138
  const absoluteSkillPath = `${scope === "global" ? options.homeDir : options.cwd}/${skillFilePath.slice(2)}`;
135
- if (await pathExists(options.fs, absoluteSkillPath)) {
136
- throw new Error(`Skill already exists: ${displayPath}`);
139
+ if (!options.force && (await pathExists(options.fs, absoluteSkillPath))) {
140
+ throw new UserError(`Skill already exists: ${displayPath}\nRe-run with --force to overwrite it, or remove it first.`);
137
141
  }
138
142
  await runMutations([
139
143
  fileMutation.ensureDirectory({
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { getAgentConfig, resolveAgentSupport, resolveSkillDir, supportedAgents } from "./configs.js";
5
+ import { UserError } from "@poe-code/user-error";
5
6
  import { hasOwnErrorCode } from "./error-codes.js";
6
7
  import { appendExcludeBlock, removeExcludeBlock } from "./git-exclude.js";
7
8
  import { resolveSkillReference } from "./resolve-skill-reference.js";
@@ -88,8 +89,9 @@ function formatResolutionFailureError(failures) {
88
89
  lines.push(` - ${searchedPath}`);
89
90
  }
90
91
  }
92
+ lines.push("Install one with poe-code skill install <agent> --name <name> --file <path>.");
91
93
  }
92
- return new Error(lines.join("\n"));
94
+ return new UserError(lines.join("\n"));
93
95
  }
94
96
  function copyDirectory(sourcePath, targetPath) {
95
97
  fs.mkdirSync(targetPath, { recursive: true });
@@ -1,6 +1,8 @@
1
1
  export interface AgentSkillConfig {
2
2
  globalSkillDir: string;
3
3
  localSkillDir: string;
4
+ /** Set only when the directories deviate from the agent's own dot-directory. */
5
+ dirNote?: string;
4
6
  }
5
7
  export type SkillScope = "global" | "local";
6
8
  export declare const supportedAgents: readonly string[];
@@ -24,7 +24,8 @@ const agentSkillConfigs = {
24
24
  },
25
25
  goose: {
26
26
  globalSkillDir: "~/.agents/skills",
27
- localSkillDir: ".agents/skills"
27
+ localSkillDir: ".agents/skills",
28
+ dirNote: "Goose discovers skills from the shared .agents/skills convention, not a goose-specific directory."
28
29
  }
29
30
  };
30
31
  export const supportedAgents = Object.freeze(Object.keys(agentSkillConfigs));
@@ -6,7 +6,9 @@ Shared YAML frontmatter parsing for poe-code packages.
6
6
 
7
7
  ```ts
8
8
  import {
9
+ FrontmatterKindError,
9
10
  FrontmatterParseError,
11
+ isFrontmatterKindError,
10
12
  parseFrontmatter,
11
13
  parseFrontmatterDocument,
12
14
  stringifyFrontmatter
@@ -17,6 +19,7 @@ import {
17
19
  - `parseFrontmatterDocument(source)` returns `{ frontmatter, body, errors, lineCounter }` for callers that need YAML diagnostics.
18
20
  - `stringifyFrontmatter(frontmatter, body)` writes `---` fences, YAML, and the body.
19
21
  - `FrontmatterParseError` is thrown for malformed frontmatter, invalid YAML, non-object frontmatter, and stringify failures.
22
+ - `FrontmatterKindError` extends `FrontmatterParseError` and carries `expectedKind` / `foundKind` so callers can report a document kind mismatch instead of a missing file. `isFrontmatterKindError(error)` narrows to it.
20
23
 
21
24
  When no leading frontmatter block exists, parsing returns `{ frontmatter: {}, body: source }`.
22
25
  The returned `body` is sliced from the original input and is otherwise byte-for-byte unchanged.
@@ -1,3 +1,3 @@
1
1
  export { splitFrontmatterBlock, type FrontmatterBlock, type SplitFrontmatterResult } from "./fences.js";
2
- export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument, type ParsedFrontmatter, type ParsedFrontmatterDocument, type ParseFrontmatterOptions } from "./parse.js";
2
+ export { FrontmatterKindError, FrontmatterParseError, isFrontmatterKindError, parseFrontmatter, parseFrontmatterDocument, type ParsedFrontmatter, type ParsedFrontmatterDocument, type ParseFrontmatterOptions } from "./parse.js";
3
3
  export { stringifyFrontmatter } from "./stringify.js";
@@ -1,3 +1,3 @@
1
1
  export { splitFrontmatterBlock } from "./fences.js";
2
- export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument } from "./parse.js";
2
+ export { FrontmatterKindError, FrontmatterParseError, isFrontmatterKindError, parseFrontmatter, parseFrontmatterDocument } from "./parse.js";
3
3
  export { stringifyFrontmatter } from "./stringify.js";
@@ -16,5 +16,14 @@ export interface ParseFrontmatterOptions {
16
16
  export declare class FrontmatterParseError extends Error {
17
17
  constructor(message: string);
18
18
  }
19
+ export declare class FrontmatterKindError extends FrontmatterParseError {
20
+ readonly expectedKind: string;
21
+ readonly foundKind: string;
22
+ constructor(message: string, kinds: {
23
+ expected: string;
24
+ found: string;
25
+ });
26
+ }
27
+ export declare function isFrontmatterKindError(error: unknown): error is FrontmatterKindError;
19
28
  export declare function parseFrontmatter(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatter;
20
29
  export declare function parseFrontmatterDocument(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatterDocument;
@@ -6,6 +6,22 @@ export class FrontmatterParseError extends Error {
6
6
  this.name = "FrontmatterParseError";
7
7
  }
8
8
  }
9
+ export class FrontmatterKindError extends FrontmatterParseError {
10
+ expectedKind;
11
+ foundKind;
12
+ constructor(message, kinds) {
13
+ super(message);
14
+ this.name = "FrontmatterKindError";
15
+ this.expectedKind = kinds.expected;
16
+ this.foundKind = kinds.found;
17
+ }
18
+ }
19
+ export function isFrontmatterKindError(error) {
20
+ return (error instanceof Error &&
21
+ error.name === "FrontmatterKindError" &&
22
+ typeof error.expectedKind === "string" &&
23
+ typeof error.foundKind === "string");
24
+ }
9
25
  export function parseFrontmatter(source, options = {}) {
10
26
  const split = splitFrontmatter(source);
11
27
  if (split.raw === undefined) {
@@ -1,4 +1,11 @@
1
- export declare function renderAgentMessage(text: string): void;
1
+ /**
2
+ * Status of the agent output being rendered.
3
+ *
4
+ * `streaming` covers partial/in-progress content, which has not reached any outcome yet and so must not
5
+ * claim success. `success`/`error` are terminal outcomes known to the caller.
6
+ */
7
+ export type AcpOutputState = "streaming" | "success" | "error";
8
+ export declare function renderAgentMessage(text: string, state?: AcpOutputState): void;
2
9
  export declare function renderToolStart(kind: string, title: string): void;
3
10
  export declare function renderToolComplete(kind: string): void;
4
11
  export declare function renderReasoning(text: string): void;
@@ -25,8 +25,13 @@ function colorForKind(kind) {
25
25
  function writeLine(line) {
26
26
  getAcpWriter()(line);
27
27
  }
28
- function agentPrefix() {
29
- return `${color.green.bold("")} agent: `;
28
+ const STATE_GLYPHS = {
29
+ streaming: () => color.dim("·"),
30
+ success: () => color.green.bold("✓"),
31
+ error: () => color.red.bold("✗")
32
+ };
33
+ function agentPrefix(state) {
34
+ return `${STATE_GLYPHS[state]()} agent: `;
30
35
  }
31
36
  function formatCost(costUsd) {
32
37
  return new Intl.NumberFormat("en-US", {
@@ -36,7 +41,7 @@ function formatCost(costUsd) {
36
41
  maximumFractionDigits: 6
37
42
  }).format(costUsd);
38
43
  }
39
- export function renderAgentMessage(text) {
44
+ export function renderAgentMessage(text, state = "streaming") {
40
45
  const format = resolveOutputFormat();
41
46
  if (format === "markdown") {
42
47
  writeLine(`- **agent:** ${text}`);
@@ -47,7 +52,7 @@ export function renderAgentMessage(text) {
47
52
  return;
48
53
  }
49
54
  const rendered = renderMarkdown(text).trimEnd();
50
- writeLine(`${agentPrefix()}${rendered}`);
55
+ writeLine(`${agentPrefix(state)}${rendered}`);
51
56
  }
52
57
  export function renderToolStart(kind, title) {
53
58
  const format = resolveOutputFormat();
@@ -85,7 +90,7 @@ export function renderReasoning(text) {
85
90
  writeLine(JSON.stringify({ event: "reasoning", text }));
86
91
  return;
87
92
  }
88
- writeLine(color.dim(` ${truncate(text, 80)}`));
93
+ writeLine(color.dim(` · ${truncate(text, 80)}`));
89
94
  }
90
95
  export function renderUsage(tokens) {
91
96
  const format = resolveOutputFormat();
@@ -109,7 +114,7 @@ export function renderUsage(tokens) {
109
114
  return;
110
115
  }
111
116
  writeLine("");
112
- writeLine(color.green(`✓ tokens: ${tokens.input} in${cached} → ${tokens.output} out${cost}`));
117
+ writeLine(color.dim( tokens: ${tokens.input} in${cached} → ${tokens.output} out${cost}`));
113
118
  }
114
119
  export function renderPermissionRejected(title) {
115
120
  const format = resolveOutputFormat();
@@ -1,3 +1,4 @@
1
1
  export { renderAgentMessage, renderToolStart, renderToolComplete, renderReasoning, renderUsage, renderError, renderPermissionRejected } from "./components.js";
2
+ export type { AcpOutputState } from "./components.js";
2
3
  export { getAcpWriter, withAcpWriter } from "./writer.js";
3
4
  export type { AcpLineWriter } from "./writer.js";
@@ -1,6 +1,7 @@
1
1
  export declare function formatCommandNotFound(input: {
2
2
  unknownCommand: string;
3
3
  helpCommand: string;
4
+ suggestions?: readonly string[];
4
5
  }): {
5
6
  label: string;
6
7
  hint: string;
@@ -8,6 +9,7 @@ export declare function formatCommandNotFound(input: {
8
9
  export declare function formatCommandNotFoundPanel(input: {
9
10
  unknownCommand: string;
10
11
  helpCommand: string;
12
+ suggestions?: readonly string[];
11
13
  title?: string;
12
14
  }): {
13
15
  title: string;
@@ -5,15 +5,21 @@ export function formatCommandNotFound(input) {
5
5
  const unknown = unknownInput.length > 0
6
6
  ? unknownInput
7
7
  : "<command>";
8
+ const suggestions = input.suggestions ?? [];
9
+ const didYouMean = suggestions.length > 0
10
+ ? `
11
+ ${text.muted("Did you mean:")} ${suggestions.map((suggestion) => text.command(suggestion)).join(text.muted(", "))}${text.muted("?")}`
12
+ : "";
8
13
  return {
9
- label: `${typography.bold("Unknown command:")} ${text.command(unknown)}`,
14
+ label: `${typography.bold("Unknown command:")} ${text.command(unknown)}${didYouMean}`,
10
15
  hint: `${text.muted("Run")} ${text.usageCommand(input.helpCommand)} ${text.muted("for available commands.")}`
11
16
  };
12
17
  }
13
18
  export function formatCommandNotFoundPanel(input) {
14
19
  const message = formatCommandNotFound({
15
20
  unknownCommand: input.unknownCommand,
16
- helpCommand: input.helpCommand
21
+ helpCommand: input.helpCommand,
22
+ suggestions: input.suggestions
17
23
  });
18
24
  return {
19
25
  title: input.title ?? "command not found",