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.
- package/composition.json +7 -2
- package/dist/cli.js +55 -34
- package/dist/composition.json +7 -2
- package/dist/human-in-loop/approvals-commands.js +18 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/renderer.js +4 -1
- package/dist/user-error.d.ts +8 -0
- package/dist/user-error.js +10 -0
- package/node_modules/@poe-code/agent-defs/README.md +14 -1
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/capabilities.d.ts +14 -0
- package/node_modules/@poe-code/agent-defs/dist/capabilities.js +63 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +2 -1
- package/node_modules/@poe-code/agent-defs/dist/index.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.js +3 -0
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +7 -0
- package/node_modules/@poe-code/frontmatter/README.md +3 -0
- package/node_modules/@poe-code/frontmatter/dist/index.d.ts +1 -1
- package/node_modules/@poe-code/frontmatter/dist/index.js +1 -1
- package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +9 -0
- package/node_modules/@poe-code/frontmatter/dist/parse.js +16 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +20 -8
- package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +4 -0
- package/node_modules/@poe-code/user-error/README.md +56 -0
- package/node_modules/@poe-code/user-error/dist/index.d.ts +17 -0
- package/node_modules/@poe-code/user-error/dist/index.js +21 -0
- package/node_modules/@poe-code/user-error/package.json +24 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-design/dist/acp/components.d.ts +8 -1
- package/node_modules/toolcraft-design/dist/acp/components.js +11 -6
- package/node_modules/toolcraft-design/dist/acp/index.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/components/command-errors.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/components/command-errors.js +8 -2
- package/node_modules/toolcraft-design/dist/components/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/components/index.js +1 -1
- package/node_modules/toolcraft-design/dist/components/table.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/components/table.js +52 -5
- package/node_modules/toolcraft-design/dist/components/template.d.ts +9 -0
- package/node_modules/toolcraft-design/dist/components/template.js +27 -2
- package/node_modules/toolcraft-design/dist/index.d.ts +2 -2
- package/node_modules/toolcraft-design/dist/index.js +2 -2
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +5 -0
- package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +15 -1
- package/node_modules/toolcraft-design/dist/prompts/primitives/spinner.js +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- 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
|
|
503
|
-
|
|
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
|
|
569
|
-
if (
|
|
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
|
|
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
|
|
681
|
+
return { kind: "unreachable" };
|
|
673
682
|
}
|
|
674
683
|
const text = stdout.trim();
|
|
675
|
-
|
|
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 {
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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(`
|
|
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.
|
|
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";
|
|
@@ -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
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
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(
|
|
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
|
}
|