@oharness/engine 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/claude.d.ts +34 -0
- package/dist/acp/claude.js +122 -0
- package/dist/acp/claude.js.map +1 -0
- package/dist/acp/client.d.ts +193 -0
- package/dist/acp/client.js +279 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/runner.d.ts +80 -0
- package/dist/acp/runner.js +159 -0
- package/dist/acp/runner.js.map +1 -0
- package/dist/agent/agent.d.ts +51 -1
- package/dist/agent/agent.js +322 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/hooks.js +6 -6
- package/dist/agent/hooks.js.map +1 -1
- package/dist/agent/session.d.ts +2 -0
- package/dist/agent/session.js +19 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/skills.d.ts +16 -3
- package/dist/agent/skills.js +36 -2
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/subagent.d.ts +24 -2
- package/dist/agent/subagent.js +76 -16
- package/dist/agent/subagent.js.map +1 -1
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/manager.js +4 -0
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/oauth.d.ts +33 -0
- package/dist/auth/oauth.js +59 -8
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/providers/anthropic.d.ts +25 -5
- package/dist/auth/providers/anthropic.js +47 -6
- package/dist/auth/providers/anthropic.js.map +1 -1
- package/dist/auth/providers/chatgpt.d.ts +8 -0
- package/dist/auth/providers/chatgpt.js +180 -0
- package/dist/auth/providers/chatgpt.js.map +1 -0
- package/dist/auth/providers/openrouter.d.ts +2 -0
- package/dist/auth/providers/openrouter.js +101 -0
- package/dist/auth/providers/openrouter.js.map +1 -0
- package/dist/auth/types.d.ts +15 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/plugin.d.ts +6 -1
- package/dist/browser/plugin.js.map +1 -1
- package/dist/browser/session.d.ts +47 -0
- package/dist/browser/session.js +177 -2
- package/dist/browser/session.js.map +1 -1
- package/dist/browser/tools.js +139 -18
- package/dist/browser/tools.js.map +1 -1
- package/dist/computer/cua-driver.d.ts +60 -1
- package/dist/computer/cua-driver.js +149 -5
- package/dist/computer/cua-driver.js.map +1 -1
- package/dist/computer/helper-client.d.ts +3 -1
- package/dist/computer/helper-client.js +3 -0
- package/dist/computer/helper-client.js.map +1 -1
- package/dist/computer/index.d.ts +3 -2
- package/dist/computer/index.js +1 -1
- package/dist/computer/index.js.map +1 -1
- package/dist/computer/mode.d.ts +15 -0
- package/dist/computer/mode.js +11 -1
- package/dist/computer/mode.js.map +1 -1
- package/dist/computer/session.d.ts +36 -7
- package/dist/computer/session.js +118 -17
- package/dist/computer/session.js.map +1 -1
- package/dist/computer/target.d.ts +19 -0
- package/dist/computer/target.js.map +1 -1
- package/dist/computer/tools.js +135 -8
- package/dist/computer/tools.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/foreign.d.ts +81 -0
- package/dist/foreign.js +198 -0
- package/dist/foreign.js.map +1 -0
- package/dist/harness.d.ts +79 -0
- package/dist/harness.js +241 -17
- package/dist/harness.js.map +1 -1
- package/dist/mcp/manager.d.ts +15 -0
- package/dist/mcp/manager.js +28 -1
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/registry.d.ts +2 -0
- package/dist/mcp/registry.js +1 -0
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/registry.json +394 -1
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +38 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugin-market.d.ts +57 -0
- package/dist/plugin-market.js +229 -0
- package/dist/plugin-market.js.map +1 -0
- package/dist/plugin-market.json +1246 -0
- package/dist/plugins.d.ts +49 -2
- package/dist/plugins.js +252 -10
- package/dist/plugins.js.map +1 -1
- package/dist/providers/catalog.json +14 -3
- package/dist/providers/models-dev.d.ts +15 -0
- package/dist/providers/models-dev.js +14 -0
- package/dist/providers/models-dev.js.map +1 -1
- package/dist/providers/piai.d.ts +38 -1
- package/dist/providers/piai.js +70 -2
- package/dist/providers/piai.js.map +1 -1
- package/dist/providers/registry.d.ts +16 -1
- package/dist/providers/registry.js +42 -11
- package/dist/providers/registry.js.map +1 -1
- package/dist/runtime-plugins/builtin/studio.d.ts +7 -0
- package/dist/runtime-plugins/builtin/studio.js +69 -4
- package/dist/runtime-plugins/builtin/studio.js.map +1 -1
- package/dist/tools/builtin/bash.js +6 -10
- package/dist/tools/builtin/bash.js.map +1 -1
- package/dist/util/shell.d.ts +30 -0
- package/dist/util/shell.js +70 -0
- package/dist/util/shell.js.map +1 -1
- package/dist/util/toml.d.ts +22 -0
- package/dist/util/toml.js +305 -0
- package/dist/util/toml.js.map +1 -0
- package/package.json +4 -4
- package/skills/media-generation/SKILL.md +22 -1
package/dist/harness.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { Catalog, CatalogProvider } from "./providers/catalog.ts";
|
|
|
27
27
|
import type { EffortChoice, ModelSpec } from "./providers/types.ts";
|
|
28
28
|
import type { BrowserTargetFactory } from "./browser/target.ts";
|
|
29
29
|
import type { BrowserUsePlugin, BrowserUseSession } from "./browser/plugin.ts";
|
|
30
|
+
import type { BrowserTurnShot } from "./browser/session.ts";
|
|
30
31
|
import type { ComputerUsePlugin } from "./computer/plugin.ts";
|
|
31
32
|
import type { ComputerUseSession } from "./computer/session.ts";
|
|
32
33
|
import type { BrowserProviderPlugin } from "./browser/provider.ts";
|
|
@@ -126,6 +127,15 @@ export declare class Harness {
|
|
|
126
127
|
* offered and `task` runs everything in the foreground.
|
|
127
128
|
*/
|
|
128
129
|
taskWaker?: (sessionId: string) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Where pictures of the browser tabs a turn left go, and whether to take
|
|
132
|
+
* them at all. Set by a frontend that shows them; while unset, none are.
|
|
133
|
+
* `at` is the conversation's message count when the turn ended.
|
|
134
|
+
*/
|
|
135
|
+
turnShotSink?: {
|
|
136
|
+
enabled(): boolean;
|
|
137
|
+
save(sessionId: string, at: number, shots: BrowserTurnShot[]): void;
|
|
138
|
+
};
|
|
129
139
|
/** Browser state belongs to a conversation, not to its project harness. */
|
|
130
140
|
private readonly sessionBrowsers;
|
|
131
141
|
/** Computer Use state belongs to a conversation too. */
|
|
@@ -212,6 +222,30 @@ export declare class Harness {
|
|
|
212
222
|
* started with the browser off has no tools to switch, so that one stays as
|
|
213
223
|
* it is until a restart.
|
|
214
224
|
*/
|
|
225
|
+
/** The Claude Agent definition this harness registered, while it is on. */
|
|
226
|
+
private claudeRegistered?;
|
|
227
|
+
/**
|
|
228
|
+
* Whether Claude Agent is switched on, and whether it could be.
|
|
229
|
+
*
|
|
230
|
+
* `available` is whether this machine has the user's `claude` (and `npx`
|
|
231
|
+
* for the adapter); `unavailable` is the reason it would not run even so,
|
|
232
|
+
* such as an old Node. Detected afresh each time, since installing Claude
|
|
233
|
+
* Code is something done in another window while this page is open.
|
|
234
|
+
*/
|
|
235
|
+
claudeAgentStatus(): Promise<{
|
|
236
|
+
on: boolean;
|
|
237
|
+
available: boolean;
|
|
238
|
+
unavailable?: string;
|
|
239
|
+
}>;
|
|
240
|
+
/**
|
|
241
|
+
* Turn Claude Agent on or off, now and for later launches.
|
|
242
|
+
*
|
|
243
|
+
* Written to the user's own config, the same place `claudeAgent` is read
|
|
244
|
+
* from at startup, so the terminal frontends and the page agree. Takes
|
|
245
|
+
* effect at once: open conversations share this harness's subagent list.
|
|
246
|
+
*/
|
|
247
|
+
setClaudeAgent(on: boolean): Promise<void>;
|
|
248
|
+
private registerClaudeAgent;
|
|
215
249
|
setBrowserMode(mode: "auto" | "builtin" | "extension"): Promise<void>;
|
|
216
250
|
/**
|
|
217
251
|
* Whether permission decisions are being recorded, and who decided that.
|
|
@@ -370,6 +404,12 @@ export declare class Harness {
|
|
|
370
404
|
* happen to — and answers 404 when picked. Nothing to refresh from (no
|
|
371
405
|
* listing ever made) counts as stale. Resolves to undefined when the list
|
|
372
406
|
* was fresh enough to leave alone.
|
|
407
|
+
*
|
|
408
|
+
* The age is the oldest provider's, from `discovered`, not `discoveredAt`:
|
|
409
|
+
* that one moves whenever any single provider is listed — a sign-in, a
|
|
410
|
+
* key saved for one vendor — and read alone it kept every other provider's
|
|
411
|
+
* week-old list looking fresh. `discoveredAt` is the fallback for a file
|
|
412
|
+
* written before `discovered` existed.
|
|
373
413
|
*/
|
|
374
414
|
refreshStaleModels(maxAgeMs?: number): Promise<ModelSpec[] | undefined>;
|
|
375
415
|
/**
|
|
@@ -390,6 +430,7 @@ export declare class Harness {
|
|
|
390
430
|
* it: merging only ever grew the file, so a model the vendor withdrew stayed
|
|
391
431
|
* in the picker for good. `discovered` records which providers have a live
|
|
392
432
|
* listing, so the pi-ai snapshot stops filling in models for them.
|
|
433
|
+
* `cleared` names local servers that listed nothing, whose entries go.
|
|
393
434
|
*/
|
|
394
435
|
private saveDiscovered;
|
|
395
436
|
/**
|
|
@@ -400,6 +441,33 @@ export declare class Harness {
|
|
|
400
441
|
* which breaks the one loop that would let the agent improve itself.
|
|
401
442
|
*/
|
|
402
443
|
reloadSkills(): Promise<number>;
|
|
444
|
+
/**
|
|
445
|
+
* Switch one skill off or back on, now and for later launches.
|
|
446
|
+
*
|
|
447
|
+
* Off rather than deleted: most skills in the list are files another tool
|
|
448
|
+
* put there — Claude Code's, Codex's, a plugin's — and removing someone
|
|
449
|
+
* else's file to hide it from this harness would take it from that tool
|
|
450
|
+
* too.
|
|
451
|
+
*/
|
|
452
|
+
setSkillEnabled(name: string, on: boolean): Promise<void>;
|
|
453
|
+
/**
|
|
454
|
+
* Every markdown agent this harness reads, with the dialect each was
|
|
455
|
+
* written in: a Claude Code plugin's names tools and models the way
|
|
456
|
+
* Claude Code does, and is translated on the way in.
|
|
457
|
+
*/
|
|
458
|
+
private markdownAgents;
|
|
459
|
+
/**
|
|
460
|
+
* Take in a plugin that arrived while the harness was running.
|
|
461
|
+
*
|
|
462
|
+
* What `create` does for every plugin at launch, for one: its skills and
|
|
463
|
+
* agents join the registries, its servers connect. Without this an install
|
|
464
|
+
* from the menu would sit inert until a restart, which reads as a broken
|
|
465
|
+
* install. A server name the config already has is left to the config, the
|
|
466
|
+
* same precedence a launch gives it.
|
|
467
|
+
*/
|
|
468
|
+
adoptPlugin(directory: string): Promise<PluginDefinition | undefined>;
|
|
469
|
+
/** Undo `adoptPlugin` for a plugin whose directory is gone. */
|
|
470
|
+
dropPlugin(directory: string): Promise<void>;
|
|
403
471
|
/** Providers declared by the catalog, for `/models` and login hints. */
|
|
404
472
|
listCatalogProviders(): CatalogProvider[];
|
|
405
473
|
listModels(configuredOnly?: boolean): ModelSpec[];
|
|
@@ -472,6 +540,11 @@ export declare class Harness {
|
|
|
472
540
|
* control its conversation held, so another need not wait out the lease.
|
|
473
541
|
*/
|
|
474
542
|
private trackRun;
|
|
543
|
+
/**
|
|
544
|
+
* The tabs a turn left, beside the conversation (see BrowserUseSession.endTurn).
|
|
545
|
+
* Where the turn ended is read now, before a queued message starts the next.
|
|
546
|
+
*/
|
|
547
|
+
private pictureBrowserTurn;
|
|
475
548
|
/**
|
|
476
549
|
* A subagent's own browser and computer.
|
|
477
550
|
*
|
|
@@ -517,6 +590,12 @@ export declare class Harness {
|
|
|
517
590
|
* picked in the composer; it wins over the first-configured guess.
|
|
518
591
|
*/
|
|
519
592
|
generateText(system: string, prompt: string, signal?: AbortSignal, maxOutputTokens?: number, fallback?: string): Promise<string>;
|
|
593
|
+
/**
|
|
594
|
+
* The same one reply, from a model the caller names rather than the small
|
|
595
|
+
* one: for a chore that should cost and sound like the conversation it is
|
|
596
|
+
* about, such as the composer's next-message suggestion.
|
|
597
|
+
*/
|
|
598
|
+
generateWith(ref: string, system: string, prompt: string, signal?: AbortSignal, maxOutputTokens?: number): Promise<string>;
|
|
520
599
|
/**
|
|
521
600
|
* First model whose provider has credentials. Only the cheap synchronous
|
|
522
601
|
* check runs here — a subscription-only provider is picked up once the
|
package/dist/harness.js
CHANGED
|
@@ -6,6 +6,8 @@ import { HookRegistry } from "./agent/hooks.js";
|
|
|
6
6
|
import { Session } from "./agent/session.js";
|
|
7
7
|
import { SessionStore } from "./agent/store.js";
|
|
8
8
|
import { BUILTIN_SUBAGENTS, SubagentRegistry, loadMarkdownSubagents } from "./agent/subagent.js";
|
|
9
|
+
import { foreignAgentRoots, foreignSkillRoots } from "./foreign.js";
|
|
10
|
+
import { claudeAgent } from "./acp/claude.js";
|
|
9
11
|
import { SkillRegistry } from "./agent/skills.js";
|
|
10
12
|
import { SettingsStore } from "./agent/settings.js";
|
|
11
13
|
import { createPolicy } from "./agent/workspace.js";
|
|
@@ -73,6 +75,12 @@ export class Harness {
|
|
|
73
75
|
* offered and `task` runs everything in the foreground.
|
|
74
76
|
*/
|
|
75
77
|
taskWaker;
|
|
78
|
+
/**
|
|
79
|
+
* Where pictures of the browser tabs a turn left go, and whether to take
|
|
80
|
+
* them at all. Set by a frontend that shows them; while unset, none are.
|
|
81
|
+
* `at` is the conversation's message count when the turn ended.
|
|
82
|
+
*/
|
|
83
|
+
turnShotSink;
|
|
76
84
|
/** Browser state belongs to a conversation, not to its project harness. */
|
|
77
85
|
sessionBrowsers = new Map();
|
|
78
86
|
/** Computer Use state belongs to a conversation too. */
|
|
@@ -198,7 +206,14 @@ export class Harness {
|
|
|
198
206
|
...(config.skillDirs ?? []),
|
|
199
207
|
...(config.agentDirs ?? []),
|
|
200
208
|
],
|
|
201
|
-
|
|
209
|
+
// Claude Code's and Codex's skill and agent directories are read, so
|
|
210
|
+
// the agent can open a skill's scripts/, but not written: those belong
|
|
211
|
+
// to the user's other tools, not to anything this harness installed.
|
|
212
|
+
readableRoots: [
|
|
213
|
+
...(config.readableRoots ?? []),
|
|
214
|
+
...Object.values(foreignSkillRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
|
|
215
|
+
...Object.values(foreignAgentRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
|
|
216
|
+
],
|
|
202
217
|
});
|
|
203
218
|
this.mcp = new McpManager(this.logger, this.auth.store, (id) => this.auth.secret(id));
|
|
204
219
|
this.store = new SessionStore(config.sessionDir);
|
|
@@ -234,7 +249,7 @@ export class Harness {
|
|
|
234
249
|
: (await ensureProjectManifest(cwd)).id;
|
|
235
250
|
opts = { ...opts, cwd };
|
|
236
251
|
const baseConfig = opts.config ?? (await loadConfig(cwd));
|
|
237
|
-
const plugins = await discoverPlugins(cwd, baseConfig
|
|
252
|
+
const plugins = await discoverPlugins(cwd, pluginOptions(baseConfig));
|
|
238
253
|
// Explicit user/project config is applied after plugins, so it can always
|
|
239
254
|
// tighten policy or replace a plugin's MCP server declaration.
|
|
240
255
|
const config = mergeConfig(pluginContributions(plugins), baseConfig);
|
|
@@ -262,10 +277,16 @@ export class Harness {
|
|
|
262
277
|
})
|
|
263
278
|
.catch((err) => harness.logger.warn("session pruning failed", { error: String(err) }));
|
|
264
279
|
}
|
|
265
|
-
|
|
280
|
+
harness.skills.setDisabled(config.disabledSkills ?? []);
|
|
281
|
+
await harness.skills.discover(cwd, config.skillDirs ?? [], config.imports ?? {});
|
|
282
|
+
// Claude Agent, when the user turned it on and this machine has their
|
|
283
|
+
// own Claude Code to hand work to. Before the markdown ones, so a
|
|
284
|
+
// project can redefine it.
|
|
285
|
+
if (config.claudeAgent === true)
|
|
286
|
+
await harness.registerClaudeAgent();
|
|
266
287
|
// Markdown definitions layer over the JSON ones, so a repo can drop in an
|
|
267
288
|
// agent file without editing config.
|
|
268
|
-
for (const definition of await
|
|
289
|
+
for (const definition of await harness.markdownAgents()) {
|
|
269
290
|
harness.subagents.register(definition);
|
|
270
291
|
}
|
|
271
292
|
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
@@ -422,6 +443,50 @@ export class Harness {
|
|
|
422
443
|
* started with the browser off has no tools to switch, so that one stays as
|
|
423
444
|
* it is until a restart.
|
|
424
445
|
*/
|
|
446
|
+
/** The Claude Agent definition this harness registered, while it is on. */
|
|
447
|
+
claudeRegistered;
|
|
448
|
+
/**
|
|
449
|
+
* Whether Claude Agent is switched on, and whether it could be.
|
|
450
|
+
*
|
|
451
|
+
* `available` is whether this machine has the user's `claude` (and `npx`
|
|
452
|
+
* for the adapter); `unavailable` is the reason it would not run even so,
|
|
453
|
+
* such as an old Node. Detected afresh each time, since installing Claude
|
|
454
|
+
* Code is something done in another window while this page is open.
|
|
455
|
+
*/
|
|
456
|
+
async claudeAgentStatus() {
|
|
457
|
+
const found = await claudeAgent().catch(() => undefined);
|
|
458
|
+
return {
|
|
459
|
+
on: this.config.claudeAgent === true,
|
|
460
|
+
available: Boolean(found),
|
|
461
|
+
...(found?.runtime?.unavailable ? { unavailable: found.runtime.unavailable } : {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Turn Claude Agent on or off, now and for later launches.
|
|
466
|
+
*
|
|
467
|
+
* Written to the user's own config, the same place `claudeAgent` is read
|
|
468
|
+
* from at startup, so the terminal frontends and the page agree. Takes
|
|
469
|
+
* effect at once: open conversations share this harness's subagent list.
|
|
470
|
+
*/
|
|
471
|
+
async setClaudeAgent(on) {
|
|
472
|
+
await updateUserConfig((current) => ({ ...current, claudeAgent: on }));
|
|
473
|
+
this.config.claudeAgent = on;
|
|
474
|
+
if (on)
|
|
475
|
+
await this.registerClaudeAgent();
|
|
476
|
+
else if (this.claudeRegistered) {
|
|
477
|
+
this.subagents.unregister(this.claudeRegistered);
|
|
478
|
+
this.claudeRegistered = undefined;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async registerClaudeAgent() {
|
|
482
|
+
if (this.claudeRegistered || process.env["OHARNESS_NO_CLAUDE_AGENT"] === "1")
|
|
483
|
+
return;
|
|
484
|
+
const claude = await claudeAgent().catch(() => undefined);
|
|
485
|
+
if (!claude)
|
|
486
|
+
return;
|
|
487
|
+
this.subagents.register(claude);
|
|
488
|
+
this.claudeRegistered = claude;
|
|
489
|
+
}
|
|
425
490
|
async setBrowserMode(mode) {
|
|
426
491
|
await updateUserConfig((current) => ({ ...current, browser: { ...current.browser, mode } }));
|
|
427
492
|
this.config.browser = { ...this.config.browser, mode };
|
|
@@ -771,9 +836,9 @@ export class Harness {
|
|
|
771
836
|
* the user catalog so the next run starts with a current list.
|
|
772
837
|
*/
|
|
773
838
|
async refreshModels(providerId) {
|
|
774
|
-
const discovered = await this.providers.
|
|
775
|
-
if (discovered.length > 0) {
|
|
776
|
-
await this.saveDiscovered(discovered).catch((err) => this.logger.warn("could not persist the discovered catalog", {
|
|
839
|
+
const { changed: discovered, cleared } = await this.providers.refreshListings(providerId);
|
|
840
|
+
if (discovered.length > 0 || cleared.length > 0) {
|
|
841
|
+
await this.saveDiscovered(discovered, cleared).catch((err) => this.logger.warn("could not persist the discovered catalog", {
|
|
777
842
|
error: String(err),
|
|
778
843
|
}));
|
|
779
844
|
}
|
|
@@ -788,12 +853,22 @@ export class Harness {
|
|
|
788
853
|
* happen to — and answers 404 when picked. Nothing to refresh from (no
|
|
789
854
|
* listing ever made) counts as stale. Resolves to undefined when the list
|
|
790
855
|
* was fresh enough to leave alone.
|
|
856
|
+
*
|
|
857
|
+
* The age is the oldest provider's, from `discovered`, not `discoveredAt`:
|
|
858
|
+
* that one moves whenever any single provider is listed — a sign-in, a
|
|
859
|
+
* key saved for one vendor — and read alone it kept every other provider's
|
|
860
|
+
* week-old list looking fresh. `discoveredAt` is the fallback for a file
|
|
861
|
+
* written before `discovered` existed.
|
|
791
862
|
*/
|
|
792
863
|
async refreshStaleModels(maxAgeMs = MODEL_LIST_MAX_AGE_MS) {
|
|
793
864
|
let at = NaN;
|
|
794
865
|
try {
|
|
795
866
|
const raw = JSON.parse(await readFile(userCatalogPath(), "utf8"));
|
|
796
|
-
|
|
867
|
+
const each = raw["discovered"];
|
|
868
|
+
const times = each && typeof each === "object" && !Array.isArray(each)
|
|
869
|
+
? Object.values(each).map((value) => Date.parse(String(value))).filter(Number.isFinite)
|
|
870
|
+
: [];
|
|
871
|
+
at = times.length > 0 ? Math.min(...times) : Date.parse(String(raw["discoveredAt"] ?? ""));
|
|
797
872
|
}
|
|
798
873
|
catch {
|
|
799
874
|
// No file or an unreadable one: never listed, so due.
|
|
@@ -828,8 +903,9 @@ export class Harness {
|
|
|
828
903
|
* it: merging only ever grew the file, so a model the vendor withdrew stayed
|
|
829
904
|
* in the picker for good. `discovered` records which providers have a live
|
|
830
905
|
* listing, so the pi-ai snapshot stops filling in models for them.
|
|
906
|
+
* `cleared` names local servers that listed nothing, whose entries go.
|
|
831
907
|
*/
|
|
832
|
-
async saveDiscovered(models) {
|
|
908
|
+
async saveDiscovered(models, cleared = []) {
|
|
833
909
|
const file = userCatalogPath();
|
|
834
910
|
let existing = {};
|
|
835
911
|
try {
|
|
@@ -841,7 +917,7 @@ export class Harness {
|
|
|
841
917
|
const byId = new Map((Array.isArray(existing["models"]) ? existing["models"] : [])
|
|
842
918
|
.filter((m) => !!m && typeof m === "object")
|
|
843
919
|
.map((m) => [String(m["id"]), m]));
|
|
844
|
-
const listed = new Set(models.map((model) => model.provider));
|
|
920
|
+
const listed = new Set([...models.map((model) => model.provider), ...cleared]);
|
|
845
921
|
const fresh = new Set(models.map((model) => model.id));
|
|
846
922
|
for (const [id, model] of byId) {
|
|
847
923
|
const { provider, model: wire } = model;
|
|
@@ -872,9 +948,109 @@ export class Harness {
|
|
|
872
948
|
* which breaks the one loop that would let the agent improve itself.
|
|
873
949
|
*/
|
|
874
950
|
async reloadSkills() {
|
|
875
|
-
await this.skills.discover(this.cwd, this.config.skillDirs ?? []);
|
|
951
|
+
await this.skills.discover(this.cwd, this.config.skillDirs ?? [], this.config.imports ?? {});
|
|
876
952
|
return this.skills.size;
|
|
877
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Switch one skill off or back on, now and for later launches.
|
|
956
|
+
*
|
|
957
|
+
* Off rather than deleted: most skills in the list are files another tool
|
|
958
|
+
* put there — Claude Code's, Codex's, a plugin's — and removing someone
|
|
959
|
+
* else's file to hide it from this harness would take it from that tool
|
|
960
|
+
* too.
|
|
961
|
+
*/
|
|
962
|
+
async setSkillEnabled(name, on) {
|
|
963
|
+
await updateUserConfig((current) => {
|
|
964
|
+
const off = new Set(current.disabledSkills ?? []);
|
|
965
|
+
if (on)
|
|
966
|
+
off.delete(name);
|
|
967
|
+
else
|
|
968
|
+
off.add(name);
|
|
969
|
+
return { ...current, disabledSkills: [...off].sort() };
|
|
970
|
+
});
|
|
971
|
+
const off = new Set(this.config.disabledSkills ?? []);
|
|
972
|
+
if (on)
|
|
973
|
+
off.delete(name);
|
|
974
|
+
else
|
|
975
|
+
off.add(name);
|
|
976
|
+
this.config.disabledSkills = [...off];
|
|
977
|
+
this.skills.setDisabled(off);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Every markdown agent this harness reads, with the dialect each was
|
|
981
|
+
* written in: a Claude Code plugin's names tools and models the way
|
|
982
|
+
* Claude Code does, and is translated on the way in.
|
|
983
|
+
*/
|
|
984
|
+
async markdownAgents() {
|
|
985
|
+
const claude = new Set(this.plugins.filter((plugin) => plugin.claudeFormat).map((plugin) => plugin.agentDir));
|
|
986
|
+
const roots = (this.config.agentDirs ?? []).map((dir) => claude.has(dir) ? { dir, dialect: "claude" } : { dir });
|
|
987
|
+
return loadMarkdownSubagents(this.cwd, roots, this.config.imports ?? {});
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Take in a plugin that arrived while the harness was running.
|
|
991
|
+
*
|
|
992
|
+
* What `create` does for every plugin at launch, for one: its skills and
|
|
993
|
+
* agents join the registries, its servers connect. Without this an install
|
|
994
|
+
* from the menu would sit inert until a restart, which reads as a broken
|
|
995
|
+
* install. A server name the config already has is left to the config, the
|
|
996
|
+
* same precedence a launch gives it.
|
|
997
|
+
*/
|
|
998
|
+
async adoptPlugin(directory) {
|
|
999
|
+
const plugin = (await discoverPlugins(this.cwd, pluginOptions(this.config))).find((one) => one.directory === directory);
|
|
1000
|
+
if (!plugin)
|
|
1001
|
+
return undefined;
|
|
1002
|
+
const known = this.plugins.findIndex((one) => one.name === plugin.name);
|
|
1003
|
+
if (known >= 0)
|
|
1004
|
+
this.plugins.splice(known, 1, plugin);
|
|
1005
|
+
else
|
|
1006
|
+
this.plugins.push(plugin);
|
|
1007
|
+
const contributed = pluginContributions([plugin]);
|
|
1008
|
+
this.config.skillDirs = [...new Set([...(this.config.skillDirs ?? []), ...contributed.skillDirs])];
|
|
1009
|
+
this.config.agentDirs = [...new Set([...(this.config.agentDirs ?? []), ...contributed.agentDirs])];
|
|
1010
|
+
await this.reloadSkills();
|
|
1011
|
+
for (const definition of contributed.subagents)
|
|
1012
|
+
this.subagents.register(definition);
|
|
1013
|
+
for (const definition of await this.markdownAgents()) {
|
|
1014
|
+
this.subagents.register(definition);
|
|
1015
|
+
}
|
|
1016
|
+
for (const [name, server] of Object.entries(contributed.mcpServers)) {
|
|
1017
|
+
if (this.config.mcpServers?.[name] || this.mcp.configFor(name))
|
|
1018
|
+
continue;
|
|
1019
|
+
this.config.mcpServers = { ...(this.config.mcpServers ?? {}), [name]: server };
|
|
1020
|
+
// One server that will not start is not a failed install; its row says
|
|
1021
|
+
// why, as any connector's does.
|
|
1022
|
+
await this.addMcpServer(name, server).catch((err) => this.logger.warn("plugin server unavailable", { server: name, error: String(err) }));
|
|
1023
|
+
}
|
|
1024
|
+
return plugin;
|
|
1025
|
+
}
|
|
1026
|
+
/** Undo `adoptPlugin` for a plugin whose directory is gone. */
|
|
1027
|
+
async dropPlugin(directory) {
|
|
1028
|
+
const index = this.plugins.findIndex((one) => one.directory === directory);
|
|
1029
|
+
if (index < 0)
|
|
1030
|
+
return;
|
|
1031
|
+
const [plugin] = this.plugins.splice(index, 1);
|
|
1032
|
+
const contributed = pluginContributions([plugin]);
|
|
1033
|
+
const before = await this.markdownAgents();
|
|
1034
|
+
this.config.skillDirs = (this.config.skillDirs ?? []).filter((dir) => !contributed.skillDirs.includes(dir));
|
|
1035
|
+
this.config.agentDirs = (this.config.agentDirs ?? []).filter((dir) => !contributed.agentDirs.includes(dir));
|
|
1036
|
+
await this.reloadSkills();
|
|
1037
|
+
const after = new Set((await this.markdownAgents()).map((one) => one.name));
|
|
1038
|
+
for (const definition of [...before, ...contributed.subagents]) {
|
|
1039
|
+
if (after.has(definition.name))
|
|
1040
|
+
continue;
|
|
1041
|
+
const registered = this.subagents.get(definition.name);
|
|
1042
|
+
if (registered)
|
|
1043
|
+
this.subagents.unregister(registered);
|
|
1044
|
+
}
|
|
1045
|
+
for (const name of Object.keys(contributed.mcpServers)) {
|
|
1046
|
+
const mine = this.config.mcpServers?.[name] === contributed.mcpServers[name];
|
|
1047
|
+
if (!mine)
|
|
1048
|
+
continue;
|
|
1049
|
+
const { [name]: _gone, ...rest } = this.config.mcpServers ?? {};
|
|
1050
|
+
this.config.mcpServers = rest;
|
|
1051
|
+
await this.removeMcpServer(name);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
878
1054
|
/** Providers declared by the catalog, for `/models` and login hints. */
|
|
879
1055
|
listCatalogProviders() {
|
|
880
1056
|
return this.providers.listCatalogProviders();
|
|
@@ -975,7 +1151,8 @@ export class Harness {
|
|
|
975
1151
|
*/
|
|
976
1152
|
async forkSession(id, opts = {}) {
|
|
977
1153
|
const forked = await Session.fork(id, {
|
|
978
|
-
model: opts.model ??
|
|
1154
|
+
model: opts.model ?? "",
|
|
1155
|
+
fallbackModel: this.config.model ?? this.defaultModel(),
|
|
979
1156
|
providers: this.providers,
|
|
980
1157
|
permissionMode: this.config.permissionMode,
|
|
981
1158
|
permissionRules: this.config.permissions,
|
|
@@ -1004,7 +1181,10 @@ export class Harness {
|
|
|
1004
1181
|
/** Reload a persisted session by id; returns undefined if it is unknown. */
|
|
1005
1182
|
async resumeSession(id, model) {
|
|
1006
1183
|
const resumed = await Session.resume(id, {
|
|
1007
|
-
|
|
1184
|
+
// Empty means "the session's own": a conversation reopens on the model
|
|
1185
|
+
// it was using, and the default is only for one whose model is gone.
|
|
1186
|
+
model: model ?? "",
|
|
1187
|
+
fallbackModel: this.config.model ?? this.defaultModel(),
|
|
1008
1188
|
providers: this.providers,
|
|
1009
1189
|
permissionMode: this.config.permissionMode,
|
|
1010
1190
|
permissionRules: this.config.permissions,
|
|
@@ -1052,7 +1232,7 @@ export class Harness {
|
|
|
1052
1232
|
worktreeLauncher: () => this.worktreeLauncher,
|
|
1053
1233
|
taskWaker: () => this.taskWaker,
|
|
1054
1234
|
equipSubagent: (sessionId, label, tools) => this.equipSubagent(sessionId, label, tools),
|
|
1055
|
-
onRunStart: () => this.trackRun(computer),
|
|
1235
|
+
onRunStart: () => this.trackRun(computer, browser, session),
|
|
1056
1236
|
onAudit: (event) => { if (this.config.audit !== false)
|
|
1057
1237
|
this.runtimeAudit.publish(event); },
|
|
1058
1238
|
...overrides,
|
|
@@ -1062,11 +1242,33 @@ export class Harness {
|
|
|
1062
1242
|
* Count a run toward shutdown's drain. Its end lets go of the windows and
|
|
1063
1243
|
* control its conversation held, so another need not wait out the lease.
|
|
1064
1244
|
*/
|
|
1065
|
-
trackRun(computer) {
|
|
1245
|
+
trackRun(computer, browser, session) {
|
|
1066
1246
|
let finish;
|
|
1067
1247
|
const done = new Promise((resolve) => { finish = resolve; });
|
|
1068
1248
|
this.activeRuns.add(done);
|
|
1069
|
-
return () => {
|
|
1249
|
+
return () => {
|
|
1250
|
+
this.activeRuns.delete(done);
|
|
1251
|
+
computer?.endTurn();
|
|
1252
|
+
// A subagent's tabs are not pictured: it has no transcript of its own.
|
|
1253
|
+
if (session)
|
|
1254
|
+
this.pictureBrowserTurn(browser, session);
|
|
1255
|
+
finish();
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* The tabs a turn left, beside the conversation (see BrowserUseSession.endTurn).
|
|
1260
|
+
* Where the turn ended is read now, before a queued message starts the next.
|
|
1261
|
+
*/
|
|
1262
|
+
pictureBrowserTurn(browser, session) {
|
|
1263
|
+
if (!browser?.endTurn)
|
|
1264
|
+
return;
|
|
1265
|
+
const sink = this.turnShotSink;
|
|
1266
|
+
const at = session.messages.length;
|
|
1267
|
+
void browser
|
|
1268
|
+
.endTurn(Boolean(sink?.enabled()))
|
|
1269
|
+
.then((shots) => { if (shots.length)
|
|
1270
|
+
sink?.save(session.id, at, shots); })
|
|
1271
|
+
.catch(() => { });
|
|
1070
1272
|
}
|
|
1071
1273
|
/**
|
|
1072
1274
|
* A subagent's own browser and computer.
|
|
@@ -1235,7 +1437,15 @@ export class Harness {
|
|
|
1235
1437
|
* picked in the composer; it wins over the first-configured guess.
|
|
1236
1438
|
*/
|
|
1237
1439
|
async generateText(system, prompt, signal, maxOutputTokens = 8_000, fallback) {
|
|
1238
|
-
|
|
1440
|
+
return this.generateWith(this.config.smallModel ?? fallback ?? this.config.model ?? this.defaultModel(), system, prompt, signal, maxOutputTokens);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* The same one reply, from a model the caller names rather than the small
|
|
1444
|
+
* one: for a chore that should cost and sound like the conversation it is
|
|
1445
|
+
* about, such as the composer's next-message suggestion.
|
|
1446
|
+
*/
|
|
1447
|
+
async generateWith(ref, system, prompt, signal, maxOutputTokens = 8_000) {
|
|
1448
|
+
const model = this.providers.resolve(ref);
|
|
1239
1449
|
const provider = this.providers.getProvider(model.provider);
|
|
1240
1450
|
const parts = [];
|
|
1241
1451
|
for await (const event of provider.stream({
|
|
@@ -1260,4 +1470,18 @@ export class Harness {
|
|
|
1260
1470
|
return first.id;
|
|
1261
1471
|
}
|
|
1262
1472
|
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Plugin discovery, with `imports` deciding for the two other tools' caches
|
|
1475
|
+
* where `plugins` does not: switching Codex off is switching off its skills,
|
|
1476
|
+
* servers and plugins together.
|
|
1477
|
+
*/
|
|
1478
|
+
function pluginOptions(config) {
|
|
1479
|
+
const claudeCode = config.plugins?.claudeCode ?? config.imports?.claudeCode;
|
|
1480
|
+
const codex = config.plugins?.codex ?? config.imports?.codex;
|
|
1481
|
+
return {
|
|
1482
|
+
...config.plugins,
|
|
1483
|
+
...(claudeCode !== undefined ? { claudeCode } : {}),
|
|
1484
|
+
...(codex !== undefined ? { codex } : {}),
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1263
1487
|
//# sourceMappingURL=harness.js.map
|