@xingular/harness 1.2.0 → 1.3.1-unstable.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +26 -17
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +7362 -6239
- package/dist/sdk/agents/builtin.js +3 -3
- package/dist/sdk/agents/planResearch.d.ts +23 -1
- package/dist/sdk/agents/planResearch.js +71 -8
- package/dist/sdk/agents/profiles.d.ts +16 -4
- package/dist/sdk/agents/profiles.js +75 -12
- package/dist/sdk/annotations/bridge.d.ts +2 -0
- package/dist/sdk/annotations/bridge.js +4 -0
- package/dist/sdk/annotations/capture.d.ts +44 -2
- package/dist/sdk/annotations/capture.js +104 -3
- package/dist/sdk/annotations/delivery.d.ts +24 -17
- package/dist/sdk/annotations/delivery.js +82 -49
- package/dist/sdk/annotations/inspect.js +7 -6
- package/dist/sdk/annotations/pageScript.js +4 -4
- package/dist/sdk/annotations/part.d.ts +0 -2
- package/dist/sdk/annotations/part.js +4 -33
- package/dist/sdk/annotations/queue.d.ts +2 -2
- package/dist/sdk/annotations/queue.js +12 -7
- package/dist/sdk/annotations/serverMethods.js +11 -7
- package/dist/sdk/annotations/store.d.ts +13 -21
- package/dist/sdk/annotations/store.js +47 -37
- package/dist/sdk/capabilities/runSnapshot.js +10 -4
- package/dist/sdk/commands/list.js +2 -0
- package/dist/sdk/commands/registry.d.ts +11 -3
- package/dist/sdk/commands/registry.js +3 -2
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- package/dist/sdk/core/shell.d.ts +20 -0
- package/dist/sdk/core/shell.js +73 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
- package/dist/sdk/core/workspaceBashTool.js +16 -2
- package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
- package/dist/sdk/core/workspaceSandbox.js +52 -12
- package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
- package/dist/sdk/core/workspaceWriteOperations.js +26 -8
- package/dist/sdk/doctor/checks.js +71 -12
- package/dist/sdk/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/hub/hub.js +5 -3
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +14 -9
- package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
- package/dist/sdk/plugins/builtinRegistry.js +9 -3
- package/dist/sdk/plugins/discover.d.ts +7 -1
- package/dist/sdk/plugins/discover.js +41 -2
- package/dist/sdk/plugins/identity.js +1 -1
- package/dist/sdk/plugins/install.d.ts +6 -2
- package/dist/sdk/plugins/install.js +29 -5
- package/dist/sdk/plugins/inventory.js +3 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +21 -4
- package/dist/sdk/plugins/plugins.d.ts +2 -0
- package/dist/sdk/plugins/plugins.js +1 -0
- package/dist/sdk/plugins/proposals.d.ts +17 -0
- package/dist/sdk/plugins/proposals.js +147 -0
- package/dist/sdk/plugins/types.d.ts +1 -1
- package/dist/sdk/plugins/types.js +1 -1
- package/dist/sdk/prompt/compile.d.ts +13 -4
- package/dist/sdk/prompt/compile.js +46 -20
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +14 -2
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
- package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
- package/dist/sdk/sessions/continuation.d.ts +4 -2
- package/dist/sdk/sessions/continuation.js +7 -6
- package/dist/sdk/sessions/contracts.d.ts +22 -4
- package/dist/sdk/sessions/contracts.js +12 -7
- package/dist/sdk/sessions/planMode.js +2 -2
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessionState.d.ts +13 -0
- package/dist/sdk/sessions/sessionState.js +31 -0
- package/dist/sdk/sessions/sessions.d.ts +7 -0
- package/dist/sdk/sessions/sessions.js +73 -15
- package/dist/sdk/sessions/subagentService.d.ts +15 -0
- package/dist/sdk/sessions/subagentService.js +195 -55
- package/dist/sdk/sessions/subagents.d.ts +10 -1
- package/dist/sdk/sessions/subagents.js +4 -0
- package/dist/sdk/sessions/workSlice.d.ts +8 -5
- package/dist/sdk/sessions/workSlice.js +77 -45
- package/dist/sdk/terminal/controller.d.ts +20 -0
- package/dist/sdk/terminal/controller.js +43 -18
- package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
- package/dist/sdk/terminal/opentui/ansi.js +192 -0
- package/dist/sdk/terminal/opentui/model.d.ts +163 -0
- package/dist/sdk/terminal/opentui/model.js +482 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
- package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
- package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +2 -0
- package/dist/sdk/terminal/piExtension.js +98 -15
- package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
- package/dist/sdk/terminal/subagentMonitor.js +29 -8
- package/dist/sdk/terminal/terminals.d.ts +16 -0
- package/dist/sdk/terminal/terminals.js +130 -26
- package/dist/sdk/terminal/trajectory.d.ts +6 -1
- package/dist/sdk/terminal/trajectory.js +8 -0
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
- package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
- package/dist/sdk/terminal/tui/annotateSend.js +40 -0
- package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
- package/dist/sdk/terminal/tui/chrome.js +13 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +16 -11
- package/dist/sdk/terminal/tui/commands.d.ts +4 -0
- package/dist/sdk/terminal/tui/commands.js +12 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
- package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
- package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
- package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
- package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
- package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
- package/dist/sdk/tools/defaultSkills.d.ts +4 -0
- package/dist/sdk/tools/defaultSkills.js +20 -0
- package/dist/sdk/tools/definitions.d.ts +3 -1
- package/dist/sdk/tools/definitions.js +17 -1
- package/dist/sdk/tools/dispatch.js +12 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/sessionName.d.ts +9 -0
- package/dist/sdk/tools/sessionName.js +58 -11
- package/dist/sdk/tools/spawnSubagent.js +9 -7
- package/dist/sdk/tools/todo.d.ts +15 -0
- package/dist/sdk/tools/todo.js +64 -0
- package/dist/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +3331 -1584
- package/dist/terminal-pi.js +421 -133
- package/docs/README.md +3 -0
- package/docs/architecture/00-overview.md +1 -1
- package/docs/architecture/01-extension-points.md +1 -1
- package/docs/architecture/02-sessions-and-runs.md +73 -36
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +24 -13
- package/docs/architecture/06-plugins-and-capabilities.md +46 -10
- package/docs/architecture/07-tools-and-gate.md +49 -8
- package/docs/architecture/09-accounts-and-connectors.md +8 -2
- package/docs/architecture/10-server-protocol.md +68 -16
- package/docs/architecture/11-terminal.md +94 -45
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/14-surfaces-and-parity.md +4 -1
- package/docs/architecture/README.md +20 -18
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
- package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
- package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
- package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
- package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
- package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +11 -6
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
- package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
- package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
- package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
- package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
- package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
- package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
- package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
- package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
- package/docs/doctor.md +14 -1
- package/docs/parity.md +16 -13
- package/docs/releases/1.3.0.md +21 -0
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/README.md +9 -6
- package/docs/terminal-tui.md +127 -65
- package/package.json +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
- package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
- package/dist/sdk/tools/checklist.d.ts +0 -9
- package/dist/sdk/tools/checklist.js +0 -55
|
@@ -160,6 +160,23 @@ function validNote(note, id) {
|
|
|
160
160
|
if (!note || note.id !== id || note.author !== 'user' || !Number.isSafeInteger(note.version) || note.version < 1 || !Array.isArray(note.grants) || !Array.isArray(note.history)) {
|
|
161
161
|
throw new HarnessError('invalid_data', `Note ${id} is not a valid note.`);
|
|
162
162
|
}
|
|
163
|
+
return upgradeDelivery(note);
|
|
164
|
+
}
|
|
165
|
+
/** Notes written before PROP-0048 had a queue and a "fixed" claim: read them as pending or sent. */
|
|
166
|
+
function upgradeDelivery(note) {
|
|
167
|
+
const delivery = note.delivery;
|
|
168
|
+
if (!delivery)
|
|
169
|
+
return note;
|
|
170
|
+
if (delivery.state === 'queued')
|
|
171
|
+
delivery.state = 'pending';
|
|
172
|
+
else if (delivery.state === 'fixed') {
|
|
173
|
+
delivery.state = 'sent';
|
|
174
|
+
delivery.sentAt ??= delivery.fixedAt;
|
|
175
|
+
}
|
|
176
|
+
delete delivery.fixedAt;
|
|
177
|
+
delete delivery.summary;
|
|
178
|
+
if (delivery.state !== 'pending' && delivery.state !== 'sent')
|
|
179
|
+
note.delivery = null;
|
|
163
180
|
return note;
|
|
164
181
|
}
|
|
165
182
|
export function read(root, id) {
|
|
@@ -240,7 +257,7 @@ export function update(root, id, expectedVersion, patch) {
|
|
|
240
257
|
if (patch.status !== 'open' && patch.status !== 'resolved')
|
|
241
258
|
throw invalid('A note is open or resolved.');
|
|
242
259
|
note.status = patch.status;
|
|
243
|
-
if (patch.status === 'resolved' && note.delivery?.state === '
|
|
260
|
+
if (patch.status === 'resolved' && note.delivery?.state === 'pending')
|
|
244
261
|
note.delivery = null;
|
|
245
262
|
}
|
|
246
263
|
if (patch.target !== undefined)
|
|
@@ -263,6 +280,9 @@ export function unshare(root, id, principal) {
|
|
|
263
280
|
const who = validatePrincipal(principal);
|
|
264
281
|
const note = current(root, id, undefined);
|
|
265
282
|
note.grants = note.grants.filter((grant) => !samePrincipal(grant.principal, who));
|
|
283
|
+
// A session that may no longer read the note is not sent it either.
|
|
284
|
+
if (note.delivery?.state === 'pending' && who.sessionId && note.delivery.sessionId === who.sessionId)
|
|
285
|
+
note.delivery = null;
|
|
266
286
|
return save(root, note, 'unshare');
|
|
267
287
|
}
|
|
268
288
|
/** The host withdrew what a binding stood for (a tab closed or was shared again): every grant made with it ends. */
|
|
@@ -320,45 +340,48 @@ export function importNotes(root, notes) {
|
|
|
320
340
|
}
|
|
321
341
|
return { imported, skipped };
|
|
322
342
|
}
|
|
323
|
-
// ---- Delivery to
|
|
343
|
+
// ---- Delivery to a session (PROP-0048) ----------------------------------------------------------
|
|
324
344
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
345
|
+
* The note is shared with the session and waits there, unsent, until the person sends it. Calling it
|
|
346
|
+
* again keeps the original time, so the send order stays the order the notes were made.
|
|
327
347
|
*/
|
|
328
|
-
export function
|
|
348
|
+
export function markPending(root, id, expectedVersion, sessionId, options = {}) {
|
|
329
349
|
const session = safeId(sessionId);
|
|
330
350
|
const note = current(root, id, expectedVersion);
|
|
331
351
|
if (note.status === 'resolved')
|
|
332
|
-
throw invalid('Reopen this note before
|
|
352
|
+
throw invalid('Reopen this note before sending it.');
|
|
333
353
|
if (!note.grants.some((grant) => grant.principal.sessionId === session))
|
|
334
354
|
note.grants.push({ principal: { sessionId: session }, grantedAt: now() });
|
|
335
|
-
const
|
|
355
|
+
const previous = note.delivery?.state === 'pending' && note.delivery.sessionId === session ? note.delivery : null;
|
|
356
|
+
const confirmationRequired = options.confirm === true || (previous?.confirm === true && options.confirmed !== true);
|
|
336
357
|
note.delivery = {
|
|
337
|
-
state: '
|
|
358
|
+
state: 'pending',
|
|
338
359
|
sessionId: session,
|
|
339
|
-
queuedAt: now(),
|
|
340
|
-
...(options.autoSend ? { autoSend: true } : {}),
|
|
360
|
+
queuedAt: previous?.queuedAt ?? now(),
|
|
361
|
+
...(options.autoSend || previous?.autoSend ? { autoSend: true } : {}),
|
|
341
362
|
...(confirmationRequired ? { confirm: true } : {})
|
|
342
363
|
};
|
|
343
|
-
return save(root, note, '
|
|
344
|
-
}
|
|
345
|
-
/** Takes the note out of the queue. It stays shared; unshare withdraws that. */
|
|
346
|
-
export function dequeue(root, id, expectedVersion) {
|
|
347
|
-
const note = current(root, id, expectedVersion);
|
|
348
|
-
if (note.delivery?.state !== 'queued')
|
|
349
|
-
throw invalid('This note is not waiting to be sent.');
|
|
350
|
-
note.delivery = null;
|
|
351
|
-
return save(root, note, 'dequeue');
|
|
364
|
+
return save(root, note, 'pending');
|
|
352
365
|
}
|
|
353
|
-
/** The notes
|
|
354
|
-
export function
|
|
366
|
+
/** The unsent notes of one session, oldest first. `auto` keeps only those to send when it is idle. */
|
|
367
|
+
export function pending(root, sessionId, filter = {}) {
|
|
355
368
|
const ids = filter.ids ? new Set(filter.ids) : null;
|
|
356
369
|
return all(root)
|
|
357
|
-
.filter((note) => note.status === 'open' && note.delivery?.state === '
|
|
370
|
+
.filter((note) => note.status === 'open' && note.delivery?.state === 'pending' && note.delivery.sessionId === sessionId)
|
|
358
371
|
.filter((note) => !filter.auto || (note.delivery?.autoSend === true && note.delivery.confirm !== true))
|
|
359
372
|
.filter((note) => !ids || ids.has(note.id))
|
|
360
373
|
.sort((left, right) => (left.delivery.queuedAt).localeCompare(right.delivery.queuedAt) || left.id.localeCompare(right.id));
|
|
361
374
|
}
|
|
375
|
+
/** `/annotate:clear`: the session's unsent notes are resolved and no longer shared with it. They can be reopened. */
|
|
376
|
+
export function clearPending(root, sessionId) {
|
|
377
|
+
const session = safeId(sessionId);
|
|
378
|
+
return pending(root, session).map((note) => {
|
|
379
|
+
note.status = 'resolved';
|
|
380
|
+
note.delivery = null;
|
|
381
|
+
note.grants = note.grants.filter((grant) => grant.principal.sessionId !== session);
|
|
382
|
+
return save(root, note, 'clear');
|
|
383
|
+
});
|
|
384
|
+
}
|
|
362
385
|
/** The notes went to the session as part of `runId`. */
|
|
363
386
|
export function markSent(root, ids, sessionId, runId) {
|
|
364
387
|
const session = safeId(sessionId);
|
|
@@ -367,8 +390,8 @@ export function markSent(root, ids, sessionId, runId) {
|
|
|
367
390
|
for (const note of notes) {
|
|
368
391
|
if (note.status !== 'open')
|
|
369
392
|
throw invalid('Resolved notes cannot be sent.');
|
|
370
|
-
if (note.delivery?.state !== '
|
|
371
|
-
throw invalid('This note is not
|
|
393
|
+
if (note.delivery?.state !== 'pending' || note.delivery.sessionId !== session)
|
|
394
|
+
throw invalid('This note is not waiting to be sent to this session.');
|
|
372
395
|
if (note.delivery.confirm)
|
|
373
396
|
throw invalid('Confirm this note before sending it.');
|
|
374
397
|
}
|
|
@@ -379,16 +402,3 @@ export function markSent(root, ids, sessionId, runId) {
|
|
|
379
402
|
return save(root, note, 'deliver');
|
|
380
403
|
});
|
|
381
404
|
}
|
|
382
|
-
/** The agent says it acted on a note shared with it. The note stays open until a person resolves it. */
|
|
383
|
-
export function markFixed(root, id, caller, summary) {
|
|
384
|
-
const note = current(root, id, undefined);
|
|
385
|
-
if (!visibleTo(note, caller))
|
|
386
|
-
throw notFound('Note');
|
|
387
|
-
const sessionId = note.delivery?.sessionId ?? caller.sessionId ?? note.grants.find((grant) => grant.principal.sessionId)?.principal.sessionId;
|
|
388
|
-
if (!sessionId)
|
|
389
|
-
throw invalid('This note was never sent to a session.');
|
|
390
|
-
note.delivery = { ...(note.delivery ?? { queuedAt: now() }), state: 'fixed', sessionId, fixedAt: now(), summary: bounded(summary.trim(), 2000, true, 'summary') };
|
|
391
|
-
delete note.delivery.autoSend;
|
|
392
|
-
delete note.delivery.confirm;
|
|
393
|
-
return save(root, note, 'mark_fixed');
|
|
394
|
-
}
|
|
@@ -6,11 +6,16 @@ const path = (root, runId) => join(root, 'runs', safeId(runId), 'capabilities.js
|
|
|
6
6
|
function strings(value) {
|
|
7
7
|
return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === 'string' && item.length > 0))] : [];
|
|
8
8
|
}
|
|
9
|
+
/** Tool names that were renamed; a pinned run keeps the current name so a continuation can use it. */
|
|
10
|
+
const RENAMED_TOOLS = { checklist_write: 'todo_write' };
|
|
11
|
+
function tools(value) {
|
|
12
|
+
return [...new Set(strings(value).map((name) => RENAMED_TOOLS[name] ?? name))];
|
|
13
|
+
}
|
|
9
14
|
export function capabilitySet(value) {
|
|
10
15
|
if (!value || typeof value !== 'object' || value.version !== 1)
|
|
11
16
|
throw invalid('A capability set must use version 1.');
|
|
12
17
|
const record = value;
|
|
13
|
-
return { version: 1, tools:
|
|
18
|
+
return { version: 1, tools: tools(record.tools), connectors: strings(record.connectors), contributions: strings(record.contributions) };
|
|
14
19
|
}
|
|
15
20
|
/** Writes a run's capability set once. A resumed or duplicate launch cannot replace it. */
|
|
16
21
|
export function freezeRun(root, runId, caller, set) {
|
|
@@ -18,11 +23,12 @@ export function freezeRun(root, runId, caller, set) {
|
|
|
18
23
|
const snapshot = {
|
|
19
24
|
version: 1,
|
|
20
25
|
caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null },
|
|
21
|
-
tools:
|
|
26
|
+
tools: tools(set.tools),
|
|
22
27
|
connectors: strings(set.connectors),
|
|
23
28
|
contributions: strings(set.contributions)
|
|
24
29
|
};
|
|
25
|
-
const
|
|
30
|
+
const stored = existsSync(target) ? readJson(target) : null;
|
|
31
|
+
const existing = stored ? { ...stored, tools: tools(stored.tools) } : null;
|
|
26
32
|
if (existing) {
|
|
27
33
|
if (JSON.stringify(existing) !== JSON.stringify(snapshot))
|
|
28
34
|
throw new HarnessError('capabilities_frozen', 'This run already has a different frozen capability set.');
|
|
@@ -40,7 +46,7 @@ export function loadRun(root, runId) {
|
|
|
40
46
|
return {
|
|
41
47
|
version: 1,
|
|
42
48
|
caller: { agentId: typeof caller.agentId === 'string' ? caller.agentId : null, projectId: typeof caller.projectId === 'string' ? caller.projectId : null },
|
|
43
|
-
tools:
|
|
49
|
+
tools: tools(record.tools),
|
|
44
50
|
connectors: strings(record.connectors),
|
|
45
51
|
contributions: strings(record.contributions)
|
|
46
52
|
};
|
|
@@ -22,6 +22,8 @@ export function listCommands(plugins) {
|
|
|
22
22
|
aliases.forEach((alias) => taken.add(alias));
|
|
23
23
|
}
|
|
24
24
|
for (const command of BUILTIN_PLUGIN_REGISTRY.commands()) {
|
|
25
|
+
if (command.rootOnly)
|
|
26
|
+
continue;
|
|
25
27
|
const name = `${command.prefix}:${command.name}`;
|
|
26
28
|
out.set(name, { name, description: command.description, group: 'Plugins', source: 'plugin', owner: command.pluginId });
|
|
27
29
|
}
|
|
@@ -121,6 +121,13 @@ export declare const CORE_COMMANDS: readonly [{
|
|
|
121
121
|
readonly group: "Inspect";
|
|
122
122
|
readonly kind: "session";
|
|
123
123
|
readonly methods: readonly ["sessions.export", "sessions.run"];
|
|
124
|
+
}, {
|
|
125
|
+
readonly name: "todo";
|
|
126
|
+
readonly aliases: readonly ["checklist"];
|
|
127
|
+
readonly description: "Show the current todo list";
|
|
128
|
+
readonly group: "Inspect";
|
|
129
|
+
readonly kind: "session";
|
|
130
|
+
readonly methods: readonly ["runs.todo"];
|
|
124
131
|
}, {
|
|
125
132
|
readonly name: "trajectory";
|
|
126
133
|
readonly args: "[run-id]";
|
|
@@ -173,15 +180,16 @@ export declare const CORE_COMMANDS: readonly [{
|
|
|
173
180
|
readonly kind: "page";
|
|
174
181
|
}, {
|
|
175
182
|
readonly name: "plugins";
|
|
176
|
-
readonly description: "
|
|
183
|
+
readonly description: "Manage local plugins, packages, trust and settings";
|
|
177
184
|
readonly group: "Harness";
|
|
178
185
|
readonly kind: "page";
|
|
179
|
-
readonly methods: readonly ["plugins.
|
|
186
|
+
readonly methods: readonly ["plugins.list", "plugins.attach", "plugins.detach", "plugins.trust", "plugins.untrust", "plugins.configure", "plugins.install", "plugins.update", "plugins.remove", "hub.list"];
|
|
180
187
|
}, {
|
|
181
188
|
readonly name: "hub";
|
|
182
|
-
readonly description: "Browse
|
|
189
|
+
readonly description: "Browse the Community catalog and manage Hub installs";
|
|
183
190
|
readonly group: "Harness";
|
|
184
191
|
readonly kind: "page";
|
|
192
|
+
readonly methods: readonly ["hub.list", "hub.install", "hub.uninstall"];
|
|
185
193
|
}, {
|
|
186
194
|
readonly name: "components";
|
|
187
195
|
readonly description: "Browse skills, prompts and rules for this Harness scope";
|
|
@@ -18,6 +18,7 @@ export const CORE_COMMANDS = [
|
|
|
18
18
|
{ name: 'compact', args: '[instructions]', description: 'Compact the context', group: 'Session', kind: 'session', methods: ['sessions.run'] },
|
|
19
19
|
{ name: 'copy', description: 'Copy the last reply', group: 'Inspect', kind: 'local', methods: ['sessions.lastReply'] },
|
|
20
20
|
{ name: 'export', args: '[path.html|path.jsonl]', description: 'Export this branch to HTML or JSONL', group: 'Inspect', kind: 'session', methods: ['sessions.export', 'sessions.run'] },
|
|
21
|
+
{ name: 'todo', aliases: ['checklist'], description: 'Show the current todo list', group: 'Inspect', kind: 'session', methods: ['runs.todo'] },
|
|
21
22
|
{ name: 'trajectory', args: '[run-id]', description: 'Inspect recorded runs for this session', group: 'Inspect', kind: 'page', methods: ['runs.list', 'runs.events'] },
|
|
22
23
|
{ name: 'reload', description: 'Reload trusted resources and keybindings', group: 'Harness', kind: 'session', methods: ['sessions.reload'] },
|
|
23
24
|
{ name: 'accounts', description: 'Sign in, switch, check and remove Pi accounts', group: 'Harness', kind: 'page', methods: ['accounts.list'] },
|
|
@@ -27,8 +28,8 @@ export const CORE_COMMANDS = [
|
|
|
27
28
|
{ name: 'connections', description: 'Manage local and remote MCP connections', group: 'Harness', kind: 'page' },
|
|
28
29
|
{ name: 'mcp', description: 'Open Connections in the Harness browser', group: 'Harness', kind: 'page' },
|
|
29
30
|
{ name: 'tools', description: 'Inspect available tools, owners and approvals', group: 'Harness', kind: 'page' },
|
|
30
|
-
{ name: 'plugins', description: '
|
|
31
|
-
{ name: 'hub', description: 'Browse
|
|
31
|
+
{ name: 'plugins', description: 'Manage local plugins, packages, trust and settings', group: 'Harness', kind: 'page', methods: ['plugins.list', 'plugins.attach', 'plugins.detach', 'plugins.trust', 'plugins.untrust', 'plugins.configure', 'plugins.install', 'plugins.update', 'plugins.remove', 'hub.list'] },
|
|
32
|
+
{ name: 'hub', description: 'Browse the Community catalog and manage Hub installs', group: 'Harness', kind: 'page', methods: ['hub.list', 'hub.install', 'hub.uninstall'] },
|
|
32
33
|
{ name: 'components', description: 'Browse skills, prompts and rules for this Harness scope', group: 'Harness', kind: 'page' },
|
|
33
34
|
{ name: 'settings', description: 'Session, terminal and browser settings', group: 'Harness', kind: 'page' },
|
|
34
35
|
{ name: 'doctor', description: 'Check account, model, tools and runtime readiness', group: 'Harness', kind: 'page', methods: ['doctor.start', 'doctor.read'] },
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** What a link names, before anything touches the filesystem. */
|
|
2
|
+
export type LinkTarget = {
|
|
3
|
+
kind: 'web';
|
|
4
|
+
url: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'mail';
|
|
7
|
+
address: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'path';
|
|
10
|
+
path: string;
|
|
11
|
+
};
|
|
12
|
+
/** What the operating system is asked to open, after the file it names was checked. */
|
|
13
|
+
export type OpenedTarget = {
|
|
14
|
+
kind: 'web';
|
|
15
|
+
url: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'mail';
|
|
18
|
+
address: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'file';
|
|
21
|
+
path: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'directory';
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
/** Hands an opened target to the operating system. Replaced in tests, and by a host that has its own opener. */
|
|
27
|
+
export type LinkOpener = (target: OpenedTarget) => Promise<void>;
|
|
28
|
+
export interface LinkOpenCommand {
|
|
29
|
+
command: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What a link names, or null when it names nothing a surface may open. Pure: a path is not
|
|
34
|
+
* checked against the filesystem here, so hover and rendering can classify without I/O.
|
|
35
|
+
*/
|
|
36
|
+
export declare function linkTarget(raw: string | null | undefined, options?: {
|
|
37
|
+
base?: string | null;
|
|
38
|
+
}): LinkTarget | null;
|
|
39
|
+
/** The target with its path checked, or the reason the operating system must not be asked. */
|
|
40
|
+
export declare function openableTarget(target: LinkTarget): OpenedTarget;
|
|
41
|
+
/** The address the operating system is given, and the argv that carries it without a shell. */
|
|
42
|
+
export declare function linkOpenCommand(target: OpenedTarget, platform?: NodeJS.Platform): LinkOpenCommand;
|
|
43
|
+
/** The default opener: the operating system's own handler for this kind of target. */
|
|
44
|
+
export declare function openWithSystem(target: OpenedTarget): Promise<void>;
|
|
45
|
+
/** Checks the target and opens it. Returns what was opened, for a caller that reports it. */
|
|
46
|
+
export declare function openLink(target: LinkTarget, opener?: LinkOpener): Promise<OpenedTarget>;
|
|
47
|
+
/** Classifies and opens in one step: the door a host calls for a link a person clicked. */
|
|
48
|
+
export declare function openLinkTarget(raw: string, options?: {
|
|
49
|
+
base?: string | null;
|
|
50
|
+
opener?: LinkOpener;
|
|
51
|
+
}): Promise<OpenedTarget>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// What a link points at, and the one door that hands it to the operating system. Both front ends
|
|
2
|
+
// classify with `linkTarget` and open through `openLink`, so a destination one surface accepts is
|
|
3
|
+
// the destination the other accepts.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
7
|
+
import { HarnessError } from "./errors.js";
|
|
8
|
+
const MAX_LENGTH = 4096;
|
|
9
|
+
const CONTROL_CHARACTERS = /[\x00-\x1f\x7f-\x9f]/u;
|
|
10
|
+
const WEB_ADDRESS = /^https?:/iu;
|
|
11
|
+
const MAIL_ADDRESS = /^mailto:/iu;
|
|
12
|
+
const FILE_ADDRESS = /^file:/iu;
|
|
13
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
|
|
14
|
+
const LINE_SUFFIX = /:\d+(?::\d+)?$/u;
|
|
15
|
+
/** A folder the system would launch or install rather than show. */
|
|
16
|
+
const BUNDLE = /\.(?:app|appex|bundle|framework|kext|plugin|prefpane|workflow|xpc)$/iu;
|
|
17
|
+
/** A document whose default application runs it, so a click would execute rather than open. */
|
|
18
|
+
const LAUNCHABLE = /\.(?:action|appimage|bat|cmd|command|desktop|dmg|exe|jar|lnk|mpkg|osax|pkg|ps1|scpt|sh|terminal|tool|url|vb|vbs|webloc|workflow)$/iu;
|
|
19
|
+
const unopenable = (message) => new HarnessError('link_not_openable', message, 'Open the file from the project instead.');
|
|
20
|
+
/**
|
|
21
|
+
* What a link names, or null when it names nothing a surface may open. Pure: a path is not
|
|
22
|
+
* checked against the filesystem here, so hover and rendering can classify without I/O.
|
|
23
|
+
*/
|
|
24
|
+
export function linkTarget(raw, options = {}) {
|
|
25
|
+
const value = raw?.trim();
|
|
26
|
+
if (!value || value.length > MAX_LENGTH || CONTROL_CHARACTERS.test(value) || value.startsWith('#'))
|
|
27
|
+
return null;
|
|
28
|
+
if (WEB_ADDRESS.test(value)) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || !url.hostname || url.username || url.password)
|
|
32
|
+
return null;
|
|
33
|
+
return { kind: 'web', url: url.href };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (MAIL_ADDRESS.test(value)) {
|
|
40
|
+
const address = value.slice('mailto:'.length).split('?')[0].trim();
|
|
41
|
+
return address && address.includes('@') && !/\s/u.test(address) ? { kind: 'mail', address } : null;
|
|
42
|
+
}
|
|
43
|
+
const path = filePath(value, options.base);
|
|
44
|
+
return path ? { kind: 'path', path } : null;
|
|
45
|
+
}
|
|
46
|
+
function filePath(value, base) {
|
|
47
|
+
let raw = value;
|
|
48
|
+
if (FILE_ADDRESS.test(raw)) {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(raw);
|
|
51
|
+
if (url.hostname && url.hostname !== 'localhost')
|
|
52
|
+
return null;
|
|
53
|
+
raw = url.pathname;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const address = raw.split('#')[0].split('?')[0].replace(LINE_SUFFIX, '');
|
|
60
|
+
let decoded;
|
|
61
|
+
try {
|
|
62
|
+
decoded = decodeURIComponent(address);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!decoded || decoded.includes('\0') || decoded.startsWith('~') || decoded.startsWith('//'))
|
|
68
|
+
return null;
|
|
69
|
+
if (isAbsolute(decoded))
|
|
70
|
+
return decoded;
|
|
71
|
+
if (SCHEME.test(decoded))
|
|
72
|
+
return null;
|
|
73
|
+
if (!base || !isAbsolute(base))
|
|
74
|
+
return null;
|
|
75
|
+
return resolve(base, decoded);
|
|
76
|
+
}
|
|
77
|
+
/** The target with its path checked, or the reason the operating system must not be asked. */
|
|
78
|
+
export function openableTarget(target) {
|
|
79
|
+
if (target.kind !== 'path')
|
|
80
|
+
return target;
|
|
81
|
+
let stats = statsOf(target.path);
|
|
82
|
+
let path = target.path;
|
|
83
|
+
if (stats.isSymbolicLink()) {
|
|
84
|
+
try {
|
|
85
|
+
path = realpathSync(target.path);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw unopenable('That link points at a file that is not there.');
|
|
89
|
+
}
|
|
90
|
+
stats = statsOf(path);
|
|
91
|
+
}
|
|
92
|
+
if (stats.isDirectory()) {
|
|
93
|
+
if (BUNDLE.test(path))
|
|
94
|
+
throw unopenable('That link points at an application, not a document.');
|
|
95
|
+
return { kind: 'directory', path };
|
|
96
|
+
}
|
|
97
|
+
if (!stats.isFile())
|
|
98
|
+
throw unopenable('That link does not point at a file or a folder.');
|
|
99
|
+
if (LAUNCHABLE.test(path))
|
|
100
|
+
throw unopenable('That kind of file runs when it is opened, so a link cannot open it.');
|
|
101
|
+
if ((stats.mode & 0o111) !== 0)
|
|
102
|
+
throw unopenable('That file is executable, so a link cannot open it.');
|
|
103
|
+
return { kind: 'file', path };
|
|
104
|
+
}
|
|
105
|
+
function statsOf(path) {
|
|
106
|
+
try {
|
|
107
|
+
return lstatSync(path);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw unopenable('That link points at a file that is not there.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The address the operating system is given, and the argv that carries it without a shell. */
|
|
114
|
+
export function linkOpenCommand(target, platform = process.platform) {
|
|
115
|
+
const address = target.kind === 'web' ? target.url : target.kind === 'mail' ? `mailto:${target.address}` : target.path;
|
|
116
|
+
if (platform === 'darwin')
|
|
117
|
+
return { command: 'open', args: [address] };
|
|
118
|
+
if (platform === 'win32')
|
|
119
|
+
return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', address] };
|
|
120
|
+
if (platform === 'linux' || platform === 'freebsd' || platform === 'openbsd')
|
|
121
|
+
return { command: 'xdg-open', args: [address] };
|
|
122
|
+
throw unopenable(`Opening links is not supported on ${platform}.`);
|
|
123
|
+
}
|
|
124
|
+
/** The default opener: the operating system's own handler for this kind of target. */
|
|
125
|
+
export async function openWithSystem(target) {
|
|
126
|
+
const { command, args } = linkOpenCommand(target);
|
|
127
|
+
await new Promise((done, fail) => {
|
|
128
|
+
let child;
|
|
129
|
+
try {
|
|
130
|
+
child = spawn(command, args, { detached: process.platform !== 'win32', stdio: 'ignore', windowsHide: true });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
fail(error);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
child.once('error', fail);
|
|
137
|
+
child.once('spawn', () => {
|
|
138
|
+
child.unref();
|
|
139
|
+
done();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** Checks the target and opens it. Returns what was opened, for a caller that reports it. */
|
|
144
|
+
export async function openLink(target, opener = openWithSystem) {
|
|
145
|
+
const opened = openableTarget(target);
|
|
146
|
+
await opener(opened);
|
|
147
|
+
return opened;
|
|
148
|
+
}
|
|
149
|
+
/** Classifies and opens in one step: the door a host calls for a link a person clicked. */
|
|
150
|
+
export async function openLinkTarget(raw, options = {}) {
|
|
151
|
+
const target = linkTarget(raw, { base: options.base });
|
|
152
|
+
if (!target)
|
|
153
|
+
throw unopenable('This link cannot be opened here.');
|
|
154
|
+
return openLink(target, options.opener);
|
|
155
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ShellKind = 'bash' | 'powershell';
|
|
2
|
+
export interface Shell {
|
|
3
|
+
kind: ShellKind;
|
|
4
|
+
executable: string;
|
|
5
|
+
/** Arguments placed before the command text. */
|
|
6
|
+
args: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ShellLookup {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
/** Whether a file exists at an absolute path. */
|
|
11
|
+
exists?: (path: string) => boolean;
|
|
12
|
+
/** The first match for an executable name on the search path. */
|
|
13
|
+
onPath?: (name: string) => string | null;
|
|
14
|
+
}
|
|
15
|
+
/** The shell for this machine, resolved once. Null when none was found. */
|
|
16
|
+
export declare function hostShell(): Shell | null;
|
|
17
|
+
export declare function resolveShell(platform: NodeJS.Platform, lookup?: ShellLookup): Shell | null;
|
|
18
|
+
/** The full argument list that runs `command` in `shell`. */
|
|
19
|
+
export declare function shellArguments(shell: Shell, command: string): string[];
|
|
20
|
+
export declare function shellLabel(shell: Shell): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The shell the Bash tool runs commands in. POSIX systems use Bash. Windows uses Git Bash when it
|
|
2
|
+
// is installed and PowerShell otherwise, so a fresh machine can run commands with nothing extra.
|
|
3
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
4
|
+
import { win32 } from 'node:path';
|
|
5
|
+
import { findBinary } from "./path.js";
|
|
6
|
+
const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command'];
|
|
7
|
+
// Windows PowerShell writes the console code page by default; ask for UTF-8 like Pi does.
|
|
8
|
+
const POWERSHELL_UTF8 = 'try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n';
|
|
9
|
+
let cached;
|
|
10
|
+
/** The shell for this machine, resolved once. Null when none was found. */
|
|
11
|
+
export function hostShell() {
|
|
12
|
+
if (cached === undefined)
|
|
13
|
+
cached = resolveShell(process.platform);
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
export function resolveShell(platform, lookup = {}) {
|
|
17
|
+
const env = lookup.env ?? process.env;
|
|
18
|
+
const exists = lookup.exists ?? isFile;
|
|
19
|
+
const onPath = lookup.onPath ?? findBinary;
|
|
20
|
+
if (platform !== 'win32') {
|
|
21
|
+
const executable = ['/bin/bash', '/usr/bin/bash'].find((candidate) => exists(candidate));
|
|
22
|
+
return executable ? { kind: 'bash', executable, args: ['-c'] } : null;
|
|
23
|
+
}
|
|
24
|
+
const gitBash = [env.ProgramFiles, env['ProgramFiles(x86)']]
|
|
25
|
+
.filter((folder) => Boolean(folder))
|
|
26
|
+
.map((folder) => win32.join(folder, 'Git', 'bin', 'bash.exe'))
|
|
27
|
+
.find((candidate) => exists(candidate));
|
|
28
|
+
if (gitBash)
|
|
29
|
+
return { kind: 'bash', executable: gitBash, args: ['-c'] };
|
|
30
|
+
const bashOnPath = onPath('bash.exe');
|
|
31
|
+
if (bashOnPath && !isWslLauncher(bashOnPath, env))
|
|
32
|
+
return { kind: 'bash', executable: bashOnPath, args: ['-c'] };
|
|
33
|
+
const existing = (folder, ...parts) => {
|
|
34
|
+
if (!folder)
|
|
35
|
+
return null;
|
|
36
|
+
const candidate = win32.join(folder, ...parts);
|
|
37
|
+
return exists(candidate) ? candidate : null;
|
|
38
|
+
};
|
|
39
|
+
// PowerShell 7 first; Windows PowerShell ships with every supported Windows version.
|
|
40
|
+
const powershell = onPath('pwsh.exe')
|
|
41
|
+
?? existing(env.ProgramFiles, 'PowerShell', '7', 'pwsh.exe')
|
|
42
|
+
?? onPath('powershell.exe')
|
|
43
|
+
?? existing(env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
44
|
+
return powershell ? { kind: 'powershell', executable: powershell, args: POWERSHELL_ARGS } : null;
|
|
45
|
+
}
|
|
46
|
+
/** The full argument list that runs `command` in `shell`. */
|
|
47
|
+
export function shellArguments(shell, command) {
|
|
48
|
+
return [...shell.args, shell.kind === 'powershell' ? `${POWERSHELL_UTF8}${command}` : command];
|
|
49
|
+
}
|
|
50
|
+
export function shellLabel(shell) {
|
|
51
|
+
if (shell.kind === 'powershell')
|
|
52
|
+
return 'PowerShell';
|
|
53
|
+
return /\\git\\/iu.test(shell.executable) ? 'Git Bash' : 'Bash';
|
|
54
|
+
}
|
|
55
|
+
// System32\bash.exe starts WSL, a separate Linux system that does not see Windows paths the same
|
|
56
|
+
// way and needs its command on stdin. It is not a usable Bash for this tool.
|
|
57
|
+
function isWslLauncher(path, env) {
|
|
58
|
+
const normalized = win32.normalize(path).toLowerCase();
|
|
59
|
+
const systemRoot = env.SystemRoot;
|
|
60
|
+
if (systemRoot) {
|
|
61
|
+
return ['System32', 'Sysnative'].some((folder) => normalized === win32.join(systemRoot, folder, 'bash.exe').toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/u.test(normalized);
|
|
64
|
+
}
|
|
65
|
+
function isFile(path) {
|
|
66
|
+
try {
|
|
67
|
+
accessSync(path, constants.F_OK);
|
|
68
|
+
return statSync(path).isFile();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import type { BashOperations } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { type Shell } from "./shell.js";
|
|
1
3
|
/** Quotes command text and escapes terminal control/format characters for approval details. */
|
|
2
4
|
export declare function formatBashCommandForApproval(command: string): string;
|
|
5
|
+
interface ShellToolFactories<T> {
|
|
6
|
+
createBashTool: (cwd: string, options: {
|
|
7
|
+
operations: BashOperations;
|
|
8
|
+
}) => T;
|
|
9
|
+
createPowerShellTool: (cwd: string, options: {
|
|
10
|
+
operations: BashOperations;
|
|
11
|
+
}) => T;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The `bash` tool for this machine. The capability keeps the name `bash` on every platform; on a
|
|
15
|
+
* Windows machine without Git Bash it runs PowerShell and carries PowerShell's description.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createWorkspaceShellTool<T>(pi: ShellToolFactories<T>, cwd: string, operations: BashOperations, shell?: Shell | null, platform?: NodeJS.Platform): T;
|
|
3
18
|
/** Adds the explicit pre-execution escape request to Pi's native Bash tool. */
|
|
4
|
-
export declare function withWorkspaceWriteScope<T>(tool: T): T;
|
|
19
|
+
export declare function withWorkspaceWriteScope<T>(tool: T, note?: string): T;
|
|
20
|
+
export {};
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
2
|
import { COMMAND_TIMEOUT_CEILING_SECONDS, COMMAND_TIMEOUT_MAX_SECONDS, DEFAULT_COMMAND_TIMEOUT_SECONDS, resolveCommandTimeout } from "./commandBudget.js";
|
|
3
|
+
import { hostShell, shellLabel } from "./shell.js";
|
|
3
4
|
import { runWithWorkspaceWriteScope } from "./workspaceSandbox.js";
|
|
4
5
|
/** Quotes command text and escapes terminal control/format characters for approval details. */
|
|
5
6
|
export function formatBashCommandForApproval(command) {
|
|
6
7
|
return JSON.stringify(command).replace(/[\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The `bash` tool for this machine. The capability keeps the name `bash` on every platform; on a
|
|
11
|
+
* Windows machine without Git Bash it runs PowerShell and carries PowerShell's description.
|
|
12
|
+
*/
|
|
13
|
+
export function createWorkspaceShellTool(pi, cwd, operations, shell = hostShell(), platform = process.platform) {
|
|
14
|
+
const native = shell?.kind === 'powershell'
|
|
15
|
+
? { ...pi.createPowerShellTool(cwd, { operations }), name: 'bash' }
|
|
16
|
+
: pi.createBashTool(cwd, { operations });
|
|
17
|
+
const note = platform === 'win32' && shell
|
|
18
|
+
? ` On this machine commands run in ${shellLabel(shell)}. Windows has no workspace sandbox, so each command asks for approval unless Full Permission is on, and plan mode cannot run commands.`
|
|
19
|
+
: '';
|
|
20
|
+
return withWorkspaceWriteScope(native, note);
|
|
21
|
+
}
|
|
8
22
|
/** Adds the explicit pre-execution escape request to Pi's native Bash tool. */
|
|
9
|
-
export function withWorkspaceWriteScope(tool) {
|
|
23
|
+
export function withWorkspaceWriteScope(tool, note = '') {
|
|
10
24
|
const native = tool;
|
|
11
25
|
const execute = native.execute.bind(tool);
|
|
12
26
|
return {
|
|
13
27
|
...tool,
|
|
14
|
-
description: `${native.description} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
|
|
28
|
+
description: `${native.description}${note} Workspace writes are the default. Set workspaceWriteScope to "outside" to request approval before broader writes. Commands run for ${DEFAULT_COMMAND_TIMEOUT_SECONDS} seconds unless timeout says otherwise; above ${COMMAND_TIMEOUT_CEILING_SECONDS} seconds a reason is required, and ${COMMAND_TIMEOUT_MAX_SECONDS} seconds is the maximum.`,
|
|
15
29
|
parameters: Type.Object({
|
|
16
30
|
command: Type.String({ description: 'Shell command to execute' }),
|
|
17
31
|
timeout: Type.Optional(Type.Number({ description: `Timeout in seconds (default ${DEFAULT_COMMAND_TIMEOUT_SECONDS})` })),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BashOperations } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { type Shell } from "./shell.js";
|
|
2
3
|
/** `read-only` (plan mode) permits no workspace writes, no network and no outside processes. */
|
|
3
4
|
export type WorkspaceWriteScope = 'workspace' | 'outside' | 'read-only';
|
|
4
5
|
export declare function runWithWorkspaceWriteScope<T>(scope: WorkspaceWriteScope, action: () => T): T;
|
|
@@ -21,11 +22,20 @@ export interface WorkspaceBashOptions {
|
|
|
21
22
|
workspaceWriteScope?: () => WorkspaceWriteScope | undefined;
|
|
22
23
|
/** Exposed for platform-specific tests; production uses the current operating system. */
|
|
23
24
|
platform?: NodeJS.Platform;
|
|
25
|
+
/** Exposed for tests; production uses the machine's shell (`hostShell`). */
|
|
26
|
+
shell?: () => Shell | null;
|
|
24
27
|
}
|
|
28
|
+
/** Why a Windows command asks first: Windows has no write-boundary sandbox the harness can use. */
|
|
29
|
+
export declare const WINDOWS_UNSANDBOXED_REASON = "Windows has no workspace write-boundary sandbox, so this command would run without one. It will not run until this one-call request is approved.";
|
|
25
30
|
export interface WorkspaceSandboxCommand {
|
|
26
31
|
executable: string;
|
|
27
32
|
args: string[];
|
|
28
33
|
marker: string;
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The operating system's write-boundary sandbox for this platform, or null when it is not
|
|
37
|
+
* installed. Bash cannot run without it, so readiness diagnostics report it before a command runs.
|
|
38
|
+
*/
|
|
39
|
+
export declare function workspaceSandboxBackend(platform?: NodeJS.Platform): string | null;
|
|
30
40
|
export declare function workspaceSandboxCommand(platform: NodeJS.Platform, workspaceRoot: string, cwd: string, command: string, additionalWriteRoots?: readonly string[], readOnlyTempRoot?: string | null): WorkspaceSandboxCommand;
|
|
31
41
|
export declare function createWorkspaceBashOperations(workspaceRoot: string | (() => string | null), options?: WorkspaceBashOptions): BashOperations;
|