@oharness/engine 0.11.0 → 0.14.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.
Files changed (81) hide show
  1. package/dist/agent/agent.d.ts +3 -0
  2. package/dist/agent/agent.js +32 -12
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/background-usage.d.ts +34 -0
  5. package/dist/agent/background-usage.js +53 -0
  6. package/dist/agent/background-usage.js.map +1 -0
  7. package/dist/agent/background.d.ts +2 -0
  8. package/dist/agent/background.js +11 -0
  9. package/dist/agent/background.js.map +1 -1
  10. package/dist/agent/memory-backup.d.ts +1 -1
  11. package/dist/agent/memory-backup.js +1 -1
  12. package/dist/agent/memory-backup.js.map +1 -1
  13. package/dist/agent/memory-digest.d.ts +107 -0
  14. package/dist/agent/memory-digest.js +208 -0
  15. package/dist/agent/memory-digest.js.map +1 -0
  16. package/dist/agent/memory-dream.d.ts +106 -0
  17. package/dist/agent/memory-dream.js +237 -0
  18. package/dist/agent/memory-dream.js.map +1 -0
  19. package/dist/agent/memory-ledger.d.ts +154 -0
  20. package/dist/agent/memory-ledger.js +179 -0
  21. package/dist/agent/memory-ledger.js.map +1 -0
  22. package/dist/agent/memory-provider.d.ts +12 -0
  23. package/dist/agent/memory-recall.d.ts +2 -0
  24. package/dist/agent/memory-recall.js +5 -2
  25. package/dist/agent/memory-recall.js.map +1 -1
  26. package/dist/agent/memory-usage.d.ts +20 -0
  27. package/dist/agent/memory-usage.js +16 -1
  28. package/dist/agent/memory-usage.js.map +1 -1
  29. package/dist/agent/memory.d.ts +3 -0
  30. package/dist/agent/memory.js +24 -4
  31. package/dist/agent/memory.js.map +1 -1
  32. package/dist/agent/skills.d.ts +16 -3
  33. package/dist/agent/skills.js +36 -2
  34. package/dist/agent/skills.js.map +1 -1
  35. package/dist/agent/store.d.ts +7 -0
  36. package/dist/agent/store.js +14 -1
  37. package/dist/agent/store.js.map +1 -1
  38. package/dist/agent/subagent.d.ts +15 -2
  39. package/dist/agent/subagent.js +70 -16
  40. package/dist/agent/subagent.js.map +1 -1
  41. package/dist/config-files.d.ts +6 -1
  42. package/dist/config-files.js +7 -2
  43. package/dist/config-files.js.map +1 -1
  44. package/dist/config.d.ts +9 -0
  45. package/dist/config.js +2 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/foreign.d.ts +81 -0
  48. package/dist/foreign.js +198 -0
  49. package/dist/foreign.js.map +1 -0
  50. package/dist/harness.d.ts +47 -0
  51. package/dist/harness.js +186 -13
  52. package/dist/harness.js.map +1 -1
  53. package/dist/mcp/registry.d.ts +2 -0
  54. package/dist/mcp/registry.js +1 -0
  55. package/dist/mcp/registry.js.map +1 -1
  56. package/dist/mcp/registry.json +394 -1
  57. package/dist/paths.d.ts +19 -0
  58. package/dist/paths.js +38 -0
  59. package/dist/paths.js.map +1 -1
  60. package/dist/plugin-market.d.ts +57 -0
  61. package/dist/plugin-market.js +229 -0
  62. package/dist/plugin-market.js.map +1 -0
  63. package/dist/plugin-market.json +1246 -0
  64. package/dist/plugins.d.ts +49 -2
  65. package/dist/plugins.js +252 -10
  66. package/dist/plugins.js.map +1 -1
  67. package/dist/runtime-plugins/builtin/dreaming-coordinator.d.ts +244 -0
  68. package/dist/runtime-plugins/builtin/dreaming-coordinator.js +548 -0
  69. package/dist/runtime-plugins/builtin/dreaming-coordinator.js.map +1 -0
  70. package/dist/runtime-plugins/builtin/dreaming.d.ts +30 -0
  71. package/dist/runtime-plugins/builtin/dreaming.js +231 -0
  72. package/dist/runtime-plugins/builtin/dreaming.js.map +1 -0
  73. package/dist/runtime-plugins/builtin/memory.d.ts +89 -2
  74. package/dist/runtime-plugins/builtin/memory.js +186 -5
  75. package/dist/runtime-plugins/builtin/memory.js.map +1 -1
  76. package/dist/runtime-plugins/host.js +2 -0
  77. package/dist/runtime-plugins/host.js.map +1 -1
  78. package/dist/util/toml.d.ts +22 -0
  79. package/dist/util/toml.js +305 -0
  80. package/dist/util/toml.js.map +1 -0
  81. package/package.json +2 -2
