toolcraft 0.0.136 → 0.0.138

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 (58) hide show
  1. package/composition.json +7 -2
  2. package/dist/cli.js +55 -34
  3. package/dist/composition.json +7 -2
  4. package/dist/human-in-loop/approvals-commands.js +18 -28
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/renderer.js +4 -1
  8. package/dist/user-error.d.ts +8 -0
  9. package/dist/user-error.js +10 -0
  10. package/node_modules/@poe-code/agent-defs/README.md +14 -1
  11. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +1 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +1 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +1 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +1 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +1 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +1 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +1 -0
  20. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +1 -0
  21. package/node_modules/@poe-code/agent-defs/dist/capabilities.d.ts +14 -0
  22. package/node_modules/@poe-code/agent-defs/dist/capabilities.js +63 -0
  23. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +2 -1
  24. package/node_modules/@poe-code/agent-defs/dist/index.js +1 -0
  25. package/node_modules/@poe-code/agent-defs/dist/registry.js +3 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +7 -0
  27. package/node_modules/@poe-code/frontmatter/README.md +3 -0
  28. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +1 -1
  29. package/node_modules/@poe-code/frontmatter/dist/index.js +1 -1
  30. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +9 -0
  31. package/node_modules/@poe-code/frontmatter/dist/parse.js +16 -0
  32. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +20 -8
  33. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +1 -0
  34. package/node_modules/@poe-code/process-runner/dist/types.d.ts +2 -0
  35. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +4 -0
  36. package/node_modules/@poe-code/user-error/README.md +56 -0
  37. package/node_modules/@poe-code/user-error/dist/index.d.ts +17 -0
  38. package/node_modules/@poe-code/user-error/dist/index.js +21 -0
  39. package/node_modules/@poe-code/user-error/package.json +24 -0
  40. package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
  41. package/node_modules/toolcraft-design/dist/acp/components.d.ts +8 -1
  42. package/node_modules/toolcraft-design/dist/acp/components.js +11 -6
  43. package/node_modules/toolcraft-design/dist/acp/index.d.ts +1 -0
  44. package/node_modules/toolcraft-design/dist/components/command-errors.d.ts +2 -0
  45. package/node_modules/toolcraft-design/dist/components/command-errors.js +8 -2
  46. package/node_modules/toolcraft-design/dist/components/index.d.ts +1 -1
  47. package/node_modules/toolcraft-design/dist/components/index.js +1 -1
  48. package/node_modules/toolcraft-design/dist/components/table.d.ts +1 -0
  49. package/node_modules/toolcraft-design/dist/components/table.js +52 -5
  50. package/node_modules/toolcraft-design/dist/components/template.d.ts +9 -0
  51. package/node_modules/toolcraft-design/dist/components/template.js +27 -2
  52. package/node_modules/toolcraft-design/dist/index.d.ts +2 -2
  53. package/node_modules/toolcraft-design/dist/index.js +2 -2
  54. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +5 -0
  55. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +15 -1
  56. package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +1 -1
  57. package/node_modules/toolcraft-schema/package.json +1 -1
  58. package/package.json +6 -4
