terminal-pilot 0.0.48 → 0.0.50

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 (84) hide show
  1. package/dist/cli.js +584 -163
  2. package/dist/cli.js.map +4 -4
  3. package/dist/commands/close-session.js.map +1 -1
  4. package/dist/commands/create-session.js.map +1 -1
  5. package/dist/commands/daemon-runtime.js.map +1 -1
  6. package/dist/commands/fill.js.map +1 -1
  7. package/dist/commands/get-session.js.map +1 -1
  8. package/dist/commands/index.js +61 -11
  9. package/dist/commands/index.js.map +4 -4
  10. package/dist/commands/install.js +65 -15
  11. package/dist/commands/install.js.map +4 -4
  12. package/dist/commands/installer.js +21 -7
  13. package/dist/commands/installer.js.map +2 -2
  14. package/dist/commands/list-sessions.js.map +1 -1
  15. package/dist/commands/press-key.js.map +1 -1
  16. package/dist/commands/read-history.js.map +1 -1
  17. package/dist/commands/read-screen.js.map +1 -1
  18. package/dist/commands/resize.js.map +1 -1
  19. package/dist/commands/runtime.js.map +1 -1
  20. package/dist/commands/screenshot.js.map +1 -1
  21. package/dist/commands/send-signal.js.map +1 -1
  22. package/dist/commands/type.js.map +1 -1
  23. package/dist/commands/uninstall.js +21 -7
  24. package/dist/commands/uninstall.js.map +2 -2
  25. package/dist/commands/wait-for-exit.js.map +1 -1
  26. package/dist/commands/wait-for.js.map +1 -1
  27. package/dist/composition.json +6 -1
  28. package/dist/testing/cli-repl.js +584 -163
  29. package/dist/testing/cli-repl.js.map +4 -4
  30. package/dist/testing/qa-cli.js +584 -163
  31. package/dist/testing/qa-cli.js.map +4 -4
  32. package/node_modules/@poe-code/agent-defs/README.md +14 -1
  33. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +2 -1
  34. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +1 -0
  35. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +2 -1
  36. package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -0
  37. package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +2 -1
  38. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
  39. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +2 -1
  40. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +2 -1
  41. package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +2 -1
  42. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +1 -0
  43. package/node_modules/@poe-code/agent-defs/dist/capabilities.d.ts +14 -0
  44. package/node_modules/@poe-code/agent-defs/dist/capabilities.js +63 -0
  45. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +2 -1
  46. package/node_modules/@poe-code/agent-defs/dist/index.js +1 -0
  47. package/node_modules/@poe-code/agent-defs/dist/registry.js +3 -0
  48. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +7 -0
  49. package/node_modules/@poe-code/agent-skill-config/dist/apply.d.ts +2 -0
  50. package/node_modules/@poe-code/agent-skill-config/dist/apply.js +18 -14
  51. package/node_modules/@poe-code/agent-skill-config/dist/bridge-active-skills.js +3 -1
  52. package/node_modules/@poe-code/agent-skill-config/dist/configs.d.ts +2 -0
  53. package/node_modules/@poe-code/agent-skill-config/dist/configs.js +2 -1
  54. package/node_modules/@poe-code/frontmatter/README.md +3 -0
  55. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +1 -1
  56. package/node_modules/@poe-code/frontmatter/dist/index.js +1 -1
  57. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +9 -0
  58. package/node_modules/@poe-code/frontmatter/dist/parse.js +16 -0
  59. package/node_modules/@poe-code/user-error/README.md +56 -0
  60. package/node_modules/@poe-code/user-error/dist/index.d.ts +17 -0
  61. package/node_modules/@poe-code/user-error/dist/index.js +21 -0
  62. package/node_modules/@poe-code/user-error/package.json +24 -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 +15 -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/help-formatter.d.ts +2 -2
  78. package/node_modules/toolcraft-design/dist/help-formatter.js +1 -1
  79. package/node_modules/toolcraft-design/dist/index.d.ts +4 -4
  80. package/node_modules/toolcraft-design/dist/index.js +3 -3
  81. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +5 -0
  82. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +15 -1
  83. package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +1 -1
  84. package/package.json +5 -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
 
@@ -24,6 +25,18 @@ const codex = allAgents.find((agent) => agent.id === "codex");
24
25
  - `formatAgentSpecifier(specifier)`: formats an agent specifier.
25
26
  - `normalizeAgentId(input)`: normalizes the agent part through the registry.
