pmx-canvas 0.4.8 → 0.5.1
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/.github/extensions/pmx-canvas/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +156 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3071 -796
- package/dist/canvas/index.js +122 -104
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/pump.d.ts +9 -0
- package/dist/types/client/App.d.ts +1 -1
- package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
- package/dist/types/client/canvas/BarHint.d.ts +22 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
- package/dist/types/client/canvas/CommandBar.d.ts +10 -0
- package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
- package/dist/types/client/canvas/EmptyState.d.ts +11 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
- package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/Minimap.d.ts +12 -1
- package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
- package/dist/types/client/canvas/ToolRail.d.ts +21 -0
- package/dist/types/client/canvas/TopBar.d.ts +7 -0
- package/dist/types/client/canvas/canvas-area.d.ts +39 -0
- package/dist/types/client/canvas/create-in-view.d.ts +18 -0
- package/dist/types/client/canvas/import-files.d.ts +8 -0
- package/dist/types/client/canvas/kind-colors.d.ts +3 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
- package/dist/types/client/canvas/use-now.d.ts +6 -0
- package/dist/types/client/icons.d.ts +11 -4
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
- package/dist/types/client/state/canvas-store.d.ts +49 -5
- package/dist/types/client/state/human-store.d.ts +26 -0
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +37 -5
- package/dist/types/client/state/node-factory.d.ts +0 -1
- package/dist/types/client/state/presence-store.d.ts +78 -0
- package/dist/types/client/state/session-store.d.ts +176 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/server/agent-presence.d.ts +192 -0
- package/dist/types/server/ax-gate-ttl.d.ts +6 -0
- package/dist/types/server/ax-interaction.d.ts +4 -10
- package/dist/types/server/ax-state-manager.d.ts +21 -2
- package/dist/types/server/ax-state.d.ts +49 -3
- package/dist/types/server/ax-wait.d.ts +13 -0
- package/dist/types/server/canvas-db.d.ts +9 -1
- package/dist/types/server/canvas-operations.d.ts +11 -0
- package/dist/types/server/canvas-state.d.ts +27 -13
- package/dist/types/server/canvas-validation.d.ts +0 -14
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/human-presence.d.ts +40 -0
- package/dist/types/server/index.d.ts +29 -3
- package/dist/types/server/intent-registry.d.ts +7 -0
- package/dist/types/server/mutation-history.d.ts +15 -2
- package/dist/types/server/operations/invoker.d.ts +13 -2
- package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
- package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
- package/dist/types/server/operations/registry.d.ts +11 -0
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/scope-fence.d.ts +62 -0
- package/dist/types/server/server.d.ts +0 -12
- package/dist/types/shared/agent-presence.d.ts +130 -0
- package/dist/types/shared/approval-gates.d.ts +19 -0
- package/dist/types/shared/auto-arrange.d.ts +0 -1
- package/dist/types/shared/ax-kinds.d.ts +11 -0
- package/dist/types/shared/human-presence.d.ts +31 -0
- package/dist/types/shared/iframe-probe.d.ts +8 -0
- package/dist/types/shared/scope-fence.d.ts +21 -0
- package/docs/RELEASE.md +20 -0
- package/docs/ax-host-adapter-contract.md +30 -14
- package/docs/cli.md +37 -1
- package/docs/environment.md +3 -0
- package/docs/http-api.md +222 -5
- package/docs/mcp.md +6 -5
- package/docs/node-types.md +3 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/multi-agent-live.png +0 -0
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +1 -1
- package/package.json +4 -4
- package/skills/pmx-canvas/SKILL.md +112 -17
- package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
- package/skills/pmx-canvas/references/full-reference.md +55 -16
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
- package/skills/pmx-canvas-testing/SKILL.md +17 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/copilot.ts +15 -8
- package/src/cli/commands/nodes.ts +1 -19
- package/src/cli/commands/pump.ts +263 -0
- package/src/cli/index.ts +2 -5
- package/src/cli/shared.ts +1 -1
- package/src/client/App.tsx +209 -585
- package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
- package/src/client/canvas/AttentionHistory.tsx +0 -10
- package/src/client/canvas/BarHint.tsx +49 -0
- package/src/client/canvas/CanvasNode.tsx +488 -136
- package/src/client/canvas/CanvasViewport.tsx +190 -94
- package/src/client/canvas/CommandBar.tsx +140 -0
- package/src/client/canvas/CommandPalette.tsx +133 -58
- package/src/client/canvas/ConnectionBanner.tsx +65 -0
- package/src/client/canvas/ContextMenu.tsx +88 -27
- package/src/client/canvas/ContextPinBar.tsx +2 -2
- package/src/client/canvas/EdgeLayer.tsx +121 -38
- package/src/client/canvas/EmptyState.tsx +93 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
- package/src/client/canvas/ExternalWriters.tsx +308 -0
- package/src/client/canvas/FocusFieldLayer.tsx +12 -1
- package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
- package/src/client/canvas/IntentLayer.tsx +1 -1
- package/src/client/canvas/Minimap.tsx +117 -190
- package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
- package/src/client/canvas/SelectionBar.tsx +175 -23
- package/src/client/canvas/SessionPanel.tsx +481 -0
- package/src/client/canvas/SessionReceipt.tsx +141 -0
- package/src/client/canvas/ShortcutOverlay.tsx +21 -6
- package/src/client/canvas/SnapshotPanel.tsx +166 -80
- package/src/client/canvas/TextPrompt.tsx +128 -0
- package/src/client/canvas/ToolRail.tsx +447 -0
- package/src/client/canvas/TopBar.tsx +308 -0
- package/src/client/canvas/auto-fit.ts +1 -5
- package/src/client/canvas/canvas-area.ts +74 -0
- package/src/client/canvas/create-in-view.ts +48 -0
- package/src/client/canvas/import-files.ts +64 -0
- package/src/client/canvas/kind-colors.ts +22 -0
- package/src/client/canvas/snap-guides.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +54 -0
- package/src/client/canvas/use-now.ts +17 -0
- package/src/client/canvas/use-pan-zoom.ts +13 -2
- package/src/client/icons.tsx +57 -18
- package/src/client/nodes/ExtAppFrame.tsx +86 -20
- package/src/client/nodes/GroupNode.tsx +9 -28
- package/src/client/nodes/InlineFormatBar.tsx +4 -3
- package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
- package/src/client/nodes/MarkdownNode.tsx +59 -24
- package/src/client/nodes/MdFormatBar.tsx +2 -4
- package/src/client/nodes/inline-editor-commands.ts +17 -6
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/state/attention-bridge.ts +3 -0
- package/src/client/state/canvas-store.ts +285 -90
- package/src/client/state/human-store.ts +175 -0
- package/src/client/state/iframe-mode.ts +33 -9
- package/src/client/state/intent-bridge.ts +75 -13
- package/src/client/state/node-factory.ts +2 -8
- package/src/client/state/presence-store.ts +168 -0
- package/src/client/state/session-store.ts +419 -0
- package/src/client/state/sse-bridge.ts +93 -30
- package/src/client/theme/global.css +3071 -796
- package/src/client/types.ts +3 -1
- package/src/client/utils/platform.ts +6 -1
- package/src/mcp/canvas-access.ts +2 -2
- package/src/server/agent-presence.ts +755 -0
- package/src/server/ax-context.ts +45 -0
- package/src/server/ax-gate-ttl.ts +61 -0
- package/src/server/ax-interaction.ts +29 -1
- package/src/server/ax-state-manager.ts +78 -6
- package/src/server/ax-state.ts +82 -15
- package/src/server/ax-wait.ts +33 -0
- package/src/server/canvas-db.ts +103 -33
- package/src/server/canvas-operations.ts +58 -14
- package/src/server/canvas-state.ts +131 -58
- package/src/server/canvas-validation.ts +3 -53
- package/src/server/demo-state.json +2 -70
- package/src/server/html-surface.ts +11 -2
- package/src/server/human-presence.ts +160 -0
- package/src/server/index.ts +111 -6
- package/src/server/intent-registry.ts +13 -0
- package/src/server/mutation-history.ts +47 -2
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/http.ts +10 -1
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/invoker.ts +33 -6
- package/src/server/operations/ops/ax-presence.ts +62 -0
- package/src/server/operations/ops/ax-read.ts +8 -0
- package/src/server/operations/ops/ax-state.ts +106 -1
- package/src/server/operations/ops/ax-timeline.ts +54 -5
- package/src/server/operations/ops/ax-work.ts +46 -0
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/edges.ts +88 -2
- package/src/server/operations/ops/groups.ts +4 -3
- package/src/server/operations/ops/human-presence.ts +35 -0
- package/src/server/operations/ops/json-render.ts +11 -1
- package/src/server/operations/ops/nodes.ts +77 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +7 -4
- package/src/server/operations/registry.ts +196 -1
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +207 -0
- package/src/server/server.ts +177 -79
- package/src/server/trace-manager.ts +0 -2
- package/src/server/web-artifacts.ts +0 -1
- package/src/shared/agent-presence.ts +162 -0
- package/src/shared/approval-gates.ts +35 -0
- package/src/shared/auto-arrange.ts +121 -13
- package/src/shared/ax-kinds.ts +23 -0
- package/src/shared/human-presence.ts +30 -0
- package/src/shared/iframe-probe.ts +8 -0
- package/src/shared/scope-fence.ts +31 -0
- package/dist/types/client/canvas/DockedNode.d.ts +0 -4
- package/src/client/canvas/DockedNode.tsx +0 -165
package/src/cli/agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copilot adapter commands: copilot install-extension.
|
|
2
2
|
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { cmd, die, getStringFlag, output, parseFlags, showCommandHelp } from '../shared.js';
|
|
7
7
|
|
|
@@ -17,18 +17,23 @@ cmd(
|
|
|
17
17
|
const { flags } = parseFlags(args);
|
|
18
18
|
if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const sourcePaths = [
|
|
21
|
+
fileURLToPath(new URL('../../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url)),
|
|
22
|
+
fileURLToPath(new URL('../../../.github/extensions/pmx-canvas/steering-delivery.mjs', import.meta.url)),
|
|
23
|
+
];
|
|
24
|
+
const missingSource = sourcePaths.find((sourcePath) => !existsSync(sourcePath));
|
|
25
|
+
if (missingSource) {
|
|
26
|
+
die('Bundled Copilot extension adapter not found.', `Expected at ${missingSource}`);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const targetPath =
|
|
26
30
|
getStringFlag(flags, 'target') ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
|
|
31
|
+
const targetPaths = sourcePaths.map((sourcePath) => join(dirname(targetPath), basename(sourcePath)));
|
|
27
32
|
const dryRun = flags['dry-run'] === true;
|
|
28
|
-
const targetExists = existsSync(
|
|
33
|
+
const targetExists = targetPaths.some((candidate) => existsSync(candidate));
|
|
29
34
|
|
|
30
35
|
if (dryRun) {
|
|
31
|
-
output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
|
|
36
|
+
output({ ok: true, dryRun: true, sourcePath: sourcePaths[0], targetPath, targetExists, wrote: false });
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -37,7 +42,9 @@ cmd(
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
for (let index = 0; index < sourcePaths.length; index += 1) {
|
|
46
|
+
copyFileSync(sourcePaths[index], targetPaths[index]);
|
|
47
|
+
}
|
|
48
|
+
output({ ok: true, dryRun: false, sourcePath: sourcePaths[0], targetPath, wrote: true });
|
|
42
49
|
},
|
|
43
50
|
);
|
|
@@ -124,7 +124,6 @@ function summarizeNodeResult(node: Record<string, unknown>): Record<string, unkn
|
|
|
124
124
|
...(node.size !== undefined ? { size: node.size } : {}),
|
|
125
125
|
...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
|
|
126
126
|
...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
|
|
127
|
-
...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
|
|
128
127
|
...(node.path !== undefined ? { path: node.path } : {}),
|
|
129
128
|
...(node.url !== undefined ? { url: node.url } : {}),
|
|
130
129
|
...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
|
|
@@ -827,8 +826,6 @@ cmd(
|
|
|
827
826
|
'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
|
|
828
827
|
'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
|
|
829
828
|
'pmx-canvas node update <node-id> --pinned true',
|
|
830
|
-
'pmx-canvas node update <node-id> --dock-position right',
|
|
831
|
-
'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
|
|
832
829
|
'pmx-canvas node update <node-id> --lock-arrange',
|
|
833
830
|
'pmx-canvas node update <node-id> --width 840 --full # full node payload instead of the compact envelope',
|
|
834
831
|
],
|
|
@@ -910,25 +907,10 @@ cmd(
|
|
|
910
907
|
|
|
911
908
|
if (pinned !== undefined) body.pinned = pinned;
|
|
912
909
|
|
|
913
|
-
// --dock-position left|right|none : dock a node into the top HUD or undock it.
|
|
914
|
-
// `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
|
|
915
|
-
// guard so the null survives JSON.stringify to the server (which accepts a
|
|
916
|
-
// top-level dockPosition: null). HTTP PATCH already supports this; this is the
|
|
917
|
-
// CLI path the report (#40) found missing.
|
|
918
|
-
const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
|
|
919
|
-
let dockPosition: 'left' | 'right' | null | undefined;
|
|
920
|
-
if (dockRaw !== undefined) {
|
|
921
|
-
const v = dockRaw.trim().toLowerCase();
|
|
922
|
-
if (v === 'left' || v === 'right') dockPosition = v;
|
|
923
|
-
else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
|
|
924
|
-
else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
|
|
925
|
-
}
|
|
926
|
-
if (dockPosition !== undefined) body.dockPosition = dockPosition;
|
|
927
|
-
|
|
928
910
|
if (Object.keys(body).length === 0) {
|
|
929
911
|
die(
|
|
930
912
|
'No updates specified',
|
|
931
|
-
'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned,
|
|
913
|
+
'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
|
|
932
914
|
);
|
|
933
915
|
}
|
|
934
916
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pmx-canvas pump` — the generic reactive steering loop for hosts without a
|
|
3
|
+
* native adapter (Amp, Codex, any CLI agent). Long-polls the per-consumer
|
|
4
|
+
* delivery claim and hands each steer to a shell command; the command's exit
|
|
5
|
+
* code gates the per-consumer mark. This is the same design as the Copilot
|
|
6
|
+
* extension's delivery pump (send-then-mark, awaiting-mark memory, startup
|
|
7
|
+
* backlog watermark, old-server backoff) with the "send into the host" step
|
|
8
|
+
* generalized to "run your command".
|
|
9
|
+
*
|
|
10
|
+
* Safety: the steer text reaches the command as env (PMX_STEER_MESSAGE) and
|
|
11
|
+
* stdin — never spliced into the shell string — so hostile steer content
|
|
12
|
+
* cannot inject into the command line. `{message}` in the template expands to
|
|
13
|
+
* the SHELL VARIABLE reference "$PMX_STEER_MESSAGE" (quoted), not the text.
|
|
14
|
+
* On Windows that guarantee does not hold — cmd.exe expands `%VAR%` and then
|
|
15
|
+
* re-parses the result — so the placeholders are refused there and the command
|
|
16
|
+
* takes the message from stdin instead. See `renderExecTemplate`.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import {
|
|
20
|
+
cmd,
|
|
21
|
+
getBaseUrl,
|
|
22
|
+
getStringFlag,
|
|
23
|
+
invokeOperation,
|
|
24
|
+
parseFlags,
|
|
25
|
+
requireFlag,
|
|
26
|
+
showCommandHelp,
|
|
27
|
+
} from '../shared.js';
|
|
28
|
+
|
|
29
|
+
interface PendingSteer {
|
|
30
|
+
id: string;
|
|
31
|
+
message: string;
|
|
32
|
+
source?: string | null;
|
|
33
|
+
target?: string | null;
|
|
34
|
+
createdAt?: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function claimPending(consumer: string, waitMs: number): Promise<{ pending: PendingSteer[]; tookMs: number }> {
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
const url = `${getBaseUrl()}/api/canvas/ax/delivery/pending?consumer=${encodeURIComponent(consumer)}&limit=1&order=oldest${waitMs > 0 ? `&waitMs=${waitMs}` : ''}`;
|
|
40
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(waitMs + 15_000) });
|
|
41
|
+
if (!res.ok) throw new Error(`pending claim failed: HTTP ${res.status}`);
|
|
42
|
+
const body = (await res.json()) as { pending?: PendingSteer[] };
|
|
43
|
+
return { pending: Array.isArray(body.pending) ? body.pending : [], tookMs: Date.now() - started };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function markDelivered(id: string, consumer: string): Promise<boolean> {
|
|
47
|
+
const result = (await invokeOperation('ax.delivery.mark', { id, consumer })) as { delivered?: boolean };
|
|
48
|
+
return result.delivered === true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Windows has no `sh`, so the exec ran nowhere there. cmd.exe can host the
|
|
53
|
+
* stdin pattern, but NOT the `{message}` / `{id}` placeholders: those expand to
|
|
54
|
+
* an env-var reference so hostile steer content is never spliced into the
|
|
55
|
+
* command string, and cmd.exe expands `%VAR%` before re-parsing — a message
|
|
56
|
+
* containing `& del …` would run. Refuse the placeholders there instead of
|
|
57
|
+
* silently reintroducing the injection the substitution exists to prevent.
|
|
58
|
+
*/
|
|
59
|
+
export function renderExecTemplate(template: string, windows = process.platform === 'win32'): string {
|
|
60
|
+
if (windows) {
|
|
61
|
+
if (template.includes('{message}') || template.includes('{id}')) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'pmx-canvas pump: {message} and {id} are not supported on Windows — cmd.exe re-parses expanded variables, so a steer could inject commands. Read the message from stdin instead (the exec receives it), or use the PMX_STEER_MESSAGE / PMX_STEER_ID environment variables inside your own script.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return template;
|
|
67
|
+
}
|
|
68
|
+
return template.replaceAll('{message}', '"$PMX_STEER_MESSAGE"').replaceAll('{id}', '"$PMX_STEER_ID"');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runExec(template: string, steer: PendingSteer, consumer: string): Promise<number> {
|
|
72
|
+
const rendered = renderExecTemplate(template);
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
// `shell: true` picks sh on POSIX and ComSpec/cmd.exe on Windows, and sets
|
|
75
|
+
// windowsVerbatimArguments itself — hand-rolling the cmd.exe form escaped
|
|
76
|
+
// the quotes inside the command, so `bun "C:\path\x.mjs"` arrived as
|
|
77
|
+
// `""C:\path\x.mjs""`. Let Node own the platform quoting.
|
|
78
|
+
const child = spawn(rendered, {
|
|
79
|
+
shell: true,
|
|
80
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
PMX_STEER_MESSAGE: steer.message,
|
|
84
|
+
PMX_STEER_ID: steer.id,
|
|
85
|
+
PMX_STEER_SOURCE: steer.source ?? '',
|
|
86
|
+
// Delivery envelope (round-2 review): the command can tell the host
|
|
87
|
+
// WHO steered, whether it was addressed or a broadcast, and how old
|
|
88
|
+
// the steer is — without re-querying the board.
|
|
89
|
+
PMX_STEER_TARGET: steer.target ?? 'ALL',
|
|
90
|
+
PMX_STEER_CREATED_AT: steer.createdAt ?? '',
|
|
91
|
+
PMX_STEER_CONSUMER: consumer,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
child.stdin.write(steer.message);
|
|
95
|
+
child.stdin.end();
|
|
96
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
97
|
+
child.on('error', () => resolve(1));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function setPresence(consumer: string, label: string, parent: string | null, patch: Record<string, unknown>) {
|
|
102
|
+
try {
|
|
103
|
+
await invokeOperation('ax.presence.set', {
|
|
104
|
+
source: 'sdk',
|
|
105
|
+
agentId: consumer,
|
|
106
|
+
label,
|
|
107
|
+
...(parent ? { parentAgentId: parent } : {}),
|
|
108
|
+
...patch,
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// presence is best-effort — the pump must survive a rolling daemon
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Settle back to idle ONLY when the visible phase is still the pump's own
|
|
117
|
+
* `tooling` marker. The host the pump feeds may have set a richer explicit
|
|
118
|
+
* phase (thinking, waiting-approval) while handling the steer — an
|
|
119
|
+
* unconditional idle stomped it and active work read as Idle (0.5.0
|
|
120
|
+
* readiness, Copilot finding).
|
|
121
|
+
*/
|
|
122
|
+
async function settlePresence(consumer: string, label: string, parent: string | null): Promise<void> {
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(`${getBaseUrl()}/api/canvas/ax/presence`, { signal: AbortSignal.timeout(5_000) });
|
|
125
|
+
if (res.ok) {
|
|
126
|
+
const body = (await res.json()) as {
|
|
127
|
+
presences?: Array<{ sessionId?: string; phase?: string; detail?: string | null }>;
|
|
128
|
+
};
|
|
129
|
+
const own = body.presences?.find((p) => p.sessionId === consumer);
|
|
130
|
+
const ownToolingMarker = own?.phase === 'tooling' && (own.detail ?? '').startsWith('steer: ');
|
|
131
|
+
if (own && !ownToolingMarker && own.phase !== 'idle') return; // richer phase belongs to the host
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// presence read failed — fall through and settle; better idle than stuck tooling
|
|
135
|
+
}
|
|
136
|
+
await setPresence(consumer, label, parent, { phase: 'idle', detail: null });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cmd(
|
|
140
|
+
'pump',
|
|
141
|
+
'Reactive steering loop for CLI agents (Amp, Codex CLI): long-poll claims for a consumer and run a command per steer',
|
|
142
|
+
[
|
|
143
|
+
`pmx-canvas pump --consumer codex-cli --exec 'codex exec --full-auto {message}'`,
|
|
144
|
+
`pmx-canvas pump --consumer amp --exec 'amp -x {message}' --parent claude-code`,
|
|
145
|
+
`pmx-canvas pump --consumer testbot --exec 'cat' --once`,
|
|
146
|
+
],
|
|
147
|
+
async (args) => {
|
|
148
|
+
const { flags } = parseFlags(args, { boolFlags: ['once', 'help', 'h'] });
|
|
149
|
+
// Help before required-flag validation — `pump --help` died on
|
|
150
|
+
// "Missing required flag: --consumer" (0.5.0 Codex finding W).
|
|
151
|
+
if (flags.help || flags.h) return showCommandHelp('pump');
|
|
152
|
+
const consumer = requireFlag(
|
|
153
|
+
flags,
|
|
154
|
+
'consumer',
|
|
155
|
+
`pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
|
|
156
|
+
);
|
|
157
|
+
const execTemplate = requireFlag(
|
|
158
|
+
flags,
|
|
159
|
+
'exec',
|
|
160
|
+
`pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
|
|
161
|
+
);
|
|
162
|
+
const once = flags.once === true;
|
|
163
|
+
const waitMsRaw = Number(getStringFlag(flags, 'wait-ms') ?? 120_000);
|
|
164
|
+
const waitMs = Number.isFinite(waitMsRaw) && waitMsRaw >= 0 ? Math.min(waitMsRaw, 120_000) : 120_000;
|
|
165
|
+
const label = getStringFlag(flags, 'label') ?? consumer;
|
|
166
|
+
const parent = getStringFlag(flags, 'parent') ?? null;
|
|
167
|
+
const deliverBacklog = getStringFlag(flags, 'backlog') === 'deliver';
|
|
168
|
+
const retryDelayRaw = Number(getStringFlag(flags, 'retry-delay-ms') ?? 5_000);
|
|
169
|
+
const retryDelayMs = Number.isFinite(retryDelayRaw) && retryDelayRaw >= 0 ? retryDelayRaw : 5_000;
|
|
170
|
+
|
|
171
|
+
// Startup watermark: steers queued BEFORE the pump existed are history,
|
|
172
|
+
// not instructions — mark them silently unless --backlog=deliver.
|
|
173
|
+
if (!deliverBacklog) {
|
|
174
|
+
const { pending } = await claimPending(consumer, 0);
|
|
175
|
+
for (const steer of pending) await markDelivered(steer.id, consumer);
|
|
176
|
+
let swept = pending.length;
|
|
177
|
+
// pending is capped at limit=1 — drain the rest.
|
|
178
|
+
while (swept > 0) {
|
|
179
|
+
const next = await claimPending(consumer, 0);
|
|
180
|
+
if (next.pending.length === 0) break;
|
|
181
|
+
for (const steer of next.pending) await markDelivered(steer.id, consumer);
|
|
182
|
+
swept = next.pending.length;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Park via the guard: a pump RESTART while its host is mid-work must not
|
|
187
|
+
// knock an explicit phase back to idle either.
|
|
188
|
+
await settlePresence(consumer, label, parent);
|
|
189
|
+
console.log(`[pump] ${consumer} parked on ${getBaseUrl()} (waitMs=${waitMs}${once ? ', once' : ''})`);
|
|
190
|
+
|
|
191
|
+
const awaitingMark = new Set<string>();
|
|
192
|
+
const failures = new Map<string, number>();
|
|
193
|
+
let stopped = false;
|
|
194
|
+
let fastEmpties = 0;
|
|
195
|
+
process.on('SIGINT', () => {
|
|
196
|
+
stopped = true;
|
|
197
|
+
console.log('[pump] stopping');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
while (!stopped) {
|
|
202
|
+
let pending: PendingSteer[];
|
|
203
|
+
let tookMs: number;
|
|
204
|
+
try {
|
|
205
|
+
({ pending, tookMs } = await claimPending(consumer, waitMs));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Daemon roll or network blip: back off and re-park, never exit.
|
|
208
|
+
console.error(`[pump] claim retrying: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (pending.length === 0) {
|
|
213
|
+
// Old-server guard: a 0.4.8 daemon ignores waitMs, returning instantly —
|
|
214
|
+
// without this the loop would spin hot.
|
|
215
|
+
fastEmpties = tookMs < 1_000 ? fastEmpties + 1 : 0;
|
|
216
|
+
if (fastEmpties >= 2) await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
fastEmpties = 0;
|
|
220
|
+
const steer = pending[0]!;
|
|
221
|
+
|
|
222
|
+
// Awaiting-mark memory: the exec already ran for this id — a failed mark
|
|
223
|
+
// must not re-run the command (the duplicate-send race).
|
|
224
|
+
if (awaitingMark.has(steer.id)) {
|
|
225
|
+
if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await setPresence(consumer, label, parent, { phase: 'tooling', detail: `steer: ${steer.message.slice(0, 60)}` });
|
|
230
|
+
const age = steer.createdAt
|
|
231
|
+
? `, age ${Math.max(0, Math.round((Date.now() - Date.parse(steer.createdAt)) / 1000))}s`
|
|
232
|
+
: '';
|
|
233
|
+
console.log(
|
|
234
|
+
`[pump] steer ${steer.id} from ${steer.source ?? '?'} → ${steer.target ?? 'ALL'}${age}: ${steer.message.slice(0, 120)}`,
|
|
235
|
+
);
|
|
236
|
+
const code = await runExec(execTemplate, steer, consumer);
|
|
237
|
+
if (code === 0) {
|
|
238
|
+
awaitingMark.add(steer.id);
|
|
239
|
+
if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
|
|
240
|
+
failures.delete(steer.id);
|
|
241
|
+
await settlePresence(consumer, label, parent);
|
|
242
|
+
if (once) return;
|
|
243
|
+
} else {
|
|
244
|
+
const count = (failures.get(steer.id) ?? 0) + 1;
|
|
245
|
+
failures.set(steer.id, count);
|
|
246
|
+
console.error(`[pump] exec exited ${code} for ${steer.id} (attempt ${count}/3)`);
|
|
247
|
+
if (count >= 3) {
|
|
248
|
+
// A failed host injection is still pending work; only successful execs may mark delivery.
|
|
249
|
+
failures.delete(steer.id);
|
|
250
|
+
console.error(`[pump] stopped after three failures for ${steer.id} — steer remains pending`);
|
|
251
|
+
await setPresence(consumer, label, parent, {
|
|
252
|
+
phase: 'idle',
|
|
253
|
+
detail: `delivery failed: ${steer.id} remains pending`,
|
|
254
|
+
});
|
|
255
|
+
throw new Error(`Pump delivery failed three times for ${steer.id}; steer remains pending.`);
|
|
256
|
+
} else {
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
258
|
+
}
|
|
259
|
+
await settlePresence(consumer, label, parent);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
);
|
package/src/cli/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { runAgentCli } from './agent.js';
|
|
|
8
8
|
import { isCanvasTheme } from '../shared/themes.js';
|
|
9
9
|
import { createCanvas } from '../server/index.js';
|
|
10
10
|
import { seedDemoCanvas } from '../server/demo.js';
|
|
11
|
-
import { ensureDefaultDockedNodes } from '../server/server.js';
|
|
12
11
|
|
|
13
12
|
const args = process.argv.slice(2);
|
|
14
13
|
|
|
@@ -45,6 +44,7 @@ const AGENT_COMMANDS = new Set([
|
|
|
45
44
|
'screenshot',
|
|
46
45
|
'pin',
|
|
47
46
|
'ax',
|
|
47
|
+
'pump',
|
|
48
48
|
'undo',
|
|
49
49
|
'redo',
|
|
50
50
|
'history',
|
|
@@ -314,6 +314,7 @@ Agent CLI (works against running server):
|
|
|
314
314
|
code-graph File dependencies
|
|
315
315
|
spatial Spatial analysis
|
|
316
316
|
watch Semantic watch stream
|
|
317
|
+
pump Reactive steering loop for CLI agents (Amp, Codex CLI)
|
|
317
318
|
smoke Check that a running canvas works here
|
|
318
319
|
skills sync Sync installed copies of the bundled skills
|
|
319
320
|
copilot install-extension Install the GitHub Copilot app extension
|
|
@@ -404,10 +405,6 @@ Examples:
|
|
|
404
405
|
|
|
405
406
|
if (demo && canvas.getLayout().nodes.length === 0) {
|
|
406
407
|
seedDemoCanvas();
|
|
407
|
-
} else if (!demo) {
|
|
408
|
-
// First-run only: dock a status (left) + context (right) widget by default so
|
|
409
|
-
// a fresh canvas isn't empty. No-op once the workspace has saved state.
|
|
410
|
-
ensureDefaultDockedNodes();
|
|
411
408
|
}
|
|
412
409
|
|
|
413
410
|
console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
|
package/src/cli/shared.ts
CHANGED
|
@@ -118,7 +118,7 @@ function output(data: unknown): void {
|
|
|
118
118
|
async function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown> {
|
|
119
119
|
const base = getBaseUrl();
|
|
120
120
|
try {
|
|
121
|
-
return await new HttpOperationInvoker(base).invoke(name, input);
|
|
121
|
+
return await new HttpOperationInvoker(base, 'cli').invoke(name, input);
|
|
122
122
|
} catch (error) {
|
|
123
123
|
if (error instanceof OperationError) {
|
|
124
124
|
die(error.message);
|