@blackbelt-technology/pi-agent-dashboard 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +342 -267
- package/README.md +51 -2
- package/docs/architecture.md +266 -25
- package/package.json +14 -4
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-bus.test.ts +44 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
- package/packages/extension/src/__tests__/vcs-info-jj.test.ts +145 -0
- package/packages/extension/src/__tests__/{git-info.test.ts → vcs-info.test.ts} +6 -6
- package/packages/extension/src/bridge-context.ts +7 -0
- package/packages/extension/src/bridge.ts +142 -4
- package/packages/extension/src/command-handler.ts +6 -0
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- package/packages/extension/src/model-tracker.ts +35 -1
- package/packages/extension/src/prompt-bus.ts +4 -3
- package/packages/extension/src/prompt-expander.ts +50 -2
- package/packages/extension/src/provider-register.ts +117 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +6 -1
- package/packages/extension/src/vcs-info.ts +184 -0
- package/packages/server/package.json +4 -4
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +87 -0
- package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-unread-trigger.test.ts +4 -2
- package/packages/server/src/__tests__/jj-routes.test.ts +93 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +114 -0
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +177 -0
- package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +54 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-diff-vcs.test.ts +61 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +166 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-restart.test.ts +4 -4
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +58 -21
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +60 -2
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- package/packages/server/src/cli.ts +22 -0
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +57 -2
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- package/packages/server/src/openspec-tasks.ts +50 -19
- package/packages/server/src/pi-core-updater.ts +65 -9
- package/packages/server/src/pi-gateway.ts +6 -0
- package/packages/server/src/process-manager.ts +62 -11
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +41 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -0
- package/packages/server/src/routes/jj-routes.ts +386 -0
- package/packages/server/src/routes/provider-auth-routes.ts +9 -0
- package/packages/server/src/routes/session-routes.ts +12 -3
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +16 -9
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-diff.ts +118 -1
- package/packages/server/src/session-discovery.ts +10 -3
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +236 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/platform-jj.test.ts +339 -0
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +18 -2
- package/packages/shared/src/bootstrap-install.ts +196 -2
- package/packages/shared/src/browser-protocol.ts +112 -1
- package/packages/shared/src/config.ts +29 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/diff-types.ts +17 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/jj.ts +405 -0
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/protocol.ts +60 -2
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +19 -5
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +91 -0
- package/packages/extension/src/git-info.ts +0 -55
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepend the managed Node runtime directory to a child-process `PATH`.
|
|
3
|
+
*
|
|
4
|
+
* The managed Node lives at `<managedDir>/node/` (Windows: binaries at
|
|
5
|
+
* the root; Unix: binaries under `bin/`). When present, every spawn the
|
|
6
|
+
* dashboard controls SHALL inherit that directory at the front of its
|
|
7
|
+
* `PATH` so plain `node` / `npm` invocations inside the child process
|
|
8
|
+
* resolve to the managed runtime.
|
|
9
|
+
*
|
|
10
|
+
* Pure helper: never mutates `process.env`, returns a distinct cloned
|
|
11
|
+
* env object, no-ops when the managed runtime is absent.
|
|
12
|
+
*
|
|
13
|
+
* See change: embed-managed-node-runtime (spec: managed-node-runtime,
|
|
14
|
+
* Requirement: Spawned children inherit managed Node on PATH).
|
|
15
|
+
*/
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { getManagedDir, type ManagedPathsEnv } from "../managed-paths.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the bin directory inside the managed Node tree.
|
|
22
|
+
* Windows: `<managedDir>/node` (node.exe + npm.cmd live at root)
|
|
23
|
+
* Unix: `<managedDir>/node/bin` (node, npm, npx live under bin/)
|
|
24
|
+
*
|
|
25
|
+
* `platform` defaults to `process.platform`; tests inject `"win32"` from
|
|
26
|
+
* a Linux host to exercise the Windows layout.
|
|
27
|
+
*/
|
|
28
|
+
export function getManagedNodeBinDir(
|
|
29
|
+
env?: ManagedPathsEnv,
|
|
30
|
+
platform: NodeJS.Platform = process.platform,
|
|
31
|
+
): string {
|
|
32
|
+
const root = path.join(getManagedDir(env), "node");
|
|
33
|
+
return platform === "win32" ? root : path.join(root, "bin");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Path to the managed `node` / `node.exe` binary. */
|
|
37
|
+
export function getManagedNodeBinary(
|
|
38
|
+
env?: ManagedPathsEnv,
|
|
39
|
+
platform: NodeJS.Platform = process.platform,
|
|
40
|
+
): string {
|
|
41
|
+
const bin = getManagedNodeBinDir(env, platform);
|
|
42
|
+
return path.join(bin, platform === "win32" ? "node.exe" : "node");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True iff the managed Node runtime is installed (binary exists). */
|
|
46
|
+
export function isManagedNodePresent(
|
|
47
|
+
env?: ManagedPathsEnv,
|
|
48
|
+
platform: NodeJS.Platform = process.platform,
|
|
49
|
+
): boolean {
|
|
50
|
+
return existsSync(getManagedNodeBinary(env, platform));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return a shallow-cloned env with the managed Node bin directory
|
|
55
|
+
* prepended to `PATH`. No-op (still returns a clone) when the managed
|
|
56
|
+
* runtime is not installed or its directory is already on PATH.
|
|
57
|
+
*
|
|
58
|
+
* Never mutates the input env or `process.env`.
|
|
59
|
+
*/
|
|
60
|
+
export function prependManagedNodeToPath(
|
|
61
|
+
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
62
|
+
managedPathsEnv?: ManagedPathsEnv,
|
|
63
|
+
): NodeJS.ProcessEnv {
|
|
64
|
+
const cloned: NodeJS.ProcessEnv = { ...baseEnv };
|
|
65
|
+
if (!isManagedNodePresent(managedPathsEnv)) return cloned;
|
|
66
|
+
|
|
67
|
+
const dir = getManagedNodeBinDir(managedPathsEnv);
|
|
68
|
+
const currentPath = cloned.PATH ?? "";
|
|
69
|
+
// Avoid duplicate prepends when the dir is already at the head; cheap
|
|
70
|
+
// string contains check matches `buildSpawnEnv` style.
|
|
71
|
+
if (currentPath.split(path.delimiter).includes(dir)) return cloned;
|
|
72
|
+
|
|
73
|
+
cloned.PATH = currentPath
|
|
74
|
+
? `${dir}${path.delimiter}${currentPath}`
|
|
75
|
+
: dir;
|
|
76
|
+
return cloned;
|
|
77
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extension ↔ Server WebSocket protocol messages.
|
|
3
3
|
*/
|
|
4
|
-
import type { DashboardEvent, CommandInfo, FlowInfo, SessionSource, ImageContent, FileEntry, TurnUsage, ContextUsage, ModelInfo, PiSessionInfo, OpenSpecPhase, RoleInfo, ExtensionUiModule, DecoratorDescriptor } from "./types.js";
|
|
4
|
+
import type { DashboardEvent, CommandInfo, FlowInfo, SessionSource, ImageContent, FileEntry, TurnUsage, ContextUsage, ModelInfo, ProviderInfo, PiSessionInfo, OpenSpecPhase, RoleInfo, ExtensionUiModule, DecoratorDescriptor } from "./types.js";
|
|
5
5
|
|
|
6
6
|
// ── Extension → Server ──────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -130,6 +130,19 @@ export interface GitInfoUpdateMessage {
|
|
|
130
130
|
gitPrUrl?: string;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Bridge → server: jj workspace state for the session's cwd.
|
|
135
|
+
* Sent only when the bridge's VCS probe finds `.jj/` AND `jj` resolves
|
|
136
|
+
* via the tool registry. Cleared (sent with `jjState: null`) when the
|
|
137
|
+
* session leaves a jj repo (e.g. cwd switch). See change: add-jj-workspace-plugin.
|
|
138
|
+
*/
|
|
139
|
+
export interface JjStateUpdateMessage {
|
|
140
|
+
type: "jj_state_update";
|
|
141
|
+
sessionId: string;
|
|
142
|
+
/** `null` clears the session's `jjState` field on the server. */
|
|
143
|
+
jjState: import("./types.js").JjState | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
// OpenSpecUpdateMessage removed — server polls directly via DirectoryService
|
|
134
147
|
|
|
135
148
|
export interface ModelsListMessage {
|
|
@@ -138,6 +151,17 @@ export interface ModelsListMessage {
|
|
|
138
151
|
models: ModelInfo[];
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Bridge -> server: pi's live provider catalogue derived from
|
|
156
|
+
* `modelRegistry.authStorage` + `modelRegistry.getProviderDisplayName`.
|
|
157
|
+
* Sent alongside ModelsListMessage. See change: replace-hardcoded-provider-lists.
|
|
158
|
+
*/
|
|
159
|
+
export interface ProvidersListMessage {
|
|
160
|
+
type: "providers_list";
|
|
161
|
+
sessionId: string;
|
|
162
|
+
providers: ProviderInfo[];
|
|
163
|
+
}
|
|
164
|
+
|
|
141
165
|
export interface SessionNameUpdateMessage {
|
|
142
166
|
type: "session_name_update";
|
|
143
167
|
sessionId: string;
|
|
@@ -273,6 +297,27 @@ export interface ExtUiDecoratorMessage {
|
|
|
273
297
|
removed?: boolean;
|
|
274
298
|
}
|
|
275
299
|
|
|
300
|
+
// ── Markdown asset inlining (chat-markdown-local-images-and-math) ──
|
|
301
|
+
//
|
|
302
|
+
// Bridge → server: register a base64-encoded image asset under a content
|
|
303
|
+
// hash. Emitted by the bridge BEFORE the `message_update` / `message_end`
|
|
304
|
+
// event whose text references `pi-asset:<hash>`. Bytes ride exactly once
|
|
305
|
+
// per (session, hash) pair — subsequent references in later events emit
|
|
306
|
+
// no further `asset_register`. Persisted in `events.jsonl` alongside the
|
|
307
|
+
// referencing message events so reconnect/replay rebuilds the per-session
|
|
308
|
+
// asset registry deterministically. See change:
|
|
309
|
+
// chat-markdown-local-images-and-math.
|
|
310
|
+
export interface AssetRegisterMessage {
|
|
311
|
+
type: "asset_register";
|
|
312
|
+
sessionId: string;
|
|
313
|
+
/** Content hash (sha256 truncated to 16 hex chars). */
|
|
314
|
+
hash: string;
|
|
315
|
+
/** MIME type (one of the bridge's image allowlist). */
|
|
316
|
+
mimeType: string;
|
|
317
|
+
/** Base64-encoded file bytes. */
|
|
318
|
+
data: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
276
321
|
export type ExtensionToServerMessage =
|
|
277
322
|
| SessionRegisterMessage
|
|
278
323
|
| SessionUnregisterMessage
|
|
@@ -283,8 +328,10 @@ export type ExtensionToServerMessage =
|
|
|
283
328
|
| ExtensionUiRequestMessage
|
|
284
329
|
| FilesListMessage
|
|
285
330
|
| GitInfoUpdateMessage
|
|
331
|
+
| JjStateUpdateMessage
|
|
286
332
|
| SessionNameUpdateMessage
|
|
287
333
|
| ModelsListMessage
|
|
334
|
+
| ProvidersListMessage
|
|
288
335
|
| ModelUpdateMessage
|
|
289
336
|
| SessionsListExtensionMessage
|
|
290
337
|
| ExtensionUiDismissMessage
|
|
@@ -298,7 +345,8 @@ export type ExtensionToServerMessage =
|
|
|
298
345
|
| ProcessListMessage
|
|
299
346
|
| UiModulesListMessage
|
|
300
347
|
| UiDataListMessage
|
|
301
|
-
| ExtUiDecoratorMessage
|
|
348
|
+
| ExtUiDecoratorMessage
|
|
349
|
+
| AssetRegisterMessage;
|
|
302
350
|
|
|
303
351
|
// ── Server → Extension ──────────────────────────────────────────────
|
|
304
352
|
|
|
@@ -343,6 +391,15 @@ export interface RequestModelsMessage {
|
|
|
343
391
|
sessionId: string;
|
|
344
392
|
}
|
|
345
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Server -> bridge: ask the bridge to push a fresh providers_list.
|
|
396
|
+
* See change: replace-hardcoded-provider-lists.
|
|
397
|
+
*/
|
|
398
|
+
export interface RequestProvidersMessage {
|
|
399
|
+
type: "request_providers";
|
|
400
|
+
sessionId: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
346
403
|
export interface SetThinkingLevelMessage {
|
|
347
404
|
type: "set_thinking_level";
|
|
348
405
|
sessionId: string;
|
|
@@ -500,6 +557,7 @@ export type ServerToExtensionMessage =
|
|
|
500
557
|
| ListFilesMessage
|
|
501
558
|
| RenameSessionExtensionMessage
|
|
502
559
|
| RequestModelsMessage
|
|
560
|
+
| RequestProvidersMessage
|
|
503
561
|
| SetThinkingLevelMessage
|
|
504
562
|
| ListSessionsExtensionMessage
|
|
505
563
|
| SetModelMessage
|
|
@@ -255,6 +255,10 @@ export interface ProviderAuthStatus {
|
|
|
255
255
|
authenticated: boolean;
|
|
256
256
|
expires?: number;
|
|
257
257
|
maskedKey?: string;
|
|
258
|
+
/** Name of the env var pi-ai consults for this provider (api-key rows only). */
|
|
259
|
+
envVar?: string;
|
|
260
|
+
/** True when configured via ambient credential chain (AWS profile / GCP ADC). */
|
|
261
|
+
ambient?: boolean;
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
export interface AuthorizeResponse {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill block parser & builder.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `_expandSkillCommand` (in `@mariozechner/pi-coding-agent`) wraps skill
|
|
5
|
+
* expansions in a `<skill name="..." location="...">…</skill>\n\nargs` envelope.
|
|
6
|
+
* The dashboard's bridge expander (`packages/extension/src/prompt-expander.ts`)
|
|
7
|
+
* aligns to the same byte format. This module is the single source of truth for
|
|
8
|
+
* both producing and recovering that envelope.
|
|
9
|
+
*
|
|
10
|
+
* See change: render-skill-invocations-collapsibly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface SkillBlock {
|
|
14
|
+
/** Bare skill name (no `skill:` prefix), e.g. `"openspec-explore"`. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Absolute path to `SKILL.md`. */
|
|
17
|
+
location: string;
|
|
18
|
+
/**
|
|
19
|
+
* Skill body with the `References are relative to <baseDir>.\n\n` preamble
|
|
20
|
+
* stripped — what users see in the card. The preamble is bridge-internal
|
|
21
|
+
* orientation for the LLM and is not interesting to users.
|
|
22
|
+
*
|
|
23
|
+
* Pi's own `parseSkillBlock` returns the unstripped form (calls it `content`).
|
|
24
|
+
* We strip here so the renderer doesn't have to. If the preamble shape ever
|
|
25
|
+
* changes upstream and stripping fails, `body` falls back to the captured
|
|
26
|
+
* content verbatim.
|
|
27
|
+
*/
|
|
28
|
+
body: string;
|
|
29
|
+
/** User text after the skill name. `undefined` when no args were typed. */
|
|
30
|
+
args: string | undefined;
|
|
31
|
+
/** Slash-command form: `"/skill:" + name + (args ? " " + args : "")`. */
|
|
32
|
+
condensed: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Anchored, non-greedy match of a wrapped skill block.
|
|
37
|
+
*
|
|
38
|
+
* Why anchored: prevents a literal `<skill>` substring elsewhere in user text
|
|
39
|
+
* from matching. Why non-greedy + optional trailing args at end-of-string:
|
|
40
|
+
* forces the regex engine to extend the body to the last valid
|
|
41
|
+
* `\n</skill>(\n\nargs)?$` boundary, so SKILL.md bodies that document the
|
|
42
|
+
* `<skill>` tag (e.g. include the literal text in code samples) do not
|
|
43
|
+
* terminate the match prematurely.
|
|
44
|
+
*/
|
|
45
|
+
const SKILL_BLOCK_RE =
|
|
46
|
+
/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse a skill block from message text. Returns `null` when the input is not
|
|
50
|
+
* a well-formed skill envelope.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Strip the `References are relative to <baseDir>.\n\n` preamble from a captured
|
|
54
|
+
* `<skill>` content block. Returns the stripped body, or the input unchanged if
|
|
55
|
+
* the preamble shape doesn't match.
|
|
56
|
+
*/
|
|
57
|
+
function stripReferencesPreamble(content: string): string {
|
|
58
|
+
const m = content.match(/^References are relative to [^\n]+\.\n\n([\s\S]*)$/);
|
|
59
|
+
return m ? m[1] : content;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseSkillBlock(text: string): SkillBlock | null {
|
|
63
|
+
const m = text.match(SKILL_BLOCK_RE);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
const name = m[1];
|
|
66
|
+
const location = m[2];
|
|
67
|
+
const body = stripReferencesPreamble(m[3]);
|
|
68
|
+
const args = m[4];
|
|
69
|
+
const condensed = `/skill:${name}${args ? " " + args : ""}`;
|
|
70
|
+
return { name, location, body, args, condensed };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BuildSkillBlockArgs {
|
|
74
|
+
/** Bare skill name (no `skill:` prefix). */
|
|
75
|
+
name: string;
|
|
76
|
+
/** Absolute path to `SKILL.md`. */
|
|
77
|
+
filePath: string;
|
|
78
|
+
/** Skill base directory — `dirname(filePath)`. */
|
|
79
|
+
baseDir: string;
|
|
80
|
+
/** Skill body, frontmatter already stripped. Should be `.trim()`-ed. */
|
|
81
|
+
body: string;
|
|
82
|
+
/** Optional user-typed args appended after the closing tag. */
|
|
83
|
+
userArgs?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a skill block in the exact byte format pi's `_expandSkillCommand`
|
|
88
|
+
* produces. The output is byte-identical to pi's output for the same inputs;
|
|
89
|
+
* `parseSkillBlock(buildSkillBlock(x))` round-trips for `name`, `body`, `args`.
|
|
90
|
+
*/
|
|
91
|
+
export function buildSkillBlock(input: BuildSkillBlockArgs): string {
|
|
92
|
+
const wrapper =
|
|
93
|
+
`<skill name="${input.name}" location="${input.filePath}">\n` +
|
|
94
|
+
`References are relative to ${input.baseDir}.\n\n` +
|
|
95
|
+
`${input.body}\n` +
|
|
96
|
+
`</skill>`;
|
|
97
|
+
return input.userArgs ? `${wrapper}\n\n${input.userArgs}` : wrapper;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Condense a user-message content string for `firstMessage` / display purposes.
|
|
102
|
+
*
|
|
103
|
+
* If `text` parses as a `<skill>` envelope, returns the slash-command form
|
|
104
|
+
* (`/skill:name args`) truncated to `maxLen` chars. Otherwise returns
|
|
105
|
+
* `text.slice(0, maxLen)`. Used by session-scanner / session-discovery so the
|
|
106
|
+
* 200-char `firstMessage` shows the recognisable slash form instead of the
|
|
107
|
+
* front of an opaque wrapper.
|
|
108
|
+
*
|
|
109
|
+
* See change: render-skill-invocations-collapsibly.
|
|
110
|
+
*/
|
|
111
|
+
export function condenseForFirstMessage(text: string, maxLen: number): string {
|
|
112
|
+
const block = parseSkillBlock(text);
|
|
113
|
+
if (block) return block.condensed.slice(0, maxLen);
|
|
114
|
+
return text.slice(0, maxLen);
|
|
115
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain-order tests for the managed-Node runtime strategy.
|
|
3
|
+
*
|
|
4
|
+
* After change `embed-managed-node-runtime`, the `node` and `npm`
|
|
5
|
+
* strategy chains gain a `managedRuntimeStrategy` between
|
|
6
|
+
* `overrideStrategy` and the existing PATH/where lookup. These tests
|
|
7
|
+
* pin the precedence:
|
|
8
|
+
*
|
|
9
|
+
* 1. override (tool-overrides.json) — wins
|
|
10
|
+
* 2. managed runtime (<managedDir>/node/) — preferred over PATH
|
|
11
|
+
* 3. where / PATH — fallback
|
|
12
|
+
*
|
|
13
|
+
* `exists` is injected so no real filesystem is touched.
|
|
14
|
+
*
|
|
15
|
+
* See change: embed-managed-node-runtime (task 3.3).
|
|
16
|
+
*/
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import {
|
|
21
|
+
ToolRegistry,
|
|
22
|
+
registerDefaultTools,
|
|
23
|
+
OverridesStore,
|
|
24
|
+
} from "../index.js";
|
|
25
|
+
|
|
26
|
+
function freshRegistry(opts: {
|
|
27
|
+
exists?: (p: string) => boolean;
|
|
28
|
+
which?: (name: string) => string | null;
|
|
29
|
+
overrides?: Record<string, string>;
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
}) {
|
|
32
|
+
const store = new OverridesStore({
|
|
33
|
+
filePath: path.join(os.tmpdir(), `mr-test-${Math.random()}.json`),
|
|
34
|
+
warn: () => {},
|
|
35
|
+
});
|
|
36
|
+
for (const [k, v] of Object.entries(opts.overrides ?? {})) store.set(k, v);
|
|
37
|
+
|
|
38
|
+
const r = new ToolRegistry({
|
|
39
|
+
overrides: store,
|
|
40
|
+
platform: opts.platform ?? "linux",
|
|
41
|
+
});
|
|
42
|
+
registerDefaultTools(r, {
|
|
43
|
+
exists: opts.exists ?? (() => false),
|
|
44
|
+
which: opts.which ?? (() => null),
|
|
45
|
+
npmRootGlobal: () => "",
|
|
46
|
+
});
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const HOME = os.homedir();
|
|
51
|
+
const MANAGED_NODE_UNIX = path.join(HOME, ".pi-dashboard", "node", "bin", "node");
|
|
52
|
+
const MANAGED_NPM_UNIX = path.join(HOME, ".pi-dashboard", "node", "bin", "npm");
|
|
53
|
+
const MANAGED_NODE_WIN = path.join(HOME, ".pi-dashboard", "node", "node.exe");
|
|
54
|
+
const MANAGED_NPM_WIN = path.join(HOME, ".pi-dashboard", "node", "npm.cmd");
|
|
55
|
+
|
|
56
|
+
describe("node: managed-runtime strategy precedence", () => {
|
|
57
|
+
it("managed runtime present → returned over PATH (Unix)", () => {
|
|
58
|
+
const r = freshRegistry({
|
|
59
|
+
platform: "linux",
|
|
60
|
+
exists: (p) => p === MANAGED_NODE_UNIX,
|
|
61
|
+
which: () => "/usr/bin/node",
|
|
62
|
+
});
|
|
63
|
+
const res = r.resolve("node");
|
|
64
|
+
expect(res.ok).toBe(true);
|
|
65
|
+
expect(res.path).toBe(MANAGED_NODE_UNIX);
|
|
66
|
+
expect(res.source).toBe("managed");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("managed runtime present → returned over PATH (Windows)", () => {
|
|
70
|
+
const r = freshRegistry({
|
|
71
|
+
platform: "win32",
|
|
72
|
+
exists: (p) => p === MANAGED_NODE_WIN,
|
|
73
|
+
which: () => "C:\\node\\node.exe",
|
|
74
|
+
});
|
|
75
|
+
const res = r.resolve("node");
|
|
76
|
+
expect(res.ok).toBe(true);
|
|
77
|
+
expect(res.path).toBe(MANAGED_NODE_WIN);
|
|
78
|
+
expect(res.source).toBe("managed");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("override wins over managed runtime", () => {
|
|
82
|
+
const custom = "/opt/custom/node";
|
|
83
|
+
const r = freshRegistry({
|
|
84
|
+
platform: "linux",
|
|
85
|
+
overrides: { node: custom },
|
|
86
|
+
exists: (p) => p === custom || p === MANAGED_NODE_UNIX,
|
|
87
|
+
});
|
|
88
|
+
const res = r.resolve("node");
|
|
89
|
+
expect(res.ok).toBe(true);
|
|
90
|
+
expect(res.path).toBe(custom);
|
|
91
|
+
expect(res.source).toBe("override");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("both absent → falls through to PATH lookup", () => {
|
|
95
|
+
const r = freshRegistry({
|
|
96
|
+
platform: "linux",
|
|
97
|
+
exists: () => false,
|
|
98
|
+
which: (name) => (name === "node" ? "/usr/bin/node" : null),
|
|
99
|
+
});
|
|
100
|
+
const res = r.resolve("node");
|
|
101
|
+
expect(res.ok).toBe(true);
|
|
102
|
+
expect(res.path).toBe("/usr/bin/node");
|
|
103
|
+
expect(res.source).toBe("system");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("nothing present → ok:false with diagnostic trail", () => {
|
|
107
|
+
const r = freshRegistry({ platform: "linux", exists: () => false, which: () => null });
|
|
108
|
+
const res = r.resolve("node");
|
|
109
|
+
expect(res.ok).toBe(false);
|
|
110
|
+
const trailNames = res.tried.map((t) => t.strategy);
|
|
111
|
+
expect(trailNames).toContain("override");
|
|
112
|
+
expect(trailNames).toContain("managed");
|
|
113
|
+
expect(trailNames).toContain("where");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("npm: managed-runtime strategy precedence", () => {
|
|
118
|
+
it("managed npm present → returned over PATH (Unix)", () => {
|
|
119
|
+
const r = freshRegistry({
|
|
120
|
+
platform: "linux",
|
|
121
|
+
exists: (p) => p === MANAGED_NPM_UNIX,
|
|
122
|
+
which: () => "/usr/bin/npm",
|
|
123
|
+
});
|
|
124
|
+
const res = r.resolve("npm");
|
|
125
|
+
expect(res.ok).toBe(true);
|
|
126
|
+
expect(res.path).toBe(MANAGED_NPM_UNIX);
|
|
127
|
+
expect(res.source).toBe("managed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("managed npm present → returned over PATH (Windows)", () => {
|
|
131
|
+
const r = freshRegistry({
|
|
132
|
+
platform: "win32",
|
|
133
|
+
exists: (p) => p === MANAGED_NPM_WIN,
|
|
134
|
+
which: () => "C:\\node\\npm.cmd",
|
|
135
|
+
});
|
|
136
|
+
const res = r.resolve("npm");
|
|
137
|
+
expect(res.ok).toBe(true);
|
|
138
|
+
expect(res.path).toBe(MANAGED_NPM_WIN);
|
|
139
|
+
expect(res.source).toBe("managed");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("override wins over managed npm", () => {
|
|
143
|
+
const custom = "/opt/custom/npm";
|
|
144
|
+
const r = freshRegistry({
|
|
145
|
+
platform: "linux",
|
|
146
|
+
overrides: { npm: custom },
|
|
147
|
+
exists: (p) => p === custom || p === MANAGED_NPM_UNIX,
|
|
148
|
+
});
|
|
149
|
+
const res = r.resolve("npm");
|
|
150
|
+
expect(res.ok).toBe(true);
|
|
151
|
+
expect(res.path).toBe(custom);
|
|
152
|
+
expect(res.source).toBe("override");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("npm: managed absent + PATH present → falls through (Unix)", () => {
|
|
156
|
+
const r = freshRegistry({
|
|
157
|
+
platform: "linux",
|
|
158
|
+
exists: () => false,
|
|
159
|
+
which: (name) => (name === "npm" ? "/usr/bin/npm" : null),
|
|
160
|
+
});
|
|
161
|
+
const res = r.resolve("npm");
|
|
162
|
+
expect(res.ok).toBe(true);
|
|
163
|
+
expect(res.path).toBe("/usr/bin/npm");
|
|
164
|
+
expect(res.source).toBe("system");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
bareImportStrategy,
|
|
19
19
|
managedBinStrategy,
|
|
20
20
|
managedModuleStrategy,
|
|
21
|
+
managedRuntimeStrategy,
|
|
21
22
|
npmGlobalStrategy,
|
|
22
23
|
overrideStrategy,
|
|
23
24
|
whereStrategy,
|
|
@@ -39,14 +40,20 @@ function classify(strategyName: string): Source {
|
|
|
39
40
|
// ── Binary definitions ──────────────────────────────────────────────────────
|
|
40
41
|
|
|
41
42
|
function binaryDef(binaryName: string, deps?: StrategyDeps): ToolDefinition {
|
|
43
|
+
// The `node` binary gets the managed-Node runtime strategy prepended
|
|
44
|
+
// (after override) so the persistent <managedDir>/node/ install wins
|
|
45
|
+
// over PATH lookup. See change: embed-managed-node-runtime.
|
|
46
|
+
const isNode = binaryName === "node";
|
|
47
|
+
const strategies = [
|
|
48
|
+
overrideStrategy(binaryName, deps),
|
|
49
|
+
...(isNode ? [managedRuntimeStrategy("node", deps)] : []),
|
|
50
|
+
managedBinStrategy(binaryName, deps),
|
|
51
|
+
whereStrategy(binaryName, deps),
|
|
52
|
+
];
|
|
42
53
|
return {
|
|
43
54
|
name: binaryName,
|
|
44
55
|
kind: "binary",
|
|
45
|
-
strategies
|
|
46
|
-
overrideStrategy(binaryName, deps),
|
|
47
|
-
managedBinStrategy(binaryName, deps),
|
|
48
|
-
whereStrategy(binaryName, deps),
|
|
49
|
-
],
|
|
56
|
+
strategies,
|
|
50
57
|
classify,
|
|
51
58
|
};
|
|
52
59
|
}
|
|
@@ -283,14 +290,20 @@ function npmExecutorDef(deps?: StrategyDeps): ToolDefinition {
|
|
|
283
290
|
},
|
|
284
291
|
};
|
|
285
292
|
|
|
293
|
+
// Managed-Node runtime: prefer <managedDir>/node/{npm.cmd,bin/npm}
|
|
294
|
+
// when the runtime is installed. See change: embed-managed-node-runtime.
|
|
295
|
+
const managedNpm = managedRuntimeStrategy("npm", deps);
|
|
296
|
+
|
|
286
297
|
const winStrategies = [
|
|
287
298
|
overrideStrategy("npm", deps),
|
|
299
|
+
managedNpm,
|
|
288
300
|
npmCliBesideNodeStrategy,
|
|
289
301
|
whereStrategy("npm", deps),
|
|
290
302
|
];
|
|
291
303
|
|
|
292
304
|
const unixStrategies = [
|
|
293
305
|
overrideStrategy("npm", deps),
|
|
306
|
+
managedNpm,
|
|
294
307
|
whereStrategy("npm", deps),
|
|
295
308
|
];
|
|
296
309
|
|
|
@@ -354,6 +367,7 @@ export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps
|
|
|
354
367
|
// Native binaries — no interpreter needed.
|
|
355
368
|
registry.register(binaryDef("node", deps));
|
|
356
369
|
registry.register(binaryDef("git", deps));
|
|
370
|
+
registry.register(binaryDef("jj", deps));
|
|
357
371
|
registry.register(binaryDef("zrok", deps));
|
|
358
372
|
|
|
359
373
|
// Platform-conditional process-inspection utilities. These are only
|
|
@@ -13,6 +13,7 @@ import { createRequire } from "node:module";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { ToolResolver, isAppImageSelfHit } from "../platform/binary-lookup.js";
|
|
15
15
|
import { getManagedBin, getManagedDir } from "../managed-paths.js";
|
|
16
|
+
import { getManagedNodeBinDir } from "../platform/managed-node-path.js";
|
|
16
17
|
import * as npm from "../platform/npm.js";
|
|
17
18
|
import type { Strategy, StrategyCtx, StrategyResult } from "./types.js";
|
|
18
19
|
|
|
@@ -86,6 +87,47 @@ export function overrideStrategy(toolName: string, deps?: StrategyDeps): Strateg
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Managed Node runtime: `<managedDir>/node/{node.exe,npm.cmd,npx.cmd}`
|
|
92
|
+
* on Windows or `<managedDir>/node/bin/{node,npm,npx}` on Unix.
|
|
93
|
+
*
|
|
94
|
+
* Lets `ToolRegistry.resolve("node")` and `resolve("npm")` prefer the
|
|
95
|
+
* persistent runtime under `~/.pi-dashboard/node/` (installed by
|
|
96
|
+
* `installManagedNode`) over the system PATH lookup, while still
|
|
97
|
+
* deferring to `tool-overrides.json`.
|
|
98
|
+
*
|
|
99
|
+
* Returns `null` when the managed Node runtime is not present, so the
|
|
100
|
+
* standalone-CLI / no-Electron-resources case falls through cleanly to
|
|
101
|
+
* the existing `where`/PATH strategy.
|
|
102
|
+
*
|
|
103
|
+
* See change: embed-managed-node-runtime (spec: managed-node-runtime,
|
|
104
|
+
* Requirement: ToolRegistry resolves managed runtime first).
|
|
105
|
+
*/
|
|
106
|
+
export function managedRuntimeStrategy(
|
|
107
|
+
toolName: "node" | "npm" | "npx",
|
|
108
|
+
deps?: StrategyDeps,
|
|
109
|
+
): Strategy {
|
|
110
|
+
const { exists } = d(deps);
|
|
111
|
+
return {
|
|
112
|
+
name: "managed",
|
|
113
|
+
run(ctx): StrategyResult {
|
|
114
|
+
const dir = getManagedNodeBinDir(ctx.env, ctx.platform);
|
|
115
|
+
const isWin = ctx.platform === "win32";
|
|
116
|
+
const fileName =
|
|
117
|
+
toolName === "node"
|
|
118
|
+
? isWin
|
|
119
|
+
? "node.exe"
|
|
120
|
+
: "node"
|
|
121
|
+
: isWin
|
|
122
|
+
? `${toolName}.cmd`
|
|
123
|
+
: toolName;
|
|
124
|
+
const candidate = path.join(dir, fileName);
|
|
125
|
+
if (exists(candidate)) return { ok: true, path: candidate };
|
|
126
|
+
return { ok: false, reason: `missing: ${candidate}` };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
89
131
|
/**
|
|
90
132
|
* Managed install: `~/.pi-dashboard/node_modules/.bin/<name>(.cmd)` for
|
|
91
133
|
* binaries, or any explicit relative path under `MANAGED_DIR` for
|