26
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",
@@ -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",
@@ -3,8 +3,9 @@ export const piAgent = {
3
3
  name: "pi",
4
4
  aliases: ["pi-agent"],
5
5
  label: "Pi",
6
- summary: "Pi coding agent (spawn-only; uses local Pi auth/settings).",
6
+ summary: "Minimal AI coding agent for the terminal.",
7
7
  binaryName: "pi",
8
+ capabilities: ["spawn"],
8
9
  branding: {
9
10
  colors: {
10
11
  dark: "#F2F2F2",
@@ -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
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
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";
@@ -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
  }
@@ -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,6 +17,7 @@ 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
23
  configPath?: 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) {
@@ -0,0 +1,56 @@
1
+ # @poe-code/user-error
2
+
3
+ The shared user-error type every workspace package throws for expected user
4
+ mistakes, and the guard the CLI renders them with.
5
+
6
+ `src/cli/errors.ts` cannot be imported from `packages/*`, so package code used to
7
+ throw a plain `Error` for recoverable conditions. `src/cli/bootstrap.ts` then
8
+ treated each one as a crash: an `Error:` prefix plus a `See logs at
9
+ ~/.poe-code/logs/errors.log` pointer to a log that adds nothing for a typo. This
10
+ package gives packages a classification the CLI honours.
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { UserError, isUserError } from "@poe-code/user-error";
16
+
17
+ throw new UserError('Unknown agent "clyde". Try: claude, codex, gemini.');
18
+
19
+ throw new UserError("No API key found.", {
20
+ hint: "Create one at https://poe.com/api_key",
21
+ cause: readError
22
+ });
23
+
24
+ if (isUserError(error)) {
25
+ // render the message as guidance: no stack trace, no log pointer
26
+ }
27
+ ```
28
+
29
+ Throw a `UserError` when the user can fix the condition themselves, and say how:
30
+ name the value that was rejected and the valid ones, the path that was searched,
31
+ or the URL that issues the key. Keep plain `Error` for genuine failures — those
32
+ should reach the log.
33
+
34
+ ## Public API
35
+
36
+ - `UserError`: `Error` subclass with `name === "UserError"` and an optional
37
+ `hint` for the recovery step. Accepts the standard `cause` option.
38
+ - `isUserError(error)`: true for a `UserError` **or** any `Error` whose `name` is
39
+ `"UserError"`.
40
+
41
+ ## Cross-bundle recognition
42
+
43
+ `isUserError` matches on `error.name`, not `instanceof`. `toolcraft` publishes its
44
+ own `UserError` (`packages/toolcraft/src/user-error.ts`) as a separately released
45
+ framework, and an instance crossing a bundle boundary fails `instanceof` against
46
+ this package's class. Name matching recognises both, and mirrors the existing
47
+ `isSilentError()` precedent in `src/cli/errors.ts`. toolcraft deliberately does
48
+ not depend on this package — the coupling would run the wrong way.
49
+
50
+ ## Config Options
51
+
52
+ This package reads no configuration.
53
+
54
+ ## Environment Variables
55
+
56
+ This package reads no environment variables.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * An expected user mistake: bad input, a missing file, an unknown id, absent
3
+ * credentials. Thrown where the condition is detected so the CLI can render the
4
+ * message as guidance instead of dressing it in system-failure chrome.
5
+ */
6
+ export declare class UserError extends Error {
7
+ readonly hint?: string;
8
+ constructor(message: string, options?: ErrorOptions & {
9
+ hint?: string;
10
+ });
11
+ }
12
+ /**
13
+ * Detects user errors by name as well as identity, so an instance created in
14
+ * another bundle (toolcraft publishes its own `UserError`) is still recognised
15
+ * where `instanceof` would fail.
16
+ */
17
+ export declare function isUserError(error: unknown): boolean;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * An expected user mistake: bad input, a missing file, an unknown id, absent
3
+ * credentials. Thrown where the condition is detected so the CLI can render the
4
+ * message as guidance instead of dressing it in system-failure chrome.
5
+ */
6
+ export class UserError extends Error {
7
+ hint;
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = "UserError";
11
+ this.hint = options?.hint;
12
+ }
13
+ }
14
+ /**
15
+ * Detects user errors by name as well as identity, so an instance created in
16
+ * another bundle (toolcraft publishes its own `UserError`) is still recognised
17
+ * where `instanceof` would fail.
18
+ */
19
+ export function isUserError(error) {
20
+ return error instanceof Error && error.name === "UserError";
21
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "private": true,
3
+ "license": "MIT",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "node ../../scripts/guard-package-dist.mjs && tsc"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/poe-platform/poe-code.git",
22
+ "directory": "packages/user-error"
23
+ }
24
+ }
@@ -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;