pmx-canvas 0.1.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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { runAgentCli } from './agent.js';
|
|
7
|
+
import { createCanvas } from '../server/index.js';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
// ── Agent CLI subcommands ────────────────────────────────────
|
|
12
|
+
// If first arg is a known subcommand (not a --flag), route to the agent CLI.
|
|
13
|
+
const AGENT_COMMANDS = new Set([
|
|
14
|
+
'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
|
|
15
|
+
'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
16
|
+
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'batch', 'validate', 'serve',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const firstArg = args[0] ?? '';
|
|
20
|
+
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const mcpServerEntry = resolve(cliDir, '..', 'mcp', 'server.ts');
|
|
22
|
+
|
|
23
|
+
function hasFlag(name: string): boolean {
|
|
24
|
+
return args.includes(`--${name}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readOption(name: string): string | undefined {
|
|
28
|
+
const inlinePrefix = `--${name}=`;
|
|
29
|
+
const inline = args.find((arg) => arg.startsWith(inlinePrefix));
|
|
30
|
+
if (inline) return inline.slice(inlinePrefix.length);
|
|
31
|
+
|
|
32
|
+
const index = args.indexOf(`--${name}`);
|
|
33
|
+
if (index !== -1 && index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
34
|
+
return args[index + 1];
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readNumberOption(name: string): number | undefined {
|
|
40
|
+
const raw = readOption(name);
|
|
41
|
+
if (!raw) return undefined;
|
|
42
|
+
const value = Number(raw);
|
|
43
|
+
return Number.isFinite(value) ? value : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readCsvOption(name: string): string[] | undefined {
|
|
47
|
+
const raw = readOption(name);
|
|
48
|
+
if (!raw) return undefined;
|
|
49
|
+
const values = raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
|
|
50
|
+
return values.length > 0 ? values : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripOption(argv: string[], name: string): string[] {
|
|
54
|
+
const stripped: string[] = [];
|
|
55
|
+
for (let index = 0; index < argv.length; index++) {
|
|
56
|
+
const arg = argv[index];
|
|
57
|
+
if (arg === `--${name}`) {
|
|
58
|
+
if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) {
|
|
59
|
+
index++;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith(`--${name}=`)) continue;
|
|
64
|
+
stripped.push(arg);
|
|
65
|
+
}
|
|
66
|
+
return stripped;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function outputJson(data: unknown): void {
|
|
70
|
+
console.log(JSON.stringify(data, null, 2));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readPidFile(path: string): number | null {
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(path)) return null;
|
|
76
|
+
const raw = readFileSync(path, 'utf-8').trim();
|
|
77
|
+
if (!raw) return null;
|
|
78
|
+
const pid = Number(raw);
|
|
79
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isProcessRunning(pid: number): boolean {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
91
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function removePidFile(path: string): void {
|
|
98
|
+
try {
|
|
99
|
+
rmSync(path, { force: true });
|
|
100
|
+
} catch {
|
|
101
|
+
// Ignore cleanup failures for stale pid files.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function isHealthy(url: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url);
|
|
108
|
+
return response.ok;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readLogTail(path: string, maxLines = 20): string | null {
|
|
115
|
+
try {
|
|
116
|
+
if (!existsSync(path)) return null;
|
|
117
|
+
const lines = readFileSync(path, 'utf-8').trim().split('\n');
|
|
118
|
+
return lines.slice(-maxLines).join('\n') || null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function waitForHealth(
|
|
125
|
+
healthUrl: string,
|
|
126
|
+
timeoutMs: number,
|
|
127
|
+
getExitMessage: () => string | null,
|
|
128
|
+
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
129
|
+
const deadline = Date.now() + timeoutMs;
|
|
130
|
+
while (Date.now() < deadline) {
|
|
131
|
+
if (await isHealthy(healthUrl)) {
|
|
132
|
+
return { ok: true };
|
|
133
|
+
}
|
|
134
|
+
const exitMessage = getExitMessage();
|
|
135
|
+
if (exitMessage) {
|
|
136
|
+
return { ok: false, reason: exitMessage };
|
|
137
|
+
}
|
|
138
|
+
await Bun.sleep(250);
|
|
139
|
+
}
|
|
140
|
+
return { ok: false, reason: `Timed out waiting for ${healthUrl}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function waitForShutdown(
|
|
144
|
+
healthUrl: string,
|
|
145
|
+
timeoutMs: number,
|
|
146
|
+
pid: number | null,
|
|
147
|
+
): Promise<boolean> {
|
|
148
|
+
const deadline = Date.now() + timeoutMs;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
const responsive = await isHealthy(healthUrl);
|
|
151
|
+
const alive = pid ? isProcessRunning(pid) : false;
|
|
152
|
+
if (!responsive && !alive) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
await Bun.sleep(250);
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function startDaemonMode(options: {
|
|
161
|
+
port: number;
|
|
162
|
+
baseArgs: string[];
|
|
163
|
+
logFile: string;
|
|
164
|
+
pidFile: string;
|
|
165
|
+
waitMs: number;
|
|
166
|
+
}): Promise<void> {
|
|
167
|
+
const healthUrl = `http://localhost:${options.port}/health`;
|
|
168
|
+
const workbenchUrl = `http://localhost:${options.port}/workbench`;
|
|
169
|
+
const existingPid = readPidFile(options.pidFile);
|
|
170
|
+
|
|
171
|
+
if (await isHealthy(healthUrl)) {
|
|
172
|
+
outputJson({
|
|
173
|
+
ok: true,
|
|
174
|
+
daemon: true,
|
|
175
|
+
alreadyRunning: true,
|
|
176
|
+
pid: existingPid,
|
|
177
|
+
url: workbenchUrl,
|
|
178
|
+
healthUrl,
|
|
179
|
+
logFile: options.logFile,
|
|
180
|
+
pidFile: options.pidFile,
|
|
181
|
+
});
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
mkdirSync(dirname(options.logFile), { recursive: true });
|
|
186
|
+
const logFd = openSync(options.logFile, 'a');
|
|
187
|
+
const childArgs = options.baseArgs.includes('--no-open')
|
|
188
|
+
? options.baseArgs
|
|
189
|
+
: [...options.baseArgs, '--no-open'];
|
|
190
|
+
const child = spawn(process.execPath, ['run', fileURLToPath(import.meta.url), ...childArgs], {
|
|
191
|
+
cwd: process.cwd(),
|
|
192
|
+
detached: true,
|
|
193
|
+
env: process.env,
|
|
194
|
+
stdio: ['ignore', logFd, logFd],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let exitMessage: string | null = null;
|
|
198
|
+
child.once('exit', (code, signal) => {
|
|
199
|
+
exitMessage = signal
|
|
200
|
+
? `Daemon exited via signal ${signal}`
|
|
201
|
+
: `Daemon exited with code ${code ?? 'unknown'}`;
|
|
202
|
+
});
|
|
203
|
+
child.unref();
|
|
204
|
+
|
|
205
|
+
const health = await waitForHealth(healthUrl, options.waitMs, () => exitMessage);
|
|
206
|
+
if (!health.ok) {
|
|
207
|
+
const logTail = readLogTail(options.logFile);
|
|
208
|
+
const details = logTail ? `${health.reason}\n\nRecent log output:\n${logTail}` : health.reason;
|
|
209
|
+
console.error(details);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mkdirSync(dirname(options.pidFile), { recursive: true });
|
|
214
|
+
writeFileSync(options.pidFile, `${child.pid}\n`, 'utf-8');
|
|
215
|
+
|
|
216
|
+
outputJson({
|
|
217
|
+
ok: true,
|
|
218
|
+
daemon: true,
|
|
219
|
+
pid: child.pid,
|
|
220
|
+
url: workbenchUrl,
|
|
221
|
+
healthUrl,
|
|
222
|
+
logFile: options.logFile,
|
|
223
|
+
pidFile: options.pidFile,
|
|
224
|
+
});
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function showServeStatus(options: {
|
|
229
|
+
port: number;
|
|
230
|
+
logFile: string;
|
|
231
|
+
pidFile: string;
|
|
232
|
+
}): Promise<void> {
|
|
233
|
+
const healthUrl = `http://localhost:${options.port}/health`;
|
|
234
|
+
const url = `http://localhost:${options.port}/workbench`;
|
|
235
|
+
const pid = readPidFile(options.pidFile);
|
|
236
|
+
const pidRunning = pid ? isProcessRunning(pid) : false;
|
|
237
|
+
const responsive = await isHealthy(healthUrl);
|
|
238
|
+
const running = responsive || pidRunning;
|
|
239
|
+
if (!running && existsSync(options.pidFile) && !pidRunning) {
|
|
240
|
+
removePidFile(options.pidFile);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
outputJson({
|
|
244
|
+
ok: true,
|
|
245
|
+
daemon: true,
|
|
246
|
+
running,
|
|
247
|
+
responsive,
|
|
248
|
+
pid,
|
|
249
|
+
pidRunning,
|
|
250
|
+
url,
|
|
251
|
+
healthUrl,
|
|
252
|
+
logFile: options.logFile,
|
|
253
|
+
pidFile: options.pidFile,
|
|
254
|
+
pidFileExists: existsSync(options.pidFile),
|
|
255
|
+
});
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function stopServeDaemon(options: {
|
|
260
|
+
port: number;
|
|
261
|
+
logFile: string;
|
|
262
|
+
pidFile: string;
|
|
263
|
+
waitMs: number;
|
|
264
|
+
}): Promise<void> {
|
|
265
|
+
const healthUrl = `http://localhost:${options.port}/health`;
|
|
266
|
+
const url = `http://localhost:${options.port}/workbench`;
|
|
267
|
+
const pid = readPidFile(options.pidFile);
|
|
268
|
+
const responsive = await isHealthy(healthUrl);
|
|
269
|
+
|
|
270
|
+
if (!pid) {
|
|
271
|
+
if (!responsive) {
|
|
272
|
+
removePidFile(options.pidFile);
|
|
273
|
+
outputJson({
|
|
274
|
+
ok: true,
|
|
275
|
+
daemon: true,
|
|
276
|
+
stopped: false,
|
|
277
|
+
running: false,
|
|
278
|
+
reason: 'No running daemon found.',
|
|
279
|
+
url,
|
|
280
|
+
healthUrl,
|
|
281
|
+
logFile: options.logFile,
|
|
282
|
+
pidFile: options.pidFile,
|
|
283
|
+
});
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
outputJson({
|
|
288
|
+
ok: false,
|
|
289
|
+
daemon: true,
|
|
290
|
+
error: `Server on port ${options.port} is responsive, but no pid file was found at ${options.pidFile}.`,
|
|
291
|
+
hint: 'Restart with `pmx-canvas serve --daemon` or provide the correct --pid-file.',
|
|
292
|
+
url,
|
|
293
|
+
healthUrl,
|
|
294
|
+
logFile: options.logFile,
|
|
295
|
+
pidFile: options.pidFile,
|
|
296
|
+
});
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isProcessRunning(pid)) {
|
|
301
|
+
removePidFile(options.pidFile);
|
|
302
|
+
outputJson({
|
|
303
|
+
ok: true,
|
|
304
|
+
daemon: true,
|
|
305
|
+
stopped: false,
|
|
306
|
+
running: responsive,
|
|
307
|
+
reason: `Removed stale pid file for ${pid}.`,
|
|
308
|
+
pid,
|
|
309
|
+
url,
|
|
310
|
+
healthUrl,
|
|
311
|
+
logFile: options.logFile,
|
|
312
|
+
pidFile: options.pidFile,
|
|
313
|
+
});
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
process.kill(pid, 'SIGTERM');
|
|
318
|
+
const stopped = await waitForShutdown(healthUrl, options.waitMs, pid);
|
|
319
|
+
const stillResponsive = await isHealthy(healthUrl);
|
|
320
|
+
const pidRunning = isProcessRunning(pid);
|
|
321
|
+
if (stopped || (!stillResponsive && !pidRunning)) {
|
|
322
|
+
removePidFile(options.pidFile);
|
|
323
|
+
outputJson({
|
|
324
|
+
ok: true,
|
|
325
|
+
daemon: true,
|
|
326
|
+
stopped: true,
|
|
327
|
+
pid,
|
|
328
|
+
url,
|
|
329
|
+
healthUrl,
|
|
330
|
+
logFile: options.logFile,
|
|
331
|
+
pidFile: options.pidFile,
|
|
332
|
+
});
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
outputJson({
|
|
337
|
+
ok: false,
|
|
338
|
+
daemon: true,
|
|
339
|
+
stopped: false,
|
|
340
|
+
error: `Timed out waiting for daemon ${pid} to stop.`,
|
|
341
|
+
pid,
|
|
342
|
+
responsive: stillResponsive,
|
|
343
|
+
pidRunning,
|
|
344
|
+
url,
|
|
345
|
+
healthUrl,
|
|
346
|
+
logFile: options.logFile,
|
|
347
|
+
pidFile: options.pidFile,
|
|
348
|
+
});
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function runMcpServerProcess(): Promise<void> {
|
|
353
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
354
|
+
const child = spawn(process.execPath, ['run', mcpServerEntry], {
|
|
355
|
+
stdio: 'inherit',
|
|
356
|
+
env: process.env,
|
|
357
|
+
});
|
|
358
|
+
child.on('error', rejectPromise);
|
|
359
|
+
child.on('exit', (code, signal) => {
|
|
360
|
+
if (code === 0) {
|
|
361
|
+
resolvePromise();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
rejectPromise(new Error(
|
|
365
|
+
signal
|
|
366
|
+
? `MCP server exited via signal ${signal}`
|
|
367
|
+
: `MCP server exited with code ${code ?? 'unknown'}`,
|
|
368
|
+
));
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const serveSubcommand = firstArg === 'serve' ? args[1] ?? '' : '';
|
|
374
|
+
|
|
375
|
+
if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
|
|
376
|
+
const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
|
|
377
|
+
const daemonLogFile = resolve(readOption('log-file') ?? `.pmx-canvas/daemon-${port}.log`);
|
|
378
|
+
const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
|
|
379
|
+
const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
|
|
380
|
+
|
|
381
|
+
if (hasFlag('help') || args.includes('-h')) {
|
|
382
|
+
console.log(`
|
|
383
|
+
pmx-canvas serve ${serveSubcommand}
|
|
384
|
+
|
|
385
|
+
Usage:
|
|
386
|
+
pmx-canvas serve ${serveSubcommand} [--port=PORT] [--pid-file=PATH] [--log-file=PATH]${serveSubcommand === 'stop' ? ' [--wait-ms=MS]' : ''}
|
|
387
|
+
`);
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (serveSubcommand === 'status') {
|
|
392
|
+
await showServeStatus({
|
|
393
|
+
port,
|
|
394
|
+
logFile: daemonLogFile,
|
|
395
|
+
pidFile: daemonPidFile,
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
await stopServeDaemon({
|
|
399
|
+
port,
|
|
400
|
+
logFile: daemonLogFile,
|
|
401
|
+
pidFile: daemonPidFile,
|
|
402
|
+
waitMs: daemonWaitMs,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (firstArg === 'serve') {
|
|
408
|
+
args.shift();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (AGENT_COMMANDS.has(firstArg) && firstArg !== 'serve') {
|
|
412
|
+
await runAgentCli(args);
|
|
413
|
+
} else if (args.includes('--mcp')) {
|
|
414
|
+
// MCP server mode: stdio transport, auto-starts canvas on first tool call
|
|
415
|
+
await runMcpServerProcess();
|
|
416
|
+
} else {
|
|
417
|
+
// "serve" is also accessible via flags (backward compat)
|
|
418
|
+
const port = parseInt(readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? '4313');
|
|
419
|
+
const demo = hasFlag('demo');
|
|
420
|
+
const noOpen = hasFlag('no-open');
|
|
421
|
+
const daemon = hasFlag('daemon');
|
|
422
|
+
const themeArg = readOption('theme');
|
|
423
|
+
const webviewAutomation = hasFlag('webview-automation');
|
|
424
|
+
const webviewBackend = readOption('webview-backend');
|
|
425
|
+
const webviewChromePath = readOption('webview-chrome-path');
|
|
426
|
+
const webviewChromeArgv = readCsvOption('webview-chrome-argv');
|
|
427
|
+
const webviewDataDir = readOption('webview-data-dir');
|
|
428
|
+
const webviewWidth = readNumberOption('webview-width');
|
|
429
|
+
const webviewHeight = readNumberOption('webview-height');
|
|
430
|
+
const daemonLogFile = resolve(readOption('log-file') ?? `.pmx-canvas/daemon-${port}.log`);
|
|
431
|
+
const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
|
|
432
|
+
const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
|
|
433
|
+
const webviewBackendOption: 'chrome' | 'webkit' | undefined =
|
|
434
|
+
webviewBackend === 'chrome' || webviewBackend === 'webkit'
|
|
435
|
+
? webviewBackend
|
|
436
|
+
: undefined;
|
|
437
|
+
if (themeArg && ['dark', 'light', 'high-contrast'].includes(themeArg)) {
|
|
438
|
+
process.env.PMX_CANVAS_THEME = themeArg;
|
|
439
|
+
}
|
|
440
|
+
const help = hasFlag('help') || args.includes('-h');
|
|
441
|
+
|
|
442
|
+
if (help) {
|
|
443
|
+
console.log(`
|
|
444
|
+
pmx-canvas — Spatial canvas workbench for coding agents
|
|
445
|
+
|
|
446
|
+
Usage:
|
|
447
|
+
pmx-canvas [server-options] Start the canvas server
|
|
448
|
+
pmx-canvas <command> [options] Run agent CLI commands
|
|
449
|
+
|
|
450
|
+
Server options:
|
|
451
|
+
--port=PORT Server port (default: 4313)
|
|
452
|
+
--demo Start with sample nodes
|
|
453
|
+
--no-open Don't open browser automatically
|
|
454
|
+
--daemon Start in detached background mode and wait for health
|
|
455
|
+
--log-file=PATH Daemon log file (default: ./.pmx-canvas/daemon-${port}.log)
|
|
456
|
+
--pid-file=PATH Optional daemon PID file (default: ./.pmx-canvas/daemon-${port}.pid)
|
|
457
|
+
--wait-ms=MS Health-check wait budget for daemon mode (default: 10000)
|
|
458
|
+
--theme=THEME Theme: dark (default), light, high-contrast
|
|
459
|
+
--webview-automation Start a headless Bun.WebView automation session for /workbench
|
|
460
|
+
--webview-backend=BACKEND Bun.WebView backend: chrome or webkit
|
|
461
|
+
--webview-width=PX Automation WebView width (default: 1280)
|
|
462
|
+
--webview-height=PX Automation WebView height (default: 800)
|
|
463
|
+
--webview-chrome-path=PATH Chrome/Chromium executable for Bun.WebView
|
|
464
|
+
--webview-chrome-argv=CSV Extra Chrome args for Bun.WebView, comma-separated
|
|
465
|
+
--webview-data-dir=PATH Persist automation browser storage in PATH
|
|
466
|
+
--mcp Run as MCP server (stdio transport)
|
|
467
|
+
--help, -h Show this help
|
|
468
|
+
|
|
469
|
+
Agent CLI (works against running server):
|
|
470
|
+
node add|list|get|update|remove Manage nodes
|
|
471
|
+
edge add|list|remove Manage edges
|
|
472
|
+
webview status|start|evaluate|resize|screenshot|stop
|
|
473
|
+
Manage Bun.WebView automation session
|
|
474
|
+
search <query> Search nodes
|
|
475
|
+
open Open the current workbench in a browser
|
|
476
|
+
layout Full canvas state
|
|
477
|
+
status Quick summary
|
|
478
|
+
serve status Show daemon status for a given port/pid file
|
|
479
|
+
serve stop Stop a daemon started with serve --daemon
|
|
480
|
+
arrange [--layout grid|column|flow] Auto-arrange nodes
|
|
481
|
+
batch --file ./ops.json Run a JSON batch of operations
|
|
482
|
+
validate Check layout collisions and containment
|
|
483
|
+
validate spec Validate json-render/graph payloads without creating nodes
|
|
484
|
+
watch [--json] [--events ...] Watch low-token semantic canvas changes
|
|
485
|
+
focus <node-id> Pan to node
|
|
486
|
+
pin <ids...> | --list | --clear Manage context pins
|
|
487
|
+
undo / redo / history Time travel
|
|
488
|
+
snapshot save|list|restore|diff|delete
|
|
489
|
+
Manage snapshots
|
|
490
|
+
group create|add|remove Manage groups
|
|
491
|
+
web-artifact build Build bundled web artifacts
|
|
492
|
+
clear --yes Clear canvas
|
|
493
|
+
code-graph File dependencies
|
|
494
|
+
spatial Spatial analysis
|
|
495
|
+
watch Semantic watch stream
|
|
496
|
+
|
|
497
|
+
Run any command with --help for details and examples:
|
|
498
|
+
pmx-canvas node add --help
|
|
499
|
+
pmx-canvas edge --help
|
|
500
|
+
|
|
501
|
+
MCP Integration:
|
|
502
|
+
Add to your agent's MCP config:
|
|
503
|
+
{
|
|
504
|
+
"mcpServers": {
|
|
505
|
+
"canvas": {
|
|
506
|
+
"command": "bunx",
|
|
507
|
+
"args": ["pmx-canvas", "--mcp"]
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
Examples:
|
|
513
|
+
pmx-canvas Start server + browser
|
|
514
|
+
pmx-canvas --no-open --demo Start server headless with sample data
|
|
515
|
+
pmx-canvas serve --daemon --no-open Start a reliable background daemon
|
|
516
|
+
pmx-canvas serve status Show daemon health and pid status
|
|
517
|
+
pmx-canvas serve stop Stop the default daemon for this port
|
|
518
|
+
pmx-canvas --no-open --webview-automation Start server + headless Bun.WebView automation
|
|
519
|
+
pmx-canvas --webview-automation --webview-backend=chrome Start browser + Chrome-backed automation
|
|
520
|
+
pmx-canvas node add --type markdown --title "Hello World" Add a node
|
|
521
|
+
pmx-canvas node add --type webpage --url "https://example.com" Add a webpage node
|
|
522
|
+
pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
|
|
523
|
+
pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
|
|
524
|
+
pmx-canvas node list List all nodes
|
|
525
|
+
pmx-canvas node schema --type json-render Show running-server schema info
|
|
526
|
+
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx
|
|
527
|
+
pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
|
|
528
|
+
pmx-canvas open Open the workbench in a browser
|
|
529
|
+
pmx-canvas webview status Show WebView automation status
|
|
530
|
+
pmx-canvas webview screenshot --output ./canvas.png Save a WebView screenshot
|
|
531
|
+
pmx-canvas search "auth" Find nodes
|
|
532
|
+
pmx-canvas arrange --layout column Auto-arrange
|
|
533
|
+
pmx-canvas batch --file ./canvas-ops.json Run batch canvas ops
|
|
534
|
+
pmx-canvas validate Check layout collisions
|
|
535
|
+
pmx-canvas watch --events context-pin,move-end Watch semantic deltas
|
|
536
|
+
pmx-canvas clear --dry-run Preview destructive op
|
|
537
|
+
`);
|
|
538
|
+
process.exit(0);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (daemon) {
|
|
542
|
+
const baseArgs = stripOption(stripOption(stripOption(stripOption(args, 'daemon'), 'log-file'), 'pid-file'), 'wait-ms');
|
|
543
|
+
await startDaemonMode({
|
|
544
|
+
port,
|
|
545
|
+
baseArgs,
|
|
546
|
+
logFile: daemonLogFile,
|
|
547
|
+
pidFile: daemonPidFile,
|
|
548
|
+
waitMs: daemonWaitMs,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const canvas = createCanvas({ port });
|
|
553
|
+
const automationWebView =
|
|
554
|
+
webviewAutomation
|
|
555
|
+
? {
|
|
556
|
+
...(webviewBackendOption ? { backend: webviewBackendOption } : {}),
|
|
557
|
+
...(webviewChromePath ? { chromePath: webviewChromePath } : {}),
|
|
558
|
+
...(webviewChromeArgv ? { chromeArgv: webviewChromeArgv } : {}),
|
|
559
|
+
...(webviewDataDir ? { dataStoreDir: webviewDataDir } : {}),
|
|
560
|
+
...(webviewWidth !== undefined ? { width: webviewWidth } : {}),
|
|
561
|
+
...(webviewHeight !== undefined ? { height: webviewHeight } : {}),
|
|
562
|
+
}
|
|
563
|
+
: false;
|
|
564
|
+
try {
|
|
565
|
+
await canvas.start({ open: !noOpen, automationWebView });
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
568
|
+
console.error(`Failed to start PMX Canvas: ${message}`);
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (demo && canvas.getLayout().nodes.length === 0) {
|
|
573
|
+
const n1 = canvas.addNode({
|
|
574
|
+
type: 'markdown',
|
|
575
|
+
title: 'Welcome to PMX Canvas',
|
|
576
|
+
content: '# PMX Canvas Workbench\n\nA spatial canvas for coding agents.\n\n## Features\n- Infinite 2D canvas with pan/zoom\n- Multiple node types\n- Edges between nodes\n- Real-time SSE updates\n- HTTP API for agent control',
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const n2 = canvas.addNode({
|
|
580
|
+
type: 'markdown',
|
|
581
|
+
title: 'Getting Started',
|
|
582
|
+
content: `# Quick Start\n\n\`\`\`bash\n# Add a node via CLI\npmx-canvas node add --type markdown --title "Hello" --content "# World"\n\n# List nodes\npmx-canvas node list\n\n# Get canvas state\npmx-canvas layout\n\`\`\``,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const n3 = canvas.addNode({
|
|
586
|
+
type: 'status',
|
|
587
|
+
title: 'Agent Status',
|
|
588
|
+
content: 'Ready',
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
canvas.addEdge({ from: n1, to: n2, type: 'flow', label: 'next' });
|
|
592
|
+
canvas.addEdge({ from: n2, to: n3, type: 'flow' });
|
|
593
|
+
canvas.arrange('grid');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
|
|
597
|
+
console.log(` Health: http://localhost:${canvas.port}/health\n`);
|
|
598
|
+
if (webviewAutomation) {
|
|
599
|
+
const webviewStatus = canvas.getAutomationWebViewStatus();
|
|
600
|
+
console.log(` Bun.WebView automation: ${webviewStatus.active ? 'active' : 'inactive'}`);
|
|
601
|
+
if (webviewStatus.lastError) {
|
|
602
|
+
console.log(` Last WebView error: ${webviewStatus.lastError}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
console.log(' Agent CLI:');
|
|
606
|
+
console.log(' pmx-canvas node add --type markdown --title "Hello"');
|
|
607
|
+
console.log(' pmx-canvas node list');
|
|
608
|
+
console.log(' pmx-canvas search "query"');
|
|
609
|
+
console.log(' pmx-canvas --help (all commands)');
|
|
610
|
+
console.log('\n Press Ctrl+C to stop\n');
|
|
611
|
+
|
|
612
|
+
process.on('SIGINT', () => {
|
|
613
|
+
console.log('\nShutting down...');
|
|
614
|
+
canvas.stop();
|
|
615
|
+
process.exit(0);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
process.on('SIGTERM', () => {
|
|
619
|
+
canvas.stop();
|
|
620
|
+
process.exit(0);
|
|
621
|
+
});
|
|
622
|
+
}
|
package/src/cli/watch.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_SEMANTIC_WATCH_EVENT_TYPES,
|
|
3
|
+
formatCompactWatchEvent,
|
|
4
|
+
SemanticWatchReducer,
|
|
5
|
+
type SemanticWatchEvent,
|
|
6
|
+
type SemanticWatchEventType,
|
|
7
|
+
type SseMessage,
|
|
8
|
+
} from '../shared/semantic-attention.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
ALL_SEMANTIC_WATCH_EVENT_TYPES,
|
|
12
|
+
formatCompactWatchEvent,
|
|
13
|
+
SemanticWatchReducer,
|
|
14
|
+
};
|
|
15
|
+
export type {
|
|
16
|
+
SemanticWatchEvent,
|
|
17
|
+
SemanticWatchEventType,
|
|
18
|
+
SseMessage,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function parseSemanticEventFilter(raw: string | undefined): Set<SemanticWatchEventType> {
|
|
22
|
+
const all = new Set<SemanticWatchEventType>(ALL_SEMANTIC_WATCH_EVENT_TYPES);
|
|
23
|
+
if (!raw || raw.trim().length === 0) return all;
|
|
24
|
+
|
|
25
|
+
const values = raw
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((value) => value.trim())
|
|
28
|
+
.filter((value): value is SemanticWatchEventType => all.has(value as SemanticWatchEventType));
|
|
29
|
+
return new Set(values);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function* parseSseStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<SseMessage> {
|
|
33
|
+
const reader = stream.getReader();
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
let buffer = '';
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
while (true) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done) break;
|
|
41
|
+
buffer += decoder.decode(value, { stream: true });
|
|
42
|
+
|
|
43
|
+
while (true) {
|
|
44
|
+
const separatorIndex = buffer.indexOf('\n\n');
|
|
45
|
+
if (separatorIndex === -1) break;
|
|
46
|
+
const rawEvent = buffer.slice(0, separatorIndex);
|
|
47
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
48
|
+
const parsed = parseSseMessage(rawEvent);
|
|
49
|
+
if (parsed) yield parsed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buffer += decoder.decode();
|
|
54
|
+
if (buffer.trim().length > 0) {
|
|
55
|
+
const parsed = parseSseMessage(buffer);
|
|
56
|
+
if (parsed) yield parsed;
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseSseMessage(raw: string): SseMessage | null {
|
|
64
|
+
const lines = raw.split(/\r?\n/);
|
|
65
|
+
let event = 'message';
|
|
66
|
+
let id: string | undefined;
|
|
67
|
+
const dataLines: string[] = [];
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith(':') || line.trim().length === 0) continue;
|
|
71
|
+
const separatorIndex = line.indexOf(':');
|
|
72
|
+
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
73
|
+
let value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1);
|
|
74
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
75
|
+
|
|
76
|
+
if (field === 'event') event = value;
|
|
77
|
+
if (field === 'id') id = value;
|
|
78
|
+
if (field === 'data') dataLines.push(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (dataLines.length === 0) return null;
|
|
82
|
+
const rawData = dataLines.join('\n');
|
|
83
|
+
try {
|
|
84
|
+
return { event, id, data: JSON.parse(rawData) };
|
|
85
|
+
} catch {
|
|
86
|
+
return { event, id, data: rawData };
|
|
87
|
+
}
|
|
88
|
+
}
|