toolcraft 0.0.20 → 0.0.21
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/README.md +43 -30
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +3 -1
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.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/gemini-cli.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/gemini-cli.js +16 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.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/poe-agent.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +2 -2
- package/node_modules/@poe-code/agent-defs/dist/index.js +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +2 -1
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +15 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.js +26 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +6 -3
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +14 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +11 -4
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +6 -1
- package/node_modules/@poe-code/design-system/dist/index.d.ts +2 -1
- package/node_modules/@poe-code/design-system/dist/index.js +1 -0
- package/node_modules/@poe-code/task-list/README.md +43 -26
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +19 -2
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +13 -11
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +17 -15
- package/node_modules/@poe-code/task-list/dist/types.d.ts +2 -0
- package/node_modules/auth-store/dist/provider-store.js +4 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,12 +59,12 @@ export const greet = defineCommand({
|
|
|
59
59
|
description: "Say hello",
|
|
60
60
|
params: S.Object({
|
|
61
61
|
name: S.String({ description: "Who to greet" }),
|
|
62
|
-
loud: S.Optional(S.Boolean({ default: false }))
|
|
62
|
+
loud: S.Optional(S.Boolean({ default: false }))
|
|
63
63
|
}),
|
|
64
64
|
handler: async ({ params }) => {
|
|
65
65
|
const message = `Hello, ${params.name}`;
|
|
66
66
|
return { message: params.loud ? message.toUpperCase() : message };
|
|
67
|
-
}
|
|
67
|
+
}
|
|
68
68
|
});
|
|
69
69
|
```
|
|
70
70
|
|
|
@@ -75,7 +75,7 @@ import { greet } from "./commands/greet.js";
|
|
|
75
75
|
|
|
76
76
|
export const root = defineGroup({
|
|
77
77
|
name: "mytool",
|
|
78
|
-
children: [greet]
|
|
78
|
+
children: [greet]
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -178,6 +178,8 @@ The same `root` flows into all three. No duplication.
|
|
|
178
178
|
|
|
179
179
|
**Tree**: the `root` group is a `defineGroup` whose children are commands and sub-groups. Any depth. CLI flags, MCP tool names, and SDK methods are derived from the path.
|
|
180
180
|
|
|
181
|
+
**CLI help**: group help lists visible child commands with their parameter tokens inline. Required options appear as `--name <type>`, optional options and defaults appear in brackets like `[--limit <number>]`, and positional parameters render as positional tokens like `<name>` or `[name]` depending on whether they are required. Command-specific `--help` still shows the detailed parameter table.
|
|
182
|
+
|
|
181
183
|
## Secrets
|
|
182
184
|
|
|
183
185
|
Declare env-backed secrets on a command or group. Toolcraft reads `process.env` at command-run time and passes the values to the handler:
|
|
@@ -188,12 +190,12 @@ const deploy = defineCommand({
|
|
|
188
190
|
params: S.Object({ service: S.String() }),
|
|
189
191
|
secrets: {
|
|
190
192
|
apiKey: { env: "DEPLOY_API_KEY", description: "Required for /deploy endpoint" },
|
|
191
|
-
debugToken: { env: "DEPLOY_DEBUG", optional: true }
|
|
193
|
+
debugToken: { env: "DEPLOY_DEBUG", optional: true }
|
|
192
194
|
},
|
|
193
195
|
handler: async ({ params, secrets }) => {
|
|
194
196
|
// secrets.apiKey: string
|
|
195
197
|
// secrets.debugToken: string | undefined
|
|
196
|
-
}
|
|
198
|
+
}
|
|
197
199
|
});
|
|
198
200
|
```
|
|
199
201
|
|
|
@@ -205,13 +207,14 @@ Required secrets that aren't set produce a `UserError` with the env var name and
|
|
|
205
207
|
defineCommand({
|
|
206
208
|
// ...
|
|
207
209
|
requires: {
|
|
208
|
-
auth: true,
|
|
209
|
-
apiVersion: ">=1.2.0",
|
|
210
|
-
check: async (ctx) => ({
|
|
210
|
+
auth: true, // fails if POE_API_KEY (or runner-specified env) is missing
|
|
211
|
+
apiVersion: ">=1.2.0", // fails if runner reports older apiVersion
|
|
212
|
+
check: async (ctx) => ({
|
|
213
|
+
// arbitrary async gate
|
|
211
214
|
ok: ctx.fs.exists(".lock") === false,
|
|
212
|
-
message: ".lock present, refusing to run"
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
+
message: ".lock present, refusing to run"
|
|
216
|
+
})
|
|
217
|
+
}
|
|
215
218
|
});
|
|
216
219
|
```
|
|
217
220
|
|
|
@@ -239,7 +242,7 @@ defineCommand<Services>({
|
|
|
239
242
|
handler: async ({ params, db, logger }) => {
|
|
240
243
|
logger.info("running");
|
|
241
244
|
return db.query(params.id);
|
|
242
|
-
}
|
|
245
|
+
}
|
|
243
246
|
});
|
|
244
247
|
```
|
|
245
248
|
|
|
@@ -257,8 +260,8 @@ defineCommand({
|
|
|
257
260
|
rich: (result, { renderTable }) =>
|
|
258
261
|
console.log(renderTable({ rows: result.rows, columns: ["id"] })),
|
|
259
262
|
markdown: (result) => `Found ${result.rows.length} rows`,
|
|
260
|
-
json: (result) => result
|
|
261
|
-
}
|
|
263
|
+
json: (result) => result
|
|
264
|
+
}
|
|
262
265
|
});
|
|
263
266
|
```
|
|
264
267
|
|
|
@@ -273,13 +276,13 @@ defineGroup({
|
|
|
273
276
|
name: "github",
|
|
274
277
|
mcp: {
|
|
275
278
|
transport: "stdio",
|
|
276
|
-
command: "github-mcp-server"
|
|
279
|
+
command: "github-mcp-server"
|
|
277
280
|
},
|
|
278
281
|
tools: ["create_issue", "list_issues"],
|
|
279
282
|
rename: {
|
|
280
|
-
create_issue: "issues.create"
|
|
283
|
+
create_issue: "issues.create"
|
|
281
284
|
},
|
|
282
|
-
children: []
|
|
285
|
+
children: []
|
|
283
286
|
});
|
|
284
287
|
```
|
|
285
288
|
|
|
@@ -301,21 +304,21 @@ defineGroup({
|
|
|
301
304
|
name: "deploy",
|
|
302
305
|
humanInLoop: {
|
|
303
306
|
mode: "async",
|
|
304
|
-
message: ({ commandPath, params }) => `Run ${commandPath} for ${params.target}
|
|
307
|
+
message: ({ commandPath, params }) => `Run ${commandPath} for ${params.target}?`
|
|
305
308
|
},
|
|
306
309
|
children: [
|
|
307
310
|
defineCommand({
|
|
308
311
|
name: "prod",
|
|
309
312
|
params: S.Object({ target: S.String() }),
|
|
310
|
-
handler: async ({ params }) => ({ target: params.target })
|
|
313
|
+
handler: async ({ params }) => ({ target: params.target })
|
|
311
314
|
}),
|
|
312
315
|
defineCommand({
|
|
313
316
|
name: "preview",
|
|
314
317
|
params: S.Object({ target: S.String() }),
|
|
315
|
-
humanInLoop: null,
|
|
316
|
-
handler: async ({ params }) => ({ target: params.target })
|
|
317
|
-
})
|
|
318
|
-
]
|
|
318
|
+
humanInLoop: null, // opt out
|
|
319
|
+
handler: async ({ params }) => ({ target: params.target })
|
|
320
|
+
})
|
|
321
|
+
]
|
|
319
322
|
});
|
|
320
323
|
```
|
|
321
324
|
|
|
@@ -329,7 +332,7 @@ Wire the same `humanInLoop` options into every entrypoint:
|
|
|
329
332
|
```ts
|
|
330
333
|
const humanInLoop = {
|
|
331
334
|
provider: slackApprovalProvider({ channel: "#deploys", client }),
|
|
332
|
-
taskList: { dir: ".toolcraft/approvals.yaml", format: "yaml-file" as const }
|
|
335
|
+
taskList: { dir: ".toolcraft/approvals.yaml", format: "yaml-file" as const }
|
|
333
336
|
};
|
|
334
337
|
|
|
335
338
|
await runCLI(root, { humanInLoop });
|
|
@@ -354,7 +357,11 @@ Async results must be JSON-serializable; non-serializable returns mark the appro
|
|
|
354
357
|
A minimal Slack-style provider:
|
|
355
358
|
|
|
356
359
|
```ts
|
|
357
|
-
import type {
|
|
360
|
+
import type {
|
|
361
|
+
ApprovalRequest,
|
|
362
|
+
ApprovalResult,
|
|
363
|
+
HumanInLoopProvider
|
|
364
|
+
} from "@poe-code/agent-human-in-loop";
|
|
358
365
|
|
|
359
366
|
export function slackApprovalProvider(opts: {
|
|
360
367
|
channel: string;
|
|
@@ -380,14 +387,18 @@ export function slackApprovalProvider(opts: {
|
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
return { outcome: "declined" };
|
|
383
|
-
}
|
|
390
|
+
}
|
|
384
391
|
};
|
|
385
392
|
}
|
|
386
393
|
```
|
|
387
394
|
|
|
388
395
|
## Errors
|
|
389
396
|
|
|
390
|
-
Throw `UserError` for expected, user-facing failures. The CLI prints the message without a stack trace and sets exit code 1; MCP and SDK surface the message as the error body. Any other thrown error is treated as unexpected and shows a stack with `--debug
|
|
397
|
+
Throw `UserError` for expected, user-facing failures. The CLI prints the message without a stack trace and sets exit code 1; MCP and SDK surface the message as the error body. Usage mistakes include a pointer to the relevant command help. Any other thrown error is treated as unexpected and shows a trimmed stack with `--debug`; use `--debug=raw` to include framework and runtime frames.
|
|
398
|
+
|
|
399
|
+
HTTP-style errors with request/response context print the request, status, and a response-body snippet by default. `--verbose` or `--debug` prints headers and the full request/response bodies, with authorization headers redacted.
|
|
400
|
+
|
|
401
|
+
Enable structured error reports with `errorReports: true`, `errorReports: { dir }`, or `TOOLCRAFT_ERROR_REPORTS=1`. Reports are written under `.toolcraft/errors` by default, include argv, parsed params, resolved secret presence, structured error fields, stack/cause chains, and HTTP transcripts, and redact declared secrets plus parameter names that look sensitive.
|
|
391
402
|
|
|
392
403
|
## Migrating from a folder of scripts
|
|
393
404
|
|
|
@@ -405,6 +416,7 @@ If you have an existing MCP server you want to keep running, use the MCP proxy:
|
|
|
405
416
|
## Environment variables
|
|
406
417
|
|
|
407
418
|
- `TOOLCRAFT_MCP_REFRESH` — MCP proxy cache refresh (`unset` = use cache, `1`/`true` = refresh all, comma-separated names = refresh those).
|
|
419
|
+
- `TOOLCRAFT_ERROR_REPORTS=1` — enables structured error report files for CLI, MCP, and SDK surfaces that wire `errorReports`.
|
|
408
420
|
- Per-command `secrets` declarations name additional env vars. They are read at command run time and passed to the handler.
|
|
409
421
|
|
|
410
422
|
## API reference
|
|
@@ -444,19 +456,20 @@ If you have an existing MCP server you want to keep running, use the MCP proxy:
|
|
|
444
456
|
- `presets?: boolean` — enables `--preset <path>` for loading parameter defaults from JSON files.
|
|
445
457
|
- `apiVersion?: string` — for `requires.apiVersion`.
|
|
446
458
|
- `humanInLoop?: HumanInLoopRuntimeOptions`
|
|
459
|
+
- `errorReports?: boolean | { dir?: string }`
|
|
447
460
|
- `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`).
|
|
448
461
|
|
|
449
462
|
### `createSDK(root, options)`
|
|
450
463
|
|
|
451
464
|
- `casing?: "camel"` — generated SDK member style.
|
|
452
|
-
- `services?` / `humanInLoop?` / `apiVersion?`
|
|
465
|
+
- `services?` / `humanInLoop?` / `apiVersion?` / `errorReports?`
|
|
453
466
|
- `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`).
|
|
454
467
|
|
|
455
468
|
### `createMCPServer(root, options)` / `runMCP(root, options)`
|
|
456
469
|
|
|
457
470
|
- `name: string`
|
|
458
471
|
- `version: string`
|
|
459
|
-
- `services?` / `humanInLoop?` / `apiVersion?`
|
|
472
|
+
- `services?` / `humanInLoop?` / `apiVersion?` / `errorReports?`
|
|
460
473
|
- `projectRoot?: string` — root used for MCP proxy cache files (`.toolcraft/mcp/*.json`).
|
|
461
474
|
- `tools?: string[]` — allowlist of MCP tool names or group prefixes. Tool names are `__`-joined snake_case path segments (`root__bot__create`); a prefix like `root__bot` includes every descendant tool.
|
|
462
475
|
- `omitRootToolNamePrefix?: boolean` — defaults to `false`. Set to `true` to omit the root group name from single-root MCP tool names (`bot__create`).
|
|
@@ -468,7 +481,7 @@ If you have an existing MCP server you want to keep running, use the MCP proxy:
|
|
|
468
481
|
type HumanInLoopRuntimeOptions = {
|
|
469
482
|
provider?: HumanInLoopProvider;
|
|
470
483
|
taskList?: TaskList | { dir: string; format: "markdown-dir" | "yaml-file" };
|
|
471
|
-
listName?: string;
|
|
484
|
+
listName?: string; // defaults to "approvals"
|
|
472
485
|
binPath?: { execPath: string; entryArgs: readonly string[] };
|
|
473
486
|
};
|
|
474
487
|
```
|
package/dist/sdk.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface CreateSDKOptions<TServices extends object = Record<string, unkn
|
|
|
64
64
|
services?: TServices;
|
|
65
65
|
casing?: "camel";
|
|
66
66
|
humanInLoop?: HumanInLoopRuntimeOptions;
|
|
67
|
+
apiVersion?: string;
|
|
67
68
|
projectRoot?: string;
|
|
68
69
|
errorReports?: ErrorReportsOption;
|
|
69
70
|
}
|
|
@@ -72,4 +73,5 @@ export declare function createSDK<TRootInfo, TServices extends object = Record<s
|
|
|
72
73
|
}, options?: CreateSDKOptions<TServices>): TRootInfo extends {
|
|
73
74
|
children: infer TChildren extends readonly unknown[];
|
|
74
75
|
} ? SDKChildrenShape<TChildren, undefined, undefined> : EmptyRecord;
|
|
76
|
+
export declare function createSDK<TServices extends object = Record<string, unknown>>(root: Group<TServices>, options?: CreateSDKOptions<TServices>): Record<string, unknown>;
|
|
75
77
|
export {};
|
package/dist/sdk.js
CHANGED
|
@@ -269,7 +269,9 @@ function createResolvedSDK(root, options = {}) {
|
|
|
269
269
|
return undefined;
|
|
270
270
|
}
|
|
271
271
|
};
|
|
272
|
-
await assertCommandRequirements(node, { ...baseContext, params: undefined }
|
|
272
|
+
await assertCommandRequirements(node, { ...baseContext, params: undefined }, {
|
|
273
|
+
apiVersion: options.apiVersion
|
|
274
|
+
});
|
|
273
275
|
const paramsSchema = filterSchemaForScope(node.params, "sdk");
|
|
274
276
|
if (paramsSchema === undefined || paramsSchema.kind !== "object") {
|
|
275
277
|
throw new ToolcraftBugError(`command "${node.name}" must define an object params schema for SDK.`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const geminiCliAgent = {
|
|
2
|
+
id: "gemini-cli",
|
|
3
|
+
name: "gemini-cli",
|
|
4
|
+
aliases: ["gemini"],
|
|
5
|
+
label: "Gemini CLI",
|
|
6
|
+
summary: "Configure Google's Gemini CLI to use a compatible Google generations API.",
|
|
7
|
+
binaryName: "gemini",
|
|
8
|
+
configPath: "~/.gemini/settings.json",
|
|
9
|
+
apiShapes: ["google-generations"],
|
|
10
|
+
branding: {
|
|
11
|
+
colors: {
|
|
12
|
+
dark: "#8AB4F8",
|
|
13
|
+
light: "#1A73E8"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { claudeCodeAgent } from "./claude-code.js";
|
|
2
2
|
export { claudeDesktopAgent } from "./claude-desktop.js";
|
|
3
3
|
export { codexAgent } from "./codex.js";
|
|
4
|
+
export { geminiCliAgent } from "./gemini-cli.js";
|
|
4
5
|
export { openCodeAgent } from "./opencode.js";
|
|
5
6
|
export { kimiAgent } from "./kimi.js";
|
|
6
7
|
export { gooseAgent } from "./goose.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { claudeCodeAgent } from "./claude-code.js";
|
|
2
2
|
export { claudeDesktopAgent } from "./claude-desktop.js";
|
|
3
3
|
export { codexAgent } from "./codex.js";
|
|
4
|
+
export { geminiCliAgent } from "./gemini-cli.js";
|
|
4
5
|
export { openCodeAgent } from "./opencode.js";
|
|
5
6
|
export { kimiAgent } from "./kimi.js";
|
|
6
7
|
export { gooseAgent } from "./goose.js";
|
|
@@ -3,6 +3,7 @@ export const poeAgentAgent = {
|
|
|
3
3
|
name: "poe-agent",
|
|
4
4
|
label: "Poe Agent",
|
|
5
5
|
summary: "Run one-shot prompts with the built-in Poe agent runtime.",
|
|
6
|
+
apiShapes: ["openai-responses", "openai-chat-completions"],
|
|
6
7
|
configPath: "~/.poe-code/config.json",
|
|
7
8
|
branding: {
|
|
8
9
|
colors: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AgentDefinition } from "./types.js";
|
|
1
|
+
export type { AgentDefinition, ApiShapeId } from "./types.js";
|
|
2
2
|
export type { AgentSpecifier } from "./specifier.js";
|
|
3
|
-
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
3
|
+
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
4
4
|
export { allAgents, resolveAgentId } from "./registry.js";
|
|
5
5
|
export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
1
|
+
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
2
2
|
export { allAgents, resolveAgentId } from "./registry.js";
|
|
3
3
|
export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { claudeCodeAgent, claudeDesktopAgent, codexAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
1
|
+
import { claudeCodeAgent, claudeDesktopAgent, codexAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
2
2
|
export const allAgents = [
|
|
3
3
|
claudeCodeAgent,
|
|
4
4
|
claudeDesktopAgent,
|
|
5
5
|
codexAgent,
|
|
6
|
+
geminiCliAgent,
|
|
6
7
|
openCodeAgent,
|
|
7
8
|
kimiAgent,
|
|
8
9
|
gooseAgent,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ApiShapeId = "openai-chat-completions" | "openai-responses" | "anthropic-messages" | "google-generations";
|
|
1
2
|
export interface AgentDefinition {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
@@ -6,6 +7,7 @@ export interface AgentDefinition {
|
|
|
6
7
|
aliases?: string[];
|
|
7
8
|
/** Binary name for CLI agents. Optional for GUI-only apps like Claude Desktop. */
|
|
8
9
|
binaryName?: string;
|
|
10
|
+
readonly apiShapes?: readonly ApiShapeId[];
|
|
9
11
|
configPath: string;
|
|
10
12
|
branding: {
|
|
11
13
|
colors: {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface BrowserProcess {
|
|
2
|
+
once(event: "error", listener: (error: Error) => void): this;
|
|
3
|
+
once(event: "spawn", listener: () => void): this;
|
|
4
|
+
unref(): void;
|
|
5
|
+
}
|
|
6
|
+
type SpawnBrowserProcess = (command: string, args: string[], options: {
|
|
7
|
+
detached: true;
|
|
8
|
+
stdio: "ignore";
|
|
9
|
+
}) => BrowserProcess;
|
|
10
|
+
export interface OpenExternalOptions {
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
spawnProcess?: SpawnBrowserProcess;
|
|
13
|
+
}
|
|
14
|
+
export declare function openExternal(url: string, options?: OpenExternalOptions): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
export async function openExternal(url, options = {}) {
|
|
4
|
+
const parsed = new URL(url);
|
|
5
|
+
const { command, args } = browserCommand(parsed.href, options.platform ?? process.platform);
|
|
6
|
+
await launchBrowser(command, args, options.spawnProcess ?? spawn);
|
|
7
|
+
}
|
|
8
|
+
function browserCommand(url, platform) {
|
|
9
|
+
if (platform === "darwin") {
|
|
10
|
+
return { command: "open", args: [url] };
|
|
11
|
+
}
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
14
|
+
}
|
|
15
|
+
return { command: "xdg-open", args: [url] };
|
|
16
|
+
}
|
|
17
|
+
function launchBrowser(command, args, spawnProcess) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawnProcess(command, args, { detached: true, stdio: "ignore" });
|
|
20
|
+
child.once("error", reject);
|
|
21
|
+
child.once("spawn", () => {
|
|
22
|
+
child.unref();
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -4,5 +4,5 @@ export { createInitialState } from "./state.js";
|
|
|
4
4
|
export { resolveBindings } from "./keymap.js";
|
|
5
5
|
export type { Effect, ExplorerEvent } from "./events.js";
|
|
6
6
|
export type { BindingTarget, ExplorerBindingDefaults, ExplorerBuiltinCommand, ResolvedBindings } from "./keymap.js";
|
|
7
|
-
export type { Action, ActionContext, Detail, DetailCtx, DetailItem, Dirty, ExplorerConfig, ExplorerLayoutMode, ExplorerSize, ExplorerState, Row, Tone } from "./state.js";
|
|
7
|
+
export type { Action, ActionContext, Detail, DetailCtx, DetailItem, Dirty, ExplorerConfig, ExplorerLayoutMode, ExplorerSize, ExplorerState, ReorderContext, Row, Tone } from "./state.js";
|
|
8
8
|
export declare function singleDetail<R>(fn: (row: Row, ctx: DetailCtx) => string | Promise<string>): Detail<R>;
|
|
@@ -19,8 +19,8 @@ const builtinBindings = {
|
|
|
19
19
|
detailScrollUp: ["Ctrl+b"],
|
|
20
20
|
extendSelectionUp: ["Shift+up"],
|
|
21
21
|
extendSelectionDown: ["Shift+down"],
|
|
22
|
-
reorderUp: ["
|
|
23
|
-
reorderDown: ["
|
|
22
|
+
reorderUp: ["Shift+up", "K"],
|
|
23
|
+
reorderDown: ["Shift+down", "J"]
|
|
24
24
|
};
|
|
25
25
|
const baseBuiltinCommands = [
|
|
26
26
|
"quit",
|
|
@@ -49,7 +49,10 @@ const reservedActionIds = new Set(["quit"]);
|
|
|
49
49
|
export function resolveBindings(config, defaults = {}) {
|
|
50
50
|
const commands = config.reorder === undefined
|
|
51
51
|
? baseBuiltinCommands
|
|
52
|
-
: [
|
|
52
|
+
: [
|
|
53
|
+
...baseBuiltinCommands.filter((command) => command !== "extendSelectionUp" && command !== "extendSelectionDown"),
|
|
54
|
+
...reorderCommands
|
|
55
|
+
];
|
|
53
56
|
const commandBindings = new Map();
|
|
54
57
|
const flatBindings = new Map();
|
|
55
58
|
const targetKeys = new Map();
|
|
@@ -13,6 +13,12 @@ export function renderFooter(state, screen, layout) {
|
|
|
13
13
|
if (x >= rect.x + rect.width) {
|
|
14
14
|
break;
|
|
15
15
|
}
|
|
16
|
+
if (hint.bracketed === false) {
|
|
17
|
+
const text = `${hint.key} ${hint.label}`;
|
|
18
|
+
screen.put(x, y, text, hint.running ? styles.muted : {});
|
|
19
|
+
x += text.length + 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
16
22
|
screen.put(x, y, `[${hint.key}]`, hint.running ? styles.muted : styles.accent);
|
|
17
23
|
x += hint.key.length + 2;
|
|
18
24
|
screen.put(x, y, ` ${hint.label}`, hint.running ? styles.muted : {});
|
|
@@ -37,9 +43,17 @@ function footerHints(state) {
|
|
|
37
43
|
}
|
|
38
44
|
hints.push({ key: "?", label: "help", running: false });
|
|
39
45
|
hints.push({ key: "Ctrl+P", label: "palette", running: false });
|
|
46
|
+
if (hasShiftReorderBindings(state)) {
|
|
47
|
+
hints.push({ key: "⇧↑↓", label: "reorder (within state)", running: false, bracketed: false });
|
|
48
|
+
}
|
|
40
49
|
hints.push({ key: "q", label: "quit", running: false });
|
|
41
50
|
return hints;
|
|
42
51
|
}
|
|
52
|
+
function hasShiftReorderBindings(state) {
|
|
53
|
+
const up = state.bindings.keysByTarget.get("builtin:reorderUp") ?? [];
|
|
54
|
+
const down = state.bindings.keysByTarget.get("builtin:reorderDown") ?? [];
|
|
55
|
+
return up.includes("Shift+up") && down.includes("Shift+down");
|
|
56
|
+
}
|
|
43
57
|
function actionKey(entry, fallback) {
|
|
44
58
|
const key = entry.action?.key;
|
|
45
59
|
if (Array.isArray(key)) {
|
|
@@ -36,7 +36,7 @@ class ExplorerRuntime {
|
|
|
36
36
|
});
|
|
37
37
|
this.runtimeHandles = {
|
|
38
38
|
refresh: async () => {
|
|
39
|
-
await this.
|
|
39
|
+
await this.refreshRowsFromSource();
|
|
40
40
|
},
|
|
41
41
|
suspendAnd: async (fn) => this.suspendAnd(fn),
|
|
42
42
|
toast: (msg, tone) => {
|
|
@@ -54,7 +54,7 @@ class ExplorerRuntime {
|
|
|
54
54
|
try {
|
|
55
55
|
this.startTerminal();
|
|
56
56
|
this.render();
|
|
57
|
-
this.
|
|
57
|
+
this.loadRows().catch((error) => {
|
|
58
58
|
this.fail(error);
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -76,10 +76,14 @@ class ExplorerRuntime {
|
|
|
76
76
|
this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
async
|
|
79
|
+
async loadRows() {
|
|
80
80
|
const rows = await this.config.rows();
|
|
81
81
|
this.dispatch({ type: "rowsLoaded", rows });
|
|
82
82
|
}
|
|
83
|
+
async refreshRowsFromSource() {
|
|
84
|
+
await this.config.refresh?.();
|
|
85
|
+
await this.loadRows();
|
|
86
|
+
}
|
|
83
87
|
dispatch(event) {
|
|
84
88
|
if (this.stopped) {
|
|
85
89
|
return;
|
|
@@ -128,7 +132,10 @@ class ExplorerRuntime {
|
|
|
128
132
|
}
|
|
129
133
|
async persistOrder(orderedIds, previousRows) {
|
|
130
134
|
try {
|
|
131
|
-
await this.config.reorder?.onReorder(orderedIds
|
|
135
|
+
await this.config.reorder?.onReorder(orderedIds, {
|
|
136
|
+
refresh: this.runtimeHandles.refresh,
|
|
137
|
+
toast: this.runtimeHandles.toast
|
|
138
|
+
});
|
|
132
139
|
}
|
|
133
140
|
catch (error) {
|
|
134
141
|
this.showToast(error instanceof Error ? error.message : "Could not persist order", "error");
|
|
@@ -51,13 +51,18 @@ export interface ActionContext<R> {
|
|
|
51
51
|
confirm: (prompt: string) => Promise<boolean>;
|
|
52
52
|
exit: (after?: () => void | Promise<void>) => void;
|
|
53
53
|
}
|
|
54
|
+
export interface ReorderContext {
|
|
55
|
+
refresh: () => Promise<void>;
|
|
56
|
+
toast: (msg: string, tone?: Tone) => void;
|
|
57
|
+
}
|
|
54
58
|
export interface ExplorerConfig<R> {
|
|
55
59
|
title: string;
|
|
56
60
|
rows: () => Promise<Row[]>;
|
|
61
|
+
refresh?: () => Promise<void>;
|
|
57
62
|
detail: Detail<R>;
|
|
58
63
|
actions: Action<R>[];
|
|
59
64
|
reorder?: {
|
|
60
|
-
onReorder: (orderedIds: string[]) => void | Promise<void>;
|
|
65
|
+
onReorder: (orderedIds: string[], ctx?: ReorderContext) => void | Promise<void>;
|
|
61
66
|
};
|
|
62
67
|
multiSelect?: boolean;
|
|
63
68
|
keybindOverrides?: Record<string, string | string[]>;
|
|
@@ -19,13 +19,14 @@ export { renderTable } from "./components/table.js";
|
|
|
19
19
|
export type { TableColumn, RenderTableOptions } from "./components/table.js";
|
|
20
20
|
export { renderTemplate } from "./components/template.js";
|
|
21
21
|
export type { RenderTemplateOptions, TemplateEscape } from "./components/template.js";
|
|
22
|
+
export { openExternal } from "./components/browser.js";
|
|
22
23
|
export * as acp from "./acp/index.js";
|
|
23
24
|
export * as dashboard from "./dashboard/index.js";
|
|
24
25
|
export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
|
|
25
26
|
export type { Dashboard, DashboardOptions } from "./dashboard/index.js";
|
|
26
27
|
export * as explorer from "./explorer/index.js";
|
|
27
28
|
export { runExplorer, singleDetail } from "./explorer/index.js";
|
|
28
|
-
export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, Tone, } from "./explorer/index.js";
|
|
29
|
+
export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, ReorderContext, Tone, } from "./explorer/index.js";
|
|
29
30
|
export * as prompts from "./prompts/index.js";
|
|
30
31
|
export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
|
|
31
32
|
export type { SelectOptions, MultiselectOptions, TextOptions, ConfirmOptions, PasswordOptions, SpinnerOptions, WithSpinnerOptions } from "./prompts/index.js";
|
|
@@ -15,6 +15,7 @@ export { formatCommandNotFound } from "./components/command-errors.js";
|
|
|
15
15
|
export { formatCommandNotFoundPanel } from "./components/command-errors.js";
|
|
16
16
|
export { renderTable } from "./components/table.js";
|
|
17
17
|
export { renderTemplate } from "./components/template.js";
|
|
18
|
+
export { openExternal } from "./components/browser.js";
|
|
18
19
|
// ACP rendering
|
|
19
20
|
export * as acp from "./acp/index.js";
|
|
20
21
|
// Dashboard
|
|
@@ -6,11 +6,11 @@ Multi-list task manager with pluggable storage backends.
|
|
|
6
6
|
|
|
7
7
|
`@poe-code/task-list` exposes one API over these backends:
|
|
8
8
|
|
|
9
|
-
| Backend
|
|
10
|
-
|
|
|
11
|
-
| `markdown-dir` | One Markdown file per task, organized into subdirectories per list.
|
|
12
|
-
| `yaml-file`
|
|
13
|
-
| `gh-issues`
|
|
9
|
+
| Backend | Storage |
|
|
10
|
+
| -------------- | ---------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `markdown-dir` | One Markdown file per task, organized into subdirectories per list. |
|
|
12
|
+
| `yaml-file` | One YAML document with a top-level `lists:` mapping. |
|
|
13
|
+
| `gh-issues` | GitHub Issues in one repository, ordered and state-tracked through a GitHub Project v2 Status field. |
|
|
14
14
|
|
|
15
15
|
The task lifecycle is `draft -> planned -> in-progress -> done -> archived`. `archived` is terminal.
|
|
16
16
|
|
|
@@ -18,13 +18,13 @@ The task lifecycle is `draft -> planned -> in-progress -> done -> archived`. `ar
|
|
|
18
18
|
|
|
19
19
|
- `openTaskList(options)`: opens a task store and returns a `TaskList`
|
|
20
20
|
- `TaskList`: top-level interface for listing lists, querying all tasks, and resolving qualified IDs
|
|
21
|
-
- `Tasks`: per-list interface for create, update, `fire`, `canFire`, `events`, delete, and list operations
|
|
21
|
+
- `Tasks`: per-list interface for create, update, `fire`, `canFire`, `events`, delete, `move`, `reorder`, and list operations
|
|
22
22
|
- `Task`: normalized task record with `list`, `id`, `qualifiedId`, `name`, `state`, `description`, and `metadata`
|
|
23
23
|
- `TaskState`: `"draft" | "planned" | "in-progress" | "done" | "archived"`
|
|
24
24
|
- `TaskDefaults`: default `metadata` applied when creating new tasks
|
|
25
25
|
- `StateMachineDef` / `EventDef`: exported types for custom task lifecycle definitions passed via `openTaskList({ stateMachine })`
|
|
26
26
|
- `defaultStateMachine`: exported default lifecycle with `plan`, `start`, `complete`, and `archive` events
|
|
27
|
-
- Error classes: `TaskNotFoundError`, `TaskAlreadyExistsError`, `InvalidTransitionError`, `MalformedTaskError`
|
|
27
|
+
- Error classes: `TaskNotFoundError`, `TaskAlreadyExistsError`, `InvalidTransitionError`, `MalformedTaskError`, `OrderMismatchError`, `AnchorNotFoundError`
|
|
28
28
|
|
|
29
29
|
## State Machines
|
|
30
30
|
|
|
@@ -48,21 +48,23 @@ Pass a custom machine with `openTaskList({ stateMachine })`. If omitted, the pac
|
|
|
48
48
|
|
|
49
49
|
## Options
|
|
50
50
|
|
|
51
|
-
| Option
|
|
52
|
-
|
|
|
53
|
-
| `type`
|
|
54
|
-
| `path`
|
|
55
|
-
| `defaults`
|
|
56
|
-
| `create`
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
51
|
+
| Option | Type | Default | Behavior |
|
|
52
|
+
| ----------------- | ---------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `type` | `"markdown-dir" \| "yaml-file" \| "gh-issues"` | required | Selects the backend implementation. |
|
|
54
|
+
| `path` | `string` | required | Root directory for `markdown-dir` or YAML file path for `yaml-file`. |
|
|
55
|
+
| `defaults` | `TaskDefaults` | `{ metadata: {} }` | Seeds omitted metadata on new tasks only. New tasks always start at the configured state machine's initial state. |
|
|
56
|
+
| `create` | `boolean` | `false` | Creates missing storage for the selected backend when enabled. |
|
|
57
|
+
| `singleList` | `string` | unset | `markdown-dir` only. Treats `path` as one list with this name instead of one subdirectory per list. |
|
|
58
|
+
| `frontmatterMode` | `"strict" \| "passthrough"` | `"strict"` | `markdown-dir` only. `passthrough` preserves non-task frontmatter and allows files without a frontmatter block. |
|
|
59
|
+
| `lockStaleMs` | `number` | `30_000` | Stale threshold passed to backend file locking. |
|
|
60
|
+
| `lockRetries` | `number` | `20` | Retry count passed to backend file locking. |
|
|
61
|
+
| `fs` | `TaskListFs` | `node:fs/promises` adapter | Injectable filesystem, primarily for tests. |
|
|
62
|
+
| `stateMachine` | `StateMachineDef` | `defaultStateMachine` | Overrides the task lifecycle used by `create`, `fire`, `canFire`, and `events`. |
|
|
61
63
|
|
|
62
64
|
## Env vars
|
|
63
65
|
|
|
64
|
-
| Env var
|
|
65
|
-
|
|
|
66
|
+
| Env var | Behavior |
|
|
67
|
+
| --------- | --------------------------------------------------------------- |
|
|
66
68
|
| `GH_HOST` | Defers to gh CLI's host configuration; set GH_HOST to override. |
|
|
67
69
|
|
|
68
70
|
## Usage
|
|
@@ -88,6 +90,21 @@ await planning.create({
|
|
|
88
90
|
await planning.fire("ship-readme", "plan");
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
By default, `markdown-dir` stores tasks as `<path>/<list>/<nn-id>.md`. Set `singleList` to treat the root directory as one list, which is how plan folders such as `docs/plans` are exposed through the task API:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const plans = await openTaskList({
|
|
97
|
+
type: "markdown-dir",
|
|
98
|
+
path: "/repo/docs/plans",
|
|
99
|
+
singleList: "plans",
|
|
100
|
+
frontmatterMode: "passthrough"
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await plans.list("plans").reorder(["api-shape-providers", "memory"]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`frontmatterMode: "strict"` expects full task frontmatter (`kind`, `version`, `name`, `state`, and related task fields). `frontmatterMode: "passthrough"` keeps unrelated frontmatter keys as task metadata and writes back only the task-owned fields (`name`, `description`, `state`), so existing plan metadata is preserved.
|
|
107
|
+
|
|
91
108
|
### `yaml-file`
|
|
92
109
|
|
|
93
110
|
```ts
|
|
@@ -225,14 +242,14 @@ poe-code tasks sync <list> --workflow ./WORKFLOW.md --repo octo-org/octo-repo --
|
|
|
225
242
|
|
|
226
243
|
`<list>` and `--project` both use `<owner>/<number>` project syntax. `--workflow` defaults to `./WORKFLOW.md`. `--repo` overrides the task repository from workflow frontmatter. `--states` overrides the required state list from workflow frontmatter. `--json` prints the report object as JSON. `--yes` confirms non-interactive sync; it is only used by `poe-code tasks sync`.
|
|
227
244
|
|
|
228
|
-
| Option
|
|
229
|
-
|
|
|
230
|
-
| `--workflow <path>`
|
|
231
|
-
| `--repo <owner/name>`
|
|
245
|
+
| Option | Commands | Behavior |
|
|
246
|
+
| -------------------------- | ---------------- | --------------------------------------------------- |
|
|
247
|
+
| `--workflow <path>` | `verify`, `sync` | Workflow file path. Defaults to `./WORKFLOW.md`. |
|
|
248
|
+
| `--repo <owner/name>` | `verify`, `sync` | GitHub repository owner/name. |
|
|
232
249
|
| `--project <owner/number>` | `verify`, `sync` | GitHub Project v2 owner/number. Overrides `<list>`. |
|
|
233
|
-
| `--states <csv>`
|
|
234
|
-
| `--json`
|
|
235
|
-
| `--yes`
|
|
250
|
+
| `--states <csv>` | `verify`, `sync` | Required task state names. |
|
|
251
|
+
| `--json` | `verify`, `sync` | Prints the report as JSON. |
|
|
252
|
+
| `--yes` | `sync` | Confirms non-interactive provisioning. |
|
|
236
253
|
|
|
237
254
|
The `Status` field name and option names are matched case-sensitively. The field must be named `Status`, and required option names must match exactly. For example, `status` is treated as a missing field, and `Done` is treated as missing when the required state is `done`.
|
|
238
255
|
|
|
@@ -159,6 +159,15 @@ const UPDATE_ISSUE_MUTATION = `mutation UpdateIssue($input: UpdateIssueInput!) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
}`;
|
|
162
|
+
const ADD_COMMENT_MUTATION = `mutation AddComment($input: AddCommentInput!) {
|
|
163
|
+
addComment(input: $input) {
|
|
164
|
+
commentEdge {
|
|
165
|
+
node {
|
|
166
|
+
id
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}`;
|
|
162
171
|
const UPDATE_PROJECT_ITEM_POSITION_MUTATION = `mutation UpdateProjectItemPosition($input: UpdateProjectV2ItemPositionInput!) {
|
|
163
172
|
updateProjectV2ItemPosition(input: $input) {
|
|
164
173
|
clientMutationId
|
|
@@ -329,6 +338,14 @@ function createTasksView(name, session, context) {
|
|
|
329
338
|
await updateProjectItemStatus(projectItemId, event, session, context);
|
|
330
339
|
return fetchIssueTask(id, name, session, context);
|
|
331
340
|
},
|
|
341
|
+
async comment(id, body) {
|
|
342
|
+
await context.client.graphql(ADD_COMMENT_MUTATION, {
|
|
343
|
+
input: {
|
|
344
|
+
subjectId: await resolveIssueId(id, name, context),
|
|
345
|
+
body
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
},
|
|
332
349
|
async canFire(id, event) {
|
|
333
350
|
return findEvent(session.stateMachine, id, event) !== undefined;
|
|
334
351
|
},
|
|
@@ -587,7 +604,7 @@ function mapIssueToTask(options) {
|
|
|
587
604
|
return {
|
|
588
605
|
list: options.listName,
|
|
589
606
|
id,
|
|
590
|
-
qualifiedId: `${options.listName}
|
|
607
|
+
qualifiedId: `${options.listName}#${id}`,
|
|
591
608
|
name: options.issue.title,
|
|
592
609
|
description: options.issue.body ?? "",
|
|
593
610
|
state: options.statusName ?? options.initialState,
|
|
@@ -639,7 +656,7 @@ function singleListError(listName) {
|
|
|
639
656
|
return new Error(`gh-issues backend has a single list ${listName}`);
|
|
640
657
|
}
|
|
641
658
|
function parseQualifiedId(qualifiedId, listName) {
|
|
642
|
-
const prefix = `${listName}
|
|
659
|
+
const prefix = `${listName}#`;
|
|
643
660
|
if (!qualifiedId.startsWith(prefix) || qualifiedId.length === prefix.length) {
|
|
644
661
|
throw new Error(`Invalid qualified task id "${qualifiedId}".`);
|
|
645
662
|
}
|
|
@@ -180,7 +180,7 @@ function metadataFromFrontmatter(frontmatter, mode) {
|
|
|
180
180
|
}
|
|
181
181
|
return metadata;
|
|
182
182
|
}
|
|
183
|
-
function createTask(list, id, frontmatter, body, mode) {
|
|
183
|
+
function createTask(list, id, frontmatter, body, mode, sourcePath) {
|
|
184
184
|
return {
|
|
185
185
|
list,
|
|
186
186
|
id,
|
|
@@ -188,7 +188,8 @@ function createTask(list, id, frontmatter, body, mode) {
|
|
|
188
188
|
name: frontmatter.name,
|
|
189
189
|
state: frontmatter.state,
|
|
190
190
|
description: body,
|
|
191
|
-
metadata: metadataFromFrontmatter(frontmatter, mode)
|
|
191
|
+
metadata: metadataFromFrontmatter(frontmatter, mode),
|
|
192
|
+
...(sourcePath !== undefined && { sourcePath: path.resolve(sourcePath) })
|
|
192
193
|
};
|
|
193
194
|
}
|
|
194
195
|
function serializeTaskDocument(frontmatter, description) {
|
|
@@ -223,7 +224,7 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
|
|
|
223
224
|
return {
|
|
224
225
|
path: filePath,
|
|
225
226
|
frontmatter,
|
|
226
|
-
task: createTask(list, id, frontmatter, document.body, mode)
|
|
227
|
+
task: createTask(list, id, frontmatter, document.body, mode, filePath)
|
|
227
228
|
};
|
|
228
229
|
}
|
|
229
230
|
const parsedFilename = parseActiveFilename(path.basename(filePath));
|
|
@@ -238,7 +239,7 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
|
|
|
238
239
|
return {
|
|
239
240
|
path: filePath,
|
|
240
241
|
frontmatter,
|
|
241
|
-
task: createTask(list, id, effectiveFrontmatter, document.body, mode)
|
|
242
|
+
task: createTask(list, id, effectiveFrontmatter, document.body, mode, filePath)
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
async function findActiveTaskFilename(fs, listDirectoryPath, id) {
|
|
@@ -613,7 +614,7 @@ function createTasksView(deps, layout, list) {
|
|
|
613
614
|
const frontmatter = createdFrontmatter(deps.defaults, input, stateMachine.initial, deps.frontmatterMode);
|
|
614
615
|
const description = input.description ?? "";
|
|
615
616
|
await writeAtomically(deps.fs, targetPath, serializeTaskDocument(frontmatter, description));
|
|
616
|
-
return createTask(list, input.id, frontmatter, description, deps.frontmatterMode);
|
|
617
|
+
return createTask(list, input.id, frontmatter, description, deps.frontmatterMode, targetPath);
|
|
617
618
|
});
|
|
618
619
|
},
|
|
619
620
|
async update(id, patch) {
|
|
@@ -623,7 +624,7 @@ function createTasksView(deps, layout, list) {
|
|
|
623
624
|
const nextFrontmatter = updatedFrontmatter(existing.frontmatter, existing.task, patch, deps.frontmatterMode);
|
|
624
625
|
const description = patch.description ?? existing.task.description;
|
|
625
626
|
await writeAtomically(deps.fs, existing.path, serializeTaskDocument(nextFrontmatter, description));
|
|
626
|
-
return createTask(list, id, nextFrontmatter, description, deps.frontmatterMode);
|
|
627
|
+
return createTask(list, id, nextFrontmatter, description, deps.frontmatterMode, existing.path);
|
|
627
628
|
});
|
|
628
629
|
},
|
|
629
630
|
async fire(id, eventName, opts) {
|
|
@@ -641,7 +642,6 @@ function createTasksView(deps, layout, list) {
|
|
|
641
642
|
}
|
|
642
643
|
await event.onExit?.(existing.task);
|
|
643
644
|
const nextFrontmatter = firedFrontmatter(existing.frontmatter, existing.task, event.to, deps.frontmatterMode, opts?.metadataPatch);
|
|
644
|
-
const nextTask = createTask(list, id, nextFrontmatter, existing.task.description, deps.frontmatterMode);
|
|
645
645
|
const serializedTask = serializeTaskDocument(nextFrontmatter, existing.task.description);
|
|
646
646
|
if (event.to === "archived") {
|
|
647
647
|
const targetPath = archivedTaskPath(deps.path, layout, list, id);
|
|
@@ -652,10 +652,12 @@ function createTasksView(deps, layout, list) {
|
|
|
652
652
|
await writeAtomically(deps.fs, existing.path, serializedTask);
|
|
653
653
|
await deps.fs.mkdir(archiveDirectoryPath(deps.path, layout, list), { recursive: true });
|
|
654
654
|
await deps.fs.rename(existing.path, targetPath);
|
|
655
|
+
const nextTask = createTask(list, id, nextFrontmatter, existing.task.description, deps.frontmatterMode, targetPath);
|
|
655
656
|
await event.onEnter?.(nextTask);
|
|
656
657
|
return nextTask;
|
|
657
658
|
}
|
|
658
659
|
await writeAtomically(deps.fs, existing.path, serializedTask);
|
|
660
|
+
const nextTask = createTask(list, id, nextFrontmatter, existing.task.description, deps.frontmatterMode, existing.path);
|
|
659
661
|
await event.onEnter?.(nextTask);
|
|
660
662
|
return nextTask;
|
|
661
663
|
};
|
|
@@ -695,7 +697,7 @@ function createTasksView(deps, layout, list) {
|
|
|
695
697
|
async move(id, anchor) {
|
|
696
698
|
validateTaskId(id);
|
|
697
699
|
return withListLock(async () => {
|
|
698
|
-
const { entries
|
|
700
|
+
const { entries } = await readActiveTasks();
|
|
699
701
|
const fromIndex = entries.findIndex((entry) => entry.id === id);
|
|
700
702
|
if (fromIndex < 0) {
|
|
701
703
|
throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
|
|
@@ -716,7 +718,7 @@ function createTasksView(deps, layout, list) {
|
|
|
716
718
|
}
|
|
717
719
|
ordered.splice(insertIndex, 0, id);
|
|
718
720
|
await rewriteMovedPrefix(id, ordered);
|
|
719
|
-
return
|
|
721
|
+
return (await getTaskFile(id)).task;
|
|
720
722
|
});
|
|
721
723
|
},
|
|
722
724
|
async reorder(ids) {
|
|
@@ -724,7 +726,7 @@ function createTasksView(deps, layout, list) {
|
|
|
724
726
|
validateTaskId(id);
|
|
725
727
|
}
|
|
726
728
|
return withListLock(async () => {
|
|
727
|
-
const { entries
|
|
729
|
+
const { entries } = await readActiveTasks();
|
|
728
730
|
const currentIds = entries.map((entry) => entry.id);
|
|
729
731
|
const currentSet = new Set(currentIds);
|
|
730
732
|
const inputSet = new Set(ids);
|
|
@@ -734,7 +736,7 @@ function createTasksView(deps, layout, list) {
|
|
|
734
736
|
throw new OrderMismatchError({ missing, extra });
|
|
735
737
|
}
|
|
736
738
|
await rewriteListPrefixes(ids);
|
|
737
|
-
return ids.map((id) =>
|
|
739
|
+
return Promise.all(ids.map(async (id) => (await getTaskFile(id)).task));
|
|
738
740
|
});
|
|
739
741
|
}
|
|
740
742
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { acquireFileLock } from "@poe-code/file-lock";
|
|
2
3
|
import { isMap, parseDocument } from "yaml";
|
|
3
4
|
import storeSchema from "../schema/store.schema.json" with { type: "json" };
|
|
@@ -61,7 +62,7 @@ function metadataFromTaskRecord(taskRecord) {
|
|
|
61
62
|
}
|
|
62
63
|
return metadata;
|
|
63
64
|
}
|
|
64
|
-
function createTask(list, id, taskRecord) {
|
|
65
|
+
function createTask(list, id, taskRecord, sourcePath) {
|
|
65
66
|
return {
|
|
66
67
|
list,
|
|
67
68
|
id,
|
|
@@ -69,7 +70,8 @@ function createTask(list, id, taskRecord) {
|
|
|
69
70
|
name: taskRecord.name,
|
|
70
71
|
state: taskRecord.state,
|
|
71
72
|
description: descriptionFromTaskRecord(taskRecord),
|
|
72
|
-
metadata: metadataFromTaskRecord(taskRecord)
|
|
73
|
+
metadata: metadataFromTaskRecord(taskRecord),
|
|
74
|
+
...(sourcePath !== undefined && { sourcePath: path.resolve(sourcePath) })
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
function matchesFilter(task, filter) {
|
|
@@ -347,7 +349,7 @@ function createTasksView(deps, list) {
|
|
|
347
349
|
}
|
|
348
350
|
const entries = Object.entries(listRecord)
|
|
349
351
|
.map(([id, taskRecord]) => ({
|
|
350
|
-
task: createTask(list, id, taskRecord),
|
|
352
|
+
task: createTask(list, id, taskRecord, deps.path),
|
|
351
353
|
raw: taskRecord
|
|
352
354
|
}))
|
|
353
355
|
.filter(({ task }) => matchesFilter(task, filter));
|
|
@@ -374,7 +376,7 @@ function createTasksView(deps, list) {
|
|
|
374
376
|
async get(id) {
|
|
375
377
|
validateTaskId(id);
|
|
376
378
|
const { store } = await readStore(deps.fs, deps.path, validStates);
|
|
377
|
-
return createTask(list, id, getTaskOrThrow(store, list, id));
|
|
379
|
+
return createTask(list, id, getTaskOrThrow(store, list, id), deps.path);
|
|
378
380
|
},
|
|
379
381
|
async create(input) {
|
|
380
382
|
assertCreateDoesNotSetState(input);
|
|
@@ -388,7 +390,7 @@ function createTasksView(deps, list) {
|
|
|
388
390
|
const taskRecord = createTaskRecord(deps.defaults, input, stateMachine.initial);
|
|
389
391
|
document.setIn(["lists", list, input.id], taskRecord);
|
|
390
392
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
391
|
-
return createTask(list, input.id, taskRecord);
|
|
393
|
+
return createTask(list, input.id, taskRecord, deps.path);
|
|
392
394
|
});
|
|
393
395
|
},
|
|
394
396
|
async update(id, patch) {
|
|
@@ -410,7 +412,7 @@ function createTasksView(deps, list) {
|
|
|
410
412
|
}
|
|
411
413
|
}
|
|
412
414
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
413
|
-
return createTask(list, id, nextTaskRecord);
|
|
415
|
+
return createTask(list, id, nextTaskRecord, deps.path);
|
|
414
416
|
});
|
|
415
417
|
},
|
|
416
418
|
async fire(id, eventName, opts) {
|
|
@@ -418,7 +420,7 @@ function createTasksView(deps, list) {
|
|
|
418
420
|
return withStoreLock(deps, async () => {
|
|
419
421
|
const { document, store } = await readStore(deps.fs, deps.path, validStates);
|
|
420
422
|
const existing = getTaskOrThrow(store, list, id);
|
|
421
|
-
const task = createTask(list, id, existing);
|
|
423
|
+
const task = createTask(list, id, existing, deps.path);
|
|
422
424
|
const event = assertFireableTaskEvent(task, eventName);
|
|
423
425
|
const guardResult = event.guard?.(task) ?? true;
|
|
424
426
|
if (guardResult !== true) {
|
|
@@ -438,7 +440,7 @@ function createTasksView(deps, list) {
|
|
|
438
440
|
}
|
|
439
441
|
}
|
|
440
442
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
441
|
-
const nextTask = createTask(list, id, nextTaskRecord);
|
|
443
|
+
const nextTask = createTask(list, id, nextTaskRecord, deps.path);
|
|
442
444
|
await event.onEnter?.(nextTask);
|
|
443
445
|
return nextTask;
|
|
444
446
|
});
|
|
@@ -446,7 +448,7 @@ function createTasksView(deps, list) {
|
|
|
446
448
|
async canFire(id, eventName) {
|
|
447
449
|
validateTaskId(id);
|
|
448
450
|
const { store } = await readStore(deps.fs, deps.path, validStates);
|
|
449
|
-
const task = createTask(list, id, getTaskOrThrow(store, list, id));
|
|
451
|
+
const task = createTask(list, id, getTaskOrThrow(store, list, id), deps.path);
|
|
450
452
|
const event = findEvent(stateMachine, task.state, eventName);
|
|
451
453
|
if (event === undefined) {
|
|
452
454
|
return false;
|
|
@@ -456,7 +458,7 @@ function createTasksView(deps, list) {
|
|
|
456
458
|
async events(id) {
|
|
457
459
|
validateTaskId(id);
|
|
458
460
|
const { store } = await readStore(deps.fs, deps.path, validStates);
|
|
459
|
-
const task = createTask(list, id, getTaskOrThrow(store, list, id));
|
|
461
|
+
const task = createTask(list, id, getTaskOrThrow(store, list, id), deps.path);
|
|
460
462
|
return eventsFromState(stateMachine, task.state);
|
|
461
463
|
},
|
|
462
464
|
async delete(id) {
|
|
@@ -497,7 +499,7 @@ function createTasksView(deps, list) {
|
|
|
497
499
|
}
|
|
498
500
|
listNode.items.splice(insertIndex, 0, movedPair);
|
|
499
501
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
500
|
-
return createTask(list, id, taskRecord);
|
|
502
|
+
return createTask(list, id, taskRecord, deps.path);
|
|
501
503
|
});
|
|
502
504
|
},
|
|
503
505
|
async reorder(ids) {
|
|
@@ -531,7 +533,7 @@ function createTasksView(deps, list) {
|
|
|
531
533
|
});
|
|
532
534
|
listNode.items.splice(0, listNode.items.length, ...orderedActive, ...archivedPairs);
|
|
533
535
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
534
|
-
const tasks = ids.map((id) => createTask(list, id, getTaskOrThrow(store, list, id)));
|
|
536
|
+
const tasks = ids.map((id) => createTask(list, id, getTaskOrThrow(store, list, id), deps.path));
|
|
535
537
|
return tasks;
|
|
536
538
|
});
|
|
537
539
|
}
|
|
@@ -559,7 +561,7 @@ export async function yamlFileBackend(deps) {
|
|
|
559
561
|
continue;
|
|
560
562
|
const entries = Object.entries(listRecord)
|
|
561
563
|
.map(([id, taskRecord]) => ({
|
|
562
|
-
task: createTask(listName, id, taskRecord),
|
|
564
|
+
task: createTask(listName, id, taskRecord, deps.path),
|
|
563
565
|
raw: taskRecord
|
|
564
566
|
}))
|
|
565
567
|
.filter(({ task }) => matchesFilter(task, filter));
|
|
@@ -578,7 +580,7 @@ export async function yamlFileBackend(deps) {
|
|
|
578
580
|
const { document, store } = await readStore(deps.fs, deps.path, validStates);
|
|
579
581
|
const taskRecord = getTaskOrThrow(store, sourceListName, id);
|
|
580
582
|
if (sourceListName === targetListName) {
|
|
581
|
-
return createTask(targetListName, id, taskRecord);
|
|
583
|
+
return createTask(targetListName, id, taskRecord, deps.path);
|
|
582
584
|
}
|
|
583
585
|
if (getTaskRecord(store, targetListName, id)) {
|
|
584
586
|
throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
|
|
@@ -586,7 +588,7 @@ export async function yamlFileBackend(deps) {
|
|
|
586
588
|
document.deleteIn(["lists", sourceListName, id]);
|
|
587
589
|
document.setIn(["lists", targetListName, id], taskRecord);
|
|
588
590
|
await writeAtomically(deps.fs, deps.path, serializeDocument(document));
|
|
589
|
-
return createTask(targetListName, id, taskRecord);
|
|
591
|
+
return createTask(targetListName, id, taskRecord, deps.path);
|
|
590
592
|
});
|
|
591
593
|
};
|
|
592
594
|
return {
|
|
@@ -8,6 +8,7 @@ export interface Task {
|
|
|
8
8
|
state: string;
|
|
9
9
|
description: string;
|
|
10
10
|
metadata: Record<string, unknown>;
|
|
11
|
+
sourcePath?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface TaskCreate {
|
|
13
14
|
id?: string;
|
|
@@ -45,6 +46,7 @@ export interface Tasks {
|
|
|
45
46
|
create(input: TaskCreate): Promise<Task>;
|
|
46
47
|
update(id: string, patch: TaskUpdate): Promise<Task>;
|
|
47
48
|
fire(id: string, event: string, opts?: TaskFireOptions): Promise<Task>;
|
|
49
|
+
comment?(id: string, body: string): Promise<void>;
|
|
48
50
|
canFire(id: string, event: string): Promise<boolean>;
|
|
49
51
|
events(id: string): Promise<readonly string[]>;
|
|
50
52
|
delete(id: string): Promise<void>;
|
|
@@ -20,9 +20,11 @@ export class MigratingSecretStore {
|
|
|
20
20
|
return legacyValue;
|
|
21
21
|
}
|
|
22
22
|
async set(value) {
|
|
23
|
-
|
|
23
|
+
await this.store.set(value);
|
|
24
|
+
await this.legacyStore?.set(value);
|
|
24
25
|
}
|
|
25
26
|
async delete() {
|
|
26
|
-
|
|
27
|
+
await this.store.delete();
|
|
28
|
+
await this.legacyStore?.delete();
|
|
27
29
|
}
|
|
28
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"jsonc-parser": "^3.3.1",
|
|
51
51
|
"smol-toml": "^1.3.0",
|
|
52
52
|
"tiny-stdio-mcp-server": "^0.1.0",
|
|
53
|
-
"toolcraft-schema": "^0.0.
|
|
53
|
+
"toolcraft-schema": "^0.0.21",
|
|
54
54
|
"yaml": "^2.8.2"
|
|
55
55
|
},
|
|
56
56
|
"files": [
|