@@ -0,0 +1,198 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { agentsHome, claudeHome, claudeStateFile, codexHome } from "./paths.js";
4
+ import { claudeServer } from "./plugins.js";
5
+ import { parseJson } from "./util/json.js";
6
+ import { parseToml } from "./util/toml.js";
7
+ /**
8
+ * Skill directories, split by level so each can sit beneath this harness's
9
+ * own directory at the same level: a skill in `~/.oharness/skills` overrides
10
+ * one of the same name in `~/.claude/skills`, and a project's own beats both.
11
+ */
12
+ export function foreignSkillRoots(cwd, config = {}) {
13
+ const user = [];
14
+ const project = [];
15
+ if (config.codex !== false) {
16
+ user.push({ dir: path.join(agentsHome(), "skills"), source: "codex" });
17
+ user.push({ dir: path.join(codexHome(), "skills"), source: "codex" });
18
+ project.push({ dir: path.join(cwd, ".agents", "skills"), source: "codex" });
19
+ }
20
+ if (config.claudeCode !== false) {
21
+ user.push({ dir: path.join(claudeHome(), "skills"), source: "claude" });
22
+ project.push({ dir: path.join(cwd, ".claude", "skills"), source: "claude" });
23
+ }
24
+ return { user, project };
25
+ }
26
+ /** Agent directories, on the same terms. Codex has no agent files to read. */
27
+ export function foreignAgentRoots(cwd, config = {}) {
28
+ if (config.claudeCode === false)
29
+ return { user: [], project: [] };
30
+ return {
31
+ user: [{ dir: path.join(claudeHome(), "agents"), source: "claude" }],
32
+ project: [{ dir: path.join(cwd, ".claude", "agents"), source: "claude" }],
33
+ };
34
+ }
35
+ /**
36
+ * Every server Claude Code and Codex declare for this project, in that order,
37
+ * then those inside their plugins. A plugin from either is loaded for its
38
+ * skills and agents, but its servers are never started on their own — see
39
+ * `pluginContributions` — so they are offered here like any other.
40
+ */
41
+ export async function foreignMcpServers(cwd, config = {}, plugins = []) {
42
+ return [
43
+ ...(config.claudeCode === false ? [] : await claudeMcpServers(cwd)),
44
+ ...(config.codex === false ? [] : await codexMcpServers()),
45
+ ...pluginServers(plugins.filter((plugin) => plugin.source === "claude" ? config.claudeCode !== false : plugin.source === "codex" && config.codex !== false)),
46
+ ];
47
+ }
48
+ function pluginServers(plugins) {
49
+ return plugins.flatMap((plugin) => Object.entries(plugin.mcpServers ?? {}).map(([name, config]) => ({
50
+ name,
51
+ source: plugin.source,
52
+ scope: "plugin",
53
+ plugin: plugin.name,
54
+ ...(plugin.title ? { pluginTitle: plugin.title } : {}),
55
+ file: plugin.directory,
56
+ config,
57
+ ...secretFlag(config),
58
+ })));
59
+ }
60
+ /** Find one by the key the connector list sends back. */
61
+ export async function findForeignServer(cwd, key, config = {}, plugins = []) {
62
+ return (await foreignMcpServers(cwd, config, plugins)).find((one) => one.source === key.source && one.scope === key.scope && one.name === key.name &&
63
+ (one.plugin ?? "") === (key.plugin ?? ""));
64
+ }
65
+ async function claudeMcpServers(cwd) {
66
+ const out = [];
67
+ const state = claudeStateFile();
68
+ const raw = await readJson(state);
69
+ if (isRecord(raw)) {
70
+ out.push(...fromClaude(raw.mcpServers, "user", state));
71
+ const projects = isRecord(raw.projects) ? raw.projects : {};
72
+ const here = projects[path.resolve(cwd)];
73
+ if (isRecord(here))
74
+ out.push(...fromClaude(here.mcpServers, "local", state));
75
+ }
76
+ const shared = path.join(cwd, ".mcp.json");
77
+ const checkedIn = await readJson(shared);
78
+ if (isRecord(checkedIn))
79
+ out.push(...fromClaude(checkedIn.mcpServers, "project", shared));
80
+ return out;
81
+ }
82
+ function fromClaude(servers, scope, file) {
83
+ if (!isRecord(servers))
84
+ return [];
85
+ return Object.entries(servers).map(([name, entry]) => {
86
+ const translated = claudeServer(entry);
87
+ return "config" in translated
88
+ ? { name, source: "claude", scope, file, config: translated.config, ...secretFlag(translated.config) }
89
+ : { name, source: "claude", scope, file, reason: translated.reason };
90
+ });
91
+ }
92
+ async function codexMcpServers() {
93
+ const file = path.join(codexHome(), "config.toml");
94
+ let table;
95
+ try {
96
+ table = parseToml(await readFile(file, "utf8"));
97
+ }
98
+ catch {
99
+ // Missing is the common case; a file Codex itself can read but this
100
+ // cannot is rarer, and either way there is nothing here to offer.
101
+ return [];
102
+ }
103
+ const servers = table["mcp_servers"];
104
+ if (!isTable(servers))
105
+ return [];
106
+ const out = [];
107
+ for (const [name, entry] of Object.entries(servers)) {
108
+ if (!isTable(entry))
109
+ continue;
110
+ // Switched off in Codex is a decision this list should not undo.
111
+ if (entry["enabled"] === false)
112
+ continue;
113
+ const config = codexServer(entry);
114
+ out.push(config
115
+ ? { name, source: "codex", scope: "user", file, config, ...secretFlag(config) }
116
+ : { name, source: "codex", scope: "user", file, reason: "malformed" });
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * One `[mcp_servers.<name>]` table in this harness's shape.
122
+ *
123
+ * Codex names environment variables rather than writing values in a few
124
+ * places — `bearer_token_env_var`, `env_http_headers` — and those become
125
+ * `${env:NAME}` references, so the token stays in the environment here too.
126
+ */
127
+ function codexServer(entry) {
128
+ const timeout = typeof entry["tool_timeout_sec"] === "number" ? { timeoutMs: Math.round(entry["tool_timeout_sec"] * 1000) } : {};
129
+ const allowed = strings(entry["enabled_tools"]);
130
+ const tools = allowed ? { allowedTools: allowed } : {};
131
+ if (typeof entry["command"] === "string") {
132
+ const args = strings(entry["args"]);
133
+ const env = stringTable(entry["env"]);
134
+ return {
135
+ type: "stdio",
136
+ command: entry["command"],
137
+ ...(args ? { args } : {}),
138
+ ...(env ? { env } : {}),
139
+ ...(typeof entry["cwd"] === "string" ? { cwd: entry["cwd"] } : {}),
140
+ ...tools,
141
+ ...timeout,
142
+ };
143
+ }
144
+ if (typeof entry["url"] === "string") {
145
+ const headers = { ...stringTable(entry["http_headers"]) };
146
+ for (const [header, variable] of Object.entries(stringTable(entry["env_http_headers"]) ?? {})) {
147
+ headers[header] = `\${env:${variable}}`;
148
+ }
149
+ if (typeof entry["bearer_token_env_var"] === "string") {
150
+ headers["Authorization"] = `Bearer \${env:${entry["bearer_token_env_var"]}}`;
151
+ }
152
+ return {
153
+ type: "http",
154
+ url: entry["url"],
155
+ ...(Object.keys(headers).length ? { headers } : {}),
156
+ ...tools,
157
+ ...timeout,
158
+ };
159
+ }
160
+ return undefined;
161
+ }
162
+ /**
163
+ * Whether a credential is written out rather than referenced: any header, or
164
+ * an environment variable whose name says it is one. Only those: they are
165
+ * where credentials go, and `NODE_ENV=production` is not worth a warning.
166
+ */
167
+ function secretFlag(config) {
168
+ const entries = Object.entries((config.type === "http" ? config.headers : config.env) ?? {});
169
+ const credential = (name) => config.type === "http" || /key|token|secret|pass|auth|credential/i.test(name);
170
+ const literal = (value) => value !== "" && !/^(Bearer )?\$\{(env|auth):[^}]+\}$/.test(value);
171
+ return entries.some(([name, value]) => credential(name) && literal(value)) ? { inlineSecret: true } : {};
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ async function readJson(file) {
175
+ try {
176
+ const parsed = parseJson(await readFile(file, "utf8"));
177
+ return parsed.ok ? parsed.value : undefined;
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
183
+ function strings(value) {
184
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
185
+ }
186
+ function stringTable(value) {
187
+ if (!isTable(value))
188
+ return undefined;
189
+ const out = Object.fromEntries(Object.entries(value).filter(([, v]) => typeof v === "string"));
190
+ return Object.keys(out).length ? out : undefined;
191
+ }
192
+ function isTable(value) {
193
+ return !!value && typeof value === "object" && !Array.isArray(value);
194
+ }
195
+ function isRecord(value) {
196
+ return !!value && typeof value === "object" && !Array.isArray(value);
197
+ }
198
+ //# sourceMappingURL=foreign.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foreign.js","sourceRoot":"","sources":["../src/foreign.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAiC3C;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,SAAuB,EAAE;IACtE,MAAM,IAAI,GAAkB,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAkB,EAAE,CAAC;IAClC,IAAI,MAAM,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACvE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACtE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,SAAuB,EAAE;IACtE,IAAI,MAAM,CAAC,UAAU,KAAK,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAClE,OAAO;QACL,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACpE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;KAC1E,CAAC;AACJ,CAAC;AAgCD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,SAAuB,EAAE,EACzB,UAA8B,EAAE;IAEhC,OAAO;QACL,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACnE,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,eAAe,EAAE,CAAC;QAC1D,GAAG,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CACzC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;KACnH,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,OAA2B;IAChD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAChC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAiB,EAAE,CAAC,CAAC;QAC9E,IAAI;QACJ,MAAM,EAAE,MAAM,CAAC,MAAuB;QACtC,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,MAAM,CAAC,IAAI;QACnB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,IAAI,EAAE,MAAM,CAAC,SAAS;QACtB,MAAM;QACN,GAAG,UAAU,CAAC,MAAM,CAAC;KACtB,CAAC,CAAC,CACJ,CAAC;AACJ,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,GAAqE,EACrE,SAAuB,EAAE,EACzB,UAA8B,EAAE;IAEhC,OAAO,CAAC,MAAM,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CACzD,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI;QACpF,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAC5C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;IAChC,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,IAAI,QAAQ,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,QAAQ,CAAC,SAAS,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1F,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,UAAU,CAAC,OAAgB,EAAE,KAA6B,EAAE,IAAY;IAC/E,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE;QACnD,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,QAAQ,IAAI,UAAU;YAC3B,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;YACtG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,eAAe;IAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;IACnD,IAAI,KAAgB,CAAC;IACrB,IAAI,CAAC;QACH,KAAK,GAAG,SAAS,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,kEAAkE;QAClE,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC;IACrC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,SAAS;QAC9B,iEAAiE;QACjE,IAAI,KAAK,CAAC,SAAS,CAAC,KAAK,KAAK;YAAE,SAAS;QACzC,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAClC,GAAG,CAAC,IAAI,CACN,MAAM;YACJ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,EAAE;YAC/E,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,KAAgB;IACnC,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,kBAAkB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjI,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvD,IAAI,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,OAAO;YACL,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvB,GAAG,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,KAAK;YACR,GAAG,OAAO;SACX,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,OAAO,GAA2B,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;QAClF,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC9F,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,QAAQ,GAAG,CAAC;QAC1C,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,sBAAsB,CAAC,KAAK,QAAQ,EAAE,CAAC;YACtD,OAAO,CAAC,eAAe,CAAC,GAAG,iBAAiB,KAAK,CAAC,sBAAsB,CAAC,GAAG,CAAC;QAC/E,CAAC;QACD,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC;YACjB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,GAAG,KAAK;YACR,GAAG,OAAO;SACX,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,MAAuB;IACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7F,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,wCAAwC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnH,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,IAAI,CAAC,oCAAoC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrG,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3G,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAU,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAChE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAA4B;IAC3C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7G,CAAC;AAED,SAAS,WAAW,CAAC,KAA4B;IAC/C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAA2B,CAAC;IACzH,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACnD,CAAC;AAED,SAAS,OAAO,CAAC,KAA4B;IAC3C,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC"}
package/dist/harness.d.ts CHANGED
@@ -22,6 +22,7 @@ import { Scheduler } from "./schedule/scheduler.ts";
22
22
  import type { ScheduleDefinition } from "./schedule/scheduler.ts";
23
23
  import type { McpServerStatus } from "./mcp/manager.ts";
24
24
  import { ProviderRegistry } from "./providers/registry.ts";
25
+ import type { Usage } from "./core/types.ts";
25
26
  import type { VerifyResult } from "./providers/registry.ts";
26
27
  import type { Catalog, CatalogProvider } from "./providers/catalog.ts";
27
28
  import type { EffortChoice, ModelSpec } from "./providers/types.ts";
@@ -441,6 +442,33 @@ export declare class Harness {
441
442
  * which breaks the one loop that would let the agent improve itself.
442
443
  */
443
444
  reloadSkills(): Promise<number>;
445
+ /**
446
+ * Switch one skill off or back on, now and for later launches.
447
+ *
448
+ * Off rather than deleted: most skills in the list are files another tool
449
+ * put there — Claude Code's, Codex's, a plugin's — and removing someone
450
+ * else's file to hide it from this harness would take it from that tool
451
+ * too.
452
+ */
453
+ setSkillEnabled(name: string, on: boolean): Promise<void>;
454
+ /**
455
+ * Every markdown agent this harness reads, with the dialect each was
456
+ * written in: a Claude Code plugin's names tools and models the way
457
+ * Claude Code does, and is translated on the way in.
458
+ */
459
+ private markdownAgents;
460
+ /**
461
+ * Take in a plugin that arrived while the harness was running.
462
+ *
463
+ * What `create` does for every plugin at launch, for one: its skills and
464
+ * agents join the registries, its servers connect. Without this an install
465
+ * from the menu would sit inert until a restart, which reads as a broken
466
+ * install. A server name the config already has is left to the config, the
467
+ * same precedence a launch gives it.
468
+ */
469
+ adoptPlugin(directory: string): Promise<PluginDefinition | undefined>;
470
+ /** Undo `adoptPlugin` for a plugin whose directory is gone. */
471
+ dropPlugin(directory: string): Promise<void>;
444
472
  /** Providers declared by the catalog, for `/models` and login hints. */
445
473
  listCatalogProviders(): CatalogProvider[];
446
474
  listModels(configuredOnly?: boolean): ModelSpec[];
@@ -569,6 +597,17 @@ export declare class Harness {
569
597
  * about, such as the composer's next-message suggestion.
570
598
  */
571
599
  generateWith(ref: string, system: string, prompt: string, signal?: AbortSignal, maxOutputTokens?: number): Promise<string>;
600
+ /**
601
+ * One reply from the small model, with what it cost. Given a `purpose`, the
602
+ * call is also recorded as background usage, so the usage page can show
603
+ * what chores outside any conversation spend.
604
+ */
605
+ generateTextMetered(system: string, prompt: string, options?: {
606
+ signal?: AbortSignal;
607
+ purpose?: string;
608
+ maxOutputTokens?: number;
609
+ }): Promise<MeteredReply>;
610
+ private generateMetered;
572
611
  /**
573
612
  * First model whose provider has credentials. Only the cheap synchronous
574
613
  * check runs here — a subscription-only provider is picked up once the
@@ -576,3 +615,11 @@ export declare class Harness {
576
615
  */
577
616
  private defaultModel;
578
617
  }
618
+ /** One background reply and what it cost. */
619
+ export interface MeteredReply {
620
+ text: string;
621
+ /** The model id that answered. */
622
+ model: string;
623
+ usage: Usage;
624
+ costUsd: number;
625
+ }
package/dist/harness.js CHANGED
@@ -6,12 +6,14 @@ 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";
9
10
  import { claudeAgent } from "./acp/claude.js";
10
11
  import { SkillRegistry } from "./agent/skills.js";
11
12
  import { SettingsStore } from "./agent/settings.js";
12
13
  import { createPolicy } from "./agent/workspace.js";
13
14
  import { PluginExtensions } from "./runtime-plugins/extensions.js";
14
- import { createMemoryPlugin, MemoryService } from "./runtime-plugins/builtin/memory.js";
15
+ import { createMemoryPlugin, MemoryBridge, MemoryService } from "./runtime-plugins/builtin/memory.js";
16
+ import { createDreamingPlugin } from "./runtime-plugins/builtin/dreaming.js";
15
17
  import { projectMemoryDirectory, resolveMemoryDirectories } from "./runtime-plugins/builtin/memory-migrate.js";
16
18
  import { resolveMemoryPolicy } from "./agent/memory-limits.js";
17
19
  import { siteUrl } from "./auth/providers/account.js";
@@ -30,8 +32,10 @@ import { loadUserConfig, updateUserConfig } from "./config.js";
30
32
  import { parseCron } from "./schedule/cron.js";
31
33
  import { createRoutineTool } from "./tools/builtin/routine.js";
32
34
  import { ProviderRegistry } from "./providers/registry.js";
33
- import { userMessage } from "./core/types.js";
35
+ import { emptyUsage, userMessage } from "./core/types.js";
36
+ import { BackgroundUsage } from "./agent/background-usage.js";
34
37
  import { loadCatalog, userCatalogPath } from "./providers/catalog.js";
38
+ import { estimateCost } from "./providers/types.js";
35
39
  import { builtinTools } from "./tools/builtin/index.js";
36
40
  import { createBrowserUsePlugin } from "./browser/plugin.js";
37
41
  import { ToolRegistry } from "./tools/registry.js";
@@ -205,7 +209,14 @@ export class Harness {
205
209
  ...(config.skillDirs ?? []),
206
210
  ...(config.agentDirs ?? []),
207
211
  ],
208
- ...(config.readableRoots !== undefined ? { readableRoots: config.readableRoots } : {}),
212
+ // Claude Code's and Codex's skill and agent directories are read, so
213
+ // the agent can open a skill's scripts/, but not written: those belong
214
+ // to the user's other tools, not to anything this harness installed.
215
+ readableRoots: [
216
+ ...(config.readableRoots ?? []),
217
+ ...Object.values(foreignSkillRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
218
+ ...Object.values(foreignAgentRoots(this.cwd, config.imports)).flat().map((root) => root.dir),
219
+ ],
209
220
  });
210
221
  this.mcp = new McpManager(this.logger, this.auth.store, (id) => this.auth.secret(id));
211
222
  this.store = new SessionStore(config.sessionDir);
@@ -241,7 +252,7 @@ export class Harness {
241
252
  : (await ensureProjectManifest(cwd)).id;
242
253
  opts = { ...opts, cwd };
243
254
  const baseConfig = opts.config ?? (await loadConfig(cwd));
244
- const plugins = await discoverPlugins(cwd, baseConfig.plugins);
255
+ const plugins = await discoverPlugins(cwd, pluginOptions(baseConfig));
245
256
  // Explicit user/project config is applied after plugins, so it can always
246
257
  // tighten policy or replace a plugin's MCP server declaration.
247
258
  const config = mergeConfig(pluginContributions(plugins), baseConfig);
@@ -269,7 +280,8 @@ export class Harness {
269
280
  })
270
281
  .catch((err) => harness.logger.warn("session pruning failed", { error: String(err) }));
271
282
  }
272
- await harness.skills.discover(cwd, config.skillDirs ?? []);
283
+ harness.skills.setDisabled(config.disabledSkills ?? []);
284
+ await harness.skills.discover(cwd, config.skillDirs ?? [], config.imports ?? {});
273
285
  // Claude Agent, when the user turned it on and this machine has their
274
286
  // own Claude Code to hand work to. Before the markdown ones, so a
275
287
  // project can redefine it.
@@ -277,7 +289,7 @@ export class Harness {
277
289
  await harness.registerClaudeAgent();
278
290
  // Markdown definitions layer over the JSON ones, so a repo can drop in an
279
291
  // agent file without editing config.
280
- for (const definition of await loadMarkdownSubagents(cwd, config.agentDirs ?? [])) {
292
+ for (const definition of await harness.markdownAgents()) {
281
293
  harness.subagents.register(definition);
282
294
  }
283
295
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
@@ -301,7 +313,9 @@ export class Harness {
301
313
  const memoryDirs = memoryEnabled
302
314
  ? await resolveMemoryDirectories({ cwd, ...(memoryConfigured ? { configured: memoryConfigured } : {}), logger: harness.logger })
303
315
  : { project: memoryConfigured ? path.resolve(cwd, memoryConfigured) : projectMemoryDirectory(cwd) };
304
- const memory = createMemoryPlugin({ cwd, projectId: stableProjectId, directory: memoryDirs.project,
316
+ // How dreaming reaches memory, and adds to its page: plugins cannot see each other.
317
+ const memoryBridge = new MemoryBridge();
318
+ const memory = createMemoryPlugin({ cwd, projectId: stableProjectId, directory: memoryDirs.project, bridge: memoryBridge,
305
319
  ...(memoryDirs.user ? { userDirectory: memoryDirs.user } : {}),
306
320
  policy: resolveMemoryPolicy(config.memory),
307
321
  identity: async () => {
@@ -333,7 +347,32 @@ export class Harness {
333
347
  * to cost. The plugin wraps all three.
334
348
  */
335
349
  const studio = createStudioPlugin({ access: () => mediaAccess(harness.auth) });
336
- await harness.pluginHost.boot([{ plugin: memory, builtin: true }, { plugin: studio, required: false, builtin: true }, ...platform, ...(opts.runtimePlugins ?? [])], config.runtimePlugins);
350
+ /**
351
+ * Background memory maintenance, built on memory and separate from it,
352
+ * so `{ "id": "dreaming", "enabled": false }` removes it and leaves
353
+ * memory as it was. Booted after memory, whose services it reads.
354
+ * Not for an ephemeral harness: it has no conversations to read.
355
+ */
356
+ const dreaming = opts.ephemeral ? [] : [{
357
+ plugin: createDreamingPlugin({
358
+ projectId: stableProjectId,
359
+ sessions: {
360
+ list: () => harness.store.list(500, { projectId: stableProjectId }),
361
+ records: (id) => harness.store.recordsOf(id),
362
+ archive: async (id) => (await harness.store.readArchive(id)).map((entry) => entry.message),
363
+ device: () => harness.store.localDevice(),
364
+ },
365
+ memory: () => ({
366
+ ...(harness.memoryService ? { project: harness.memoryService } : {}),
367
+ ...(harness.memoryUserService ? { user: harness.memoryUserService } : {}),
368
+ }),
369
+ // Metered and tagged, so what dreaming spends shows on its page and the usage page.
370
+ generate: (system, prompt, signal) => harness.generateTextMetered(system, prompt, { ...(signal ? { signal } : {}), purpose: "dreaming" }),
371
+ bridge: memoryBridge,
372
+ }),
373
+ required: false, builtin: true,
374
+ }];
375
+ await harness.pluginHost.boot([{ plugin: memory, builtin: true }, ...dreaming, { plugin: studio, required: false, builtin: true }, ...platform, ...(opts.runtimePlugins ?? [])], config.runtimePlugins);
337
376
  // Awaited, though it touches no network: deciding whether to mirror reads
338
377
  // config and asks git for the project id, and both are local. Leaving it
339
378
  // unawaited made `syncStatus()` lie for the first few milliseconds of
@@ -939,9 +978,109 @@ export class Harness {
939
978
  * which breaks the one loop that would let the agent improve itself.
940
979
  */
941
980
  async reloadSkills() {
942
- await this.skills.discover(this.cwd, this.config.skillDirs ?? []);
981
+ await this.skills.discover(this.cwd, this.config.skillDirs ?? [], this.config.imports ?? {});
943
982
  return this.skills.size;
944
983
  }
984
+ /**
985
+ * Switch one skill off or back on, now and for later launches.
986
+ *
987
+ * Off rather than deleted: most skills in the list are files another tool
988
+ * put there — Claude Code's, Codex's, a plugin's — and removing someone
989
+ * else's file to hide it from this harness would take it from that tool
990
+ * too.
991
+ */
992
+ async setSkillEnabled(name, on) {
993
+ await updateUserConfig((current) => {
994
+ const off = new Set(current.disabledSkills ?? []);
995
+ if (on)
996
+ off.delete(name);
997
+ else
998
+ off.add(name);
999
+ return { ...current, disabledSkills: [...off].sort() };
1000
+ });
1001
+ const off = new Set(this.config.disabledSkills ?? []);
1002
+ if (on)
1003
+ off.delete(name);
1004
+ else
1005
+ off.add(name);
1006
+ this.config.disabledSkills = [...off];
1007
+ this.skills.setDisabled(off);
1008
+ }
1009
+ /**
1010
+ * Every markdown agent this harness reads, with the dialect each was
1011
+ * written in: a Claude Code plugin's names tools and models the way
1012
+ * Claude Code does, and is translated on the way in.
1013
+ */
1014
+ async markdownAgents() {
1015
+ const claude = new Set(this.plugins.filter((plugin) => plugin.claudeFormat).map((plugin) => plugin.agentDir));
1016
+ const roots = (this.config.agentDirs ?? []).map((dir) => claude.has(dir) ? { dir, dialect: "claude" } : { dir });
1017
+ return loadMarkdownSubagents(this.cwd, roots, this.config.imports ?? {});
1018
+ }
1019
+ /**
1020
+ * Take in a plugin that arrived while the harness was running.
1021
+ *
1022
+ * What `create` does for every plugin at launch, for one: its skills and
1023
+ * agents join the registries, its servers connect. Without this an install
1024
+ * from the menu would sit inert until a restart, which reads as a broken
1025
+ * install. A server name the config already has is left to the config, the
1026
+ * same precedence a launch gives it.
1027
+ */
1028
+ async adoptPlugin(directory) {
1029
+ const plugin = (await discoverPlugins(this.cwd, pluginOptions(this.config))).find((one) => one.directory === directory);
1030
+ if (!plugin)
1031
+ return undefined;
1032
+ const known = this.plugins.findIndex((one) => one.name === plugin.name);
1033
+ if (known >= 0)
1034
+ this.plugins.splice(known, 1, plugin);
1035
+ else
1036
+ this.plugins.push(plugin);
1037
+ const contributed = pluginContributions([plugin]);
1038
+ this.config.skillDirs = [...new Set([...(this.config.skillDirs ?? []), ...contributed.skillDirs])];
1039
+ this.config.agentDirs = [...new Set([...(this.config.agentDirs ?? []), ...contributed.agentDirs])];
1040
+ await this.reloadSkills();
1041
+ for (const definition of contributed.subagents)
1042
+ this.subagents.register(definition);
1043
+ for (const definition of await this.markdownAgents()) {
1044
+ this.subagents.register(definition);
1045
+ }
1046
+ for (const [name, server] of Object.entries(contributed.mcpServers)) {
1047
+ if (this.config.mcpServers?.[name] || this.mcp.configFor(name))
1048
+ continue;
1049
+ this.config.mcpServers = { ...(this.config.mcpServers ?? {}), [name]: server };
1050
+ // One server that will not start is not a failed install; its row says
1051
+ // why, as any connector's does.
1052
+ await this.addMcpServer(name, server).catch((err) => this.logger.warn("plugin server unavailable", { server: name, error: String(err) }));
1053
+ }
1054
+ return plugin;
1055
+ }
1056
+ /** Undo `adoptPlugin` for a plugin whose directory is gone. */
1057
+ async dropPlugin(directory) {
1058
+ const index = this.plugins.findIndex((one) => one.directory === directory);
1059
+ if (index < 0)
1060
+ return;
1061
+ const [plugin] = this.plugins.splice(index, 1);
1062
+ const contributed = pluginContributions([plugin]);
1063
+ const before = await this.markdownAgents();
1064
+ this.config.skillDirs = (this.config.skillDirs ?? []).filter((dir) => !contributed.skillDirs.includes(dir));
1065
+ this.config.agentDirs = (this.config.agentDirs ?? []).filter((dir) => !contributed.agentDirs.includes(dir));
1066
+ await this.reloadSkills();
1067
+ const after = new Set((await this.markdownAgents()).map((one) => one.name));
1068
+ for (const definition of [...before, ...contributed.subagents]) {
1069
+ if (after.has(definition.name))
1070
+ continue;
1071
+ const registered = this.subagents.get(definition.name);
1072
+ if (registered)
1073
+ this.subagents.unregister(registered);
1074
+ }
1075
+ for (const name of Object.keys(contributed.mcpServers)) {
1076
+ const mine = this.config.mcpServers?.[name] === contributed.mcpServers[name];
1077
+ if (!mine)
1078
+ continue;
1079
+ const { [name]: _gone, ...rest } = this.config.mcpServers ?? {};
1080
+ this.config.mcpServers = rest;
1081
+ await this.removeMcpServer(name);
1082
+ }
1083
+ }
945
1084
  /** Providers declared by the catalog, for `/models` and login hints. */
946
1085
  listCatalogProviders() {
947
1086
  return this.providers.listCatalogProviders();
@@ -1336,17 +1475,37 @@ export class Harness {
1336
1475
  * about, such as the composer's next-message suggestion.
1337
1476
  */
1338
1477
  async generateWith(ref, system, prompt, signal, maxOutputTokens = 8_000) {
1478
+ return (await this.generateMetered(ref, system, prompt, { ...(signal ? { signal } : {}), maxOutputTokens })).text;
1479
+ }
1480
+ /**
1481
+ * One reply from the small model, with what it cost. Given a `purpose`, the
1482
+ * call is also recorded as background usage, so the usage page can show
1483
+ * what chores outside any conversation spend.
1484
+ */
1485
+ async generateTextMetered(system, prompt, options = {}) {
1486
+ return this.generateMetered(this.config.smallModel ?? this.config.model ?? this.defaultModel(), system, prompt, options);
1487
+ }
1488
+ async generateMetered(ref, system, prompt, options) {
1339
1489
  const model = this.providers.resolve(ref);
1340
1490
  const provider = this.providers.getProvider(model.provider);
1341
1491
  const parts = [];
1492
+ let usage = emptyUsage();
1342
1493
  for await (const event of provider.stream({
1343
1494
  model, system, messages: [userMessage(prompt)],
1344
- maxOutputTokens: Math.min(maxOutputTokens, model.maxOutputTokens), thinking: { enabled: false, effort: "low" },
1345
- ...(signal ? { signal } : {}),
1346
- }))
1495
+ maxOutputTokens: Math.min(options.maxOutputTokens ?? 8_000, model.maxOutputTokens), thinking: { enabled: false, effort: "low" },
1496
+ ...(options.signal ? { signal: options.signal } : {}),
1497
+ })) {
1347
1498
  if (event.type === "text_delta")
1348
1499
  parts.push(event.text);
1349
- return parts.join("");
1500
+ // The end event carries the call's total; a provider that only streams
1501
+ // usage updates leaves the last of them as the total.
1502
+ else if (event.type === "usage" || event.type === "end")
1503
+ usage = event.usage ?? usage;
1504
+ }
1505
+ const costUsd = estimateCost(model, usage);
1506
+ if (options.purpose)
1507
+ await BackgroundUsage.shared.record(options.purpose, model.id, usage, costUsd).catch(() => { });
1508
+ return { text: parts.join(""), model: model.id, usage, costUsd };
1350
1509
  }
1351
1510
  /**
1352
1511
  * First model whose provider has credentials. Only the cheap synchronous
@@ -1361,4 +1520,18 @@ export class Harness {
1361
1520
  return first.id;
1362
1521
  }
1363
1522
  }
1523
+ /**
1524
+ * Plugin discovery, with `imports` deciding for the two other tools' caches
1525
+ * where `plugins` does not: switching Codex off is switching off its skills,
1526
+ * servers and plugins together.
1527
+ */
1528
+ function pluginOptions(config) {
1529
+ const claudeCode = config.plugins?.claudeCode ?? config.imports?.claudeCode;
1530
+ const codex = config.plugins?.codex ?? config.imports?.codex;
1531
+ return {
1532
+ ...config.plugins,
1533
+ ...(claudeCode !== undefined ? { claudeCode } : {}),
1534
+ ...(codex !== undefined ? { codex } : {}),
1535
+ };
1536
+ }
1364
1537
  //# sourceMappingURL=harness.js.map