agent-canvas 0.2.0 → 0.6.0

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 (98) hide show
  1. package/README.md +42 -17
  2. package/bin/agent-canvas.js +7 -7
  3. package/bin/agent-canvas.ts +48 -37
  4. package/bin/lib/commands/daemon.ts +46 -43
  5. package/bin/lib/commands/exec.ts +100 -100
  6. package/bin/lib/commands/fetch.ts +15 -15
  7. package/bin/lib/commands/install.ts +36 -36
  8. package/bin/lib/commands/instructions.ts +51 -51
  9. package/bin/lib/commands/push.ts +115 -97
  10. package/bin/lib/commands/review.ts +190 -0
  11. package/bin/lib/commands/watch.ts +24 -24
  12. package/bin/lib/config.ts +12 -12
  13. package/bin/lib/daemon-lifecycle.ts +47 -51
  14. package/bin/lib/helpers.ts +82 -76
  15. package/bin/lib/instructions/app-review.md +88 -0
  16. package/bin/lib/instructions/component-checklist.md +7 -5
  17. package/bin/lib/instructions/component-custom.md +20 -14
  18. package/bin/lib/instructions/component-diff.md +4 -3
  19. package/bin/lib/instructions/component-item.md +18 -8
  20. package/bin/lib/instructions/component-markdown.md +4 -2
  21. package/bin/lib/instructions/component-mermaid.md +12 -6
  22. package/bin/lib/instructions/component-multichoice.md +6 -2
  23. package/bin/lib/instructions/component-note.md +1 -1
  24. package/bin/lib/instructions/component-priority.md +1 -1
  25. package/bin/lib/instructions/component-rangeinput.md +9 -2
  26. package/bin/lib/instructions/component-secretinput.md +12 -11
  27. package/bin/lib/instructions/component-svg.md +62 -16
  28. package/bin/lib/instructions/component-table.md +6 -6
  29. package/bin/lib/instructions/component-usefeedback.md +8 -8
  30. package/bin/lib/instructions/component-userinput.md +6 -2
  31. package/bin/lib/instructions/flow-feature.md +22 -5
  32. package/bin/lib/instructions/flow-plan.md +5 -5
  33. package/bin/lib/instructions/flow-runbook.md +28 -25
  34. package/bin/lib/instructions/overview.md +27 -18
  35. package/bin/lib/instructions/theming.md +5 -0
  36. package/daemon/build.ts +171 -152
  37. package/daemon/client/App.tsx +1205 -1077
  38. package/daemon/client/CompareView.tsx +478 -458
  39. package/daemon/client/FileBrowser.tsx +241 -203
  40. package/daemon/client/FileViewer.tsx +220 -211
  41. package/daemon/client/OverviewView.tsx +283 -267
  42. package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
  43. package/daemon/client/RevisionSelect.tsx +111 -95
  44. package/daemon/client/SessionSwitcher.tsx +81 -81
  45. package/daemon/client/ShareDialog.tsx +211 -212
  46. package/daemon/client/apiSchemas.ts +78 -0
  47. package/daemon/client/canvas-runtime.ts +1 -1
  48. package/daemon/client/clientApi.ts +194 -206
  49. package/daemon/client/components/SecretInput.tsx +138 -138
  50. package/daemon/client/components/index.ts +2 -2
  51. package/daemon/client/domDiff.ts +341 -333
  52. package/daemon/client/errorMessage.ts +3 -0
  53. package/daemon/client/exportMarkdown.ts +222 -201
  54. package/daemon/client/lcs.ts +19 -14
  55. package/daemon/client/shareCrypto.ts +57 -57
  56. package/daemon/client/unifiedDiff.ts +660 -649
  57. package/daemon/dist/annotations-runtime.js +1 -1
  58. package/daemon/dist/client.css +2 -2
  59. package/daemon/dist/client.js +77 -80
  60. package/daemon/dist/components.js +59 -59
  61. package/daemon/dist/preact-compat.js +1 -1
  62. package/daemon/dist/review.js +361 -0
  63. package/daemon/dist/runtime.js +1 -1
  64. package/daemon/langMap.ts +24 -6
  65. package/daemon/review-widget/anchor.ts +187 -0
  66. package/daemon/review-widget/client.ts +95 -0
  67. package/daemon/review-widget/index.tsx +36 -0
  68. package/daemon/review-widget/markup.ts +71 -0
  69. package/daemon/review-widget/screenshot.ts +91 -0
  70. package/daemon/review-widget/source.ts +103 -0
  71. package/daemon/review-widget/styles.css +359 -0
  72. package/daemon/review-widget/text-modules.d.ts +4 -0
  73. package/daemon/review-widget/tsconfig.json +16 -0
  74. package/daemon/review-widget/widget.tsx +436 -0
  75. package/daemon/src/components.ts +3 -3
  76. package/daemon/src/handlers/api.ts +330 -328
  77. package/daemon/src/handlers/files.ts +112 -105
  78. package/daemon/src/handlers/static.ts +47 -46
  79. package/daemon/src/handlers/uploads.ts +61 -56
  80. package/daemon/src/paths.ts +13 -10
  81. package/daemon/src/remote-feedback.ts +192 -192
  82. package/daemon/src/review/markdown.ts +52 -0
  83. package/daemon/src/review/model.ts +132 -0
  84. package/daemon/src/review/proxy.ts +177 -0
  85. package/daemon/src/review/server.ts +199 -0
  86. package/daemon/src/review/store.ts +151 -0
  87. package/daemon/src/server.ts +127 -86
  88. package/daemon/src/share.ts +163 -169
  89. package/daemon/src/stubFetch.ts +7 -0
  90. package/daemon/tsconfig.json +19 -19
  91. package/package.json +47 -47
  92. package/skills/canvas/SKILL.md +9 -1
  93. package/bin/lib/commands/exec.test.ts +0 -27
  94. package/bin/lib/helpers.test.ts +0 -79
  95. package/daemon/src/remote-feedback.test.ts +0 -165
  96. package/daemon/src/secrets-api.test.ts +0 -123
  97. package/daemon/src/share-e2e.test.ts +0 -277
  98. package/daemon/src/share.test.ts +0 -289
