@xingular/harness 1.3.1-unstable.0 → 1.3.1-unstable.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/README.md +16 -12
- package/builtin/skills/plugin-authoring/SKILL.md +34 -0
- package/dist/cli.js +2334 -1715
- package/dist/sdk/agents/profiles.d.ts +13 -4
- package/dist/sdk/agents/profiles.js +59 -7
- 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/commands/list.js +2 -0
- package/dist/sdk/core/links.d.ts +51 -0
- package/dist/sdk/core/links.js +155 -0
- package/dist/sdk/harness.d.ts +5 -0
- package/dist/sdk/harness.js +11 -6
- package/dist/sdk/index.d.ts +1 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/plugins/builtin.js +11 -6
- 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/inventory.js +1 -1
- 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 +11 -2
- package/dist/sdk/prompt/compile.js +32 -4
- package/dist/sdk/prompt/standard.d.ts +1 -1
- package/dist/sdk/prompt/standard.js +1 -1
- package/dist/sdk/server/methods.js +10 -0
- 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 +45 -5
- package/dist/sdk/sessions/subagentService.js +3 -3
- package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
- package/dist/sdk/terminal/opentui/theme.js +1 -0
- package/dist/sdk/terminal/piExtension.js +71 -2
- package/dist/sdk/terminal/terminals.d.ts +9 -0
- package/dist/sdk/terminal/terminals.js +46 -14
- 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/commandPages.d.ts +1 -1
- package/dist/sdk/terminal/tui/commandPages.js +2 -7
- package/dist/sdk/terminal/tui/commands.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.js +1 -0
- package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
- package/dist/sdk/terminal/tui/keybindings.js +1 -1
- package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +12 -1
- 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/pluginsPage.d.ts +6 -0
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +99 -1
- 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 +7 -0
- package/dist/sdk/tools/sessionName.js +53 -8
- package/dist/sdk/world/world.d.ts +0 -2
- package/dist/terminal-opentui.js +2019 -1269
- package/dist/terminal-pi.js +106 -7
- package/docs/README.md +3 -0
- package/docs/architecture/02-sessions-and-runs.md +11 -5
- package/docs/architecture/03-variants.md +1 -3
- package/docs/architecture/04-prompt.md +16 -7
- package/docs/architecture/06-plugins-and-capabilities.md +36 -4
- package/docs/architecture/07-tools-and-gate.md +20 -5
- package/docs/architecture/10-server-protocol.md +52 -7
- package/docs/architecture/11-terminal.md +50 -19
- package/docs/architecture/12-storage.md +1 -0
- package/docs/architecture/13-world-tools.md +8 -6
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
- package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
- package/docs/architecture/proposals/README.md +1 -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/parity.md +7 -4
- package/docs/releases/1.3.1-unstable.1.md +8 -0
- package/docs/releases/1.3.1-unstable.2.md +11 -0
- package/docs/releases/README.md +7 -4
- package/docs/terminal-tui.md +62 -23
- 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
|
@@ -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
|
-
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
package/dist/sdk/harness.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Hub, type HubOptions } from "./hub/hub.js";
|
|
|
4
4
|
import { type SecretStore } from "./accounts/secrets.js";
|
|
5
5
|
import { Bus } from "./core/bus.js";
|
|
6
6
|
import { type Directory, type HarnessEnv } from "./core/env.js";
|
|
7
|
+
import { type LinkOpener } from "./core/links.js";
|
|
7
8
|
import { ConnectedApps } from "./mcp/service.js";
|
|
8
9
|
import type { AppOAuthClients } from "./mcp/types.js";
|
|
9
10
|
import { Lessons } from "./history/lessons.js";
|
|
@@ -69,6 +70,8 @@ export interface HarnessOptions {
|
|
|
69
70
|
hookImporter?: (address: string) => Promise<unknown>;
|
|
70
71
|
/** Where managed worktrees are made, the prefix of branches they get, and how many `prune` keeps. */
|
|
71
72
|
worktrees?: WorktreesOptions;
|
|
73
|
+
/** How a link a person clicked reaches the operating system. Defaults to the system's own opener. */
|
|
74
|
+
linkOpener?: LinkOpener;
|
|
72
75
|
}
|
|
73
76
|
/** What starting on a data root found and repaired. */
|
|
74
77
|
export interface Recovery {
|
|
@@ -99,6 +102,8 @@ export declare class Harness {
|
|
|
99
102
|
readonly env: Env;
|
|
100
103
|
readonly stateDir: string;
|
|
101
104
|
readonly policy: ToolPolicy;
|
|
105
|
+
/** How a link a person clicked reaches the operating system (page 10, `links.open`). */
|
|
106
|
+
readonly linkOpener: LinkOpener;
|
|
102
107
|
readonly accounts: Accounts;
|
|
103
108
|
readonly approvals: Approvals;
|
|
104
109
|
readonly runs: RunRegistry;
|
package/dist/sdk/harness.js
CHANGED
|
@@ -15,6 +15,7 @@ import { DEFAULT_PRODUCT_NAME, EMPTY_DIRECTORY } from "./core/env.js";
|
|
|
15
15
|
import { HarnessError } from "./core/errors.js";
|
|
16
16
|
import { readJsonOr } from "./core/fs.js";
|
|
17
17
|
import { closeIndex } from "./core/index.js";
|
|
18
|
+
import { openWithSystem } from "./core/links.js";
|
|
18
19
|
import { ConnectedApps } from "./mcp/service.js";
|
|
19
20
|
import { migrateConnectionScopes } from "./mcp/scopeMigration.js";
|
|
20
21
|
import { Lessons } from "./history/lessons.js";
|
|
@@ -26,6 +27,7 @@ import { VariantTrust } from "./variant/trust.js";
|
|
|
26
27
|
import * as notes from "./wiki/notes.js";
|
|
27
28
|
import * as pluginApi from "./plugins/api.js";
|
|
28
29
|
import * as pluginInstall from "./plugins/install.js";
|
|
30
|
+
import * as pluginProposals from "./plugins/proposals.js";
|
|
29
31
|
import * as attachments from "./plugins/attachments.js";
|
|
30
32
|
import { HARNESS_PLUGINS } from "./plugins/builtin.js";
|
|
31
33
|
import { World } from "./world/world.js";
|
|
@@ -72,6 +74,8 @@ export class Harness {
|
|
|
72
74
|
env;
|
|
73
75
|
stateDir;
|
|
74
76
|
policy;
|
|
77
|
+
/** How a link a person clicked reaches the operating system (page 10, `links.open`). */
|
|
78
|
+
linkOpener;
|
|
75
79
|
accounts;
|
|
76
80
|
approvals;
|
|
77
81
|
runs;
|
|
@@ -118,6 +122,7 @@ export class Harness {
|
|
|
118
122
|
this.env = new Env(options.dataRoot || null, options.directory ?? EMPTY_DIRECTORY, options.productName ?? DEFAULT_PRODUCT_NAME, this.bus);
|
|
119
123
|
this.hub = new Hub(this, options.hub);
|
|
120
124
|
this.policy = options.policy ?? {};
|
|
125
|
+
this.linkOpener = options.linkOpener ?? openWithSystem;
|
|
121
126
|
configureBuiltins(options.builtins ?? []);
|
|
122
127
|
this.trust = new VariantTrust(this.stateDir);
|
|
123
128
|
this.refineHost = options.refineHost ?? null;
|
|
@@ -189,7 +194,7 @@ export class Harness {
|
|
|
189
194
|
worktrees: this.worktrees
|
|
190
195
|
});
|
|
191
196
|
this.sideQuestions = new SideQuestions({ env: this.env, sessions: this.sessions, runs: this.runs });
|
|
192
|
-
this.world = new World({ root: () => this.env.root, stateDir: this.stateDir, secrets, browser: this.browserEnabled ? undefined : false
|
|
197
|
+
this.world = new World({ root: () => this.env.root, stateDir: this.stateDir, secrets, browser: this.browserEnabled ? undefined : false });
|
|
193
198
|
// The harness's own world tools come first; a tool an application provides under the same name is not added again.
|
|
194
199
|
this.sessions.provide(this.world.provider());
|
|
195
200
|
this.doctor = new Doctor({
|
|
@@ -200,7 +205,7 @@ export class Harness {
|
|
|
200
205
|
this.refine = new Refinements({ root: () => this.env.root, bus: this.bus, trust: this.trust, host: () => this.refineHost, runtime: refineRuntime });
|
|
201
206
|
this.lessons = new Lessons({ root: () => this.env.root, runtime: refineRuntime });
|
|
202
207
|
this.pageNotes = new NoteDeliveries({ root: () => this.env.root, bus: this.bus, terminals: () => this.terminals ?? null });
|
|
203
|
-
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, usage: this.usage, usageLimits: this.usageLimits, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params) }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
208
|
+
this.terminals = new Terminals({ trust: this.trust, subagents: this.subagents, sideQuestions: this.sideQuestions, resolveCapabilities: (caller, tools, cwd) => capabilities.resolve(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools }), env: this.env, stateDir: this.stateDir, accounts: this.accounts, runs: this.runs, approvals: this.approvals, apps: this.apps, policy: this.policy, bus: this.bus, plugins: this.plugins, usage: this.usage, usageLimits: this.usageLimits, pluginControl: { list: (scope) => pluginApi.list(this, scope), attach: (params, on) => pluginApi.attach(this, params, on), trust: (scope) => pluginApi.trust(this, scope), untrust: (scope) => pluginApi.untrust(this, scope), configure: (params) => pluginApi.configure(this, params), install: params => pluginInstall.install(this, params), remove: params => pluginInstall.remove(this, params), proposals: { list: () => pluginProposals.list(this.env.root), preview: id => pluginProposals.preview(this.env.root, id), accept: (id, hash) => pluginProposals.accept(this, id, hash), reject: (id, hash) => pluginProposals.reject(this.env.root, id, hash) } }, assets: options.assets, hub: this.hub, world: this.world, doctor: this.doctor, worktrees: this.worktrees, noteCapture: this.noteCapture, pageNotes: this.pageNotes });
|
|
204
209
|
this.ready = this.apps.start().catch((error) => log('connected apps', error instanceof Error ? error.message : String(error)));
|
|
205
210
|
this.runs.listen((event, run) => {
|
|
206
211
|
this.accounts.onRunEvent(event, run);
|
|
@@ -215,9 +220,9 @@ export class Harness {
|
|
|
215
220
|
if (runEnded)
|
|
216
221
|
indexRunUsage(this.env.root, event.runId);
|
|
217
222
|
if (runEnded && run?.sessionId) {
|
|
218
|
-
// Notes sent
|
|
219
|
-
//
|
|
220
|
-
|
|
223
|
+
// Notes sent while this session was working go out now (runEnded waits for the terminal to
|
|
224
|
+
// see its run as ended, and tries again while it is still busy).
|
|
225
|
+
void this.pageNotes.runEnded(run.sessionId).catch((error) => log('page notes', error instanceof Error ? error.message : String(error)));
|
|
221
226
|
}
|
|
222
227
|
if ((event.type === 'completed' || event.type === 'failed') && run) {
|
|
223
228
|
if (event.type === 'completed')
|
|
@@ -294,7 +299,7 @@ export class Harness {
|
|
|
294
299
|
if (!sessionId || recoveredSessions.has(sessionId))
|
|
295
300
|
continue;
|
|
296
301
|
recoveredSessions.add(sessionId);
|
|
297
|
-
|
|
302
|
+
void this.pageNotes.runEnded(sessionId).catch((error) => log('page notes', error instanceof Error ? error.message : String(error)));
|
|
298
303
|
}
|
|
299
304
|
catch {
|
|
300
305
|
// A malformed run record does not prevent the rest of recovery.
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { hubCatalogSchema, hubEntrySchema, hubOverviewSchema, hubReceiptSchema,
|
|
|
6
6
|
export { Bus, log, type Notice } from "./core/bus.js";
|
|
7
7
|
export { EMPTY_DIRECTORY, type Directory, type HarnessEnv } from "./core/env.js";
|
|
8
8
|
export { HarnessError, invalid, notFound, toErrorShape, type ErrorShape } from "./core/errors.js";
|
|
9
|
+
export { linkOpenCommand, linkTarget, openableTarget, openLink, openLinkTarget, openWithSystem, type LinkOpener, type LinkTarget, type OpenedTarget } from "./core/links.js";
|
|
9
10
|
export { findBinary, searchPath } from "./core/path.js";
|
|
10
11
|
export { fileSecretStore, memorySecretStore, secretKey, type SecretStore } from "./accounts/secrets.js";
|
|
11
12
|
export type { Account, AccountsOverview, Pool } from "./accounts/store.js";
|
package/dist/sdk/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { hubCatalogSchema, hubEntrySchema, hubOverviewSchema, hubReceiptSchema }
|
|
|
15
15
|
export { Bus, log } from "./core/bus.js";
|
|
16
16
|
export { EMPTY_DIRECTORY } from "./core/env.js";
|
|
17
17
|
export { HarnessError, invalid, notFound, toErrorShape } from "./core/errors.js";
|
|
18
|
+
export { linkOpenCommand, linkTarget, openableTarget, openLink, openLinkTarget, openWithSystem } from "./core/links.js";
|
|
18
19
|
export { findBinary, searchPath } from "./core/path.js";
|
|
19
20
|
export { fileSecretStore, memorySecretStore, secretKey } from "./accounts/secrets.js";
|
|
20
21
|
export { dispatch as harnessCall } from "./tools/dispatch.js";
|
|
@@ -7,7 +7,7 @@ import { sliceOf } from "../sessions/workSlice.js";
|
|
|
7
7
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS } from "../sessions/subagents.js";
|
|
8
8
|
import { subagentTools } from "../tools/spawnSubagent.js";
|
|
9
9
|
import { SESSION_NAME } from "../tools/sessionName.js";
|
|
10
|
-
import { RECALL_TOOLS, SKILL_TOOLS, TRAJECTORY_TOOLS, WIKI_READ_TOOLS, WIKI_WRITE_TOOLS } from "../tools/definitions.js";
|
|
10
|
+
import { PLUGIN_PROPOSAL_TOOLS, RECALL_TOOLS, SKILL_TOOLS, TRAJECTORY_TOOLS, WIKI_READ_TOOLS, WIKI_WRITE_TOOLS } from "../tools/definitions.js";
|
|
11
11
|
import { FIRST_PARTY_PLUGINS } from "./firstParty.js";
|
|
12
12
|
import { WEB_PLUGIN } from "../web/tools.js";
|
|
13
13
|
import { KEYLESS_SEARCH, webPart } from "../web/part.js";
|
|
@@ -93,6 +93,7 @@ const CONNECTED_APPS_PLUGIN = {
|
|
|
93
93
|
const DOCTOR_PLUGIN = { id: 'doctor', title: 'Doctor', description: 'Local readiness checks and explicitly confirmed live probes for accounts, models and tools.', required: 'Every local terminal has readiness diagnostics.' };
|
|
94
94
|
const WORKTREES_PLUGIN = { id: 'worktrees', title: 'Worktrees', description: 'Create and manage isolated git worktrees, move changes between them, and restore removed worktrees.', required: 'Managed worktrees are available to every local terminal.' };
|
|
95
95
|
const HUB_PLUGIN = { id: 'hub', title: 'Hub', description: 'Browse and install reviewed skills, plugins and documents from configured registries.', required: 'Reviewed Harness resources are available to every host.' };
|
|
96
|
+
const PLUGIN_AUTHORING = { id: 'plugin-authoring', title: 'Plugin authoring', description: 'Propose plugins for a person to review.', required: 'Plugin proposals are reviewed before installation.', tools: tools(PLUGIN_PROPOSAL_TOOLS) };
|
|
96
97
|
const REFINEMENT_PLUGIN = { id: 'refinement', title: 'Refinement', description: 'Improve variants from recorded evidence through isolated refinement runs and reviewed candidates.', required: 'Variant refinement is available to every host.' };
|
|
97
98
|
const CAPABILITIES_PLUGIN = { id: 'capabilities', title: 'Capability discovery', description: 'Inspect the callable tools of the current run, including their input schemas. Discovery never grants access.', required: 'Every interactive session can inspect its own registered capabilities.', tools: [{ name: GET_CAPABILITIES, description: 'Inspect current capabilities' }] };
|
|
98
99
|
const USAGE_PLUGIN = {
|
|
@@ -139,6 +140,7 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
139
140
|
serverMethods: connectedAppServerMethods
|
|
140
141
|
},
|
|
141
142
|
{ descriptor: HUB_PLUGIN, serverMethods: hubServerMethods },
|
|
143
|
+
{ descriptor: PLUGIN_AUTHORING, ...toolContribution(PLUGIN_PROPOSAL_TOOLS, 31) },
|
|
142
144
|
{ descriptor: REFINEMENT_PLUGIN, serverMethods: refinementServerMethods },
|
|
143
145
|
{ descriptor: SIDE_QUESTIONS_PLUGIN, serverMethods: sideQuestionServerMethods },
|
|
144
146
|
{
|
|
@@ -154,12 +156,14 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
154
156
|
{
|
|
155
157
|
descriptor: ANNOTATIONS_PLUGIN,
|
|
156
158
|
commandPrefix: 'annotate',
|
|
157
|
-
defaultCommand: '
|
|
159
|
+
defaultCommand: 'open',
|
|
158
160
|
commands: [
|
|
159
|
-
{ name: '
|
|
160
|
-
{ name: '
|
|
161
|
+
{ name: 'open', rootOnly: true, description: 'Annotate a page or app and send it to the agent: /annotate: [url | --app <port> | --bridge]' },
|
|
162
|
+
{ name: 'notes', description: 'Every saved web annotation, searchable' },
|
|
163
|
+
{ name: 'submit', description: 'Send this session\'s unsent annotations with your request: /annotate:submit <request>' },
|
|
164
|
+
{ name: 'clear', description: 'Resolve this session\'s unsent annotations and stop sharing them' }
|
|
161
165
|
],
|
|
162
|
-
createWorldPart: ({ options }) => annotationsPart({ root: options.root
|
|
166
|
+
createWorldPart: ({ options }) => annotationsPart({ root: options.root }),
|
|
163
167
|
serverMethods: annotationServerMethods
|
|
164
168
|
},
|
|
165
169
|
{ descriptor: CAPABILITIES_PLUGIN },
|
|
@@ -169,7 +173,8 @@ export const BUILTIN_PLUGIN_REGISTRY = createBuiltinPluginRegistry([
|
|
|
169
173
|
defaultCommand: 'analysis',
|
|
170
174
|
commands: [
|
|
171
175
|
{ name: 'analysis', description: 'Token, cost and latency totals: /usage:analysis [today | 7d | 30d | all]' },
|
|
172
|
-
{ name: 'limit', description: 'Provider plan windows, balances and rate limits: /usage:limit' }
|
|
176
|
+
{ name: 'limit', description: 'Provider plan windows, balances and rate limits: /usage:limit' },
|
|
177
|
+
{ name: 'accounts', description: 'Provider limits across all accounts: /usage:accounts' }
|
|
173
178
|
],
|
|
174
179
|
serverMethods: usageServerMethods
|
|
175
180
|
},
|
|
@@ -14,6 +14,8 @@ export interface BuiltinPluginCommand {
|
|
|
14
14
|
name: string;
|
|
15
15
|
description: string;
|
|
16
16
|
legacyBare: boolean;
|
|
17
|
+
/** Reached only as the namespace root (`/annotate:`); it has no `/<prefix>:<name>` route and is not listed by name. */
|
|
18
|
+
rootOnly?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export interface BuiltinWorldContext {
|
|
19
21
|
options: WorldOptions;
|
|
@@ -36,7 +38,7 @@ export interface BuiltinPluginModule {
|
|
|
36
38
|
descriptor: PluginDescriptor;
|
|
37
39
|
/** Stable terminal command namespace; defaults to the plugin id. */
|
|
38
40
|
commandPrefix?: string;
|
|
39
|
-
/** Command invoked by the namespace root, such as `/annotate:`. */
|
|
41
|
+
/** Command invoked by the namespace root, such as `/annotate:`. Mark it `rootOnly` to give it no named route. */
|
|
40
42
|
defaultCommand?: string;
|
|
41
43
|
/** Stable order for model tools, independent of descriptor display order. */
|
|
42
44
|
toolOrder?: number;
|
|
@@ -27,6 +27,10 @@ export function createBuiltinPluginRegistry(modules) {
|
|
|
27
27
|
if (module.defaultCommand && !module.commands?.some(command => command.name === module.defaultCommand)) {
|
|
28
28
|
throw new Error(`Unknown default command for built-in plugin ${id}: ${module.defaultCommand}`);
|
|
29
29
|
}
|
|
30
|
+
for (const command of module.commands ?? []) {
|
|
31
|
+
if (command.rootOnly && command.name !== module.defaultCommand)
|
|
32
|
+
throw new Error(`Root-only command ${prefix}:${command.name} must be the default command of built-in plugin ${id}.`);
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
const commands = modules.flatMap(module => (module.commands ?? []).map(command => ({
|
|
32
36
|
pluginId: module.descriptor.id,
|
|
@@ -34,12 +38,14 @@ export function createBuiltinPluginRegistry(modules) {
|
|
|
34
38
|
...command,
|
|
35
39
|
legacyBare: !!module.descriptor.required
|
|
36
40
|
})));
|
|
37
|
-
const
|
|
41
|
+
const allRoutes = new Map(commands.map(command => [`${command.prefix}:${command.name}`, command]));
|
|
42
|
+
const routed = commands.filter(command => !command.rootOnly);
|
|
43
|
+
const byRoute = new Map(routed.map(command => [`${command.prefix}:${command.name}`, command]));
|
|
38
44
|
const byName = new Map();
|
|
39
|
-
for (const command of
|
|
45
|
+
for (const command of routed)
|
|
40
46
|
byName.set(command.name, [...(byName.get(command.name) ?? []), command]);
|
|
41
47
|
const defaults = new Map(modules.flatMap(module => module.defaultCommand
|
|
42
|
-
? [[module.commandPrefix ?? module.descriptor.id,
|
|
48
|
+
? [[module.commandPrefix ?? module.descriptor.id, allRoutes.get(`${module.commandPrefix ?? module.descriptor.id}:${module.defaultCommand}`)]]
|
|
43
49
|
: []));
|
|
44
50
|
const toolModules = modules.map((module, index) => ({ module, index })).filter(({ module }) => module.tools)
|
|
45
51
|
.sort((left, right) => (left.module.toolOrder ?? left.index) - (right.module.toolOrder ?? right.index));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `context` is an instruction file Pi reads on its own: `AGENTS.md`, `SYSTEM.md` and their kin.
|
|
3
3
|
* `hook` is a package's hooks file, which the harness runs itself; Pi never loads it.
|
|
4
|
+
* `agent` is a subagent profile a package brings; the harness reads it, Pi never does.
|
|
4
5
|
*/
|
|
5
|
-
export type FoundKind = 'extension' | 'skill' | 'prompt' | 'theme' | 'context' | 'hook';
|
|
6
|
+
export type FoundKind = 'extension' | 'skill' | 'prompt' | 'theme' | 'context' | 'hook' | 'agent';
|
|
6
7
|
/** Where a found item comes from. `builtin` is one of the Pi command's own extensions. */
|
|
7
8
|
export type FoundOrigin = 'shared' | 'package' | 'project' | 'home' | 'builtin';
|
|
8
9
|
export interface Found {
|
|
@@ -33,6 +34,11 @@ export type Resolver = (input: ResolveInput) => Promise<Found[]>;
|
|
|
33
34
|
/** The extensions built into the `pi` command. A terminal loads them; an SDK session has none. */
|
|
34
35
|
export declare const PI_BUILTINS: readonly ["llama.cpp", "codemode", "tool-search", "mcp"];
|
|
35
36
|
export declare const BUILTIN_PREFIX = "builtin:";
|
|
37
|
+
/**
|
|
38
|
+
* The subagent profiles in a package's agents folder: Markdown files whose frontmatter has a
|
|
39
|
+
* `name` and a `description`. Anything else there (notes, a README) is not a profile.
|
|
40
|
+
*/
|
|
41
|
+
export declare function packageProfiles(root: string, folder: string): string[];
|
|
36
42
|
/**
|
|
37
43
|
* Runs an install with the package manager's install scripts switched off, so fetching a
|
|
38
44
|
* package runs none of its code. A package that builds itself on install then fails to load,
|