@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2
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/AGENTS.md +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* flow:resolve-model / flow:get-available-models
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { ExtensionAPI } from "@
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { existsSync, readFileSync } from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
@@ -373,7 +373,7 @@ async function loadPiAi(): Promise<PiAiHelpers> {
|
|
|
373
373
|
if (_piAiLoadAttempted) return {};
|
|
374
374
|
_piAiLoadAttempted = true;
|
|
375
375
|
try {
|
|
376
|
-
const mod: any = await import("@
|
|
376
|
+
const mod: any = await import("@earendil-works/pi-ai");
|
|
377
377
|
_piAiModule = { findEnvKeys: mod.findEnvKeys, getEnvApiKey: mod.getEnvApiKey };
|
|
378
378
|
return _piAiModule;
|
|
379
379
|
} catch {
|
|
@@ -436,6 +436,20 @@ function getModelRegistry(): any {
|
|
|
436
436
|
// -- Provider registration (with auto-discovery) --------------------------
|
|
437
437
|
|
|
438
438
|
async function registerEntry(pi: ExtensionAPI, name: string, entry: ProviderEntry): Promise<number> {
|
|
439
|
+
// Record snapshot SYNCHRONOUSLY before awaiting discovery so the very
|
|
440
|
+
// first providers_list push (typically fired from `session_start`
|
|
441
|
+
// shortly after `activate()` kicked off async registerEntry calls) carries
|
|
442
|
+
// the correct `custom: true` flags. Otherwise a slow / unreachable
|
|
443
|
+
// /v1/models endpoint causes custom providers from
|
|
444
|
+
// `~/.pi/agent/providers.json` to leak into Settings → Provider
|
|
445
|
+
// Authentication → API Keys until the discovery probe resolves.
|
|
446
|
+
// See change: fix-custom-provider-flag-race.
|
|
447
|
+
lastRegistered.set(name, {
|
|
448
|
+
baseUrl: entry.baseUrl,
|
|
449
|
+
apiKey: entry.apiKey,
|
|
450
|
+
api: entry.api ?? "openai-completions",
|
|
451
|
+
});
|
|
452
|
+
|
|
439
453
|
const discovered = await discoverModels(entry.baseUrl, entry.apiKey);
|
|
440
454
|
|
|
441
455
|
// Metadata (contextWindow, maxTokens, reasoning, cost, input) is resolved
|
|
@@ -464,13 +478,6 @@ async function registerEntry(pi: ExtensionAPI, name: string, entry: ProviderEntr
|
|
|
464
478
|
models,
|
|
465
479
|
});
|
|
466
480
|
|
|
467
|
-
// Record snapshot so reloadProviders can detect subsequent changes.
|
|
468
|
-
lastRegistered.set(name, {
|
|
469
|
-
baseUrl: entry.baseUrl,
|
|
470
|
-
apiKey: entry.apiKey,
|
|
471
|
-
api: entry.api ?? "openai-completions",
|
|
472
|
-
});
|
|
473
|
-
|
|
474
481
|
// Notify bridge directly (same package — no cross-package event needed)
|
|
475
482
|
onProvidersChanged?.();
|
|
476
483
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RetryTracker — synthesizes `auto_retry_start` / `auto_retry_end` events from
|
|
3
|
+
* observed pi events.
|
|
4
|
+
*
|
|
5
|
+
* Background: pi's ExtensionAPI does NOT expose `auto_retry_*` events to
|
|
6
|
+
* extensions (verified against pi 0.70/0.73 — see
|
|
7
|
+
* https://github.com/badlogic/pi-mono/discussions/2073). They fire only via
|
|
8
|
+
* `AgentSession._emit → _eventListeners` which only the embedded SDK can
|
|
9
|
+
* subscribe to.
|
|
10
|
+
*
|
|
11
|
+
* Workaround: pi-coding-agent's `_handleRetryableError` fires `message_end`
|
|
12
|
+
* for the failed assistant message BEFORE entering its retry sleep. The
|
|
13
|
+
* bridge sees that `message_end` via `pi.on("message_end")`. By matching
|
|
14
|
+
* the same regex pi-coding-agent uses internally, we can detect that a
|
|
15
|
+
* retry is about to happen and emit our own `auto_retry_start` to the
|
|
16
|
+
* dashboard. When the next non-error `message_end` or `agent_end` arrives,
|
|
17
|
+
* we emit `auto_retry_end`.
|
|
18
|
+
*
|
|
19
|
+
* `delayMs` and `maxAttempts` are unknowable from observed events (pi's
|
|
20
|
+
* settings are not exposed); we send sentinel `-1` for both. The
|
|
21
|
+
* RetryBanner renders an indeterminate "retrying…" UI in that case.
|
|
22
|
+
*
|
|
23
|
+
* See change: fix-provider-retry-infinite-loop.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Regex copied verbatim from pi-coding-agent `agent-session.js`
|
|
28
|
+
* `_isRetryableError`. If pi adds new retryable categories, this regex
|
|
29
|
+
* goes stale — but the failure mode is "tracker silently misses some
|
|
30
|
+
* retries", never "tracker breaks". Sync at major pi version bumps.
|
|
31
|
+
*/
|
|
32
|
+
export const RETRYABLE_PATTERN =
|
|
33
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
|
|
34
|
+
|
|
35
|
+
export interface SyntheticRetryEvent {
|
|
36
|
+
eventType: "auto_retry_start" | "auto_retry_end";
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Minimal shape we pluck from a `message_end` event. */
|
|
41
|
+
export interface ObservedAssistantMessage {
|
|
42
|
+
role?: string;
|
|
43
|
+
stopReason?: string;
|
|
44
|
+
errorMessage?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RetryTracker {
|
|
48
|
+
/** sessionId → 1-based attempt counter for the current retry chain. */
|
|
49
|
+
private attempt = new Map<string, number>();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Process a `message_end` event. Returns a synthetic event the bridge
|
|
53
|
+
* should ALSO forward (after the original message_end), or null.
|
|
54
|
+
*/
|
|
55
|
+
observeMessageEnd(
|
|
56
|
+
sessionId: string,
|
|
57
|
+
message: ObservedAssistantMessage | undefined | null,
|
|
58
|
+
): SyntheticRetryEvent | null {
|
|
59
|
+
if (!message || message.role !== "assistant") return null;
|
|
60
|
+
|
|
61
|
+
if (message.stopReason === "error") {
|
|
62
|
+
const err = typeof message.errorMessage === "string" ? message.errorMessage : "";
|
|
63
|
+
if (!err || !RETRYABLE_PATTERN.test(err)) return null;
|
|
64
|
+
const next = (this.attempt.get(sessionId) ?? 0) + 1;
|
|
65
|
+
this.attempt.set(sessionId, next);
|
|
66
|
+
return {
|
|
67
|
+
eventType: "auto_retry_start",
|
|
68
|
+
data: { attempt: next, maxAttempts: -1, delayMs: -1, errorMessage: err },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Non-error assistant message — clears any in-flight retry chain.
|
|
73
|
+
if (this.attempt.has(sessionId)) {
|
|
74
|
+
const last = this.attempt.get(sessionId) ?? 0;
|
|
75
|
+
this.attempt.delete(sessionId);
|
|
76
|
+
return { eventType: "auto_retry_end", data: { success: true, attempt: last } };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Process an `agent_end` event. Returns a synthetic event the bridge
|
|
83
|
+
* should forward BEFORE the original agent_end, or null.
|
|
84
|
+
*
|
|
85
|
+
* Always clears any in-flight retry tracking (terminal turn boundary).
|
|
86
|
+
*/
|
|
87
|
+
observeAgentEnd(
|
|
88
|
+
sessionId: string,
|
|
89
|
+
agentEndData: { messages?: unknown } | undefined | null,
|
|
90
|
+
): SyntheticRetryEvent | null {
|
|
91
|
+
const wasRetrying = this.attempt.has(sessionId);
|
|
92
|
+
const last = this.attempt.get(sessionId) ?? -1;
|
|
93
|
+
this.attempt.delete(sessionId);
|
|
94
|
+
if (!wasRetrying) return null;
|
|
95
|
+
|
|
96
|
+
// Inspect terminal message for error context.
|
|
97
|
+
const messages = agentEndData?.messages;
|
|
98
|
+
const lastMsg =
|
|
99
|
+
Array.isArray(messages) && messages.length > 0
|
|
100
|
+
? (messages[messages.length - 1] as ObservedAssistantMessage)
|
|
101
|
+
: undefined;
|
|
102
|
+
if (lastMsg?.stopReason === "error" && typeof lastMsg.errorMessage === "string") {
|
|
103
|
+
return {
|
|
104
|
+
eventType: "auto_retry_end",
|
|
105
|
+
data: { success: false, attempt: last, finalError: lastMsg.errorMessage },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { eventType: "auto_retry_end", data: { success: true, attempt: last } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Notify the tracker of a user abort. Clears in-flight tracking so a
|
|
113
|
+
* subsequent agent_end does not double-emit auto_retry_end.
|
|
114
|
+
*/
|
|
115
|
+
noteAbort(sessionId: string): void {
|
|
116
|
+
this.attempt.delete(sessionId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Test-only / bridge-coordination: is a retry currently in flight? */
|
|
120
|
+
isRetrying(sessionId: string): boolean {
|
|
121
|
+
return this.attempt.has(sessionId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* The spawned server runs in foreground mode (no subcommand) and writes
|
|
4
4
|
* its own PID file at ~/.pi/dashboard/server.pid.
|
|
5
5
|
*/
|
|
6
|
-
import { spawnDetached, waitForReady } from "@blackbelt-technology/pi-dashboard-shared/platform/detached-spawn.js";
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import os from "node:os";
|
|
9
6
|
import path from "node:path";
|
|
10
7
|
import { createRequire } from "node:module";
|
|
11
8
|
import { fileURLToPath } from "node:url";
|
|
12
9
|
import type { DashboardConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
import {
|
|
11
|
+
launchDashboardServer,
|
|
12
|
+
JitiNotFoundError,
|
|
13
|
+
PortConflictError,
|
|
14
|
+
EarlyExitError,
|
|
15
|
+
} from "@blackbelt-technology/pi-dashboard-shared/server-launcher.js";
|
|
16
16
|
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const require = createRequire(import.meta.url);
|
|
@@ -74,82 +74,43 @@ export function buildSpawnArgs(config: DashboardConfig): string[] {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Launch the dashboard server as a detached background process.
|
|
77
|
-
*
|
|
77
|
+
* Delegates to the shared `launchDashboardServer` primitive which owns
|
|
78
|
+
* loader resolution, argv shape, env merge, log-file policy, and
|
|
79
|
+
* readiness polling (see `packages/shared/src/server-launcher.ts`).
|
|
80
|
+
*
|
|
81
|
+
* Bridge-specific contract preserved: `DASHBOARD_STARTER=Bridge`,
|
|
82
|
+
* `stdio: "ignore"` (Bridge auto-spawn never owns the log file),
|
|
83
|
+
* 2 s health timeout (Bridge expects a fast cold-start when the
|
|
84
|
+
* server is already on the same machine).
|
|
78
85
|
*/
|
|
79
86
|
export async function launchServer(config: DashboardConfig): Promise<LaunchResult> {
|
|
80
87
|
const cliPath = resolveServerCliPath();
|
|
81
88
|
const args = buildSpawnArgs(config);
|
|
82
89
|
|
|
83
90
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
logFd = fs.openSync(logPath, "a");
|
|
92
|
-
fs.writeSync(
|
|
93
|
-
logFd,
|
|
94
|
-
`\n[${new Date().toISOString()}] bridge auto-start (parent pid ${process.pid}, port ${config.port})\n`,
|
|
95
|
-
);
|
|
96
|
-
} catch { /* if we can't open the log, spawn still works */ }
|
|
97
|
-
|
|
98
|
-
// Spawn server via the detached-spawn primitive. The loader is always
|
|
99
|
-
// URL-wrapped (Node needs file:// for --import on Windows drive letters).
|
|
100
|
-
// The entry is URL-wrapped only on Windows + non-tsx loader (Node parses
|
|
101
|
-
// drive letters as URL schemes in argv); on POSIX the entry MUST be raw
|
|
102
|
-
// because jiti's resolver misbehaves on file:// URL entries. See
|
|
103
|
-
// openspec/changes/archive/2026-04-24-fix-windows-entry-script-url.
|
|
104
|
-
const loader = resolveJitiImport();
|
|
105
|
-
const wrapEntry = shouldUrlWrapEntry(loader);
|
|
106
|
-
// entry is gated by shouldUrlWrapEntry(loader): returns true only on
|
|
107
|
-
// Windows + non-tsx (where URL wrap is required); false on POSIX
|
|
108
|
-
// where jiti needs the raw path (file:// URL entries trigger jiti's
|
|
109
|
-
// `<cwd>/file:/...` misresolution bug).
|
|
110
|
-
const entry = wrapEntry ? toFileUrl(cliPath) : cliPath;
|
|
111
|
-
const r = await spawnDetached({
|
|
112
|
-
cmd: process.execPath,
|
|
113
|
-
args: ["--import", loader, entry, ...args], // ban:raw-node-import-ok: entry gated by shouldUrlWrapEntry
|
|
114
|
-
env: { ...process.env, DASHBOARD_STARTER: "Bridge" },
|
|
115
|
-
logFd,
|
|
91
|
+
await launchDashboardServer({
|
|
92
|
+
cliPath,
|
|
93
|
+
extraArgs: args,
|
|
94
|
+
stdio: "ignore",
|
|
95
|
+
healthTimeoutMs: 2_000,
|
|
96
|
+
port: config.port,
|
|
97
|
+
starter: "Bridge",
|
|
116
98
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
99
|
+
return { success: true, message: "Server started" };
|
|
100
|
+
} catch (err: unknown) {
|
|
101
|
+
if (err instanceof JitiNotFoundError) {
|
|
102
|
+
return { success: false, message: err.message };
|
|
121
103
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return { success: false, message: `Server process failed to spawn: ${r.error ?? "unknown"}` };
|
|
104
|
+
if (err instanceof PortConflictError) {
|
|
105
|
+
return { success: false, message: err.message };
|
|
125
106
|
}
|
|
126
|
-
|
|
127
|
-
// Wait for the server to actually become available via positive
|
|
128
|
-
// HTTP probe. NO deadline — we rely on child-exit for failure
|
|
129
|
-
// detection. A timeout here only catches the pathological case
|
|
130
|
-
// "process alive but never ready", which is rarer than the
|
|
131
|
-
// false-positive case "slow cold-start mistakenly flagged as
|
|
132
|
-
// failure" (Fastify + jiti compile + session scan can take 15–30s
|
|
133
|
-
// on Windows). If the child crashes, `waitForReady` returns
|
|
134
|
-
// { ok: false, error: "child exited with code N" } via its
|
|
135
|
-
// `child` listener. If the child hangs alive-but-broken, the user
|
|
136
|
-
// can kill it manually — timers don't help that case anyway.
|
|
137
|
-
const ready = await waitForReady({
|
|
138
|
-
probe: async () => (await isDashboardRunning(config.port)).running,
|
|
139
|
-
pollIntervalMs: 300,
|
|
140
|
-
child: r.process,
|
|
141
|
-
// deadlineMs intentionally omitted — wait indefinitely.
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
if (!ready.ok) {
|
|
107
|
+
if (err instanceof EarlyExitError) {
|
|
145
108
|
return {
|
|
146
109
|
success: false,
|
|
147
|
-
message: `Server process
|
|
110
|
+
message: `Server process exited (code=${err.code}) before health check. See ~/.pi/dashboard/server.log`,
|
|
148
111
|
};
|
|
149
112
|
}
|
|
150
|
-
|
|
151
|
-
return { success:
|
|
152
|
-
} catch (err: any) {
|
|
153
|
-
return { success: false, message: err.message };
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
return { success: false, message };
|
|
154
115
|
}
|
|
155
116
|
}
|
|
@@ -39,7 +39,15 @@ export function sendStateSync(
|
|
|
39
39
|
// dashboard restart) is a "reattach". Server applies the configured
|
|
40
40
|
// `reattachPlacement` policy on "reattach".
|
|
41
41
|
// See change: reattach-move-to-front.
|
|
42
|
-
const
|
|
42
|
+
const isFirstRegister = !bc.hasRegisteredOnce;
|
|
43
|
+
const registerReason: "spawn" | "reattach" = isFirstRegister ? "spawn" : "reattach";
|
|
44
|
+
|
|
45
|
+
// Include the spawn correlation token (server-minted UUID injected via
|
|
46
|
+
// env var at spawn time) ONLY on the first register. Subsequent
|
|
47
|
+
// registers (reattach after dashboard restart, in-process Ctrl+F fork)
|
|
48
|
+
// omit it because the sessionId is already known to the server.
|
|
49
|
+
// See change: spawn-correlation-token (Decision 3).
|
|
50
|
+
const spawnToken = isFirstRegister ? process.env.PI_DASHBOARD_SPAWN_TOKEN : undefined;
|
|
43
51
|
|
|
44
52
|
bc.connection.send({
|
|
45
53
|
type: "session_register",
|
|
@@ -55,6 +63,7 @@ export function sendStateSync(
|
|
|
55
63
|
eventCount,
|
|
56
64
|
pid: process.pid,
|
|
57
65
|
registerReason,
|
|
66
|
+
...(spawnToken ? { spawnToken } : {}),
|
|
58
67
|
});
|
|
59
68
|
|
|
60
69
|
bc.hasRegisteredOnce = true;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared extension-slash-command dispatch branch used by both bridge.ts
|
|
3
|
+
* (sessionPrompt callback) and command-handler.ts (slash else-arm fallback).
|
|
4
|
+
*
|
|
5
|
+
* Routing-step 9 from `command-routing` spec — three-way decision:
|
|
6
|
+
* - Path B: when `pi.dispatchCommand` is a function → call it directly.
|
|
7
|
+
* - Path C: when `pi.dispatchCommand` is absent AND the bridge runs inside a
|
|
8
|
+
* dashboard-spawned headless `pi --mode rpc` AND a `connection` is wired
|
|
9
|
+
* → emit `dispatch_extension_command` to the server (server forwards to
|
|
10
|
+
* the per-session RPC keeper UDS and emits the terminal command_feedback).
|
|
11
|
+
* - Path D (stopgap, last resort): `pi.dispatchCommand` absent AND the bridge
|
|
12
|
+
* is NOT headless (tmux / wt / unrecognized spawn shape) OR no `connection`
|
|
13
|
+
* was supplied → emit `command_feedback {status:"error"}` with a pi-version
|
|
14
|
+
* reminder.
|
|
15
|
+
*
|
|
16
|
+
* If `text` is NOT an extension command, return `false` so the caller can
|
|
17
|
+
* fall through to its existing template-expansion / sendUserMessage path.
|
|
18
|
+
*
|
|
19
|
+
* Guarantees: EXACTLY ONE `started` event AND EXACTLY ONE terminal event
|
|
20
|
+
* (`completed` xor `error`) per dispatch, across all three paths combined.
|
|
21
|
+
* Path C does NOT emit a terminal event — the server emits it.
|
|
22
|
+
*
|
|
23
|
+
* See change: fix-extension-slash-commands-in-dashboard,
|
|
24
|
+
* add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
25
|
+
*/
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
import type { ExtensionToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
|
|
28
|
+
import { hasDispatchCommand, isExtensionSlashCommand, isHeadlessRpcSession } from "./bridge-context.js";
|
|
29
|
+
|
|
30
|
+
export type FeedbackSink = (msg: ExtensionToServerMessage) => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Minimal connection surface used by Path C. Concrete implementation is
|
|
34
|
+
* `ConnectionManager` (`connection.ts`) but a structural type keeps this
|
|
35
|
+
* helper unit-testable without a real WebSocket.
|
|
36
|
+
*/
|
|
37
|
+
export interface DispatchConnection {
|
|
38
|
+
send(msg: ExtensionToServerMessage): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PI_071_REQUIRED =
|
|
42
|
+
"Extension slash commands cannot be dispatched from the dashboard yet — requires pi 0.71+ (`pi.dispatchCommand`). Invoke from the pi TUI, or use the extension's tools directly.";
|
|
43
|
+
|
|
44
|
+
function emitFeedback(
|
|
45
|
+
sink: FeedbackSink | undefined,
|
|
46
|
+
sessionId: string,
|
|
47
|
+
command: string,
|
|
48
|
+
status: "started" | "completed" | "error",
|
|
49
|
+
message?: string,
|
|
50
|
+
): void {
|
|
51
|
+
if (!sink) return;
|
|
52
|
+
sink({
|
|
53
|
+
type: "event_forward",
|
|
54
|
+
sessionId,
|
|
55
|
+
event: {
|
|
56
|
+
eventType: "command_feedback",
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
data: message === undefined ? { command, status } : { command, status, message },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Try to dispatch a slash command as an extension command.
|
|
65
|
+
*
|
|
66
|
+
* @returns `true` if the helper handled the text (extension command detected;
|
|
67
|
+
* dispatch attempted or stopgap emitted). The caller MUST NOT fall
|
|
68
|
+
* through to template expansion or `sendUserMessage`.
|
|
69
|
+
* @returns `false` if `text` is not an extension slash command. The caller
|
|
70
|
+
* SHOULD continue with its existing fallback path.
|
|
71
|
+
*/
|
|
72
|
+
export async function tryDispatchExtensionCommand(
|
|
73
|
+
pi: unknown,
|
|
74
|
+
text: string,
|
|
75
|
+
sessionId: string,
|
|
76
|
+
sink: FeedbackSink | undefined,
|
|
77
|
+
connection?: DispatchConnection,
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
// Defensive: pi.getCommands() can throw on a stale ctx during dispose.
|
|
80
|
+
let commands: Array<{ name: string; source?: string }> = [];
|
|
81
|
+
try {
|
|
82
|
+
const got = (pi as any)?.getCommands?.();
|
|
83
|
+
if (Array.isArray(got)) commands = got;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.warn("[dashboard] getCommands stale on slash-dispatch", err);
|
|
86
|
+
return false; // fall through to existing path; preserve today's behavior
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isExtensionSlashCommand(text, commands)) return false;
|
|
90
|
+
|
|
91
|
+
emitFeedback(sink, sessionId, text, "started");
|
|
92
|
+
|
|
93
|
+
// Path B (preferred when available): pi 0.71+ exposes dispatchCommand.
|
|
94
|
+
if (hasDispatchCommand(pi)) {
|
|
95
|
+
try {
|
|
96
|
+
await (pi as any).dispatchCommand(text, { streamingBehavior: "followUp" });
|
|
97
|
+
emitFeedback(sink, sessionId, text, "completed");
|
|
98
|
+
} catch (err: any) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
emitFeedback(sink, sessionId, text, "error", message);
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Path C: headless RPC session, dispatchCommand absent. Hand off to the
|
|
106
|
+
// server, which writes the line to the session's RPC keeper UDS and
|
|
107
|
+
// emits the terminal command_feedback. The bridge does NOT emit a
|
|
108
|
+
// terminal event for this path — that would duplicate the reducer's
|
|
109
|
+
// started→terminal upsert. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
110
|
+
if (connection && isHeadlessRpcSession()) {
|
|
111
|
+
connection.send({
|
|
112
|
+
type: "dispatch_extension_command",
|
|
113
|
+
sessionId,
|
|
114
|
+
command: text,
|
|
115
|
+
requestId: crypto.randomUUID(),
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Path D (stopgap): no dispatchCommand and not headless (tmux / wt / unrecognized).
|
|
121
|
+
emitFeedback(sink, sessionId, text, "error", PI_071_REQUIRED);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage-limit event orderer.
|
|
3
|
+
*
|
|
4
|
+
* Tracks per-session whether an `auto_retry_start` was forwarded without a
|
|
5
|
+
* matching `auto_retry_end`, and — when an `agent_end` arrives whose terminal
|
|
6
|
+
* assistant message has a usage-limit / quota errorMessage — synthesizes an
|
|
7
|
+
* `auto_retry_end { success: false }` to emit BEFORE the `agent_end`.
|
|
8
|
+
*
|
|
9
|
+
* Pure logic (no I/O). The bridge wires this into its forwarding pipeline.
|
|
10
|
+
*
|
|
11
|
+
* See change: fix-provider-retry-infinite-loop.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const USAGE_LIMIT_PATTERN =
|
|
15
|
+
/usage[_ ]limit[_ ]reached|usage_not_included|quota[_ ]exceeded|monthly limit|hourly limit|reset after \d+[hms]/i;
|
|
16
|
+
|
|
17
|
+
export interface SyntheticEventEnvelope {
|
|
18
|
+
eventType: "auto_retry_end";
|
|
19
|
+
data: { success: false; attempt: -1; finalError: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class UsageLimitOrderer {
|
|
23
|
+
/** sessionId → true while a retry is in flight (no auto_retry_end seen yet). */
|
|
24
|
+
private pending = new Set<string>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Notify the orderer of an outbound `auto_retry_start` for sessionId.
|
|
28
|
+
*/
|
|
29
|
+
noteRetryStart(sessionId: string): void {
|
|
30
|
+
this.pending.add(sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Notify the orderer of an outbound `auto_retry_end` for sessionId.
|
|
35
|
+
* Subsequent `agent_end` events will not synthesize unless a new retry
|
|
36
|
+
* has been started.
|
|
37
|
+
*/
|
|
38
|
+
noteRetryEnd(sessionId: string): void {
|
|
39
|
+
this.pending.delete(sessionId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inspect an `agent_end` payload. If the terminal message has a
|
|
44
|
+
* usage-limit error AND we have an unmatched retry-start for this session,
|
|
45
|
+
* return the synthetic event the bridge should forward BEFORE the agent_end.
|
|
46
|
+
*
|
|
47
|
+
* Returns null when no synthesis is needed. Always clears the pending flag
|
|
48
|
+
* after a terminal agent_end (errored or not) so we don't double-synthesize.
|
|
49
|
+
*/
|
|
50
|
+
maybeSynthesize(
|
|
51
|
+
sessionId: string,
|
|
52
|
+
agentEndData: Record<string, unknown> | undefined,
|
|
53
|
+
): SyntheticEventEnvelope | null {
|
|
54
|
+
const wasPending = this.pending.has(sessionId);
|
|
55
|
+
// Always clear on agent_end: a terminal turn ends any retry tracking.
|
|
56
|
+
this.pending.delete(sessionId);
|
|
57
|
+
|
|
58
|
+
if (!wasPending || !agentEndData) return null;
|
|
59
|
+
const messages = agentEndData.messages;
|
|
60
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
61
|
+
const last = messages[messages.length - 1] as Record<string, unknown> | undefined;
|
|
62
|
+
if (!last || last.stopReason !== "error") return null;
|
|
63
|
+
const errorMessage = typeof last.errorMessage === "string" ? last.errorMessage : "";
|
|
64
|
+
if (!errorMessage || !USAGE_LIMIT_PATTERN.test(errorMessage)) return null;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
eventType: "auto_retry_end",
|
|
68
|
+
data: { success: false, attempt: -1, finalError: errorMessage },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Test-only: inspect pending state. */
|
|
73
|
+
hasPending(sessionId: string): boolean {
|
|
74
|
+
return this.pending.has(sessionId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pi-dashboard CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* The actual CLI is `../src/cli.ts`. This wrapper exists because a
|
|
6
|
+
* `#!/usr/bin/env` shebang cannot interpolate a dynamic `--import`
|
|
7
|
+
* loader path. The wrapper resolves jiti from pi's tree at runtime
|
|
8
|
+
* and re-execs Node with `--import <jiti-url> cli.ts <args>`.
|
|
9
|
+
*
|
|
10
|
+
* No tsx fallback: if jiti cannot be resolved, the wrapper exits 1
|
|
11
|
+
* with an install-hint pointing at pi. Mirrors the resolution shape
|
|
12
|
+
* in `packages/shared/src/resolve-jiti.ts` (cannot import the .ts
|
|
13
|
+
* module before a TS loader is registered, so the lookup is inlined).
|
|
14
|
+
*
|
|
15
|
+
* See change: replace-tsx-with-jiti.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const cliPath = resolve(here, "..", "src", "cli.ts");
|
|
25
|
+
|
|
26
|
+
// Mirrors packages/shared/src/resolve-jiti.ts JITI_PACKAGES.
|
|
27
|
+
const JITI_PACKAGES = ["jiti", "@mariozechner/jiti"];
|
|
28
|
+
|
|
29
|
+
/** Resolve pi's jiti register hook as a file:// URL. Returns null on miss. */
|
|
30
|
+
function resolveJitiUrl() {
|
|
31
|
+
const anchor = process.argv[1];
|
|
32
|
+
if (!anchor) return null;
|
|
33
|
+
let resolved;
|
|
34
|
+
try {
|
|
35
|
+
resolved = realpathSync(anchor);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const req = createRequire(resolved);
|
|
40
|
+
for (const pkg of JITI_PACKAGES) {
|
|
41
|
+
try {
|
|
42
|
+
const pkgJson = req.resolve(`${pkg}/package.json`);
|
|
43
|
+
const registerPath = join(dirname(pkgJson), "lib", "jiti-register.mjs");
|
|
44
|
+
return pathToFileURL(registerPath).href;
|
|
45
|
+
} catch {
|
|
46
|
+
/* try next */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const loader = resolveJitiUrl();
|
|
53
|
+
if (!loader) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
"pi-dashboard: cannot find jiti. " +
|
|
56
|
+
"Install pi: 'npm install -g @earendil-works/pi-coding-agent'\n",
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mirrors shouldUrlWrapEntry() in packages/shared/src/platform/node-spawn.ts:
|
|
62
|
+
// jiti needs the entry URL-wrapped on Windows (Node rejects raw drive-letter
|
|
63
|
+
// paths for --import). POSIX takes the raw path.
|
|
64
|
+
const entry = process.platform === "win32" ? pathToFileURL(cliPath).href : cliPath;
|
|
65
|
+
|
|
66
|
+
const child = spawn(
|
|
67
|
+
process.execPath,
|
|
68
|
+
["--import", loader, entry, ...process.argv.slice(2)],
|
|
69
|
+
{ stdio: "inherit", windowsHide: true },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
child.on("exit", (code, signal) => {
|
|
73
|
+
if (signal) {
|
|
74
|
+
// Re-raise the signal so the parent shell sees the same exit reason.
|
|
75
|
+
process.kill(process.pid, signal);
|
|
76
|
+
} else {
|
|
77
|
+
process.exit(code ?? 0);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
child.on("error", (err) => {
|
|
82
|
+
process.stderr.write(`pi-dashboard: failed to spawn Node: ${err.message}\n`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Dashboard server for monitoring and interacting with pi agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
"node": ">=22.18.0"
|
|
16
16
|
},
|
|
17
17
|
"piCompatibility": {
|
|
18
|
-
"minimum": "0.
|
|
19
|
-
"recommended": "0.
|
|
18
|
+
"minimum": "0.74.0",
|
|
19
|
+
"recommended": "0.74.0",
|
|
20
20
|
"maximum": null
|
|
21
21
|
},
|
|
22
22
|
"main": "src/cli.ts",
|
|
23
23
|
"bin": {
|
|
24
|
-
"pi-dashboard": "
|
|
24
|
+
"pi-dashboard": "bin/pi-dashboard.mjs"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
+
"bin/",
|
|
27
28
|
"src/",
|
|
28
29
|
"scripts/"
|
|
29
30
|
],
|
|
@@ -31,9 +32,9 @@
|
|
|
31
32
|
"postinstall": "node scripts/fix-pty-permissions.cjs"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@blackbelt-technology/dashboard-plugin-runtime": "^0.5.
|
|
35
|
-
"@blackbelt-technology/pi-dashboard-extension": "^0.5.
|
|
36
|
-
"@blackbelt-technology/pi-dashboard-shared": "^0.5.
|
|
35
|
+
"@blackbelt-technology/dashboard-plugin-runtime": "^0.5.2",
|
|
36
|
+
"@blackbelt-technology/pi-dashboard-extension": "^0.5.2",
|
|
37
|
+
"@blackbelt-technology/pi-dashboard-shared": "^0.5.2",
|
|
37
38
|
"@fastify/compress": "^8.3.1",
|
|
38
39
|
"@fastify/cookie": "^11.0.2",
|
|
39
40
|
"@fastify/cors": "^11.0.0",
|