package/README.md CHANGED
@@ -5,7 +5,7 @@ Interactive browser-based visual canvas for [Claude Code](https://docs.anthropic
5
5
  ![License](https://img.shields.io/npm/l/agent-canvas)
6
6
  ![npm](https://img.shields.io/npm/v/agent-canvas)
7
7
 
8
- ![Agent Canvas](.github/light.png)
8
+ ![Agent Canvas](docs/light.png)
9
9
 
10
10
  ## What it does
11
11
 
@@ -27,6 +27,7 @@ bunx agent-canvas install local # for current project only
27
27
  ```
28
28
 
29
29
  This installs:
30
+
30
31
  - The `/canvas` skill so Claude knows how to use it
31
32
  - A `SessionStart` hook that sets up session IDs automatically
32
33
 
@@ -48,14 +49,14 @@ Claude creates a JSX document, pushes it to a local daemon, and opens it in your
48
49
 
49
50
  Claude uses different canvas flows depending on the task:
50
51
 
51
- | Flow | When | Rounds |
52
- |------|------|--------|
53
- | **Feature** | Vague requests ("add auth") | Discovery → Requirements → Plan → Implementation → Summary |
54
- | **Plan** | Clear requirements | Plan → Implementation → Summary |
55
- | **Explain** | "How does X work?" | Single canvas |
56
- | **Review** | Code/architecture review | Findings → Fix Plan → Summary |
57
- | **Decision** | Choosing between options | Single canvas with comparison |
58
- | **Runbook** | User must complete manual setup or provide a secret | Instructions → secure handoff → command → result |
52
+ | Flow | When | Rounds |
53
+ | ------------ | --------------------------------------------------- | ---------------------------------------------------------- |
54
+ | **Feature** | Vague requests ("add auth") | Discovery → Requirements → Plan → Implementation → Summary |
55
+ | **Plan** | Clear requirements | Plan → Implementation → Summary |
56
+ | **Explain** | "How does X work?" | Single canvas |
57
+ | **Review** | Code/architecture review | Findings → Fix Plan → Summary |
58
+ | **Decision** | Choosing between options | Single canvas with comparison |
59
+ | **Runbook** | User must complete manual setup or provide a secret | Instructions → secure handoff → command → result |
59
60
 
60
61
  ### Local secret handoff
61
62
 
@@ -72,27 +73,51 @@ bunx agent-canvas exec \
72
73
 
73
74
  Secret resolution requires a daemon capability available to the CLI but not to canvas JavaScript. A new canvas revision, daemon shutdown, or session cleanup clears the value. The target command must not print its environment or credentials.
74
75
 
76
+ ### Review mode
77
+
78
+ Review mode puts the same comment loop on a running web application: a prototype, a mock or any dev server. Open the
79
+ application through the daemon's review proxy:
80
+
81
+ ```bash
82
+ bunx agent-canvas review open http://localhost:5173/ --project my-app
83
+ ```
84
+
85
+ The daemon serves the application on a port of its own with the widget added to every HTML page (hot reload passes
86
+ through) and opens it in the browser. To keep the application's own address, add the widget's tag to its page in
87
+ development instead: `bunx agent-canvas review snippet --project my-app`.
88
+
89
+ The widget mounts a toolbar in its own shadow root. The reviewer clicks **Comment**, picks an element or selects text,
90
+ and saves the comment as a note or sends it to the agent. The agent waits with `bunx agent-canvas review watch` (`--follow` keeps waiting),
91
+ receives each thread with its page URL, the element's role and accessible name, a CSS path, the React components that
92
+ own it with their source positions (resolved through the dev server's source maps), the trimmed markup and, when the
93
+ reviewer attaches one, a screenshot of the tab, changes the code and answers with `bunx agent-canvas review reply <thread> "<text>" --resolve`. The answer and
94
+ the thread's state show up next to the element.
95
+
96
+ Threads are stored per project under the daemon's data directory. The daemon accepts widget requests only from loopback
97
+ origins (`localhost`, `127.0.0.1`, `*.localhost`) and agent requests only from clients that send no `Origin`, so a web
98
+ page cannot queue work for the agent.
99
+
75
100
  ## Theming
76
101
 
77
102
  Agent Canvas supports light and dark themes, automatically matching your system preference.
78
103
 
79
104
  **Dark mode**
80
105
 
81
- ![](.github/dark.png)
106
+ ![](docs/dark.png)
82
107
 
83
108
  ## Development
84
109
 
85
110
  ### Reusable packages
86
111
 
87
- | Package | Use without the Agent Canvas shell |
88
- | --- | --- |
89
- | [`@fabrika/daemon-kit`](https://github.com/contember/fabrika-platform/tree/main/packages/daemon-kit) | Bun daemon lifecycle, HTTP routing, WebSocket channels and CLI waiters |
90
- | [`@fabrika/annotations`](https://github.com/contember/fabrika-platform/tree/main/packages/annotations) | Annotation state, editors and text/block/region locators |
91
- | [`@fabrika/canvas-kernel`](https://github.com/contember/fabrika-platform/tree/main/packages/canvas-kernel) | JSX compiler, revisions, renderer and interactive canvas components |
112
+ | Package | Use without the Agent Canvas shell |
113
+ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
114
+ | [`@fabrika/daemon-kit`](https://github.com/contember/fabrika-platform/tree/main/packages/daemon-kit) | Bun daemon lifecycle, HTTP routing, WebSocket channels and CLI waiters |
115
+ | [`@fabrika/annotations`](https://github.com/contember/fabrika-platform/tree/main/packages/annotations) | Annotation state, editors and text/block/region locators |
116
+ | [`@fabrika/canvas-kernel`](https://github.com/contember/fabrika-platform/tree/main/packages/canvas-kernel) | JSX compiler, revisions, renderer and interactive canvas components |
92
117
 
93
118
  The kernel depends on the other two; neither depends on the kernel. All ship as
94
- TypeScript sources from the [`fabrika-platform`](https://github.com/contember/fabrika-platform)
95
- repository. See the kernel README for the breaking 0.2 import migration.
119
+ TypeScript sources and are released together with `agent-canvas` from the
120
+ [`fabrika-platform`](https://github.com/contember/fabrika-platform) repository.
96
121
 
97
122
  ### Local commands
98
123
 
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFileSync } = require("child_process");
4
- const { join } = require("path");
3
+ const { execFileSync } = require('child_process')
4
+ const { join } = require('path')
5
5
 
6
- const script = join(__dirname, "agent-canvas.ts");
6
+ const script = join(__dirname, 'agent-canvas.ts')
7
7
 
8
8
  try {
9
- execFileSync("bun", [script, ...process.argv.slice(2)], { stdio: "inherit" });
9
+ execFileSync('bun', [script, ...process.argv.slice(2)], { stdio: 'inherit' })
10
10
  } catch (e) {
11
- if (e.status != null) process.exit(e.status);
12
- console.error("Failed to run agent-canvas. Is Bun installed? https://bun.sh");
13
- process.exit(1);
11
+ if (e.status != null) process.exit(e.status)
12
+ console.error('Failed to run agent-canvas. Is Bun installed? https://bun.sh')
13
+ process.exit(1)
14
14
  }
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { handlePush } from "./lib/commands/push.ts";
4
- import { handleFetch } from "./lib/commands/fetch.ts";
5
- import { handleWatch } from "./lib/commands/watch.ts";
6
- import { handleDaemon } from "./lib/commands/daemon.ts";
7
- import { handleInstall } from "./lib/commands/install.ts";
8
- import { handleInstructions } from "./lib/commands/instructions.ts";
9
- import { handleExec } from "./lib/commands/exec.ts";
10
- import { VERSION } from "./lib/config.ts";
3
+ import { handleDaemon } from './lib/commands/daemon.ts'
4
+ import { handleExec } from './lib/commands/exec.ts'
5
+ import { handleFetch } from './lib/commands/fetch.ts'
6
+ import { handleInstall } from './lib/commands/install.ts'
7
+ import { handleInstructions } from './lib/commands/instructions.ts'
8
+ import { handlePush } from './lib/commands/push.ts'
9
+ import { handleReview } from './lib/commands/review.ts'
10
+ import { handleWatch } from './lib/commands/watch.ts'
11
+ import { VERSION } from './lib/config.ts'
11
12
 
12
13
  function printUsage() {
13
- console.log(`agent-canvas v${VERSION} — Interactive visual canvas for Claude Code
14
+ console.log(`agent-canvas v${VERSION} — Interactive visual canvas for Claude Code
14
15
 
15
16
  Commands:
16
17
  agent-canvas install [local|global] Install skill for Claude Code
@@ -18,6 +19,7 @@ Commands:
18
19
  agent-canvas push <directory> Push canvases, open browser
19
20
  agent-canvas fetch [--session <id>] Check for feedback (returns immediately)
20
21
  agent-canvas watch [--session <id>] Wait for user feedback (blocks until submitted)
22
+ agent-canvas review <command> Comment on a running application (see: agent-canvas review --help)
21
23
  agent-canvas exec --secret ENV=id -- <command>
22
24
  Run a command with an in-memory secret
23
25
  agent-canvas daemon status Show daemon status
@@ -26,41 +28,50 @@ Commands:
26
28
  agent-canvas daemon restart Restart the daemon
27
29
 
28
30
  Environment:
29
- CANVAS_PORT Daemon port (default: 19400)`);
31
+ CANVAS_PORT Daemon port (default: 19400)`)
30
32
  }
31
33
 
32
34
  async function main() {
33
- const args = process.argv.slice(2);
35
+ const args = process.argv.slice(2)
34
36
 
35
- if (args[0] === "--version" || args[0] === "-v") {
36
- console.log(VERSION);
37
- return;
38
- }
37
+ if (args[0] === '--version' || args[0] === '-v') {
38
+ console.log(VERSION)
39
+ return
40
+ }
39
41
 
40
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
41
- printUsage();
42
- process.exit(args.length === 0 ? 1 : 0);
43
- }
42
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
43
+ printUsage()
44
+ process.exit(args.length === 0 ? 1 : 0)
45
+ }
44
46
 
45
- const command = args[0];
46
- const rest = args.slice(1);
47
+ const command = args[0]
48
+ const rest = args.slice(1)
47
49
 
48
- switch (command) {
49
- case "push": return handlePush(rest);
50
- case "fetch": return handleFetch(rest);
51
- case "watch": return handleWatch(rest);
52
- case "exec": return handleExec(rest);
53
- case "instructions": return handleInstructions(rest);
54
- case "install": return handleInstall(rest);
55
- case "daemon": return handleDaemon(rest);
56
- default:
57
- console.error(`Unknown command: ${command}`);
58
- printUsage();
59
- process.exit(1);
60
- }
50
+ switch (command) {
51
+ case 'push':
52
+ return handlePush(rest)
53
+ case 'fetch':
54
+ return handleFetch(rest)
55
+ case 'watch':
56
+ return handleWatch(rest)
57
+ case 'review':
58
+ return handleReview(rest)
59
+ case 'exec':
60
+ return handleExec(rest)
61
+ case 'instructions':
62
+ return handleInstructions(rest)
63
+ case 'install':
64
+ return handleInstall(rest)
65
+ case 'daemon':
66
+ return handleDaemon(rest)
67
+ default:
68
+ console.error(`Unknown command: ${command}`)
69
+ printUsage()
70
+ process.exit(1)
71
+ }
61
72
  }
62
73
 
63
74
  main().catch((e) => {
64
- console.error(e);
65
- process.exit(1);
66
- });
75
+ console.error(e)
76
+ process.exit(1)
77
+ })
@@ -1,48 +1,51 @@
1
- import { parseArgs } from "util";
2
- import { DAEMON_PORT, BASE_URL } from "../config.ts";
3
- import { isDaemonRunning, startDaemon, stopDaemon } from "../daemon-lifecycle.ts";
1
+ import { parseArgs } from 'util'
2
+ import { z } from 'zod'
3
+ import { BASE_URL, DAEMON_PORT } from '../config.ts'
4
+ import { isDaemonRunning, startDaemon, stopDaemon } from '../daemon-lifecycle.ts'
5
+
6
+ const healthResponseSchema = z.object({ sessions: z.array(z.string()) })
4
7
 
5
8
  export async function handleDaemon(args: string[]) {
6
- const { positionals } = parseArgs({
7
- args,
8
- allowPositionals: true,
9
- });
9
+ const { positionals } = parseArgs({
10
+ args,
11
+ allowPositionals: true,
12
+ })
10
13
 
11
- const subcommand = positionals[0];
14
+ const subcommand = positionals[0]
12
15
 
13
- switch (subcommand) {
14
- case "status": {
15
- try {
16
- const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(2000) });
17
- const data = await res.json() as any;
18
- console.log(`Daemon: running on port ${DAEMON_PORT}`);
19
- console.log(`Sessions: ${data.sessions.length > 0 ? data.sessions.join(", ") : "none"}`);
20
- } catch {
21
- console.log("Daemon: not running");
22
- }
23
- break;
24
- }
25
- case "stop": {
26
- await stopDaemon();
27
- break;
28
- }
29
- case "start": {
30
- if (await isDaemonRunning()) {
31
- console.log("Daemon is already running.");
32
- return;
33
- }
34
- await startDaemon();
35
- break;
36
- }
37
- case "restart": {
38
- // stopDaemon() already waits for the daemon to go down.
39
- await stopDaemon();
40
- await startDaemon();
41
- break;
42
- }
43
- default: {
44
- console.error("Usage: agent-canvas daemon [status|stop|start|restart]");
45
- process.exit(1);
46
- }
47
- }
16
+ switch (subcommand) {
17
+ case 'status': {
18
+ try {
19
+ const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(2000) })
20
+ const data = healthResponseSchema.parse(await res.json())
21
+ console.log(`Daemon: running on port ${DAEMON_PORT}`)
22
+ console.log(`Sessions: ${data.sessions.length > 0 ? data.sessions.join(', ') : 'none'}`)
23
+ } catch {
24
+ console.log('Daemon: not running')
25
+ }
26
+ break
27
+ }
28
+ case 'stop': {
29
+ await stopDaemon()
30
+ break
31
+ }
32
+ case 'start': {
33
+ if (await isDaemonRunning()) {
34
+ console.log('Daemon is already running.')
35
+ return
36
+ }
37
+ await startDaemon()
38
+ break
39
+ }
40
+ case 'restart': {
41
+ // stopDaemon() already waits for the daemon to go down.
42
+ await stopDaemon()
43
+ await startDaemon()
44
+ break
45
+ }
46
+ default: {
47
+ console.error('Usage: agent-canvas daemon [status|stop|start|restart]')
48
+ process.exit(1)
49
+ }
50
+ }
48
51
  }
@@ -1,129 +1,129 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { resolve } from "path";
3
- import { parseArgs } from "util";
4
- import { BASE_URL, CLI_AUTH_FILE } from "../config.ts";
5
- import { ensureDaemon } from "../daemon-lifecycle.ts";
6
- import { getSessionId } from "../helpers.ts";
1
+ import { existsSync, readFileSync } from 'fs'
2
+ import { resolve } from 'path'
3
+ import { parseArgs } from 'util'
4
+ import { BASE_URL, CLI_AUTH_FILE } from '../config.ts'
5
+ import { ensureDaemon } from '../daemon-lifecycle.ts'
6
+ import { getSessionId } from '../helpers.ts'
7
7
 
8
- const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
- const FIELD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
9
+ const FIELD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
10
10
 
11
11
  export interface SecretAssignment {
12
- envName: string;
13
- fieldId: string;
12
+ envName: string
13
+ fieldId: string
14
14
  }
15
15
 
16
16
  function isRecord(value: unknown): value is Record<string, unknown> {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
18
18
  }
19
19
 
20
20
  export function parseSecretAssignments(rawAssignments: string[]): SecretAssignment[] {
21
- if (rawAssignments.length === 0) {
22
- throw new Error("At least one --secret ENV_NAME=field-id mapping is required");
23
- }
21
+ if (rawAssignments.length === 0) {
22
+ throw new Error('At least one --secret ENV_NAME=field-id mapping is required')
23
+ }
24
24
 
25
- const assignments: SecretAssignment[] = [];
26
- const environmentNames = new Set<string>();
27
- for (const raw of rawAssignments) {
28
- const separator = raw.indexOf("=");
29
- const envName = separator === -1 ? "" : raw.slice(0, separator);
30
- const fieldId = separator === -1 ? "" : raw.slice(separator + 1);
31
- if (!ENV_NAME_RE.test(envName)) {
32
- throw new Error(`Invalid environment variable name in --secret mapping: ${raw}`);
33
- }
34
- if (!FIELD_ID_RE.test(fieldId)) {
35
- throw new Error(`Invalid secret field ID in --secret mapping: ${raw}`);
36
- }
37
- if (environmentNames.has(envName)) {
38
- throw new Error(`Duplicate environment variable mapping: ${envName}`);
39
- }
40
- environmentNames.add(envName);
41
- assignments.push({ envName, fieldId });
42
- }
43
- return assignments;
25
+ const assignments: SecretAssignment[] = []
26
+ const environmentNames = new Set<string>()
27
+ for (const raw of rawAssignments) {
28
+ const separator = raw.indexOf('=')
29
+ const envName = separator === -1 ? '' : raw.slice(0, separator)
30
+ const fieldId = separator === -1 ? '' : raw.slice(separator + 1)
31
+ if (!ENV_NAME_RE.test(envName)) {
32
+ throw new Error(`Invalid environment variable name in --secret mapping: ${raw}`)
33
+ }
34
+ if (!FIELD_ID_RE.test(fieldId)) {
35
+ throw new Error(`Invalid secret field ID in --secret mapping: ${raw}`)
36
+ }
37
+ if (environmentNames.has(envName)) {
38
+ throw new Error(`Duplicate environment variable mapping: ${envName}`)
39
+ }
40
+ environmentNames.add(envName)
41
+ assignments.push({ envName, fieldId })
42
+ }
43
+ return assignments
44
44
  }
45
45
 
46
46
  async function resolveSecretValues(sessionId: string, fieldIds: string[]): Promise<Map<string, string>> {
47
- if (!existsSync(CLI_AUTH_FILE)) throw new Error("Canvas daemon CLI capability is unavailable");
48
- const cliAuthToken = readFileSync(CLI_AUTH_FILE, "utf-8").trim();
49
- const response = await fetch(`${BASE_URL}/api/session/${encodeURIComponent(sessionId)}/secrets/resolve`, {
50
- method: "POST",
51
- headers: {
52
- "Content-Type": "application/json",
53
- "X-Agent-Canvas-CLI-Token": cliAuthToken,
54
- },
55
- body: JSON.stringify({ fields: fieldIds }),
56
- });
57
- const raw: unknown = await response.json().catch(() => null);
58
- if (!response.ok) {
59
- const message = isRecord(raw) && typeof raw.error === "string"
60
- ? raw.error
61
- : `Secret API request failed with status ${response.status}`;
62
- const missing = isRecord(raw) && Array.isArray(raw.missing)
63
- ? raw.missing.filter((field): field is string => typeof field === "string")
64
- : [];
65
- throw new Error(missing.length > 0 ? `${message}: ${missing.join(", ")}` : message);
66
- }
67
- if (!isRecord(raw) || !Array.isArray(raw.values)) {
68
- throw new Error("Secret API returned an invalid response");
69
- }
47
+ if (!existsSync(CLI_AUTH_FILE)) throw new Error('Canvas daemon CLI capability is unavailable')
48
+ const cliAuthToken = readFileSync(CLI_AUTH_FILE, 'utf-8').trim()
49
+ const response = await fetch(`${BASE_URL}/api/session/${encodeURIComponent(sessionId)}/secrets/resolve`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'X-Agent-Canvas-CLI-Token': cliAuthToken,
54
+ },
55
+ body: JSON.stringify({ fields: fieldIds }),
56
+ })
57
+ const raw: unknown = await response.json().catch(() => null)
58
+ if (!response.ok) {
59
+ const message = isRecord(raw) && typeof raw.error === 'string'
60
+ ? raw.error
61
+ : `Secret API request failed with status ${response.status}`
62
+ const missing = isRecord(raw) && Array.isArray(raw.missing)
63
+ ? raw.missing.filter((field): field is string => typeof field === 'string')
64
+ : []
65
+ throw new Error(missing.length > 0 ? `${message}: ${missing.join(', ')}` : message)
66
+ }
67
+ if (!isRecord(raw) || !Array.isArray(raw.values)) {
68
+ throw new Error('Secret API returned an invalid response')
69
+ }
70
70
 
71
- const values = new Map<string, string>();
72
- for (const entry of raw.values) {
73
- if (!isRecord(entry) || typeof entry.id !== "string" || typeof entry.value !== "string") {
74
- throw new Error("Secret API returned an invalid value entry");
75
- }
76
- values.set(entry.id, entry.value);
77
- }
78
- return values;
71
+ const values = new Map<string, string>()
72
+ for (const entry of raw.values) {
73
+ if (!isRecord(entry) || typeof entry.id !== 'string' || typeof entry.value !== 'string') {
74
+ throw new Error('Secret API returned an invalid value entry')
75
+ }
76
+ values.set(entry.id, entry.value)
77
+ }
78
+ return values
79
79
  }
80
80
 
81
81
  function executableExists(command: string): boolean {
82
- if (command.includes("/")) return existsSync(resolve(command));
83
- return Bun.spawnSync(["which", command], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
82
+ if (command.includes('/')) return existsSync(resolve(command))
83
+ return Bun.spawnSync(['which', command], { stdout: 'ignore', stderr: 'ignore' }).exitCode === 0
84
84
  }
85
85
 
86
86
  export async function spawnWithSecretEnvironment(
87
- commandArgs: string[],
88
- assignments: SecretAssignment[],
89
- values: Map<string, string>,
87
+ commandArgs: string[],
88
+ assignments: SecretAssignment[],
89
+ values: Map<string, string>,
90
90
  ): Promise<number> {
91
- const command = commandArgs[0];
92
- if (!command) throw new Error("No command specified after --");
93
- if (!executableExists(command)) throw new Error(`Executable not found: ${command}`);
91
+ const command = commandArgs[0]
92
+ if (!command) throw new Error('No command specified after --')
93
+ if (!executableExists(command)) throw new Error(`Executable not found: ${command}`)
94
94
 
95
- const environment = { ...process.env };
96
- for (const assignment of assignments) {
97
- const value = values.get(assignment.fieldId);
98
- if (value === undefined) throw new Error(`Secret is not ready: ${assignment.fieldId}`);
99
- environment[assignment.envName] = value;
100
- }
95
+ const environment = { ...process.env }
96
+ for (const assignment of assignments) {
97
+ const value = values.get(assignment.fieldId)
98
+ if (value === undefined) throw new Error(`Secret is not ready: ${assignment.fieldId}`)
99
+ environment[assignment.envName] = value
100
+ }
101
101
 
102
- const child = Bun.spawn(commandArgs, {
103
- env: environment,
104
- stdin: "inherit",
105
- stdout: "inherit",
106
- stderr: "inherit",
107
- });
108
- return child.exited;
102
+ const child = Bun.spawn(commandArgs, {
103
+ env: environment,
104
+ stdin: 'inherit',
105
+ stdout: 'inherit',
106
+ stderr: 'inherit',
107
+ })
108
+ return child.exited
109
109
  }
110
110
 
111
111
  export async function handleExec(args: string[]): Promise<void> {
112
- const { values, positionals } = parseArgs({
113
- args,
114
- options: {
115
- session: { type: "string" },
116
- secret: { type: "string", multiple: true },
117
- },
118
- allowPositionals: true,
119
- });
112
+ const { values, positionals } = parseArgs({
113
+ args,
114
+ options: {
115
+ session: { type: 'string' },
116
+ secret: { type: 'string', multiple: true },
117
+ },
118
+ allowPositionals: true,
119
+ })
120
120
 
121
- const assignments = parseSecretAssignments(values.secret ?? []);
122
- if (positionals.length === 0) throw new Error("No command specified. Put the command after --");
121
+ const assignments = parseSecretAssignments(values.secret ?? [])
122
+ if (positionals.length === 0) throw new Error('No command specified. Put the command after --')
123
123
 
124
- const sessionId = getSessionId(values.session);
125
- await ensureDaemon();
126
- const secretValues = await resolveSecretValues(sessionId, assignments.map((assignment) => assignment.fieldId));
127
- const exitCode = await spawnWithSecretEnvironment(positionals, assignments, secretValues);
128
- process.exit(exitCode);
124
+ const sessionId = getSessionId(values.session)
125
+ await ensureDaemon()
126
+ const secretValues = await resolveSecretValues(sessionId, assignments.map((assignment) => assignment.fieldId))
127
+ const exitCode = await spawnWithSecretEnvironment(positionals, assignments, secretValues)
128
+ process.exit(exitCode)
129
129
  }
@@ -1,20 +1,20 @@
1
- import { parseArgs } from "util";
2
- import { ensureDaemon } from "../daemon-lifecycle.ts";
3
- import { getSessionId, consumeFeedback } from "../helpers.ts";
1
+ import { parseArgs } from 'util'
2
+ import { ensureDaemon } from '../daemon-lifecycle.ts'
3
+ import { consumeFeedback, getSessionId } from '../helpers.ts'
4
4
 
5
5
  export async function handleFetch(args: string[]) {
6
- const { values } = parseArgs({
7
- args,
8
- options: {
9
- session: { type: "string" },
10
- },
11
- });
6
+ const { values } = parseArgs({
7
+ args,
8
+ options: {
9
+ session: { type: 'string' },
10
+ },
11
+ })
12
12
 
13
- const sessionId = getSessionId(values.session);
14
- await ensureDaemon();
13
+ const sessionId = getSessionId(values.session)
14
+ await ensureDaemon()
15
15
 
16
- const feedback = await consumeFeedback(sessionId);
17
- if (feedback) {
18
- process.stdout.write(feedback);
19
- }
16
+ const feedback = await consumeFeedback(sessionId)
17
+ if (feedback) {
18
+ process.stdout.write(feedback)
19
+ }
20
20
  }