@north-light/crouter 0.3.216 → 0.3.218
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/dist/api/client.d.ts +5 -2
- package/dist/api/client.js +7 -1
- package/dist/api/dto/canvas.d.ts +9 -0
- package/dist/api/dto/nodes.d.ts +27 -1
- package/dist/builtin-memory/04-base-worker.md +7 -1
- package/dist/builtin-memory/internal/plugins.md +67 -0
- package/dist/builtin-memory/memory-read-orientation.md +13 -0
- package/dist/clients/attach/chrome/bash-jobs.js +1 -1
- package/dist/clients/attach/photon_rs_bg.wasm +0 -0
- package/dist/clients/attach/viewer.js +516 -516
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
- package/dist/clients/inbox/tui/input.js +12 -8
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +9 -3
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/memory/edit.js +6 -1
- package/dist/commands/memory/lint.d.ts +1 -6
- package/dist/commands/memory/lint.js +18 -126
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +17 -2
- package/dist/commands/memory/read.js +15 -6
- package/dist/commands/memory/shared.d.ts +33 -3
- package/dist/commands/memory/shared.js +180 -1
- package/dist/commands/memory/write.js +6 -1
- package/dist/commands/pkg/browse/catalog.js +2 -0
- package/dist/commands/pkg/browse/model.d.ts +4 -1
- package/dist/commands/pkg/plugin-manage.js +209 -148
- package/dist/core/__tests__/human-cancel-guard.test.js +30 -12
- package/dist/core/bash-jobs.d.ts +2 -5
- package/dist/core/bash-jobs.js +4 -8
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +16 -2
- package/dist/core/human/__tests__/page.test.js +12 -0
- package/dist/core/human/component-docs.js +6 -1
- package/dist/core/human/page-schema.d.ts +14 -0
- package/dist/core/human/page-schema.js +24 -7
- package/dist/core/human/page.d.ts +2 -0
- package/dist/core/human/page.js +17 -6
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/human/tickets.js +9 -2
- package/dist/core/io.js +3 -2
- package/dist/core/manifest.d.ts +2 -0
- package/dist/core/manifest.js +5 -0
- package/dist/core/memory/extensions.d.ts +30 -0
- package/dist/core/memory/extensions.js +219 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +25 -0
- package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
- package/dist/core/substrate/frontmatter-validation.js +101 -0
- package/dist/core/substrate/index.d.ts +1 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
- package/dist/daemon/api/handlers/canvas.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +4 -3
- package/dist/daemon/api/handlers/nodes.d.ts +1 -0
- package/dist/daemon/api/handlers/nodes.js +101 -19
- package/dist/daemon/api/handlers/reports.d.ts +4 -0
- package/dist/daemon/api/handlers/reports.js +14 -8
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/manage.d.ts +3 -0
- package/dist/daemon/manage.js +14 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
- package/dist/pi-extensions/canvas-bash-valve.js +19 -6
- package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
- package/dist/pi-extensions/canvas-preview-result.js +9 -23
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
//
|
|
8
8
|
// `{id}` is the target node.
|
|
9
9
|
import { readdirSync } from 'node:fs';
|
|
10
|
-
import { resolve } from 'node:path';
|
|
10
|
+
import { basename, resolve } from 'node:path';
|
|
11
11
|
import { appendSituationalContext, closeNode, getNode, listNodes, nowIso, reviveNode, } from '../../../index.js';
|
|
12
12
|
import { formatSituationalProse } from '../../../core/runtime/situational-context.js';
|
|
13
13
|
import { assertLaunchModelRegistered, forkNode, resolveProfileId, spawnChild } from '../../../core/runtime/spawn.js';
|
|
14
14
|
import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode } from '../../../core/canvas/canvas.js';
|
|
15
15
|
import { subtreeIds } from '../../../core/canvas/nav-model.js';
|
|
16
|
+
import { ticketCountsForNodes } from '../../../core/canvas/attention.js';
|
|
16
17
|
import { contextDir, reportsDir } from '../../../core/canvas/paths.js';
|
|
17
18
|
import { nodeArtifacts } from '../../../core/canvas/history.js';
|
|
18
19
|
import { readNodeMessagesPage, readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
|
|
@@ -34,6 +35,7 @@ import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
|
34
35
|
import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
|
|
35
36
|
import { assertProfileActive, profileHome } from '../../../core/profiles/manifest.js';
|
|
36
37
|
import { fullName } from '../../../core/canvas/labels.js';
|
|
38
|
+
import { reportsForNode } from './reports.js';
|
|
37
39
|
import { getCron } from '../../../core/canvas/crons.js';
|
|
38
40
|
import { cronWakeOrigin } from '../../../core/runtime/bearings.js';
|
|
39
41
|
import { notFound, usage } from '../../../core/errors.js';
|
|
@@ -266,26 +268,106 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
266
268
|
// ---------------------------------------------------------------------------
|
|
267
269
|
// GET /v1/nodes — list (filters) ; GET /v1/nodes/{id} — detail
|
|
268
270
|
// ---------------------------------------------------------------------------
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
271
|
+
function singleQueryValue(ctx, name) {
|
|
272
|
+
const values = ctx.query.getAll(name);
|
|
273
|
+
if (values.length > 1)
|
|
274
|
+
throw usage(`${name} must appear at most once`);
|
|
275
|
+
return values[0];
|
|
276
|
+
}
|
|
277
|
+
function optionalBoolean(value, name) {
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
if (value === '1' || value === 'true')
|
|
281
|
+
return true;
|
|
282
|
+
if (value === '0' || value === 'false')
|
|
283
|
+
return false;
|
|
284
|
+
throw usage(`${name} must be true or false`);
|
|
285
|
+
}
|
|
286
|
+
function oneOf(value, name, allowed) {
|
|
287
|
+
if (value === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
if (allowed.includes(value))
|
|
290
|
+
return value;
|
|
291
|
+
throw usage(`${name} must be one of: ${allowed.join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
function parseListQuery(ctx) {
|
|
294
|
+
const allowed = new Set([
|
|
295
|
+
'status', 'lifecycle', 'kind', 'mode', 'profile_id', 'profile_prefix',
|
|
296
|
+
'parent', 'top_level', 'under', 'hanging', 'include',
|
|
297
|
+
]);
|
|
298
|
+
for (const [name] of ctx.query) {
|
|
299
|
+
if (!allowed.has(name))
|
|
300
|
+
throw usage(`unknown nodes query parameter: ${name}`);
|
|
283
301
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
302
|
+
const status = oneOf(singleQueryValue(ctx, 'status'), 'status', ['active', 'idle', 'done', 'dead', 'canceled']);
|
|
303
|
+
const lifecycle = oneOf(singleQueryValue(ctx, 'lifecycle'), 'lifecycle', ['terminal', 'resident']);
|
|
304
|
+
const kind = singleQueryValue(ctx, 'kind');
|
|
305
|
+
const mode = oneOf(singleQueryValue(ctx, 'mode'), 'mode', ['base', 'orchestrator']);
|
|
306
|
+
const profileId = singleQueryValue(ctx, 'profile_id');
|
|
307
|
+
const profilePrefix = singleQueryValue(ctx, 'profile_prefix');
|
|
308
|
+
const parent = singleQueryValue(ctx, 'parent');
|
|
309
|
+
const topLevel = optionalBoolean(singleQueryValue(ctx, 'top_level'), 'top_level');
|
|
310
|
+
const under = singleQueryValue(ctx, 'under');
|
|
311
|
+
const hanging = optionalBoolean(singleQueryValue(ctx, 'hanging'), 'hanging');
|
|
312
|
+
const include = oneOf(singleQueryValue(ctx, 'include'), 'include', ['activity']);
|
|
313
|
+
if (parent !== undefined && topLevel === true) {
|
|
314
|
+
throw usage('parent and top_level=true cannot be combined');
|
|
287
315
|
}
|
|
288
|
-
|
|
316
|
+
return {
|
|
317
|
+
...(status !== undefined ? { status } : {}),
|
|
318
|
+
...(lifecycle !== undefined ? { lifecycle } : {}),
|
|
319
|
+
...(kind !== undefined ? { kind } : {}),
|
|
320
|
+
...(mode !== undefined ? { mode } : {}),
|
|
321
|
+
...(profileId !== undefined ? { profile_id: profileId } : {}),
|
|
322
|
+
...(profilePrefix !== undefined ? { profile_prefix: profilePrefix } : {}),
|
|
323
|
+
...(parent !== undefined ? { parent } : {}),
|
|
324
|
+
...(topLevel !== undefined ? { top_level: topLevel } : {}),
|
|
325
|
+
...(under !== undefined ? { under } : {}),
|
|
326
|
+
...(hanging !== undefined ? { hanging } : {}),
|
|
327
|
+
...(include !== undefined ? { include: include } : {}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
export function handleList(ctx) {
|
|
331
|
+
const query = parseListQuery(ctx);
|
|
332
|
+
let rows = query.status !== undefined ? listNodes({ status: query.status }) : listNodes();
|
|
333
|
+
if (query.lifecycle !== undefined)
|
|
334
|
+
rows = rows.filter((row) => row.lifecycle === query.lifecycle);
|
|
335
|
+
if (query.kind !== undefined)
|
|
336
|
+
rows = rows.filter((row) => row.kind === query.kind);
|
|
337
|
+
if (query.mode !== undefined)
|
|
338
|
+
rows = rows.filter((row) => row.mode === query.mode);
|
|
339
|
+
if (query.profile_id !== undefined)
|
|
340
|
+
rows = rows.filter((row) => row.profile_id === query.profile_id);
|
|
341
|
+
if (query.profile_prefix !== undefined)
|
|
342
|
+
rows = rows.filter((row) => row.profile_id?.startsWith(query.profile_prefix));
|
|
343
|
+
if (query.parent !== undefined)
|
|
344
|
+
rows = rows.filter((row) => row.parent === query.parent);
|
|
345
|
+
if (query.top_level === true)
|
|
346
|
+
rows = rows.filter((row) => row.parent === null);
|
|
347
|
+
if (query.under !== undefined) {
|
|
348
|
+
const scope = new Set(subtreeIds(query.under));
|
|
349
|
+
rows = rows.filter((row) => scope.has(row.node_id));
|
|
350
|
+
}
|
|
351
|
+
if (query.hanging === true) {
|
|
352
|
+
rows = rows.filter((row) => row.parent !== null && getNode(row.parent) === null);
|
|
353
|
+
}
|
|
354
|
+
const counts = query.include === 'activity'
|
|
355
|
+
? ticketCountsForNodes(rows.map((row) => row.node_id))
|
|
356
|
+
: null;
|
|
357
|
+
const body = rows.map((row) => {
|
|
358
|
+
const node = toNodeSummaryDTO(row);
|
|
359
|
+
if (counts !== null) {
|
|
360
|
+
const reports = reportsForNode(row.node_id);
|
|
361
|
+
node.activity = {
|
|
362
|
+
latest_report: reports[0] ?? null,
|
|
363
|
+
final_report: row.final_report === null
|
|
364
|
+
? null
|
|
365
|
+
: reports.find((report) => basename(report.path) === row.final_report) ?? null,
|
|
366
|
+
pending_human_count: counts[row.node_id] ?? 0,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return node;
|
|
370
|
+
});
|
|
289
371
|
return { status: 200, body };
|
|
290
372
|
}
|
|
291
373
|
function handleDetail(ctx) {
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
import type { ReportDTO } from '../../../api/index.js';
|
|
1
2
|
import type { RouteTable } from '../router.js';
|
|
3
|
+
/** Read one node's pushed reports, newest first. This stays in the daemon so
|
|
4
|
+
* collection projections can include report activity without client-side N+1s. */
|
|
5
|
+
export declare function reportsForNode(id: string): ReportDTO[];
|
|
2
6
|
export declare const reportRoutes: RouteTable;
|
|
@@ -110,12 +110,9 @@ function compactToIso(stamp) {
|
|
|
110
110
|
const m = stamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);
|
|
111
111
|
return m === null ? stamp : `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const tierFilter = ctx.query.get('tier');
|
|
117
|
-
const limitRaw = ctx.query.get('limit');
|
|
118
|
-
const limit = limitRaw !== null ? Number(limitRaw) : undefined;
|
|
113
|
+
/** Read one node's pushed reports, newest first. This stays in the daemon so
|
|
114
|
+
* collection projections can include report activity without client-side N+1s. */
|
|
115
|
+
export function reportsForNode(id) {
|
|
119
116
|
const dir = reportsDir(id);
|
|
120
117
|
let files;
|
|
121
118
|
try {
|
|
@@ -136,9 +133,18 @@ function handleList(ctx) {
|
|
|
136
133
|
(m ? compactToIso(m[1]) : new Date(statSync(abs).mtimeMs).toISOString());
|
|
137
134
|
reports.push({ path: abs, tier, body: body.trim(), created });
|
|
138
135
|
}
|
|
139
|
-
// Newest first, then apply the optional tier + limit filters.
|
|
140
136
|
reports.sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : 0));
|
|
141
|
-
|
|
137
|
+
return reports;
|
|
138
|
+
}
|
|
139
|
+
function handleList(ctx) {
|
|
140
|
+
const id = ctx.params['id'];
|
|
141
|
+
requireNode(id);
|
|
142
|
+
const tierFilter = ctx.query.get('tier');
|
|
143
|
+
const limitRaw = ctx.query.get('limit');
|
|
144
|
+
const limit = limitRaw !== null ? Number(limitRaw) : undefined;
|
|
145
|
+
let out = reportsForNode(id);
|
|
146
|
+
if (tierFilter !== null)
|
|
147
|
+
out = out.filter((report) => report.tier === tierFilter);
|
|
142
148
|
if (limit !== undefined && Number.isFinite(limit) && limit >= 0)
|
|
143
149
|
out = out.slice(0, limit);
|
|
144
150
|
return { status: 200, body: out };
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -62,7 +62,7 @@ import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.
|
|
|
62
62
|
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
63
63
|
import { bindFleet, boundFleet } from '../core/runtime/fleet.js';
|
|
64
64
|
import { bindDaemonControl, clearDaemonControl } from './control.js';
|
|
65
|
-
import { spawnDaemon } from './manage.js';
|
|
65
|
+
import { execDaemon, spawnDaemon } from './manage.js';
|
|
66
66
|
import { reconcileAndMigrateBoot } from '../core/canvas/boot.js';
|
|
67
67
|
import { resolveInstallId } from '../core/canvas/install-id.js';
|
|
68
68
|
import { setInstallId } from '../core/runtime/nodes.js';
|
|
@@ -480,11 +480,10 @@ export async function runDaemon(opts = {}) {
|
|
|
480
480
|
}, HANDOVER_ACK_GRACE_MS);
|
|
481
481
|
return { graceMs: HANDOVER_ACK_GRACE_MS };
|
|
482
482
|
}
|
|
483
|
-
/** Quiesce exactly as a SIGTERM would, then
|
|
483
|
+
/** Quiesce exactly as a SIGTERM would, then start the successor.
|
|
484
484
|
* Ordering is forced: the successor's claim can only be won after this
|
|
485
|
-
* daemon has released its own
|
|
486
|
-
*
|
|
487
|
-
* picks up a generation this process never loaded. */
|
|
485
|
+
* daemon has released its own. PID 1 is replaced in place so a container
|
|
486
|
+
* does not stop between daemon generations; ordinary hosts spawn and exit. */
|
|
488
487
|
async function performHandover() {
|
|
489
488
|
try {
|
|
490
489
|
await requestCleanup();
|
|
@@ -495,6 +494,9 @@ export async function runDaemon(opts = {}) {
|
|
|
495
494
|
process.exit(1);
|
|
496
495
|
}
|
|
497
496
|
try {
|
|
497
|
+
if (process.pid === 1) {
|
|
498
|
+
await execDaemon();
|
|
499
|
+
}
|
|
498
500
|
const spawned = await spawnDaemon();
|
|
499
501
|
operationIdContext.fresh(() => {
|
|
500
502
|
emitEvent({
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -36,6 +36,9 @@ export declare function findDaemonPids(): number[];
|
|
|
36
36
|
* reaped. Best-effort throughout: a pid that dies between enumeration and signal
|
|
37
37
|
* just no-ops. */
|
|
38
38
|
export declare function sweepStrayDaemons(keepPid: number | null): number[];
|
|
39
|
+
/** Replace PID 1 with the selected daemon generation. Docker keeps a container
|
|
40
|
+
* alive only while PID 1 lives, so a handover there must preserve its pid. */
|
|
41
|
+
export declare function execDaemon(): Promise<never>;
|
|
39
42
|
export interface SpawnDaemonResult {
|
|
40
43
|
/** True when a new daemon process was spawned. */
|
|
41
44
|
started: boolean;
|
package/dist/daemon/manage.js
CHANGED
|
@@ -190,6 +190,20 @@ export function sweepStrayDaemons(keepPid) {
|
|
|
190
190
|
}
|
|
191
191
|
return strays;
|
|
192
192
|
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// daemon launch — low-level spawn/exec
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/** Replace PID 1 with the selected daemon generation. Docker keeps a container
|
|
197
|
+
* alive only while PID 1 lives, so a handover there must preserve its pid. */
|
|
198
|
+
export async function execDaemon() {
|
|
199
|
+
const execve = process.execve;
|
|
200
|
+
if (execve === undefined) {
|
|
201
|
+
throw new Error(`this Node.js ${process.version} runtime cannot replace PID 1 with the successor daemon`);
|
|
202
|
+
}
|
|
203
|
+
const executable = hostExecPath();
|
|
204
|
+
const launchArgs = await resolveDaemonLaunch();
|
|
205
|
+
return execve(executable, [executable, ...launchArgs, '--canvas-home', crtrHome()], sanitizedDaemonEnv());
|
|
206
|
+
}
|
|
193
207
|
// Daemon lifecycle transitions are bounded so callers never claim a state
|
|
194
208
|
// change before the pidfile and process liveness agree.
|
|
195
209
|
//
|
|
@@ -4,9 +4,10 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
4
4
|
* not open with a sleep (or its duration isn't statically knowable). */
|
|
5
5
|
export declare function leadingSleepSeconds(command: string): number | null;
|
|
6
6
|
/** The valve's BashOperations backend. Each tool execution closes over its own
|
|
7
|
-
* `takePurpose
|
|
8
|
-
*
|
|
9
|
-
|
|
7
|
+
* `takePurpose` and its own preview-result path, so concurrent calls cannot
|
|
8
|
+
* exchange labels or result mirrors. The purpose is consumed even when the
|
|
9
|
+
* command is invalid or refused, leaving no state to leak later. */
|
|
10
|
+
export declare function createValveOperations(nodeId: string, contextDir: string, takePurpose?: () => string | null, previewPath?: string): BashOperations;
|
|
10
11
|
type BashToolDefinition = ReturnType<typeof createBashToolDefinition>;
|
|
11
12
|
/** Build the valve-backed bash tool and, when configured, add `purpose` to its
|
|
12
13
|
* schema. Bind each purpose directly to its own execution: Pi may preflight a
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
import { spawn } from 'node:child_process';
|
|
28
28
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
29
29
|
import { homedir } from 'node:os';
|
|
30
|
+
import { dirname } from 'node:path';
|
|
30
31
|
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
|
|
31
32
|
import { readConfig } from '../core/config.js';
|
|
33
|
+
import { PREVIEW_RESULT_PATH_ENV, previewResultPath } from '../core/preview-result-path.js';
|
|
32
34
|
import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
33
35
|
import { Type } from 'typebox';
|
|
34
36
|
import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
@@ -218,9 +220,10 @@ function resolveExecutionCwd(cwd) {
|
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
222
|
/** The valve's BashOperations backend. Each tool execution closes over its own
|
|
221
|
-
* `takePurpose
|
|
222
|
-
*
|
|
223
|
-
|
|
223
|
+
* `takePurpose` and its own preview-result path, so concurrent calls cannot
|
|
224
|
+
* exchange labels or result mirrors. The purpose is consumed even when the
|
|
225
|
+
* command is invalid or refused, leaving no state to leak later. */
|
|
226
|
+
export function createValveOperations(nodeId, contextDir, takePurpose = () => null, previewPath) {
|
|
224
227
|
return {
|
|
225
228
|
exec: (command, cwd, { onData, signal, timeout, env }) => {
|
|
226
229
|
return new Promise((resolve, reject) => {
|
|
@@ -309,12 +312,20 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
309
312
|
const execution = resolveExecutionCwd(cwd);
|
|
310
313
|
if (execution.warning)
|
|
311
314
|
onData(Buffer.from(execution.warning));
|
|
312
|
-
|
|
315
|
+
// The preview path reaches crtr through the environment, never through
|
|
316
|
+
// the command text: cmd.sh, job inspection, and the doc substrate's
|
|
317
|
+
// `command` surfaces all read that text as the agent's own words.
|
|
318
|
+
let childEnv = env ?? process.env;
|
|
319
|
+
if (previewPath !== undefined) {
|
|
320
|
+
mkdirSync(dirname(previewPath), { recursive: true });
|
|
321
|
+
childEnv = { ...childEnv, [PREVIEW_RESULT_PATH_ENV]: previewPath };
|
|
322
|
+
}
|
|
323
|
+
const shellPath = resolveAdmittedHostCommand('bash', childEnv, execution.cwd);
|
|
313
324
|
const child = spawn(shellPath, ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId, shellPath], {
|
|
314
325
|
cwd: execution.cwd,
|
|
315
326
|
detached: true,
|
|
316
327
|
stdio: 'ignore',
|
|
317
|
-
env:
|
|
328
|
+
env: childEnv,
|
|
318
329
|
});
|
|
319
330
|
child.on('error', (err) => {
|
|
320
331
|
finish(() => reject(err));
|
|
@@ -418,7 +429,9 @@ export function createValveToolDefinition(nodeId, contextDir, cwd, exposePurpose
|
|
|
418
429
|
const schemaDefinition = exposePurpose ? withPurpose(baseDefinition) : baseDefinition;
|
|
419
430
|
const execute = (toolCallId, params, signal, onUpdate, ctx) => {
|
|
420
431
|
const purpose = exposePurpose ? normalizeBashJobPurpose(params['purpose']) : null;
|
|
421
|
-
const definition = createBashToolDefinition(cwd, {
|
|
432
|
+
const definition = createBashToolDefinition(cwd, {
|
|
433
|
+
operations: createValveOperations(nodeId, contextDir, () => purpose, previewResultPath(toolCallId)),
|
|
434
|
+
});
|
|
422
435
|
return definition.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
423
436
|
};
|
|
424
437
|
return { ...schemaDefinition, execute };
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
interface ToolCallEvent {
|
|
2
|
-
toolName: string;
|
|
3
|
-
toolCallId: string;
|
|
4
|
-
input: {
|
|
5
|
-
command?: string;
|
|
6
|
-
};
|
|
7
|
-
}
|
|
8
1
|
interface ToolResultEvent {
|
|
9
2
|
toolName: string;
|
|
10
3
|
toolCallId: string;
|
|
11
4
|
details?: Record<string, unknown>;
|
|
12
5
|
}
|
|
13
6
|
interface PiLike {
|
|
14
|
-
on(event: 'tool_call', handler: (event: ToolCallEvent) => void): void;
|
|
15
7
|
on(event: 'tool_result', handler: (event: ToolResultEvent) => {
|
|
16
8
|
details: Record<string, unknown>;
|
|
17
9
|
} | void): void;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// canvas-preview-result.ts — private structured result transport for crtr bash calls.
|
|
2
2
|
//
|
|
3
3
|
// A crtr leaf returns a record, but the CLI normally renders that record to
|
|
4
|
-
// stdout for the agent. stdout is model context, so
|
|
5
|
-
// bash call a private file
|
|
6
|
-
// attaches the record to Pi's
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
// stdout for the agent. stdout is model context, so the bash valve hands every
|
|
5
|
+
// bash call a private file path in the child environment, crtr atomically
|
|
6
|
+
// mirrors its record there, and this hook attaches the record to Pi's
|
|
7
|
+
// ToolResult.details. The attach viewer can render a structured preview
|
|
8
|
+
// without parsing the agent-facing text.
|
|
9
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
10
|
+
import { previewResultPath } from '../core/preview-result-path.js';
|
|
11
11
|
function isPreviewRecord(value) {
|
|
12
12
|
if (value === null || typeof value !== 'object' || typeof value['path'] !== 'string')
|
|
13
13
|
return false;
|
|
@@ -21,24 +21,10 @@ function isPreviewRecord(value) {
|
|
|
21
21
|
export function registerCanvasPreviewResult(pi) {
|
|
22
22
|
if (process.env['CRTR_NODE_ID'] === undefined || process.env['CRTR_NODE_ID'] === '')
|
|
23
23
|
return;
|
|
24
|
-
const directory = join(tmpdir(), 'crouter-preview-results');
|
|
25
|
-
const paths = new Map();
|
|
26
|
-
pi.on('tool_call', (event) => {
|
|
27
|
-
if (event.toolName !== 'bash' || typeof event.input.command !== 'string')
|
|
28
|
-
return;
|
|
29
|
-
mkdirSync(directory, { recursive: true });
|
|
30
|
-
const path = join(directory, `${encodeURIComponent(event.toolCallId)}.json`);
|
|
31
|
-
paths.set(event.toolCallId, path);
|
|
32
|
-
// Export before the complete script rather than prefixing its first simple
|
|
33
|
-
// command: agent bash calls routinely start with `cd … &&`, a compound
|
|
34
|
-
// statement, or setup commands before crtr itself.
|
|
35
|
-
event.input.command = `export CRTR_PREVIEW_RESULT_PATH=${JSON.stringify(path)}\n${event.input.command}`;
|
|
36
|
-
});
|
|
37
24
|
pi.on('tool_result', (event) => {
|
|
38
|
-
|
|
39
|
-
if (path === undefined)
|
|
25
|
+
if (event.toolName !== 'bash')
|
|
40
26
|
return;
|
|
41
|
-
|
|
27
|
+
const path = previewResultPath(event.toolCallId);
|
|
42
28
|
if (!existsSync(path))
|
|
43
29
|
return;
|
|
44
30
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -27,6 +27,33 @@ export interface PluginBundle {
|
|
|
27
27
|
endpoint: string;
|
|
28
28
|
authEnv?: string;
|
|
29
29
|
}
|
|
30
|
+
export type MemoryExtensionScalar = string | boolean | number;
|
|
31
|
+
export type MemoryExtensionFieldType = 'boolean' | 'string' | 'number' | 'enum';
|
|
32
|
+
interface MemoryExtensionDeclarationBase {
|
|
33
|
+
write_help: string;
|
|
34
|
+
edit_help: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BooleanMemoryExtensionDeclaration extends MemoryExtensionDeclarationBase {
|
|
37
|
+
type: 'boolean';
|
|
38
|
+
default?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface StringMemoryExtensionDeclaration extends MemoryExtensionDeclarationBase {
|
|
41
|
+
type: 'string';
|
|
42
|
+
default?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface NumberMemoryExtensionDeclaration extends MemoryExtensionDeclarationBase {
|
|
45
|
+
type: 'number';
|
|
46
|
+
default?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface EnumMemoryExtensionDeclaration extends MemoryExtensionDeclarationBase {
|
|
49
|
+
type: 'enum';
|
|
50
|
+
values: string[];
|
|
51
|
+
default?: string;
|
|
52
|
+
}
|
|
53
|
+
export type MemoryExtensionDeclaration = BooleanMemoryExtensionDeclaration | StringMemoryExtensionDeclaration | NumberMemoryExtensionDeclaration | EnumMemoryExtensionDeclaration;
|
|
54
|
+
/** Field declarations owned by one plugin manifest. The plugin's `name` is
|
|
55
|
+
* the namespace; keys here are its lowercase kebab-case local field names. */
|
|
56
|
+
export type MemoryExtensionDeclarations = Record<string, MemoryExtensionDeclaration>;
|
|
30
57
|
export interface PluginManifest {
|
|
31
58
|
name: string;
|
|
32
59
|
version?: string;
|
|
@@ -47,6 +74,9 @@ export interface PluginManifest {
|
|
|
47
74
|
* Archive plugins declare this in `bundle.json`; the installer copies the
|
|
48
75
|
* validated block into this synthesized manifest. */
|
|
49
76
|
kinds?: Record<string, Partial<KindConfig>>;
|
|
77
|
+
/** Plugin-owned typed metadata declarations for memory document frontmatter.
|
|
78
|
+
* Values live at `extensions.<this manifest name>.<field>`. */
|
|
79
|
+
memory_extensions?: MemoryExtensionDeclarations;
|
|
50
80
|
/** Page-component contributions: the same registration objects a scope
|
|
51
81
|
* `config.json` `page_components` block carries (kind plus its display,
|
|
52
82
|
* description, use guidance, and docs metadata). `resolvePageComponents`
|
|
@@ -446,3 +476,4 @@ export declare function defaultRemoteCanvasConfig(): RemoteCanvasConfig;
|
|
|
446
476
|
export declare function defaultKindsConfig(): Record<string, KindConfig>;
|
|
447
477
|
export declare function defaultModelLaddersConfig(): ModelLaddersConfig;
|
|
448
478
|
export declare function defaultScopeState(): ScopeState;
|
|
479
|
+
export {};
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.218",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.218",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|