pmx-canvas 0.3.1 → 0.3.3
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 +58 -0
- package/dist/canvas/index.js +69 -67
- package/dist/types/cli/agent.d.ts +9 -1
- package/dist/types/cli/daemon.d.ts +17 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +22 -1
- package/dist/types/server/server.d.ts +8 -0
- package/docs/RELEASE.md +11 -0
- package/docs/cli.md +10 -1
- package/docs/screenshot.png +0 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +15 -9
- package/skills/pmx-canvas/references/full-reference.md +7 -0
- package/src/cli/agent.ts +68 -1
- package/src/cli/daemon.ts +39 -7
- package/src/cli/index.ts +21 -1
- package/src/client/nodes/ExtAppFrame.tsx +249 -46
- package/src/server/bundled-skills.ts +5 -1
- package/src/server/canvas-operations.ts +3 -3
- package/src/server/operations/ops/batch.ts +3 -2
- package/src/server/server.ts +73 -4
|
@@ -10,4 +10,12 @@
|
|
|
10
10
|
* - Idempotent operations where possible
|
|
11
11
|
* - --yes for destructive actions, --dry-run for preview
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Extract the global `--port <n>` / `--server-url <url>` flags (any position,
|
|
15
|
+
* `=` or space-separated value) and set the invocation's target override.
|
|
16
|
+
* Returns the remaining args for command dispatch. Invalid values are a loud
|
|
17
|
+
* `die` — never a silent fallback to the default port. `--server-url` wins
|
|
18
|
+
* over `--port` when both are given.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractGlobalTargetFlags(args: string[]): string[];
|
|
21
|
+
export declare function runAgentCli(rawArgs: string[]): Promise<void>;
|
|
@@ -17,6 +17,8 @@ export declare function removePidFile(path: string): void;
|
|
|
17
17
|
export interface HealthStatus {
|
|
18
18
|
responsive: boolean;
|
|
19
19
|
workspace: string | null;
|
|
20
|
+
/** The serving process's own pid, self-reported by /health (0.3.3+ servers). */
|
|
21
|
+
pid: number | null;
|
|
20
22
|
}
|
|
21
23
|
export declare function readHealthStatus(url: string): Promise<HealthStatus>;
|
|
22
24
|
export declare function isHealthy(url: string): Promise<boolean>;
|
|
@@ -65,6 +67,21 @@ export declare function startDaemonMode(options: DaemonPaths & {
|
|
|
65
67
|
waitMs: number;
|
|
66
68
|
entry: string;
|
|
67
69
|
}): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the pid story `serve status` reports. The AUTHORITATIVE pid is the
|
|
72
|
+
* serving process's self-reported /health pid: the pid file can go stale when
|
|
73
|
+
* something other than `serve --daemon` (re)spawned the server on this port —
|
|
74
|
+
* e.g. a host adapter respawn (0.3.2 report Finding P). Reporting the dead
|
|
75
|
+
* file pid as `pid` while `running` was true made agents read a contradiction
|
|
76
|
+
* (`running: true, pidRunning: false`); report the real listener instead and
|
|
77
|
+
* mark the file explicitly stale. Pre-0.3.3 servers don't self-report a pid —
|
|
78
|
+
* for those, fall back to the pid-file view unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveDaemonPidView(pidFilePid: number | null, pidFilePidRunning: boolean, health: HealthStatus): {
|
|
81
|
+
pid: number | null;
|
|
82
|
+
pidRunning: boolean;
|
|
83
|
+
pidFileStale: boolean;
|
|
84
|
+
};
|
|
68
85
|
export declare function showServeStatus(options: DaemonPaths & {
|
|
69
86
|
entry: string;
|
|
70
87
|
}): Promise<void>;
|
|
@@ -19,7 +19,15 @@ interface ExtAppHostDimensionsTarget {
|
|
|
19
19
|
* everywhere we can test (Chrome / Codex / Playwright).
|
|
20
20
|
*/
|
|
21
21
|
export declare function isWebKitOnlyHost(userAgent: string): boolean;
|
|
22
|
-
export declare
|
|
22
|
+
export declare const WEBKIT_REMOUNT_SETTLE_MS = 1000;
|
|
23
|
+
export interface WebkitRemountTask {
|
|
24
|
+
/** Perform the remount. Return false if the node no longer needs it (skips the boot wait). */
|
|
25
|
+
remount: () => boolean;
|
|
26
|
+
/** Resolves when the remounted app genuinely boots AND finishes its bootstrap replay, or after a bounded timeout. */
|
|
27
|
+
awaitBoot: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function extAppRecoveryLog(nodeId: string, event: string): void;
|
|
30
|
+
export declare function enqueueWebkitRemount(task: WebkitRemountTask): void;
|
|
23
31
|
export declare function shouldScheduleWebKitRepaint(status: ExtAppFrameStatus, hasReplayToolResult: boolean): boolean;
|
|
24
32
|
export declare function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string;
|
|
25
33
|
export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMode, isExpanded: boolean): {
|
|
@@ -30,6 +38,19 @@ export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMo
|
|
|
30
38
|
export declare function sendExtAppBootstrapState(bridge: ExtAppBridgeNotifications, toolInput: Record<string, unknown>, toolResult: CallToolResult | undefined): Promise<void>;
|
|
31
39
|
export declare function resolveExtAppSandbox(value: unknown): string;
|
|
32
40
|
export declare function buildExtAppAxBridgeScript(axToken: string, nodeId: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Boot beacon injected into EVERY ext-app document (not gated on AX): posts one
|
|
43
|
+
* parent message the moment the iframe's scripts execute. This is the liveness
|
|
44
|
+
* signal the WebKit never-booted watchdog keys on — it distinguishes an app
|
|
45
|
+
* that is alive but boots via the 1200ms bootstrap fallback (e.g. a non-SDK
|
|
46
|
+
* widget that never sends ui/notifications/initialized) from a dead window
|
|
47
|
+
* whose scripts never ran. Without it, the watchdog treated every
|
|
48
|
+
* fallback-booted app as never-booted and remounted it up to the attempt cap —
|
|
49
|
+
* a reboot/flicker loop for exactly the apps the fallback exists to support
|
|
50
|
+
* (0.3.2 pre-release review). The token scopes the message to this component;
|
|
51
|
+
* the host also checks event.source against the live iframe.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildExtAppBootBeaconScript(frameToken: string, nodeId: string): string;
|
|
33
54
|
export declare function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string;
|
|
34
55
|
export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimensionsTarget | null | undefined, fallback: {
|
|
35
56
|
width: number;
|
|
@@ -81,6 +81,14 @@ export declare function setPrimaryWorkbenchAutoOpenEnabled(enabled: boolean): vo
|
|
|
81
81
|
export declare function isPrimaryWorkbenchAutoOpenEnabled(): boolean;
|
|
82
82
|
export declare function hasWorkbenchSubscribers(): boolean;
|
|
83
83
|
export declare function setPrimaryWorkbenchCanvasPromptHandler(handler: PrimaryWorkbenchCanvasPromptHandler | null): void;
|
|
84
|
+
/**
|
|
85
|
+
* Containment check for bundle-asset serving. Separator-agnostic: on Windows,
|
|
86
|
+
* `resolve()` returns backslash paths, so comparing against a `${dir}/` template
|
|
87
|
+
* rejected every asset — /canvas/index.js 404'd and the SPA never booted (the
|
|
88
|
+
* 0.3.1 Windows report). Normalizing both sides keeps the check exact on POSIX
|
|
89
|
+
* and correct on win32, and testable with win32-shaped fixtures on any host.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isCanvasBundlePath(distPath: string, bundleDir: string): boolean;
|
|
84
92
|
export declare function buildMacBrowserOpenScript(appName: string, url: string): string;
|
|
85
93
|
export declare function openUrlInExternalBrowser(url: string): boolean;
|
|
86
94
|
/**
|
package/docs/RELEASE.md
CHANGED
|
@@ -124,6 +124,17 @@ If this is the first release from your machine, run `bunx npm login`
|
|
|
124
124
|
once so Bun can reuse your npm credentials. CI does not need this —
|
|
125
125
|
it uses `NPM_TOKEN` directly.
|
|
126
126
|
|
|
127
|
+
## Sync consumer skill mirrors
|
|
128
|
+
|
|
129
|
+
Every release since 0.3.0 has been retested against **stale** skill mirrors:
|
|
130
|
+
consumer workspaces (e.g. `personalclaude`) carry copies of the pmx-canvas
|
|
131
|
+
skill under `.github/`, `.codex/`, `.claude/`, and `.agents/`, and testers
|
|
132
|
+
repeatedly had to sync them mid-cycle before results were trustworthy. After
|
|
133
|
+
publishing, update the installed package in the consumer workspace and re-sync
|
|
134
|
+
its four mirrors from
|
|
135
|
+
`<global npm root>/pmx-canvas/skills/pmx-canvas/` so the first test pass runs
|
|
136
|
+
against the released skill, not the previous version's guidance.
|
|
137
|
+
|
|
127
138
|
## GitHub release
|
|
128
139
|
|
|
129
140
|
After `npm publish` succeeds:
|
package/docs/cli.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
The CLI is the shell-native way to run and control PMX Canvas. It targets
|
|
4
4
|
`http://localhost:4313` by default — override with `PMX_CANVAS_URL` or
|
|
5
|
-
`PMX_CANVAS_PORT` when the server runs elsewhere
|
|
5
|
+
`PMX_CANVAS_PORT` when the server runs elsewhere, or per invocation with the
|
|
6
|
+
global `--port <n>` / `--server-url <url>` flags (any position; `--server-url`
|
|
7
|
+
wins over `--port`, and both win over the environment variables). An invalid
|
|
8
|
+
value for either flag is a hard error — the CLI never falls back silently to
|
|
9
|
+
the default port.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pmx-canvas node list --port 4750 # target a non-default daemon
|
|
13
|
+
pmx-canvas --server-url http://127.0.0.1:4750 status # same, by URL
|
|
14
|
+
```
|
|
6
15
|
|
|
7
16
|
## Server lifecycle
|
|
8
17
|
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -24,7 +24,8 @@ Humans curate agent context by pinning nodes; agents read that curation through
|
|
|
24
24
|
`workspace` must equal the intended absolute workspace root. A healthy listener on port 4313
|
|
25
25
|
may belong to another project.
|
|
26
26
|
3. **Read before write.** Search with `canvas_query { action: "search", query }` before creating
|
|
27
|
-
nodes. Read the full layout only when necessary.
|
|
27
|
+
nodes. Read the full layout only when necessary. The MCP parameter is `query` — passing the
|
|
28
|
+
HTTP API's `q` is silently ignored and returns zero results.
|
|
28
29
|
4. **Snapshot before destructive changes.** Use `canvas_snapshot` (deprecated standalone; folds into
|
|
29
30
|
the `canvas_snapshot` composite's `save` action in v0.4) before clear, restore, or a major
|
|
30
31
|
reorganization.
|
|
@@ -33,7 +34,8 @@ Humans curate agent context by pinning nodes; agents read that curation through
|
|
|
33
34
|
or edit, then pass the returned `intent.id` as `intentId` on the mutation so the ghost settles
|
|
34
35
|
into the result. Use it as much as possible to make your next move and your work visible: the
|
|
35
36
|
human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
|
|
36
|
-
high-frequency batch churn.
|
|
37
|
+
high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with
|
|
38
|
+
`ttlMs: 30000` and settle by passing `intentId` on the mutation in the same or next call.
|
|
37
39
|
6. **Mutate through current composites.** Prefer the 15 composite MCP tools below.
|
|
38
40
|
7. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when
|
|
39
41
|
appropriate and always finish with `canvas_query { action: "validate" }`.
|
|
@@ -215,13 +217,17 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
|
|
|
215
217
|
loads (e.g. the GitHub Copilot app's embedded WKWebView) can render as a black tile — a host
|
|
216
218
|
compositor paint race on the nested iframe, **not** a broken node (the session is healthy and
|
|
217
219
|
`sessionStatus` is `ready`; it renders fine in Chrome, the Codex browser, and for nodes created
|
|
218
|
-
live after the panel hydrates).
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
live after the panel hydrates). Under WebKit the canvas ATTEMPTS auto-recovery: after boot,
|
|
221
|
+
each present-at-load ext-app is remounted through a serialized, boot-aware queue (one app at a
|
|
222
|
+
time), with a watchdog retry for iframes that never boot and a re-arm when a panel that loaded
|
|
223
|
+
hidden becomes visible. Treat the auto-recovery as best-effort, NOT guaranteed — the 0.3.2
|
|
224
|
+
Copilot-host report shows tiles can still come up black in real WKWebView panels (compositing
|
|
225
|
+
success is not observable from page JS, so retries are bounded). Keep the cautionary behavior:
|
|
226
|
+
recovery is deterministic via **expand-then-close** of the black tile (forces a fresh mount in
|
|
227
|
+
the fullscreen overlay, which always paints), or open the workbench in a normal browser
|
|
228
|
+
(Chrome). Do not diagnose a healthy app session as a broken node. When reporting a black tile,
|
|
229
|
+
fetch `GET /api/canvas/debug/ext-app-recovery` from the daemon and attach the trace — it shows
|
|
230
|
+
what the recovery queue actually did in that panel.
|
|
225
231
|
- Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and
|
|
226
232
|
reflow on a live window resize in a normal browser. Some single-tab host browsers (e.g. the
|
|
227
233
|
Codex in-app browser) don't deliver live-resize events, so a resized standalone chart can look
|
|
@@ -510,6 +510,13 @@ surfaces): `canvas_batch`, `canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_i
|
|
|
510
510
|
`canvas_diff` — deprecated pending a `canvas_snapshot` composite in v0.4; the name collides with
|
|
511
511
|
the current save-snapshot tool, so the composite cannot land additively).
|
|
512
512
|
|
|
513
|
+
`canvas_batch` supports exactly these ops: `node.add`, `node.update`, `node.remove`, `graph.add`,
|
|
514
|
+
`edge.add`, `edge.remove`, `group.create`, `group.add`, `group.remove`, `pin.set`/`pin.add`/
|
|
515
|
+
`pin.remove`, `snapshot.save`, and `arrange`. Anything else fails with "Unsupported canvas_batch
|
|
516
|
+
operation" — batch is non-atomic, so earlier ops stay applied and the response carries
|
|
517
|
+
`failedIndex`. `canvas_screenshot` requires an active automation WebView: call
|
|
518
|
+
`canvas_webview { action: "start" }` first.
|
|
519
|
+
|
|
513
520
|
> **Removed in v0.3.0 → use the composite instead**: `canvas_open_mcp_app` / `canvas_add_diagram` /
|
|
514
521
|
> `canvas_build_web_artifact` → **`canvas_app`**; `canvas_webview_start` / `canvas_webview_status` /
|
|
515
522
|
> `canvas_webview_stop` / `canvas_resize` / `canvas_evaluate` → **`canvas_webview`**;
|
package/src/cli/agent.ts
CHANGED
|
@@ -89,13 +89,77 @@ interface CanvasSchemaResponse {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Per-invocation target override from the global --server-url / --port flags.
|
|
93
|
+
// Before these existed, `--port 4750` on any agent command was SILENTLY ignored
|
|
94
|
+
// and the command hit the default 4313 daemon — which once pointed a test
|
|
95
|
+
// automation WebView at a live production board.
|
|
96
|
+
let cliTargetOverride: string | null = null;
|
|
97
|
+
|
|
92
98
|
function getBaseUrl(): string {
|
|
99
|
+
if (cliTargetOverride) return cliTargetOverride;
|
|
93
100
|
const envUrl = process.env.PMX_CANVAS_URL;
|
|
94
101
|
if (envUrl) return envUrl.replace(/\/$/, '');
|
|
95
102
|
const port = process.env.PMX_CANVAS_PORT || DEFAULT_PORT;
|
|
96
103
|
return `http://localhost:${port}`;
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Extract the global `--port <n>` / `--server-url <url>` flags (any position,
|
|
108
|
+
* `=` or space-separated value) and set the invocation's target override.
|
|
109
|
+
* Returns the remaining args for command dispatch. Invalid values are a loud
|
|
110
|
+
* `die` — never a silent fallback to the default port. `--server-url` wins
|
|
111
|
+
* over `--port` when both are given.
|
|
112
|
+
*/
|
|
113
|
+
export function extractGlobalTargetFlags(args: string[]): string[] {
|
|
114
|
+
cliTargetOverride = null;
|
|
115
|
+
let portOverride: number | null = null;
|
|
116
|
+
let urlOverride: string | null = null;
|
|
117
|
+
const rest: string[] = [];
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < args.length; i++) {
|
|
120
|
+
const arg = args[i];
|
|
121
|
+
let name: 'port' | 'server-url' | null = null;
|
|
122
|
+
let value: string | undefined;
|
|
123
|
+
if (arg === '--port' || arg === '--server-url') {
|
|
124
|
+
name = arg.slice(2) as 'port' | 'server-url';
|
|
125
|
+
value = args[i + 1];
|
|
126
|
+
i += 1;
|
|
127
|
+
} else if (arg.startsWith('--port=')) {
|
|
128
|
+
name = 'port';
|
|
129
|
+
value = arg.slice('--port='.length);
|
|
130
|
+
} else if (arg.startsWith('--server-url=')) {
|
|
131
|
+
name = 'server-url';
|
|
132
|
+
value = arg.slice('--server-url='.length);
|
|
133
|
+
} else {
|
|
134
|
+
rest.push(arg);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (name === 'port') {
|
|
139
|
+
const port = Number(value);
|
|
140
|
+
if (!value || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
141
|
+
die(
|
|
142
|
+
`--port requires a port number, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
|
|
143
|
+
'Example: --port 4313. Without the flag, PMX_CANVAS_PORT / PMX_CANVAS_URL pick the target.',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
portOverride = port;
|
|
147
|
+
} else {
|
|
148
|
+
if (!value || !/^https?:\/\//.test(value)) {
|
|
149
|
+
die(
|
|
150
|
+
`--server-url requires an http(s) URL, got ${value === undefined ? 'no value' : JSON.stringify(value)}.`,
|
|
151
|
+
'Example: --server-url http://localhost:4313. Without the flag, PMX_CANVAS_URL picks the target.',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
urlOverride = value.replace(/\/$/, '');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (urlOverride) cliTargetOverride = urlOverride;
|
|
159
|
+
else if (portOverride) cliTargetOverride = `http://localhost:${portOverride}`;
|
|
160
|
+
return rest;
|
|
161
|
+
}
|
|
162
|
+
|
|
99
163
|
function die(message: string, hint?: string): never {
|
|
100
164
|
const out: Record<string, string> = { error: message };
|
|
101
165
|
if (hint) out.hint = hint;
|
|
@@ -3535,6 +3599,8 @@ Analysis:
|
|
|
3535
3599
|
|
|
3536
3600
|
Global flags:
|
|
3537
3601
|
--help, -h Show help for any command
|
|
3602
|
+
--port <n> Target daemon port for this invocation (overrides PMX_CANVAS_PORT)
|
|
3603
|
+
--server-url <url> Target server URL for this invocation (overrides PMX_CANVAS_URL; wins over --port)
|
|
3538
3604
|
|
|
3539
3605
|
Environment:
|
|
3540
3606
|
PMX_CANVAS_URL Server URL (default: http://localhost:4313)
|
|
@@ -3592,7 +3658,8 @@ async function readStdin(): Promise<string> {
|
|
|
3592
3658
|
|
|
3593
3659
|
// ── Router ───────────────────────────────────────────────────
|
|
3594
3660
|
|
|
3595
|
-
export async function runAgentCli(
|
|
3661
|
+
export async function runAgentCli(rawArgs: string[]): Promise<void> {
|
|
3662
|
+
const args = extractGlobalTargetFlags(rawArgs);
|
|
3596
3663
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
3597
3664
|
showTopLevelHelp();
|
|
3598
3665
|
return;
|
package/src/cli/daemon.ts
CHANGED
|
@@ -88,20 +88,26 @@ export function removePidFile(path: string): void {
|
|
|
88
88
|
export interface HealthStatus {
|
|
89
89
|
responsive: boolean;
|
|
90
90
|
workspace: string | null;
|
|
91
|
+
/** The serving process's own pid, self-reported by /health (0.3.3+ servers). */
|
|
92
|
+
pid: number | null;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export async function readHealthStatus(url: string): Promise<HealthStatus> {
|
|
94
96
|
try {
|
|
95
97
|
const response = await fetch(url);
|
|
96
|
-
if (!response.ok) return { responsive: false, workspace: null };
|
|
98
|
+
if (!response.ok) return { responsive: false, workspace: null, pid: null };
|
|
97
99
|
const payload = (await response.json().catch(() => null)) as unknown;
|
|
98
100
|
const workspace =
|
|
99
101
|
payload && typeof payload === 'object' && 'workspace' in payload && typeof payload.workspace === 'string'
|
|
100
102
|
? payload.workspace
|
|
101
103
|
: null;
|
|
102
|
-
|
|
104
|
+
const pid =
|
|
105
|
+
payload && typeof payload === 'object' && 'pid' in payload && typeof payload.pid === 'number'
|
|
106
|
+
? payload.pid
|
|
107
|
+
: null;
|
|
108
|
+
return { responsive: true, workspace, pid };
|
|
103
109
|
} catch {
|
|
104
|
-
return { responsive: false, workspace: null };
|
|
110
|
+
return { responsive: false, workspace: null, pid: null };
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -335,17 +341,41 @@ export async function startDaemonMode(
|
|
|
335
341
|
process.exit(0);
|
|
336
342
|
}
|
|
337
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Resolve the pid story `serve status` reports. The AUTHORITATIVE pid is the
|
|
346
|
+
* serving process's self-reported /health pid: the pid file can go stale when
|
|
347
|
+
* something other than `serve --daemon` (re)spawned the server on this port —
|
|
348
|
+
* e.g. a host adapter respawn (0.3.2 report Finding P). Reporting the dead
|
|
349
|
+
* file pid as `pid` while `running` was true made agents read a contradiction
|
|
350
|
+
* (`running: true, pidRunning: false`); report the real listener instead and
|
|
351
|
+
* mark the file explicitly stale. Pre-0.3.3 servers don't self-report a pid —
|
|
352
|
+
* for those, fall back to the pid-file view unchanged.
|
|
353
|
+
*/
|
|
354
|
+
export function resolveDaemonPidView(
|
|
355
|
+
pidFilePid: number | null,
|
|
356
|
+
pidFilePidRunning: boolean,
|
|
357
|
+
health: HealthStatus,
|
|
358
|
+
): { pid: number | null; pidRunning: boolean; pidFileStale: boolean } {
|
|
359
|
+
const authoritative = health.responsive && health.pid !== null;
|
|
360
|
+
return {
|
|
361
|
+
pid: authoritative ? health.pid : pidFilePid,
|
|
362
|
+
pidRunning: authoritative ? true : pidFilePidRunning,
|
|
363
|
+
pidFileStale: pidFilePid !== null && !pidFilePidRunning && (!health.responsive || health.pid !== pidFilePid),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
338
367
|
export async function showServeStatus(options: DaemonPaths & { entry: string }): Promise<void> {
|
|
339
368
|
const healthUrl = `http://localhost:${options.port}/health`;
|
|
340
369
|
const url = `http://localhost:${options.port}/workbench`;
|
|
341
|
-
const
|
|
342
|
-
const
|
|
370
|
+
const pidFilePid = readPidFile(options.pidFile);
|
|
371
|
+
const pidFilePidRunning = pidFilePid ? isOwnDaemonProcess(pidFilePid, options.entry) : false;
|
|
343
372
|
const health = await readHealthStatus(healthUrl);
|
|
344
373
|
const responsive = health.responsive;
|
|
345
|
-
const running = responsive ||
|
|
374
|
+
const running = responsive || pidFilePidRunning;
|
|
375
|
+
const { pid, pidRunning, pidFileStale } = resolveDaemonPidView(pidFilePid, pidFilePidRunning, health);
|
|
346
376
|
// Clean up a stale pid file, but never a concurrent starter's fresh empty
|
|
347
377
|
// spawn lock (that would let a racing `serve --daemon` double-spawn).
|
|
348
|
-
if (!running && existsSync(options.pidFile) && !
|
|
378
|
+
if (!running && existsSync(options.pidFile) && !pidFilePidRunning && !isFreshEmptyLock(options.pidFile)) {
|
|
349
379
|
removePidFile(options.pidFile);
|
|
350
380
|
}
|
|
351
381
|
|
|
@@ -357,6 +387,8 @@ export async function showServeStatus(options: DaemonPaths & { entry: string }):
|
|
|
357
387
|
workspace: health.workspace,
|
|
358
388
|
pid,
|
|
359
389
|
pidRunning,
|
|
390
|
+
pidFilePid,
|
|
391
|
+
pidFileStale,
|
|
360
392
|
url,
|
|
361
393
|
healthUrl,
|
|
362
394
|
logFile: options.logFile,
|
package/src/cli/index.ts
CHANGED
|
@@ -120,6 +120,25 @@ function stripOption(argv: string[], name: string): string[] {
|
|
|
120
120
|
return stripped;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Global target flags (--port / --server-url) may precede the subcommand, e.g.
|
|
124
|
+
// `pmx-canvas --port 4750 node list`. Skip them (and their values) when picking
|
|
125
|
+
// the subcommand to dispatch, so the agent CLI — which strips these flags via
|
|
126
|
+
// extractGlobalTargetFlags and validates them loudly — is reached instead of the
|
|
127
|
+
// server-startup fallback. Only these two flags are skipped; other leading flags
|
|
128
|
+
// (--mcp, --demo, …) still route exactly as before.
|
|
129
|
+
function commandAfterGlobalTargetFlags(argv: string[]): string {
|
|
130
|
+
for (let index = 0; index < argv.length; index++) {
|
|
131
|
+
const arg = argv[index];
|
|
132
|
+
if (arg === '--port' || arg === '--server-url') {
|
|
133
|
+
if (index + 1 < argv.length && !argv[index + 1].startsWith('-')) index++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (arg.startsWith('--port=') || arg.startsWith('--server-url=')) continue;
|
|
137
|
+
return arg;
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
123
142
|
function runMcpServerProcess(): Promise<void> {
|
|
124
143
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
125
144
|
const child = spawn(process.execPath, ['run', mcpServerEntry], {
|
|
@@ -181,7 +200,8 @@ if (firstArg === 'serve') {
|
|
|
181
200
|
args.shift();
|
|
182
201
|
}
|
|
183
202
|
|
|
184
|
-
|
|
203
|
+
const routedCommand = commandAfterGlobalTargetFlags(args);
|
|
204
|
+
if (AGENT_COMMANDS.has(routedCommand) && routedCommand !== 'serve') {
|
|
185
205
|
await runAgentCli(args);
|
|
186
206
|
} else if (args.includes('--mcp')) {
|
|
187
207
|
// MCP server mode: stdio transport, auto-starts canvas on first tool call
|