@@ -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;
@@ -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) {
@@ -14,6 +14,7 @@ const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
14
14
  export const dockerExecutionEnvFactory = {
15
15
  type: "docker",
16
16
  supportsDetach: true,
17
+ supportsWorkspaceTransfer: true,
17
18
  async open(spec) {
18
19
  const runtime = parseDockerRuntime(spec.runtime);
19
20
  const runner = spec.hostRunner ?? createHostRunner();
@@ -499,8 +500,11 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
499
500
  argv: detachedJobContext?.argv ?? ["attach", containerId],
500
501
  async status() {
501
502
  if (detachedJobContext !== null) {
502
- const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
503
- return exitCode === null ? "running" : "exited";
503
+ const detached = await readDetachedState(containerId, jobId, runner, engine, context);
504
+ if (detached.kind === "unreachable") {
505
+ return "lost";
506
+ }
507
+ return detached.kind === "exited" ? "exited" : "running";
504
508
  }
505
509
  const handle = runner.exec({
506
510
  command: engine,
@@ -565,9 +569,14 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
565
569
  async wait() {
566
570
  if (detachedJobContext !== null) {
567
571
  while (true) {
568
- const exitCode = await readDetachedExitCode(containerId, jobId, runner, engine, context);
569
- if (exitCode !== null) {
570
- return { exitCode };
572
+ const detached = await readDetachedState(containerId, jobId, runner, engine, context);
573
+ if (detached.kind === "exited") {
574
+ return { exitCode: detached.exitCode };
575
+ }
576
+ if (detached.kind === "unreachable") {
577
+ // The container is gone, so the command can never report its own exit
578
+ // code: treat it as terminated with an unknown failure.
579
+ return { exitCode: 1 };
571
580
  }
572
581
  await new Promise((resolve) => setTimeout(resolve, 25));
573
582
  }
@@ -651,7 +660,7 @@ function utf8SequenceLength(byte) {
651
660
  }
652
661
  return 0;
653
662
  }
654
- async function readDetachedExitCode(containerId, jobId, runner, engine, context) {
663
+ async function readDetachedState(containerId, jobId, runner, engine, context) {
655
664
  const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
656
665
  const handle = runner.exec({
657
666
  command: engine,
@@ -669,10 +678,13 @@ async function readDetachedExitCode(containerId, jobId, runner, engine, context)
669
678
  const stdout = await readStream(handle.stdout);
670
679
  const result = await handle.result;
671
680
  if (result.exitCode !== 0) {
672
- return null;
681
+ return { kind: "unreachable" };
673
682
  }
674
683
  const text = stdout.trim();
675
- return text.length === 0 ? null : parseCompleteDecimalExitCode(text, "detached exit marker");
684
+ if (text.length === 0) {
685
+ return { kind: "running" };
686
+ }
687
+ return { kind: "exited", exitCode: parseCompleteDecimalExitCode(text, "detached exit marker") };
676
688
  }
677
689
  function createAttachedSpec(cwd = "/workspace") {
678
690
  return {
@@ -2,6 +2,7 @@ import { createHostRunner } from "./host-runner.js";
2
2
  export const hostExecutionEnvFactory = {
3
3
  type: "host",
4
4
  supportsDetach: false,
5
+ supportsWorkspaceTransfer: false,
5
6
  async open(openSpec) {
6
7
  return {
7
8
  id: "host",
@@ -35,6 +35,8 @@ export type JobStatus = "running" | "exited" | "killed" | "lost";
35
35
  export interface ExecutionEnvFactory {
36
36
  readonly type: ExecutionEnvType;
37
37
  readonly supportsDetach?: boolean;
38
+ /** Whether uploadWorkspace/downloadWorkspace move real files, i.e. whether runner.sync means anything. */
39
+ readonly supportsWorkspaceTransfer?: boolean;
38
40
  open(spec: OpenSpec): Promise<OpenedEnv>;
39
41
  attach(envId: string, context?: AttachedJobContext): Promise<OpenedEnv>;
40
42
  }
@@ -1,5 +1,6 @@
1
1
  import { text } from "node:stream/consumers";
2
2
  import { createHostRunner } from "@poe-code/process-runner";
3
+ import { UserError } from "@poe-code/user-error";
3
4
  const DEFAULT_ENDPOINT = "https://api.github.com/graphql";
4
5
  const USER_AGENT = "poe-code-task-list/0.0.1";
5
6
  const AUTH_ERROR = "gh auth token failed; install gh, run 'gh auth login', or pass auth: { token }";
@@ -18,6 +19,9 @@ export function createGhClient(options) {
18
19
  body: JSON.stringify({ query, variables })
19
20
  });
20
21
  const body = await response.text();
22
+ if (response.status === 401) {
23
+ throw new UserError("GitHub rejected your credentials (HTTP 401). Your token is missing or expired - run 'gh auth login', or pass a valid token via auth: { token }.");
24
+ }
21
25
  if (response.status !== 200) {
22
26
  throw new Error(`GitHub GraphQL request failed with status ${response.status}: ${body}`);
23
27
  }
@@ -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
+ }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "name": "toolcraft-schema",
11
- "version": "0.0.136",
11
+ "version": "0.0.138",
12
12
  "license": "MIT"
13
13
  }
14
14
  ]
@@ -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",
@@ -8,7 +8,7 @@ export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption,
8
8
  export type { CommandInfo, OptionInfo, FormatColumnsOptions, HelpToken, HelpTokenRole } from "./help-formatter.js";
9
9
  export { formatCommandNotFound } from "./command-errors.js";
10
10
  export { formatCommandNotFoundPanel } from "./command-errors.js";
11
- export { renderTable } from "./table.js";
11
+ export { loggerTableWidth, renderTable } from "./table.js";
12
12
  export type { TableColumn, RenderTableOptions } from "./table.js";
13
13
  export { renderFileChanges } from "./file-changes.js";
14
14
  export type { FileChange, FileChangeDisplayMode, FileChangeKind, FileChangeOutputFormat, RenderFileChangesOptions } from "./file-changes.js";
@@ -5,7 +5,7 @@ export { createLogger, logger } from "./logger.js";
5
5
  export { helpFormatter, formatColumns, formatCommand, formatUsage, formatOption, formatCommandList, formatOptionList, styleHelpToken, joinHelpTokens, renderHelpTokens } from "./help-formatter.js";
6
6
  export { formatCommandNotFound } from "./command-errors.js";
7
7
  export { formatCommandNotFoundPanel } from "./command-errors.js";
8
- export { renderTable } from "./table.js";
8
+ export { loggerTableWidth, renderTable } from "./table.js";
9
9
  export { renderFileChanges } from "./file-changes.js";
10
10
  export { renderCatalog } from "./catalog.js";
11
11
  export { getTemplatePartialNames, renderTemplate, resolveTemplatePartials } from "./template.js";
@@ -12,4 +12,5 @@ export interface RenderTableOptions {
12
12
  variant?: "table" | "detail";
13
13
  maxWidth?: number;
14
14
  }
15
+ export declare function loggerTableWidth(): number | undefined;
15
16
  export declare function renderTable(options: RenderTableOptions): string;
@@ -3,6 +3,7 @@ import { resolveOutputFormat } from "../internal/output-format.js";
3
3
  import { stripAnsi } from "../internal/strip-ansi.js";
4
4
  const reset = "\x1b[0m";
5
5
  const ellipsis = "…";
6
+ const minCellWidth = 4;
6
7
  const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7
8
  function getCell(row, name) {
8
9
  return Object.prototype.hasOwnProperty.call(row, name) ? row[name] ?? "" : "";
@@ -161,6 +162,51 @@ function computeColumns(columns) {
161
162
  width: getColumnWidth(column)
162
163
  }));
163
164
  }
165
+ // Each column is framed as "│ cell ", plus the closing "│".
166
+ function frameWidth(columnCount) {
167
+ return columnCount * 3 + 1;
168
+ }
169
+ // Log output indents every line with a "│ " guide, so a table emitted through the
170
+ // logger has that much less room than the terminal. Undefined without a terminal:
171
+ // piped output has no width to fit.
172
+ export function loggerTableWidth() {
173
+ const columns = process.stdout.columns;
174
+ return columns === undefined ? undefined : columns - 3;
175
+ }
176
+ // Without an explicit budget or a TTY there is no width to fit: the consumer of the
177
+ // piped output decides, so columns keep their declared widths.
178
+ function budgetColumns(columns, maxWidth) {
179
+ if (maxWidth === undefined) {
180
+ return columns;
181
+ }
182
+ const available = maxWidth - frameWidth(columns.length);
183
+ const contentWidth = (cap) => columns.reduce((total, column) => total + Math.min(column.width, cap), 0);
184
+ if (columns.reduce((total, column) => total + column.width, 0) <= available) {
185
+ return columns;
186
+ }
187
+ // Widest-first: raise a shared cap as far as the budget allows, so narrow columns
188
+ // keep their declared width and only the columns above the cap lose room.
189
+ let cap = minCellWidth;
190
+ while (contentWidth(cap + 1) <= available) {
191
+ cap += 1;
192
+ }
193
+ const budgeted = columns.map((column) => ({ ...column, width: Math.min(column.width, cap) }));
194
+ let slack = available - contentWidth(cap);
195
+ while (slack > 0) {
196
+ const growable = budgeted.filter((column, index) => column.width < columns[index].width);
197
+ if (growable.length === 0) {
198
+ break;
199
+ }
200
+ for (const column of growable) {
201
+ if (slack === 0) {
202
+ break;
203
+ }
204
+ column.width += 1;
205
+ slack -= 1;
206
+ }
207
+ }
208
+ return budgeted;
209
+ }
164
210
  function renderBorder(columns, theme, parts) {
165
211
  const horizontal = theme.muted("─");
166
212
  const segments = columns.map((column) => horizontal.repeat(column.width + 2));
@@ -247,16 +293,17 @@ function renderTableTerminal(options) {
247
293
  }
248
294
  const separatorOptions = options;
249
295
  const includeRowSeparators = separatorOptions.rowSeparator === true || separatorOptions.rowSeparators === true;
250
- const top = renderBorder(computedColumns, theme, { left: "┌", mid: "┬", right: "┐" });
251
- const header = renderTerminalRow(computedColumns.map((column) => theme.header(column.title)), computedColumns, theme);
252
- const headerBottom = renderBorder(computedColumns, theme, { left: "├", mid: "┼", right: "┤" });
253
- const bottom = renderBorder(computedColumns, theme, { left: "", mid: "", right: "" });
296
+ const budgetedColumns = budgetColumns(computedColumns, options.maxWidth ?? process.stdout.columns);
297
+ const top = renderBorder(budgetedColumns, theme, { left: "┌", mid: "┬", right: "┐" });
298
+ const header = renderTerminalRow(budgetedColumns.map((column) => theme.header(column.title)), budgetedColumns, theme);
299
+ const headerBottom = renderBorder(budgetedColumns, theme, { left: "", mid: "", right: "" });
300
+ const bottom = renderBorder(budgetedColumns, theme, { left: "└", mid: "┴", right: "┘" });
254
301
  const renderedRows = [];
255
302
  for (const [index, row] of rows.entries()) {
256
303
  if (includeRowSeparators && index > 0) {
257
304
  renderedRows.push(headerBottom);
258
305
  }
259
- renderedRows.push(renderTerminalRow(computedColumns.map((column) => getCell(row, column.name)), computedColumns, theme));
306
+ renderedRows.push(renderTerminalRow(budgetedColumns.map((column) => getCell(row, column.name)), budgetedColumns, theme));
260
307
  }
261
308
  return [top, header, headerBottom, ...renderedRows, bottom].join("\n");
262
309
  }