thoth-agents 0.11.0 → 0.13.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/README.md +16 -5
- package/dist/agents/agent-config.d.ts +13 -0
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/{chunk-I5TXGNSG.js → chunk-5MYXVRZZ.js} +634 -366
- package/dist/chunk-6O4GKDS7.js +207 -0
- package/dist/{chunk-RWQPMWEM.js → chunk-JLU3FSBT.js} +17 -2
- package/dist/chunk-MIDGZE34.js +107 -0
- package/dist/{chunk-HACUJFKI.js → chunk-NOLDFT7O.js} +4 -230
- package/dist/{chunk-5WPP6IY7.js → chunk-VVYAQTYE.js} +2 -2
- package/dist/{chunk-DNPKVFOB.js → chunk-YCWGTDFE.js} +604 -315
- package/dist/cli/config-io.d.ts +2 -2
- package/dist/cli/index.js +94 -41
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/opencode-profiles.d.ts +10 -0
- package/dist/cli/opencode-skill-consent.d.ts +5 -0
- package/dist/cli/operations/opencode.d.ts +2 -1
- package/dist/cli/operations/types.d.ts +1 -0
- package/dist/cli/owned-skills.d.ts +16 -0
- package/dist/cli/paths.d.ts +1 -1
- package/dist/cli/providers.d.ts +1 -1
- package/dist/cli/system.d.ts +3 -1
- package/dist/cli/thoth-mem-install.d.ts +10 -10
- package/dist/cli/tui/index.js +53 -5
- package/dist/cli/tui/operations.d.ts +2 -0
- package/dist/cli/types.d.ts +2 -4
- package/dist/config/loader.d.ts +2 -5
- package/dist/config/schema.d.ts +0 -53
- package/dist/harness/adapters/opencode.d.ts +1 -1
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -8
- package/dist/hooks/foreground-fallback/index.d.ts +14 -69
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/json-error-recovery/hook.d.ts +2 -15
- package/dist/hooks/oracle-gate-verdict/hook.d.ts +4 -11
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1603 -1408
- package/dist/opencode/agents.d.ts +3 -0
- package/dist/opencode/command.d.ts +3 -0
- package/dist/opencode/context.d.ts +36 -0
- package/dist/opencode/delegation-hooks.d.ts +3 -0
- package/dist/opencode/mcp.d.ts +3 -0
- package/dist/opencode/permission-rules.d.ts +9 -0
- package/dist/opencode/permissions.d.ts +3 -0
- package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
- package/dist/opencode/profile-rpc.d.ts +688 -0
- package/dist/opencode/profile-runtime.d.ts +3 -0
- package/dist/opencode/profile-sessions.d.ts +18 -0
- package/dist/opencode/profiles/editor.d.ts +44 -0
- package/dist/opencode/profiles/index.d.ts +18 -0
- package/dist/opencode/profiles/resolver.d.ts +12 -0
- package/dist/opencode/profiles/store.d.ts +63 -0
- package/dist/opencode/profiles/types.d.ts +26 -0
- package/dist/opencode/session-hooks.d.ts +3 -0
- package/dist/opencode/session-state.d.ts +39 -0
- package/dist/opencode/skills.d.ts +3 -0
- package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
- package/dist/opencode/tool-overrides/index.d.ts +5 -0
- package/dist/opencode/tool-overrides/store.d.ts +38 -0
- package/dist/opencode/tool-overrides/types.d.ts +32 -0
- package/dist/opencode/tool-overrides/validation.d.ts +5 -0
- package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
- package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
- package/dist/opencode/tool-rpc.d.ts +428 -0
- package/dist/opencode/tools.d.ts +3 -0
- package/dist/pi.js +3 -3
- package/dist/tools/ast-grep/cli.d.ts +1 -0
- package/dist/tools/ast-grep/index.d.ts +108 -2
- package/dist/tools/ast-grep/tools.d.ts +107 -3
- package/dist/tools/define-tool.d.ts +23 -0
- package/dist/tools/lsp/tools.d.ts +97 -5
- package/dist/tui/fixtures.d.ts +2 -0
- package/dist/tui/flow.d.ts +16 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +1384 -0
- package/dist/tui/model.d.ts +84 -0
- package/dist/tui/tools/fixtures.d.ts +3 -0
- package/dist/tui/tools/flow.d.ts +11 -0
- package/dist/tui/tools/model.d.ts +97 -0
- package/dist/utils/index.d.ts +0 -1
- package/package.json +18 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/skills/thoth-sdd/SKILL.md +8 -2
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/skills/thoth-sdd/SKILL.md +8 -2
- package/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/thoth-agents.schema.json +0 -96
- package/dist/chunk-62L3ORXN.js +0 -30
- package/dist/delegation/project-id.d.ts +0 -10
- package/dist/hooks/auto-update-checker/checker.d.ts +0 -23
- package/dist/hooks/auto-update-checker/constants.d.ts +0 -11
- package/dist/hooks/auto-update-checker/index.d.ts +0 -17
- package/dist/hooks/auto-update-checker/types.d.ts +0 -22
- package/dist/utils/tmux-session-manager.d.ts +0 -63
- package/dist/utils/tmux.d.ts +0 -32
package/dist/index.js
CHANGED
|
@@ -1,28 +1,320 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatGateVerdict,
|
|
3
3
|
summarizeGateResult
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JLU3FSBT.js";
|
|
5
|
+
import {
|
|
6
|
+
profileRpc,
|
|
7
|
+
toolRpc
|
|
8
|
+
} from "./chunk-6O4GKDS7.js";
|
|
5
9
|
import {
|
|
6
10
|
createAgents,
|
|
7
|
-
renderOpenCodeAgentConfigs
|
|
8
|
-
|
|
9
|
-
spawnSync
|
|
10
|
-
} from "./chunk-HACUJFKI.js";
|
|
11
|
+
renderOpenCodeAgentConfigs
|
|
12
|
+
} from "./chunk-NOLDFT7O.js";
|
|
11
13
|
import {
|
|
12
14
|
context7,
|
|
13
15
|
exa,
|
|
14
|
-
grep_app
|
|
15
|
-
|
|
16
|
+
grep_app,
|
|
17
|
+
spawn,
|
|
18
|
+
spawnSync
|
|
19
|
+
} from "./chunk-MIDGZE34.js";
|
|
16
20
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
ModelProfileStore,
|
|
22
|
+
PROFILE_ROLES,
|
|
23
|
+
ProfileConflictError,
|
|
24
|
+
formatModelString,
|
|
20
25
|
loadPluginConfig,
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
mergeProfileModelChain,
|
|
27
|
+
resolveRoleProfile,
|
|
28
|
+
validateProfileName
|
|
29
|
+
} from "./chunk-YCWGTDFE.js";
|
|
23
30
|
|
|
24
|
-
// src/
|
|
25
|
-
|
|
31
|
+
// src/opencode/permission-rules.ts
|
|
32
|
+
var ACTIONS = {
|
|
33
|
+
read: "read",
|
|
34
|
+
glob: "glob",
|
|
35
|
+
grep: "grep",
|
|
36
|
+
list: "read",
|
|
37
|
+
codesearch: "read",
|
|
38
|
+
lsp: "read",
|
|
39
|
+
edit: "edit",
|
|
40
|
+
write: "edit",
|
|
41
|
+
patch: "edit",
|
|
42
|
+
bash: "shell",
|
|
43
|
+
task: "subagent",
|
|
44
|
+
webfetch: "webfetch",
|
|
45
|
+
exa: "exa_*",
|
|
46
|
+
external_directory: "external_directory",
|
|
47
|
+
skill: "skill",
|
|
48
|
+
question: "question",
|
|
49
|
+
todowrite: "todowrite"
|
|
50
|
+
};
|
|
51
|
+
var RESTRICTION = { allow: 0, ask: 1, deny: 2 };
|
|
52
|
+
function specificity(rule) {
|
|
53
|
+
return Number(rule.action !== "*") + 2 * Number(rule.resource !== "*");
|
|
54
|
+
}
|
|
55
|
+
function translatePermissionRules(permission) {
|
|
56
|
+
if (typeof permission === "string") {
|
|
57
|
+
return [{ action: "*", resource: "*", effect: permission }];
|
|
58
|
+
}
|
|
59
|
+
const rules = [];
|
|
60
|
+
for (const [key, value] of Object.entries(permission ?? {})) {
|
|
61
|
+
if (!value) continue;
|
|
62
|
+
const action = ACTIONS[key] ?? key;
|
|
63
|
+
const entries = typeof value === "string" ? [["*", value]] : Object.entries(value);
|
|
64
|
+
for (const [resource, effect] of entries) {
|
|
65
|
+
const existing = rules.find(
|
|
66
|
+
(rule) => rule.action === action && rule.resource === resource
|
|
67
|
+
);
|
|
68
|
+
if (!existing) rules.push({ action, resource, effect });
|
|
69
|
+
else if (RESTRICTION[effect] > RESTRICTION[existing.effect])
|
|
70
|
+
existing.effect = effect;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [
|
|
74
|
+
{ action: "*", resource: "*", effect: "ask" },
|
|
75
|
+
...rules.sort((a, b) => specificity(a) - specificity(b))
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/opencode/tool-overrides/wildcard.ts
|
|
80
|
+
function matchAction(input, pattern, platform = process.platform) {
|
|
81
|
+
const normalized = input.replaceAll("\\", "/");
|
|
82
|
+
let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
83
|
+
if (escaped.endsWith(" .*")) escaped = `${escaped.slice(0, -3)}( .*)?`;
|
|
84
|
+
return new RegExp(`^${escaped}$`, platform === "win32" ? "si" : "s").test(
|
|
85
|
+
normalized
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function countActionMatches(pattern, actions, platform = process.platform) {
|
|
89
|
+
return actions.filter((action) => matchAction(action, pattern, platform)).length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/opencode/tool-overrides/evaluator.ts
|
|
93
|
+
function appendOverrideRules(overrides) {
|
|
94
|
+
if (!overrides.length) return [];
|
|
95
|
+
return [
|
|
96
|
+
...overrides.map((override) => ({ ...override, resource: "*" })),
|
|
97
|
+
{ action: "subagent", resource: "*", effect: "deny" }
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
function evaluateAction(base, effective, action, platform = process.platform) {
|
|
101
|
+
let result = {
|
|
102
|
+
effect: "ask",
|
|
103
|
+
origin: "default"
|
|
104
|
+
};
|
|
105
|
+
for (const rule of base) {
|
|
106
|
+
if (matchAction(action, rule.action, platform) && matchAction("*", rule.resource, platform))
|
|
107
|
+
result = { effect: rule.effect, origin: "default" };
|
|
108
|
+
}
|
|
109
|
+
if (effective) {
|
|
110
|
+
const rules = appendOverrideRules(effective.overrides);
|
|
111
|
+
for (const [index, rule] of rules.entries()) {
|
|
112
|
+
if (matchAction(action, rule.action, platform))
|
|
113
|
+
result = {
|
|
114
|
+
effect: rule.effect,
|
|
115
|
+
origin: index === rules.length - 1 ? "default" : effective.origin
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/opencode/tool-overrides/store.ts
|
|
123
|
+
import { createHash, randomUUID } from "crypto";
|
|
124
|
+
import fs from "fs";
|
|
125
|
+
import os from "os";
|
|
126
|
+
import path from "path";
|
|
127
|
+
|
|
128
|
+
// src/opencode/tool-overrides/types.ts
|
|
129
|
+
var SPECIALIST_ROLES = PROFILE_ROLES.filter(
|
|
130
|
+
(role) => role !== "orchestrator"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// src/opencode/tool-overrides/validation.ts
|
|
134
|
+
function object(value) {
|
|
135
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
function parseActionOverrides(value) {
|
|
138
|
+
if (!Array.isArray(value)) throw new Error("Expected an overrides array");
|
|
139
|
+
return value.map((entry) => {
|
|
140
|
+
if (!object(entry) || typeof entry.action !== "string" || !entry.action.trim())
|
|
141
|
+
throw new Error("Action must be a non-empty pattern");
|
|
142
|
+
if (entry.effect !== "allow" && entry.effect !== "ask" && entry.effect !== "deny")
|
|
143
|
+
throw new Error("Effect must be allow, ask or deny");
|
|
144
|
+
return { action: entry.action.trim(), effect: entry.effect };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function validateSpecialistRole(role) {
|
|
148
|
+
const canonical = SPECIALIST_ROLES.find((name) => name === role);
|
|
149
|
+
if (!canonical) throw new Error(`Unknown specialist role: ${role}`);
|
|
150
|
+
return canonical;
|
|
151
|
+
}
|
|
152
|
+
function parseToolsDocument(value, warnings = []) {
|
|
153
|
+
if (!object(value) || value.version !== 1 || !object(value.roles))
|
|
154
|
+
throw new Error("Expected version 1 and a roles object");
|
|
155
|
+
const roles = {};
|
|
156
|
+
for (const [role, entry] of Object.entries(value.roles)) {
|
|
157
|
+
const canonical = SPECIALIST_ROLES.find((name) => name === role);
|
|
158
|
+
if (!canonical) {
|
|
159
|
+
warnings.push(`Unknown specialist role ignored: ${role}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!object(entry)) throw new Error(`Invalid overrides for ${role}`);
|
|
163
|
+
roles[canonical] = { overrides: parseActionOverrides(entry.overrides) };
|
|
164
|
+
}
|
|
165
|
+
return { version: 1, roles };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/opencode/tool-overrides/store.ts
|
|
169
|
+
function toolsOverridePaths(options) {
|
|
170
|
+
if (options.paths) return { ...options.paths };
|
|
171
|
+
const env = options.env ?? process.env;
|
|
172
|
+
const configDir = options.configDir ?? (env.OPENCODE_CONFIG_DIR?.trim() || path.join(
|
|
173
|
+
env.XDG_CONFIG_HOME || path.join(options.homeDir ?? os.homedir(), ".config"),
|
|
174
|
+
"opencode"
|
|
175
|
+
));
|
|
176
|
+
return {
|
|
177
|
+
global: path.join(configDir, "thoth", "tools.json"),
|
|
178
|
+
project: path.join(options.cwd, ".opencode", "thoth", "tools.json")
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
var emptyDocument = () => ({ version: 1, roles: {} });
|
|
182
|
+
var revisionOf = (raw) => raw === null ? "missing" : createHash("sha256").update(raw).digest("hex");
|
|
183
|
+
var ToolsConflictError = class extends Error {
|
|
184
|
+
constructor(scope) {
|
|
185
|
+
super(`Tools changed on disk: ${scope}; reload before saving.`);
|
|
186
|
+
this.scope = scope;
|
|
187
|
+
this.name = "ToolsConflictError";
|
|
188
|
+
}
|
|
189
|
+
scope;
|
|
190
|
+
code = "TOOLS_CONFLICT";
|
|
191
|
+
};
|
|
192
|
+
var ToolsOverrideStore = class {
|
|
193
|
+
constructor(options) {
|
|
194
|
+
this.options = options;
|
|
195
|
+
this.paths = toolsOverridePaths(options);
|
|
196
|
+
}
|
|
197
|
+
options;
|
|
198
|
+
paths;
|
|
199
|
+
raw(scope) {
|
|
200
|
+
try {
|
|
201
|
+
return fs.readFileSync(this.paths[scope], "utf8");
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === "ENOENT") return null;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
read(scope) {
|
|
208
|
+
const warnings = [];
|
|
209
|
+
let raw = null;
|
|
210
|
+
let document = emptyDocument();
|
|
211
|
+
try {
|
|
212
|
+
raw = this.raw(scope);
|
|
213
|
+
if (raw !== null)
|
|
214
|
+
document = parseToolsDocument(JSON.parse(raw), warnings);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
warnings.push(`Cannot read tools ${this.paths[scope]}: ${String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
for (const warning of warnings) this.options.warn?.(warning);
|
|
219
|
+
return { document, revision: revisionOf(raw), warnings };
|
|
220
|
+
}
|
|
221
|
+
resolve() {
|
|
222
|
+
const global = this.read("global");
|
|
223
|
+
const project = this.read("project");
|
|
224
|
+
const effective = {};
|
|
225
|
+
for (const role of SPECIALIST_ROLES) {
|
|
226
|
+
const origin = Object.hasOwn(project.document.roles, role) ? "project" : "global";
|
|
227
|
+
const entry = (origin === "project" ? project : global).document.roles[role];
|
|
228
|
+
if (entry) effective[role] = { ...entry, origin };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
global,
|
|
232
|
+
project,
|
|
233
|
+
effective,
|
|
234
|
+
warnings: [...global.warnings, ...project.warnings]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
mutate(scope, role, baseRevision, update) {
|
|
238
|
+
validateSpecialistRole(role);
|
|
239
|
+
const raw = this.raw(scope);
|
|
240
|
+
if (revisionOf(raw) !== baseRevision) throw new ToolsConflictError(scope);
|
|
241
|
+
const document = raw === null ? emptyDocument() : parseToolsDocument(JSON.parse(raw));
|
|
242
|
+
update(document);
|
|
243
|
+
const file = this.paths[scope];
|
|
244
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
245
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
246
|
+
try {
|
|
247
|
+
fs.writeFileSync(temp, `${JSON.stringify(document, null, 2)}
|
|
248
|
+
`, {
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
flag: "wx"
|
|
251
|
+
});
|
|
252
|
+
if (revisionOf(this.raw(scope)) !== baseRevision)
|
|
253
|
+
throw new ToolsConflictError(scope);
|
|
254
|
+
fs.renameSync(temp, file);
|
|
255
|
+
} finally {
|
|
256
|
+
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
257
|
+
}
|
|
258
|
+
return this.read(scope);
|
|
259
|
+
}
|
|
260
|
+
/** Save ordered rules losslessly, including duplicates; other roles survive. */
|
|
261
|
+
save(scope, role, overrides, baseRevision) {
|
|
262
|
+
const next = parseActionOverrides(overrides);
|
|
263
|
+
return this.mutate(scope, role, baseRevision, (document) => {
|
|
264
|
+
document.roles[role] = { overrides: next };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Remove a scope's role entry (project reset resumes global inheritance). */
|
|
268
|
+
reset(scope, role, baseRevision) {
|
|
269
|
+
return this.mutate(scope, role, baseRevision, (document) => {
|
|
270
|
+
delete document.roles[role];
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/opencode/agents.ts
|
|
276
|
+
var ROLE_COLORS = {
|
|
277
|
+
orchestrator: "#7c3aed",
|
|
278
|
+
explorer: "#0ea5e9",
|
|
279
|
+
librarian: "#0284c7",
|
|
280
|
+
oracle: "#f59e0b",
|
|
281
|
+
designer: "#ec4899",
|
|
282
|
+
worker: "#10b981"
|
|
283
|
+
};
|
|
284
|
+
async function registerAgents(ctx, deps) {
|
|
285
|
+
await ctx.agent.transform((editor) => {
|
|
286
|
+
for (const { name } of deps.agentDefs) {
|
|
287
|
+
const config = deps.agents[name];
|
|
288
|
+
const first = deps.effectiveModelArrays[name]?.[0];
|
|
289
|
+
const model = first?.id ?? config.model;
|
|
290
|
+
const variant = first ? first.variant : config.variant;
|
|
291
|
+
editor.update(name, (agent) => {
|
|
292
|
+
agent.description = config.description;
|
|
293
|
+
agent.system = config.prompt;
|
|
294
|
+
agent.mode = name === "orchestrator" ? "primary" : "subagent";
|
|
295
|
+
agent.hidden = config.hidden ?? false;
|
|
296
|
+
agent.color = /^#[0-9a-f]{6}$/i.test(config.color ?? "") ? config.color : ROLE_COLORS[name];
|
|
297
|
+
agent.steps = config.steps;
|
|
298
|
+
agent.permissions = translatePermissionRules(config.permission);
|
|
299
|
+
if (name !== "orchestrator") {
|
|
300
|
+
const overrides = deps.toolState.state.effective[name]?.overrides;
|
|
301
|
+
if (overrides?.length) {
|
|
302
|
+
agent.permissions.push(...appendOverrideRules(overrides));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
306
|
+
agent.model = model && separator > 0 && separator < model.length - 1 ? {
|
|
307
|
+
providerID: model.slice(0, separator),
|
|
308
|
+
id: model.slice(separator + 1),
|
|
309
|
+
...variant ? { variant } : {}
|
|
310
|
+
} : void 0;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
editor.default("orchestrator");
|
|
314
|
+
editor.remove("explore");
|
|
315
|
+
editor.remove("general");
|
|
316
|
+
});
|
|
317
|
+
}
|
|
26
318
|
|
|
27
319
|
// src/harness/opencode-init-command.ts
|
|
28
320
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -62,664 +354,224 @@ ${contract}`
|
|
|
62
354
|
};
|
|
63
355
|
}
|
|
64
356
|
|
|
65
|
-
// src/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
79
|
-
`;
|
|
80
|
-
fs2.appendFileSync(getLogFile(), logEntry);
|
|
81
|
-
} catch {
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// src/hooks/auto-update-checker/checker.ts
|
|
86
|
-
import * as fs from "fs";
|
|
87
|
-
import * as path2 from "path";
|
|
88
|
-
import { fileURLToPath } from "url";
|
|
89
|
-
|
|
90
|
-
// src/hooks/auto-update-checker/constants.ts
|
|
91
|
-
import * as os from "os";
|
|
92
|
-
import * as path from "path";
|
|
93
|
-
var PACKAGE_NAME = "thoth-agents";
|
|
94
|
-
var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
|
|
95
|
-
var NPM_FETCH_TIMEOUT = 5e3;
|
|
96
|
-
function getCacheDir() {
|
|
97
|
-
if (process.platform === "win32") {
|
|
98
|
-
return path.join(process.env.LOCALAPPDATA ?? os.homedir(), "opencode");
|
|
99
|
-
}
|
|
100
|
-
return path.join(os.homedir(), ".cache", "opencode");
|
|
101
|
-
}
|
|
102
|
-
var CACHE_DIR = getCacheDir();
|
|
103
|
-
var INSTALLED_PACKAGE_JSON = path.join(
|
|
104
|
-
CACHE_DIR,
|
|
105
|
-
"node_modules",
|
|
106
|
-
PACKAGE_NAME,
|
|
107
|
-
"package.json"
|
|
108
|
-
);
|
|
109
|
-
var configPaths = getOpenCodeConfigPaths();
|
|
110
|
-
var USER_OPENCODE_CONFIG = configPaths[0];
|
|
111
|
-
var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
|
|
357
|
+
// src/opencode/command.ts
|
|
358
|
+
async function registerCommand(ctx, deps) {
|
|
359
|
+
const command = createOpenCodeInitCommand({
|
|
360
|
+
projectRoot: deps.directory,
|
|
361
|
+
packageRoot: deps.packageRoot
|
|
362
|
+
});
|
|
363
|
+
await ctx.command.transform((editor) => {
|
|
364
|
+
editor.add({
|
|
365
|
+
name: "thoth-init",
|
|
366
|
+
description: command.description,
|
|
367
|
+
async execute({ sessionID, prompt, delivery }) {
|
|
368
|
+
const args = prompt.text.trim();
|
|
369
|
+
const text = command.template + (args ? `
|
|
112
370
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
function isDistTag(version) {
|
|
118
|
-
return !/^\d/.test(version);
|
|
119
|
-
}
|
|
120
|
-
function extractChannel(version) {
|
|
121
|
-
if (!version) return "latest";
|
|
122
|
-
if (isDistTag(version)) return version;
|
|
123
|
-
if (isPrereleaseVersion(version)) {
|
|
124
|
-
const prereleasePart = version.split("-")[1];
|
|
125
|
-
if (prereleasePart) {
|
|
126
|
-
const channelMatch = prereleasePart.match(/^(alpha|beta|rc|canary|next)/);
|
|
127
|
-
if (channelMatch) return channelMatch[1];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return "latest";
|
|
131
|
-
}
|
|
132
|
-
function getConfigPaths(directory) {
|
|
133
|
-
return [
|
|
134
|
-
path2.join(directory, ".opencode", "opencode.json"),
|
|
135
|
-
path2.join(directory, ".opencode", "opencode.jsonc"),
|
|
136
|
-
USER_OPENCODE_CONFIG,
|
|
137
|
-
USER_OPENCODE_CONFIG_JSONC
|
|
138
|
-
];
|
|
139
|
-
}
|
|
140
|
-
function getLocalDevPath(directory) {
|
|
141
|
-
for (const configPath of getConfigPaths(directory)) {
|
|
142
|
-
try {
|
|
143
|
-
if (!fs.existsSync(configPath)) continue;
|
|
144
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
145
|
-
const config = JSON.parse(stripJsonComments(content));
|
|
146
|
-
const plugins = config.plugin ?? [];
|
|
147
|
-
for (const entry of plugins) {
|
|
148
|
-
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
|
149
|
-
try {
|
|
150
|
-
return fileURLToPath(entry);
|
|
151
|
-
} catch {
|
|
152
|
-
return entry.replace("file://", "");
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
function findPackageJsonUp(startPath) {
|
|
162
|
-
try {
|
|
163
|
-
const stat = fs.statSync(startPath);
|
|
164
|
-
let dir = stat.isDirectory() ? startPath : path2.dirname(startPath);
|
|
165
|
-
for (let i = 0; i < 10; i++) {
|
|
166
|
-
const pkgPath = path2.join(dir, "package.json");
|
|
167
|
-
if (fs.existsSync(pkgPath)) {
|
|
168
|
-
try {
|
|
169
|
-
const content = fs.readFileSync(pkgPath, "utf-8");
|
|
170
|
-
const pkg = JSON.parse(content);
|
|
171
|
-
if (pkg.name === PACKAGE_NAME) return pkgPath;
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
371
|
+
${args}` : "");
|
|
372
|
+
await ctx.session.switchAgent({ sessionID, agent: "orchestrator" });
|
|
373
|
+
await ctx.session.prompt({ sessionID, text, delivery });
|
|
174
374
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
dir = parent;
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
375
|
+
});
|
|
376
|
+
});
|
|
182
377
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
378
|
+
|
|
379
|
+
// src/opencode/context.ts
|
|
380
|
+
import { fileURLToPath } from "url";
|
|
381
|
+
function loadOpenCodeDependencies(ctx) {
|
|
382
|
+
const directory = ctx.location.directory;
|
|
383
|
+
const config = loadPluginConfig(directory);
|
|
384
|
+
const agentDefs = createAgents(config);
|
|
385
|
+
const agents = renderOpenCodeAgentConfigs(config);
|
|
386
|
+
const modelArrayMap = {};
|
|
387
|
+
const runtimeChains = {};
|
|
388
|
+
const effectiveModelArrays = {};
|
|
389
|
+
for (const agent of agentDefs) {
|
|
390
|
+
if (!agent._modelArray?.length) continue;
|
|
391
|
+
modelArrayMap[agent.name] = [...agent._modelArray];
|
|
392
|
+
effectiveModelArrays[agent.name] = [...agent._modelArray];
|
|
393
|
+
runtimeChains[agent.name] = agent._modelArray.map((model) => model.id);
|
|
194
394
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
395
|
+
if (config.fallback?.enabled !== false) {
|
|
396
|
+
for (const [name, models] of Object.entries(
|
|
397
|
+
config.fallback?.chains ?? {}
|
|
398
|
+
)) {
|
|
399
|
+
if (!models?.length) continue;
|
|
400
|
+
const currentModel = agents[name]?.model;
|
|
401
|
+
const effective = effectiveModelArrays[name] ?? (currentModel ? [{ id: currentModel, variant: agents[name]?.variant }] : []);
|
|
402
|
+
for (const id of models) {
|
|
403
|
+
if (!effective.some((model) => model.id === id)) effective.push({ id });
|
|
404
|
+
}
|
|
405
|
+
runtimeChains[name] = effective.map((model) => model.id);
|
|
406
|
+
effectiveModelArrays[name] = effective;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const roster = {};
|
|
410
|
+
const configuredChains = {};
|
|
411
|
+
const baseArrays = structuredClone(effectiveModelArrays);
|
|
412
|
+
const baseRuntimeChains = structuredClone(runtimeChains);
|
|
413
|
+
for (const { name } of agentDefs) {
|
|
414
|
+
const role = name;
|
|
415
|
+
const first = effectiveModelArrays[name]?.[0];
|
|
416
|
+
roster[role] = first ? { model: first.id, variant: first.variant } : { model: agents[name]?.model, variant: agents[name]?.variant };
|
|
417
|
+
configuredChains[name] = effectiveModelArrays[name] ?? (agents[name]?.model ? [{ id: agents[name].model, variant: agents[name].variant }] : []);
|
|
418
|
+
}
|
|
419
|
+
const store = new ModelProfileStore({
|
|
420
|
+
cwd: directory,
|
|
421
|
+
roster,
|
|
422
|
+
warn: (message) => console.warn(`[thoth-agents] ${message}`)
|
|
423
|
+
});
|
|
424
|
+
const profileState = {
|
|
425
|
+
store,
|
|
426
|
+
roster,
|
|
427
|
+
selection: store.resolve(),
|
|
428
|
+
roles: {},
|
|
429
|
+
refresh() {
|
|
430
|
+
this.selection = store.resolve();
|
|
431
|
+
applyProfile();
|
|
219
432
|
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
433
|
+
};
|
|
434
|
+
function applyProfile() {
|
|
435
|
+
for (const { name } of agentDefs) {
|
|
436
|
+
const role = name;
|
|
437
|
+
profileState.roles[role] = resolveRoleProfile(
|
|
438
|
+
role,
|
|
439
|
+
profileState.selection.profile,
|
|
440
|
+
roster
|
|
441
|
+
);
|
|
442
|
+
if (profileState.selection.name === "default") {
|
|
443
|
+
if (baseArrays[name])
|
|
444
|
+
effectiveModelArrays[name] = structuredClone(baseArrays[name]);
|
|
445
|
+
else delete effectiveModelArrays[name];
|
|
446
|
+
if (baseRuntimeChains[name])
|
|
447
|
+
runtimeChains[name] = [...baseRuntimeChains[name]];
|
|
448
|
+
else delete runtimeChains[name];
|
|
449
|
+
} else {
|
|
450
|
+
const chain = mergeProfileModelChain(
|
|
451
|
+
profileState.roles[role],
|
|
452
|
+
configuredChains[name]
|
|
453
|
+
);
|
|
454
|
+
effectiveModelArrays[name] = chain;
|
|
455
|
+
runtimeChains[name] = chain.map(({ id }) => id);
|
|
233
456
|
}
|
|
234
457
|
}
|
|
235
|
-
} catch {
|
|
236
458
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
459
|
+
applyProfile();
|
|
460
|
+
const toolStore = new ToolsOverrideStore({
|
|
461
|
+
cwd: directory,
|
|
462
|
+
warn: (message) => console.warn(`[thoth-agents] ${message}`)
|
|
463
|
+
});
|
|
464
|
+
const toolState = {
|
|
465
|
+
store: toolStore,
|
|
466
|
+
state: toolStore.resolve(),
|
|
467
|
+
refresh() {
|
|
468
|
+
this.state = toolStore.resolve();
|
|
247
469
|
}
|
|
248
|
-
}
|
|
249
|
-
log(
|
|
250
|
-
"[auto-update-checker] Failed to resolve version from current directory:",
|
|
251
|
-
err
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
async function getLatestVersion(channel = "latest") {
|
|
257
|
-
const controller = new AbortController();
|
|
258
|
-
const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
|
|
259
|
-
try {
|
|
260
|
-
const response = await fetch(NPM_REGISTRY_URL, {
|
|
261
|
-
signal: controller.signal,
|
|
262
|
-
headers: { Accept: "application/json" }
|
|
263
|
-
});
|
|
264
|
-
if (!response.ok) return null;
|
|
265
|
-
const data = await response.json();
|
|
266
|
-
return data[channel] ?? data.latest ?? null;
|
|
267
|
-
} catch {
|
|
268
|
-
return null;
|
|
269
|
-
} finally {
|
|
270
|
-
clearTimeout(timeoutId);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// src/hooks/auto-update-checker/index.ts
|
|
275
|
-
function createAutoUpdateCheckerHook(ctx, options = {}) {
|
|
276
|
-
const { showStartupToast = true } = options;
|
|
277
|
-
let hasChecked = false;
|
|
470
|
+
};
|
|
278
471
|
return {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
log("[auto-update-checker] Local development mode");
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (showStartupToast) {
|
|
302
|
-
showToast(
|
|
303
|
-
ctx,
|
|
304
|
-
`thoth-agents ${displayVersion ?? "unknown"}`,
|
|
305
|
-
"thoth-agents is active.",
|
|
306
|
-
"info"
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
runBackgroundUpdateCheck(ctx).catch((err) => {
|
|
310
|
-
log("[auto-update-checker] Background update check failed:", err);
|
|
311
|
-
});
|
|
312
|
-
}, 0);
|
|
313
|
-
}
|
|
472
|
+
directory,
|
|
473
|
+
// Both src/opencode/context.ts and dist/index.js (or a root chunk) resolve
|
|
474
|
+
// the package root here; nested source modules need one extra parent.
|
|
475
|
+
packageRoot: fileURLToPath(
|
|
476
|
+
new URL(
|
|
477
|
+
import.meta.url.endsWith(".ts") ? "../.." : "..",
|
|
478
|
+
import.meta.url
|
|
479
|
+
)
|
|
480
|
+
),
|
|
481
|
+
config,
|
|
482
|
+
agentDefs,
|
|
483
|
+
agents,
|
|
484
|
+
modelArrayMap,
|
|
485
|
+
effectiveModelArrays,
|
|
486
|
+
runtimeChains,
|
|
487
|
+
profileState,
|
|
488
|
+
toolState
|
|
314
489
|
};
|
|
315
490
|
}
|
|
316
|
-
async function runBackgroundUpdateCheck(ctx) {
|
|
317
|
-
const pluginInfo = findPluginEntry(ctx.directory);
|
|
318
|
-
if (!pluginInfo) {
|
|
319
|
-
log("[auto-update-checker] Plugin not found in config");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const cachedVersion = getCachedVersion();
|
|
323
|
-
const currentVersion = cachedVersion ?? pluginInfo.pinnedVersion;
|
|
324
|
-
if (!currentVersion) {
|
|
325
|
-
log("[auto-update-checker] No version found (cached or pinned)");
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
|
|
329
|
-
const latestVersion = await getLatestVersion(channel);
|
|
330
|
-
if (!latestVersion) {
|
|
331
|
-
log(
|
|
332
|
-
"[auto-update-checker] Failed to fetch latest version for channel:",
|
|
333
|
-
channel
|
|
334
|
-
);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (currentVersion === latestVersion) {
|
|
338
|
-
log(
|
|
339
|
-
"[auto-update-checker] Already on latest version for channel:",
|
|
340
|
-
channel
|
|
341
|
-
);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
log(
|
|
345
|
-
`[auto-update-checker] Update available (${channel}): ${currentVersion} \u2192 ${latestVersion}`
|
|
346
|
-
);
|
|
347
|
-
showToast(
|
|
348
|
-
ctx,
|
|
349
|
-
`thoth-agents ${latestVersion} available`,
|
|
350
|
-
`v${currentVersion} \u2192 v${latestVersion}. Run npx thoth-agents@latest install --agent=opencode or use interactive CLI Update.`,
|
|
351
|
-
"info",
|
|
352
|
-
1e4
|
|
353
|
-
);
|
|
354
|
-
log("[auto-update-checker] Notification only; explicit CLI update required");
|
|
355
|
-
}
|
|
356
|
-
function showToast(ctx, title, message, variant = "info", duration = 3e3) {
|
|
357
|
-
ctx.client.tui.showToast({
|
|
358
|
-
body: { title, message, variant, duration }
|
|
359
|
-
}).catch(() => {
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
491
|
|
|
363
492
|
// src/hooks/delegate-task-retry/patterns.ts
|
|
364
493
|
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
365
494
|
{
|
|
366
|
-
pattern: "
|
|
367
|
-
errorType: "
|
|
368
|
-
fixHint: "
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
pattern: "category OR subagent_type",
|
|
372
|
-
errorType: "mutual_exclusion",
|
|
373
|
-
fixHint: 'Provide only one: category (e.g., "unspecified-low") OR subagent_type (e.g., "explorer").'
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
pattern: "Must provide either category or subagent_type",
|
|
377
|
-
errorType: "missing_category_or_agent",
|
|
378
|
-
fixHint: 'Add either category="unspecified-low" or subagent_type="explorer".'
|
|
495
|
+
pattern: "Unknown agent:",
|
|
496
|
+
errorType: "unknown_agent",
|
|
497
|
+
fixHint: "Use an available agent name in the agent argument."
|
|
379
498
|
},
|
|
380
499
|
{
|
|
381
|
-
pattern: "
|
|
382
|
-
errorType: "
|
|
383
|
-
fixHint: "
|
|
500
|
+
pattern: "cannot run as a subagent",
|
|
501
|
+
errorType: "primary_agent",
|
|
502
|
+
fixHint: "Choose a specialist agent, not a primary agent."
|
|
384
503
|
},
|
|
385
504
|
{
|
|
386
|
-
pattern: "
|
|
387
|
-
errorType: "
|
|
388
|
-
fixHint: "
|
|
505
|
+
pattern: "Subagent depth limit reached",
|
|
506
|
+
errorType: "depth_limit",
|
|
507
|
+
fixHint: "Return to the root coordinator; nesting depth is 1. Do not retry nested delegation."
|
|
389
508
|
},
|
|
390
509
|
{
|
|
391
|
-
pattern: "
|
|
392
|
-
errorType: "
|
|
393
|
-
fixHint: "
|
|
510
|
+
pattern: "Subagent denied:",
|
|
511
|
+
errorType: "permission_denied",
|
|
512
|
+
fixHint: "Return the permission blocker to the root coordinator; do not bypass the denial."
|
|
394
513
|
},
|
|
395
514
|
{
|
|
396
|
-
pattern: "
|
|
397
|
-
errorType: "
|
|
398
|
-
fixHint: "
|
|
515
|
+
pattern: "Invalid arguments",
|
|
516
|
+
errorType: "invalid_arguments",
|
|
517
|
+
fixHint: "Supply agent, description and prompt; optional model, sessionID and background must have the native types."
|
|
399
518
|
}
|
|
400
519
|
];
|
|
401
520
|
function detectDelegateTaskError(output) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (output.includes(pattern.pattern)) {
|
|
407
|
-
return {
|
|
408
|
-
errorType: pattern.errorType,
|
|
409
|
-
originalOutput: output
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return null;
|
|
521
|
+
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
522
|
+
(entry) => output.includes(entry.pattern)
|
|
523
|
+
);
|
|
524
|
+
return pattern ? { errorType: pattern.errorType, originalOutput: output } : null;
|
|
414
525
|
}
|
|
415
526
|
|
|
416
527
|
// src/hooks/delegate-task-retry/guidance.ts
|
|
417
|
-
function extractAvailableList(output) {
|
|
418
|
-
const match = output.match(/Allowed agents:\s*(.+)$/m);
|
|
419
|
-
if (match) return match[1].trim();
|
|
420
|
-
const available = output.match(/Available[^:]*:\s*(.+)$/m);
|
|
421
|
-
if (available) return available[1].trim();
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
528
|
function buildRetryGuidance(errorInfo) {
|
|
425
529
|
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
426
|
-
(
|
|
530
|
+
(entry) => entry.errorType === errorInfo.errorType
|
|
427
531
|
);
|
|
428
|
-
if (!pattern) {
|
|
429
|
-
return "\n[delegate-task retry] Fix parameters and retry with corrected arguments.";
|
|
430
|
-
}
|
|
431
|
-
const available = extractAvailableList(errorInfo.originalOutput);
|
|
432
532
|
const lines = [
|
|
433
|
-
"",
|
|
434
|
-
"[delegate-task retry suggestion]",
|
|
533
|
+
"[subagent retry suggestion]",
|
|
435
534
|
`Error type: ${errorInfo.errorType}`,
|
|
436
|
-
`Fix: ${pattern.
|
|
535
|
+
`Fix: ${pattern?.fixHint ?? "Correct the native subagent arguments before retrying."}`
|
|
437
536
|
];
|
|
438
|
-
if (
|
|
439
|
-
lines.push(`Available: ${available}`);
|
|
440
|
-
}
|
|
441
|
-
if (errorInfo.errorType === "background_agent_not_allowed") {
|
|
537
|
+
if (errorInfo.errorType !== "depth_limit" && errorInfo.errorType !== "permission_denied") {
|
|
442
538
|
lines.push(
|
|
443
|
-
"
|
|
444
|
-
"Use
|
|
445
|
-
"If the host enables `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, collect results with `task_status` before reporting completion.",
|
|
446
|
-
"Examples:",
|
|
447
|
-
'task(description="...", prompt="...", subagent_type="explorer", background=true)',
|
|
448
|
-
'task_status(task_id="ses_...", wait=true)'
|
|
539
|
+
'Example: subagent({agent:"explorer", description:"Locate entrypoint", prompt:"Find the bounded entrypoint"})',
|
|
540
|
+
"Use background:true for independent work; completion arrives automatically. Use sessionID to clarify or continue the same child, including while running. Do not poll."
|
|
449
541
|
);
|
|
450
|
-
return lines.join("\n");
|
|
451
542
|
}
|
|
452
|
-
lines.push(
|
|
453
|
-
"Retry now with corrected parameters. Example:",
|
|
454
|
-
'task(description="...", prompt="...", subagent_type="explorer")'
|
|
455
|
-
);
|
|
456
543
|
return lines.join("\n");
|
|
457
544
|
}
|
|
458
545
|
|
|
459
546
|
// src/hooks/delegate-task-retry/hook.ts
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
547
|
+
function appendErrorGuidance(event, guidance) {
|
|
548
|
+
const properties = Object.getOwnPropertyDescriptors(event.error);
|
|
549
|
+
properties.message = {
|
|
550
|
+
value: `${event.error.message}
|
|
551
|
+
${guidance}`,
|
|
552
|
+
configurable: true,
|
|
553
|
+
writable: true,
|
|
554
|
+
enumerable: true
|
|
555
|
+
};
|
|
556
|
+
properties.stack = {
|
|
557
|
+
value: event.error.stack,
|
|
558
|
+
configurable: true,
|
|
559
|
+
writable: true,
|
|
560
|
+
enumerable: false
|
|
561
|
+
};
|
|
562
|
+
event.error = Object.create(Object.getPrototypeOf(event.error), properties);
|
|
563
|
+
}
|
|
564
|
+
function createDelegateTaskRetryHook() {
|
|
565
|
+
return async (event) => {
|
|
566
|
+
if (event.tool !== "subagent" || event.status !== "error") return;
|
|
567
|
+
if (event.error.message.includes("[subagent retry suggestion]")) return;
|
|
568
|
+
const detected = detectDelegateTaskError(event.error.message);
|
|
569
|
+
if (!detected) return;
|
|
570
|
+
appendErrorGuidance(event, buildRetryGuidance(detected));
|
|
472
571
|
};
|
|
473
572
|
}
|
|
474
573
|
|
|
475
|
-
// src/hooks/foreground-fallback/index.ts
|
|
476
|
-
var RATE_LIMIT_PATTERNS = [
|
|
477
|
-
/\b429\b/,
|
|
478
|
-
/rate.?limit/i,
|
|
479
|
-
/too many requests/i,
|
|
480
|
-
/quota.?exceeded/i,
|
|
481
|
-
/usage.?exceeded/i,
|
|
482
|
-
/usage limit/i,
|
|
483
|
-
/overloaded/i,
|
|
484
|
-
/resource.?exhausted/i,
|
|
485
|
-
/insufficient.?quota/i,
|
|
486
|
-
/high concurrency/i,
|
|
487
|
-
/reduce concurrency/i
|
|
488
|
-
];
|
|
489
|
-
function isRateLimitError(error) {
|
|
490
|
-
if (!error || typeof error !== "object") return false;
|
|
491
|
-
const err = error;
|
|
492
|
-
const text = [
|
|
493
|
-
err.message ?? "",
|
|
494
|
-
String(err.data?.statusCode ?? ""),
|
|
495
|
-
err.data?.message ?? "",
|
|
496
|
-
err.data?.responseBody ?? ""
|
|
497
|
-
].join(" ");
|
|
498
|
-
return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
|
|
499
|
-
}
|
|
500
|
-
function parseModel(model) {
|
|
501
|
-
const slash = model.indexOf("/");
|
|
502
|
-
if (slash <= 0 || slash >= model.length - 1) return null;
|
|
503
|
-
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
504
|
-
}
|
|
505
|
-
var DEDUP_WINDOW_MS = 5e3;
|
|
506
|
-
var ForegroundFallbackManager = class {
|
|
507
|
-
constructor(client, chains, enabled) {
|
|
508
|
-
this.client = client;
|
|
509
|
-
this.chains = chains;
|
|
510
|
-
this.enabled = enabled;
|
|
511
|
-
}
|
|
512
|
-
client;
|
|
513
|
-
chains;
|
|
514
|
-
enabled;
|
|
515
|
-
/** sessionID → last observed model string ("providerID/modelID") */
|
|
516
|
-
sessionModel = /* @__PURE__ */ new Map();
|
|
517
|
-
/** sessionID → agent name (populated from message.updated info.agent field) */
|
|
518
|
-
sessionAgent = /* @__PURE__ */ new Map();
|
|
519
|
-
/** sessionID → set of models already attempted this session */
|
|
520
|
-
sessionTried = /* @__PURE__ */ new Map();
|
|
521
|
-
/** Sessions with an active fallback switch in flight */
|
|
522
|
-
inProgress = /* @__PURE__ */ new Set();
|
|
523
|
-
/** sessionID → timestamp of last trigger (for deduplication) */
|
|
524
|
-
lastTrigger = /* @__PURE__ */ new Map();
|
|
525
|
-
/**
|
|
526
|
-
* Process an OpenCode plugin event.
|
|
527
|
-
* Call this from the plugin's `event` hook for every event received.
|
|
528
|
-
*/
|
|
529
|
-
async handleEvent(rawEvent) {
|
|
530
|
-
if (!this.enabled) return;
|
|
531
|
-
const event = rawEvent;
|
|
532
|
-
if (!event?.type) return;
|
|
533
|
-
switch (event.type) {
|
|
534
|
-
case "message.updated": {
|
|
535
|
-
const info = event.properties?.info;
|
|
536
|
-
if (!info) break;
|
|
537
|
-
const sessionID = info.sessionID;
|
|
538
|
-
if (!sessionID) break;
|
|
539
|
-
if (typeof info.agent === "string") {
|
|
540
|
-
this.sessionAgent.set(sessionID, info.agent);
|
|
541
|
-
}
|
|
542
|
-
if (typeof info.providerID === "string" && typeof info.modelID === "string") {
|
|
543
|
-
this.sessionModel.set(
|
|
544
|
-
sessionID,
|
|
545
|
-
`${info.providerID}/${info.modelID}`
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
if (info.error && isRateLimitError(info.error)) {
|
|
549
|
-
await this.tryFallback(sessionID);
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
case "session.error": {
|
|
554
|
-
const props = event.properties;
|
|
555
|
-
if (props?.sessionID && props.error && isRateLimitError(props.error)) {
|
|
556
|
-
await this.tryFallback(props.sessionID);
|
|
557
|
-
}
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
case "session.status": {
|
|
561
|
-
const props = event.properties;
|
|
562
|
-
if (!props?.sessionID || props.status?.type !== "retry") break;
|
|
563
|
-
const msg = props.status.message?.toLowerCase() ?? "";
|
|
564
|
-
if (msg.includes("rate limit") || msg.includes("usage limit") || msg.includes("usage exceeded") || msg.includes("quota exceeded") || msg.includes("high concurrency") || msg.includes("reduce concurrency")) {
|
|
565
|
-
await this.tryFallback(props.sessionID);
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
case "subagent.session.created": {
|
|
570
|
-
const props = event.properties;
|
|
571
|
-
if (props?.sessionID && typeof props.agentName === "string") {
|
|
572
|
-
this.sessionAgent.set(props.sessionID, props.agentName);
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "session.deleted": {
|
|
577
|
-
const props = event.properties;
|
|
578
|
-
const id = props?.info?.id ?? props?.sessionID;
|
|
579
|
-
if (id) {
|
|
580
|
-
this.sessionModel.delete(id);
|
|
581
|
-
this.sessionAgent.delete(id);
|
|
582
|
-
this.sessionTried.delete(id);
|
|
583
|
-
this.inProgress.delete(id);
|
|
584
|
-
this.lastTrigger.delete(id);
|
|
585
|
-
}
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
getSessionModel(sessionId) {
|
|
591
|
-
return this.sessionModel.get(sessionId);
|
|
592
|
-
}
|
|
593
|
-
// ---------------------------------------------------------------------------
|
|
594
|
-
// Core fallback logic
|
|
595
|
-
// ---------------------------------------------------------------------------
|
|
596
|
-
async tryFallback(sessionID) {
|
|
597
|
-
if (!sessionID) return;
|
|
598
|
-
if (this.inProgress.has(sessionID)) return;
|
|
599
|
-
const now = Date.now();
|
|
600
|
-
if (now - (this.lastTrigger.get(sessionID) ?? 0) < DEDUP_WINDOW_MS) return;
|
|
601
|
-
this.lastTrigger.set(sessionID, now);
|
|
602
|
-
this.inProgress.add(sessionID);
|
|
603
|
-
try {
|
|
604
|
-
const currentModel = this.sessionModel.get(sessionID);
|
|
605
|
-
const agentName = this.sessionAgent.get(sessionID);
|
|
606
|
-
const chain = this.resolveChain(agentName, currentModel);
|
|
607
|
-
if (!chain.length) {
|
|
608
|
-
log("[foreground-fallback] no chain configured", {
|
|
609
|
-
sessionID,
|
|
610
|
-
agentName
|
|
611
|
-
});
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
if (!this.sessionTried.has(sessionID)) {
|
|
615
|
-
this.sessionTried.set(sessionID, /* @__PURE__ */ new Set());
|
|
616
|
-
}
|
|
617
|
-
const tried = this.sessionTried.get(sessionID);
|
|
618
|
-
if (currentModel) tried.add(currentModel);
|
|
619
|
-
const nextModel = chain.find((m) => !tried.has(m));
|
|
620
|
-
if (!nextModel) {
|
|
621
|
-
log("[foreground-fallback] fallback chain exhausted", {
|
|
622
|
-
sessionID,
|
|
623
|
-
agentName,
|
|
624
|
-
tried: [...tried]
|
|
625
|
-
});
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
tried.add(nextModel);
|
|
629
|
-
const ref = parseModel(nextModel);
|
|
630
|
-
if (!ref) {
|
|
631
|
-
log("[foreground-fallback] invalid model format", {
|
|
632
|
-
sessionID,
|
|
633
|
-
nextModel
|
|
634
|
-
});
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const result = await this.client.session.messages({
|
|
638
|
-
path: { id: sessionID }
|
|
639
|
-
});
|
|
640
|
-
const messages = result.data ?? [];
|
|
641
|
-
const lastUser = [...messages].reverse().find((m) => m.info.role === "user");
|
|
642
|
-
if (!lastUser) {
|
|
643
|
-
log("[foreground-fallback] no user message found", { sessionID });
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
try {
|
|
647
|
-
await this.client.session.abort({ path: { id: sessionID } });
|
|
648
|
-
} catch {
|
|
649
|
-
}
|
|
650
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
651
|
-
await this.client.session.promptAsync({
|
|
652
|
-
path: { id: sessionID },
|
|
653
|
-
body: {
|
|
654
|
-
// biome-ignore lint/suspicious/noExplicitAny: parts array from message history
|
|
655
|
-
parts: lastUser.parts,
|
|
656
|
-
model: ref
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
this.sessionModel.set(sessionID, nextModel);
|
|
660
|
-
log("[foreground-fallback] switched to fallback model", {
|
|
661
|
-
sessionID,
|
|
662
|
-
agentName,
|
|
663
|
-
from: currentModel,
|
|
664
|
-
to: nextModel
|
|
665
|
-
});
|
|
666
|
-
} catch (err) {
|
|
667
|
-
log("[foreground-fallback] fallback attempt failed", {
|
|
668
|
-
sessionID,
|
|
669
|
-
error: err instanceof Error ? err.message : String(err)
|
|
670
|
-
});
|
|
671
|
-
} finally {
|
|
672
|
-
this.inProgress.delete(sessionID);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
// ---------------------------------------------------------------------------
|
|
676
|
-
// Chain resolution
|
|
677
|
-
// ---------------------------------------------------------------------------
|
|
678
|
-
/**
|
|
679
|
-
* Determine the fallback chain to use for a session.
|
|
680
|
-
*
|
|
681
|
-
* Priority:
|
|
682
|
-
* 1. Agent name known AND has a configured chain → return it directly
|
|
683
|
-
* 2. Agent name known but NO chain configured → return [] (no fallback;
|
|
684
|
-
* do NOT bleed into other agents' chains which would re-prompt the
|
|
685
|
-
* session with a model belonging to a completely different agent)
|
|
686
|
-
* 3. Agent name unknown, current model known → search all chains for
|
|
687
|
-
* the model to infer which chain to use
|
|
688
|
-
* 4. Nothing matches → flatten all chains as a last resort (only
|
|
689
|
-
* reached when both agent name and current model are unavailable)
|
|
690
|
-
*/
|
|
691
|
-
resolveChain(agentName, currentModel) {
|
|
692
|
-
if (agentName) {
|
|
693
|
-
return this.chains[agentName] ?? [];
|
|
694
|
-
}
|
|
695
|
-
if (currentModel) {
|
|
696
|
-
for (const chain of Object.values(this.chains)) {
|
|
697
|
-
if (chain.includes(currentModel)) return chain;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
const all = [];
|
|
701
|
-
const seen = /* @__PURE__ */ new Set();
|
|
702
|
-
for (const chain of Object.values(this.chains)) {
|
|
703
|
-
for (const m of chain) {
|
|
704
|
-
if (!seen.has(m)) {
|
|
705
|
-
seen.add(m);
|
|
706
|
-
all.push(m);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return all;
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
|
|
714
574
|
// src/hooks/json-error-recovery/hook.ts
|
|
715
|
-
var JSON_ERROR_TOOL_EXCLUDE_LIST = [
|
|
716
|
-
"bash",
|
|
717
|
-
"read",
|
|
718
|
-
"glob",
|
|
719
|
-
"webfetch",
|
|
720
|
-
"grep_app_searchgithub",
|
|
721
|
-
"exa_web_search_exa"
|
|
722
|
-
];
|
|
723
575
|
var JSON_ERROR_PATTERNS = [
|
|
724
576
|
/json parse error/i,
|
|
725
577
|
/failed to parse json/i,
|
|
@@ -731,7 +583,6 @@ var JSON_ERROR_PATTERNS = [
|
|
|
731
583
|
/json[^\n]*unexpected eof/i
|
|
732
584
|
];
|
|
733
585
|
var JSON_ERROR_REMINDER_MARKER = "[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]";
|
|
734
|
-
var JSON_ERROR_EXCLUDED_TOOLS = new Set(JSON_ERROR_TOOL_EXCLUDE_LIST);
|
|
735
586
|
var JSON_ERROR_REMINDER = `
|
|
736
587
|
[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
|
|
737
588
|
|
|
@@ -744,56 +595,74 @@ STOP and do this NOW:
|
|
|
744
595
|
|
|
745
596
|
DO NOT repeat the exact same invalid call.
|
|
746
597
|
`;
|
|
747
|
-
function createJsonErrorRecoveryHook(
|
|
748
|
-
return {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const hasJsonError = JSON_ERROR_PATTERNS.some(
|
|
755
|
-
(pattern) => pattern.test(outputText)
|
|
756
|
-
);
|
|
757
|
-
if (hasJsonError) {
|
|
758
|
-
output.output += `
|
|
759
|
-
${JSON_ERROR_REMINDER}`;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
598
|
+
function createJsonErrorRecoveryHook() {
|
|
599
|
+
return async (event) => {
|
|
600
|
+
if (event.tool !== "subagent" || event.status !== "error") return;
|
|
601
|
+
const text = event.error.message;
|
|
602
|
+
if (text.includes(JSON_ERROR_REMINDER_MARKER)) return;
|
|
603
|
+
if (!JSON_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return;
|
|
604
|
+
appendErrorGuidance(event, JSON_ERROR_REMINDER);
|
|
762
605
|
};
|
|
763
606
|
}
|
|
764
607
|
|
|
765
608
|
// src/hooks/oracle-gate-verdict/hook.ts
|
|
766
|
-
var ORACLE_AGENT = getAgentRole("oracle").name;
|
|
767
609
|
function createOracleGateVerdictHook(ctx) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
610
|
+
const delivered = /* @__PURE__ */ new WeakSet();
|
|
611
|
+
return async (event) => {
|
|
612
|
+
if (event.tool !== "subagent" || event.status !== "completed") return;
|
|
613
|
+
if (delivered.has(event.result)) return;
|
|
614
|
+
if (!event.input || typeof event.input !== "object" || !("agent" in event.input) || event.input.agent !== "oracle")
|
|
615
|
+
return;
|
|
616
|
+
const output = event.result.output;
|
|
617
|
+
if (!output || typeof output !== "object" || !("status" in output) || output.status !== "completed")
|
|
618
|
+
return;
|
|
619
|
+
const content = event.result.content;
|
|
620
|
+
if (content === void 0) return;
|
|
621
|
+
const text = typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
622
|
+
const raw = "output" in output && typeof output.output === "string" && text.includes(output.output) ? output.output : void 0;
|
|
623
|
+
const message = raw ?? text;
|
|
624
|
+
if (!message.trim().split(/\r\n|\r|\n/).includes("```thoth-result"))
|
|
625
|
+
return;
|
|
626
|
+
const end = text.lastIndexOf("</subagent>");
|
|
627
|
+
const tail = end < 0 ? "" : text.slice(end + "</subagent>".length);
|
|
628
|
+
if (tail.split(/\r\n|\r|\n/).some((line) => line.startsWith("thoth-gate-verdict ")))
|
|
629
|
+
return;
|
|
630
|
+
if (Array.isArray(content) && content.some((part) => {
|
|
631
|
+
if (part.type !== "text") return false;
|
|
632
|
+
const lines = part.text.split(/\r\n|\r|\n/);
|
|
633
|
+
return lines.length === 3 && lines[0] === "" && lines[1] === "" && lines[2].startsWith("thoth-gate-verdict ");
|
|
634
|
+
}))
|
|
635
|
+
return;
|
|
636
|
+
const bare = raw === void 0 || !raw.trimStart().startsWith("<subagent ");
|
|
637
|
+
const summary = {
|
|
638
|
+
...summarizeGateResult(bare ? message : "", { cwd: ctx.directory }),
|
|
639
|
+
agent: "oracle"
|
|
640
|
+
};
|
|
641
|
+
const verdict = formatGateVerdict(summary);
|
|
642
|
+
event.result = {
|
|
643
|
+
...event.result,
|
|
644
|
+
content: typeof content === "string" ? `${content}
|
|
787
645
|
|
|
788
|
-
${verdict}`
|
|
789
|
-
return;
|
|
790
|
-
output.output += `
|
|
646
|
+
${verdict}` : [...content, { type: "text", text: `
|
|
791
647
|
|
|
792
|
-
${verdict}
|
|
793
|
-
}
|
|
648
|
+
${verdict}` }]
|
|
649
|
+
};
|
|
650
|
+
delivered.add(event.result);
|
|
794
651
|
};
|
|
795
652
|
}
|
|
796
653
|
|
|
654
|
+
// src/opencode/delegation-hooks.ts
|
|
655
|
+
async function registerDelegationHooks(ctx, deps) {
|
|
656
|
+
const retry = createDelegateTaskRetryHook();
|
|
657
|
+
const recover = createJsonErrorRecoveryHook();
|
|
658
|
+
const verdict = createOracleGateVerdictHook({ directory: deps.directory });
|
|
659
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
660
|
+
await retry(event);
|
|
661
|
+
await recover(event);
|
|
662
|
+
await verdict(event);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
797
666
|
// src/mcp/index.ts
|
|
798
667
|
function createAllBuiltinMcps() {
|
|
799
668
|
return {
|
|
@@ -811,271 +680,941 @@ function createBuiltinMcps(disabledMcps = []) {
|
|
|
811
680
|
);
|
|
812
681
|
}
|
|
813
682
|
|
|
814
|
-
// src/
|
|
815
|
-
|
|
683
|
+
// src/opencode/mcp.ts
|
|
684
|
+
async function registerMcp(ctx, deps) {
|
|
685
|
+
await ctx.mcp.transform((editor) => {
|
|
686
|
+
for (const [name, config] of Object.entries(
|
|
687
|
+
createBuiltinMcps(deps.config.disabled_mcps)
|
|
688
|
+
)) {
|
|
689
|
+
editor.set(name, config);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
816
693
|
|
|
817
|
-
// src/
|
|
818
|
-
|
|
694
|
+
// src/opencode/permissions.ts
|
|
695
|
+
async function registerPermissions(ctx, deps) {
|
|
696
|
+
const specialists = new Set(
|
|
697
|
+
deps.agentDefs.map(({ name }) => name).filter((name) => name !== "orchestrator")
|
|
698
|
+
);
|
|
699
|
+
await ctx.permission.hook("evaluate", (event) => {
|
|
700
|
+
if (event.action !== "subagent" || !event.agent || !specialists.has(event.agent))
|
|
701
|
+
return;
|
|
702
|
+
event.effect = "deny";
|
|
703
|
+
event.message = `Thoth role "${event.agent}" cannot delegate: only orchestrator may call subagent.`;
|
|
704
|
+
});
|
|
705
|
+
}
|
|
819
706
|
|
|
820
|
-
// src/
|
|
821
|
-
import {
|
|
822
|
-
import
|
|
823
|
-
import { createRequire as createRequire3 } from "module";
|
|
824
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
707
|
+
// src/opencode/profile-rpc-handlers.ts
|
|
708
|
+
import { createHash as createHash2 } from "crypto";
|
|
709
|
+
import path2 from "path";
|
|
825
710
|
|
|
826
|
-
// src/
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
711
|
+
// src/opencode/profile-sessions.ts
|
|
712
|
+
var SessionProfileSelections = class {
|
|
713
|
+
constructor(storage, projectDir) {
|
|
714
|
+
this.storage = storage;
|
|
715
|
+
this.projectDir = projectDir;
|
|
716
|
+
}
|
|
717
|
+
storage;
|
|
718
|
+
projectDir;
|
|
719
|
+
parse(value) {
|
|
720
|
+
if (typeof value === "string")
|
|
721
|
+
return { name: value, projectDir: this.projectDir };
|
|
722
|
+
if (value && typeof value === "object" && "name" in value && "projectDir" in value && typeof value.name === "string" && typeof value.projectDir === "string")
|
|
723
|
+
return { name: value.name, projectDir: value.projectDir };
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
async set(sessionID, name, projectDir = this.projectDir) {
|
|
727
|
+
const key = `model-profiles/session/${sessionID}`;
|
|
728
|
+
if (name === void 0) await this.storage.remove(key);
|
|
729
|
+
else await this.storage.set(key, { name, projectDir });
|
|
730
|
+
}
|
|
731
|
+
/** Collect before mutating storage so pagination remains stable. */
|
|
732
|
+
async all() {
|
|
733
|
+
const prefix = "model-profiles/session/";
|
|
734
|
+
const selections = [];
|
|
735
|
+
let after;
|
|
736
|
+
do {
|
|
737
|
+
const page = await this.storage.scan({ prefix, after });
|
|
738
|
+
for (const entry of page.entries) {
|
|
739
|
+
const value = this.parse(entry.value);
|
|
740
|
+
if (entry.key.startsWith(prefix) && value)
|
|
741
|
+
selections.push({
|
|
742
|
+
...value,
|
|
743
|
+
sessionID: entry.key.slice(prefix.length)
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
after = page.next;
|
|
747
|
+
} while (after !== void 0);
|
|
748
|
+
return selections;
|
|
749
|
+
}
|
|
750
|
+
async get(sessionID) {
|
|
751
|
+
return this.parse(
|
|
752
|
+
await this.storage.get(`model-profiles/session/${sessionID}`)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// src/opencode/profile-rpc-handlers.ts
|
|
758
|
+
function revision(profile) {
|
|
759
|
+
const canonical = profile === null ? null : {
|
|
760
|
+
agents: Object.fromEntries(
|
|
761
|
+
PROFILE_ROLES.filter((role) => profile.agents[role]).map((role) => [
|
|
762
|
+
role,
|
|
763
|
+
{
|
|
764
|
+
model: profile.agents[role]?.model,
|
|
765
|
+
variant: profile.agents[role]?.variant
|
|
766
|
+
}
|
|
767
|
+
])
|
|
768
|
+
)
|
|
769
|
+
};
|
|
770
|
+
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
771
|
+
}
|
|
772
|
+
var RequestError = class extends Error {
|
|
773
|
+
constructor(code, message) {
|
|
774
|
+
super(message);
|
|
775
|
+
this.code = code;
|
|
776
|
+
}
|
|
777
|
+
code;
|
|
778
|
+
};
|
|
779
|
+
async function registerProfileRpc(ctx, deps) {
|
|
780
|
+
const { store, roster } = deps.profileState;
|
|
781
|
+
const sessions = new SessionProfileSelections(ctx.storage, deps.directory);
|
|
782
|
+
async function capture(sessionID) {
|
|
783
|
+
const session = await ctx.session.get({ sessionID });
|
|
784
|
+
const ref = session.model ?? (await ctx.agent.get({ agentID: session.agent ?? "orchestrator" })).data.model;
|
|
785
|
+
return ref ? {
|
|
786
|
+
model: `${ref.providerID}/${ref.id}`,
|
|
787
|
+
...ref.variant ? { variant: ref.variant } : {}
|
|
788
|
+
} : {};
|
|
789
|
+
}
|
|
790
|
+
function snapshot(sessionName, change) {
|
|
791
|
+
const warnings = [];
|
|
792
|
+
const global = store.read("global", warnings);
|
|
793
|
+
const project = store.read("project", warnings);
|
|
794
|
+
const selections = {
|
|
795
|
+
...sessionName === void 0 ? {} : { session: sessionName },
|
|
796
|
+
...project.active === void 0 ? {} : { project: project.active },
|
|
797
|
+
...global.active === void 0 ? {} : { global: global.active }
|
|
798
|
+
};
|
|
799
|
+
if (change) {
|
|
800
|
+
if (change.name === void 0) delete selections[change.scope];
|
|
801
|
+
else selections[change.scope] = change.name;
|
|
802
|
+
}
|
|
803
|
+
let effective = {
|
|
804
|
+
name: "default",
|
|
805
|
+
selectedBy: "default",
|
|
806
|
+
profile: store.defaultProfile()
|
|
807
|
+
};
|
|
808
|
+
for (const selectedBy of ["session", "project", "global"]) {
|
|
809
|
+
const name = selections[selectedBy];
|
|
810
|
+
if (!name) continue;
|
|
811
|
+
if (name === "default") {
|
|
812
|
+
effective = { name, selectedBy, profile: store.defaultProfile() };
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
const source = Object.hasOwn(project.profiles, name) ? "project" : "global";
|
|
816
|
+
const profile = (source === "project" ? project : global).profiles[name];
|
|
817
|
+
if (Object.hasOwn(
|
|
818
|
+
(source === "project" ? project : global).profiles,
|
|
819
|
+
name
|
|
820
|
+
) && (selectedBy !== "global" || Object.hasOwn(global.profiles, name))) {
|
|
821
|
+
effective = { name, source, selectedBy, profile };
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
warnings.push(
|
|
825
|
+
`Missing ${selectedBy} model profile "${name}"; falling back.`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const profiles = [];
|
|
829
|
+
for (const [source, document] of [
|
|
830
|
+
["global", global],
|
|
831
|
+
["project", project]
|
|
832
|
+
]) {
|
|
833
|
+
for (const [name, profile] of Object.entries(document.profiles))
|
|
834
|
+
profiles.push({
|
|
835
|
+
name,
|
|
836
|
+
scope: source,
|
|
837
|
+
source,
|
|
838
|
+
profile,
|
|
839
|
+
revision: revision(profile),
|
|
840
|
+
shadowed: source === "global" && Object.hasOwn(project.profiles, name)
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
profiles,
|
|
845
|
+
defaultProfile: store.defaultProfile(),
|
|
846
|
+
roles: [...PROFILE_ROLES],
|
|
847
|
+
selections,
|
|
848
|
+
effective,
|
|
849
|
+
warnings
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async function catalog() {
|
|
853
|
+
const [providers, models] = await Promise.all([
|
|
854
|
+
ctx.provider.list(),
|
|
855
|
+
ctx.model.list({ types: ["chat"] })
|
|
856
|
+
]);
|
|
857
|
+
return providers.data.map((provider) => ({
|
|
858
|
+
id: provider.id,
|
|
859
|
+
name: provider.name,
|
|
860
|
+
models: models.data.filter((model) => model.providerID === provider.id && model.enabled).map((model) => ({
|
|
861
|
+
id: model.id,
|
|
862
|
+
name: model.name,
|
|
863
|
+
variants: model.variants.map((variant) => variant.id)
|
|
864
|
+
}))
|
|
865
|
+
}));
|
|
866
|
+
}
|
|
867
|
+
async function state(sessionID) {
|
|
868
|
+
return {
|
|
869
|
+
...snapshot(
|
|
870
|
+
sessionID ? (await sessions.get(sessionID))?.name : void 0
|
|
871
|
+
),
|
|
872
|
+
...sessionID ? { orchestrator: await capture(sessionID) } : {},
|
|
873
|
+
providers: await catalog()
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function available(profile, providers, name) {
|
|
877
|
+
for (const role of PROFILE_ROLES) {
|
|
878
|
+
const entry = resolveRoleProfile(role, profile, roster);
|
|
879
|
+
if (!entry.model) continue;
|
|
880
|
+
const slash = entry.model.indexOf("/");
|
|
881
|
+
const providerID = entry.model.slice(0, slash);
|
|
882
|
+
const modelID = entry.model.slice(slash + 1);
|
|
883
|
+
const model = providers.find((provider) => provider.id === providerID)?.models.find((candidate) => candidate.id === modelID);
|
|
884
|
+
if (!model || entry.variant && !model.variants.includes(entry.variant))
|
|
885
|
+
throw new RequestError(
|
|
886
|
+
"MODEL_UNAVAILABLE",
|
|
887
|
+
`Unavailable model or variant in profile "${name}" for ${role}: ${entry.model}${entry.variant ? `#${entry.variant}` : ""}. Nothing changed.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function activate(input) {
|
|
892
|
+
if (input.scope === "session" && !input.sessionID)
|
|
893
|
+
throw new RequestError(
|
|
894
|
+
"INVALID_REQUEST",
|
|
895
|
+
"Open a session to change its profile selection."
|
|
896
|
+
);
|
|
897
|
+
const previous = input.sessionID ? (await sessions.get(input.sessionID))?.name : void 0;
|
|
898
|
+
const providers = await catalog();
|
|
899
|
+
if (input.name !== void 0) {
|
|
900
|
+
const target = store.lookup(
|
|
901
|
+
input.name,
|
|
902
|
+
input.scope === "global" ? "global" : void 0
|
|
903
|
+
);
|
|
904
|
+
if (!target)
|
|
905
|
+
throw new RequestError(
|
|
906
|
+
"INVALID_REQUEST",
|
|
907
|
+
`Profile not found: ${input.name}`
|
|
908
|
+
);
|
|
909
|
+
available(target.profile, providers, input.name);
|
|
910
|
+
}
|
|
911
|
+
if (input.scope !== "session") {
|
|
912
|
+
const defaults = snapshot(void 0, input);
|
|
913
|
+
available(defaults.effective.profile, providers, defaults.effective.name);
|
|
914
|
+
}
|
|
915
|
+
const next = snapshot(previous, input);
|
|
916
|
+
available(next.effective.profile, providers, next.effective.name);
|
|
917
|
+
if (input.scope === "session" && input.sessionID)
|
|
918
|
+
await sessions.set(input.sessionID, input.name);
|
|
919
|
+
else {
|
|
920
|
+
if (input.scope === "global") store.setGlobalActive(input.name);
|
|
921
|
+
else store.setProjectPin(input.name);
|
|
922
|
+
deps.profileState.refresh();
|
|
923
|
+
await ctx.agent.reload();
|
|
924
|
+
}
|
|
925
|
+
const orchestrator = resolveRoleProfile(
|
|
926
|
+
"orchestrator",
|
|
927
|
+
next.effective.profile,
|
|
928
|
+
roster
|
|
929
|
+
);
|
|
930
|
+
if (input.sessionID && orchestrator.model) {
|
|
931
|
+
const slash = orchestrator.model.indexOf("/");
|
|
932
|
+
await ctx.session.switchModel({
|
|
933
|
+
sessionID: input.sessionID,
|
|
934
|
+
model: {
|
|
935
|
+
providerID: orchestrator.model.slice(0, slash),
|
|
936
|
+
id: orchestrator.model.slice(slash + 1),
|
|
937
|
+
...orchestrator.variant ? { variant: orchestrator.variant } : {}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return state(input.sessionID);
|
|
942
|
+
}
|
|
943
|
+
let pending = Promise.resolve();
|
|
944
|
+
function handler(method, run) {
|
|
945
|
+
return (raw) => {
|
|
946
|
+
const operation = pending.then(async () => {
|
|
947
|
+
const parsed = profileRpc.methods[method].input.safeParse(raw);
|
|
948
|
+
if (!parsed.success)
|
|
949
|
+
return {
|
|
950
|
+
ok: false,
|
|
951
|
+
error: { code: "INVALID_REQUEST", message: parsed.error.message }
|
|
952
|
+
};
|
|
953
|
+
try {
|
|
954
|
+
return {
|
|
955
|
+
ok: true,
|
|
956
|
+
value: await run(parsed.data)
|
|
957
|
+
};
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const code = error instanceof ProfileConflictError ? "PROFILE_CONFLICT" : error instanceof RequestError ? error.code : "OPERATION_FAILED";
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
error: {
|
|
963
|
+
code,
|
|
964
|
+
message: error instanceof Error ? error.message : String(error)
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
pending = operation;
|
|
970
|
+
return operation;
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function writable(name) {
|
|
974
|
+
try {
|
|
975
|
+
validateProfileName(name, true);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
throw new RequestError("INVALID_REQUEST", String(error));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function current(scope, name) {
|
|
981
|
+
const document = store.read(scope);
|
|
982
|
+
return Object.hasOwn(document.profiles, name) ? document.profiles[name] : null;
|
|
983
|
+
}
|
|
984
|
+
function projectStore(projectDir) {
|
|
985
|
+
return projectDir === deps.directory ? store : new ModelProfileStore({
|
|
986
|
+
cwd: projectDir,
|
|
987
|
+
roster,
|
|
988
|
+
paths: {
|
|
989
|
+
global: store.paths.global,
|
|
990
|
+
project: path2.join(
|
|
991
|
+
projectDir,
|
|
992
|
+
".opencode",
|
|
993
|
+
"thoth",
|
|
994
|
+
"profiles.json"
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
function identity(project, name) {
|
|
1000
|
+
const found = project.lookup(name);
|
|
1001
|
+
return found ? found.scope ? `${project.paths[found.scope]}:${name}` : "default" : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
async function maintenance(scope, name, newName) {
|
|
1004
|
+
const selections = await sessions.all();
|
|
1005
|
+
const target = `${store.paths[scope]}:${name}`;
|
|
1006
|
+
const affected = selections.filter(
|
|
1007
|
+
(selection) => identity(projectStore(selection.projectDir), selection.name) === target
|
|
1008
|
+
);
|
|
1009
|
+
const projects = new Map(
|
|
1010
|
+
[
|
|
1011
|
+
.../* @__PURE__ */ new Set([deps.directory, ...selections.map((s) => s.projectDir)])
|
|
1012
|
+
].map((dir) => [dir, projectStore(dir)])
|
|
1013
|
+
);
|
|
1014
|
+
const pins = [];
|
|
1015
|
+
for (const project of projects.values()) {
|
|
1016
|
+
const pin = project.read("project").active;
|
|
1017
|
+
if (pin && identity(project, pin) === target) pins.push(project);
|
|
1018
|
+
}
|
|
1019
|
+
if (newName !== void 0) {
|
|
1020
|
+
for (const [dir, project] of projects) {
|
|
1021
|
+
let after2 = function(pointer, globalPointer = false) {
|
|
1022
|
+
if (!pointer) return void 0;
|
|
1023
|
+
if (pointer === "default") return "default";
|
|
1024
|
+
if (globalPointer && !Object.hasOwn(global.profiles, pointer))
|
|
1025
|
+
return void 0;
|
|
1026
|
+
const source = Object.hasOwn(local.profiles, pointer) ? "project" : "global";
|
|
1027
|
+
if (!Object.hasOwn(
|
|
1028
|
+
(source === "project" ? local : global).profiles,
|
|
1029
|
+
pointer
|
|
1030
|
+
))
|
|
1031
|
+
return void 0;
|
|
1032
|
+
return `${project.paths[source]}:${pointer}`;
|
|
1033
|
+
}, guard2 = function(before, next) {
|
|
1034
|
+
const expected = before === target ? `${store.paths[scope]}:${newName}` : before;
|
|
1035
|
+
if (next !== expected)
|
|
1036
|
+
throw new RequestError(
|
|
1037
|
+
"PROFILE_SHADOWED",
|
|
1038
|
+
`Renaming to "${newName}" collides with a shadowing profile in ${dir}. Nothing changed.`
|
|
1039
|
+
);
|
|
1040
|
+
};
|
|
1041
|
+
var after = after2, guard = guard2;
|
|
1042
|
+
const global = structuredClone(project.read("global"));
|
|
1043
|
+
const local = structuredClone(project.read("project"));
|
|
1044
|
+
const changed = scope === "global" ? global : project.paths.project === store.paths.project ? local : void 0;
|
|
1045
|
+
if (changed && Object.hasOwn(changed.profiles, name)) {
|
|
1046
|
+
changed.profiles[newName] = changed.profiles[name];
|
|
1047
|
+
delete changed.profiles[name];
|
|
1048
|
+
if (changed.active === name) changed.active = newName;
|
|
1049
|
+
}
|
|
1050
|
+
if (pins.includes(project)) local.active = newName;
|
|
1051
|
+
for (const selection of selections.filter(
|
|
1052
|
+
(s) => s.projectDir === dir
|
|
1053
|
+
)) {
|
|
1054
|
+
const before = identity(project, selection.name);
|
|
1055
|
+
guard2(before, after2(before === target ? newName : selection.name));
|
|
1056
|
+
}
|
|
1057
|
+
const pin = project.read("project").active;
|
|
1058
|
+
guard2(pin ? identity(project, pin) : void 0, after2(local.active));
|
|
1059
|
+
const active = project.read("global").active;
|
|
1060
|
+
const beforeGlobal = active && project.lookup(active, "global") ? identity(project, active) : void 0;
|
|
1061
|
+
guard2(beforeGlobal, after2(global.active, true));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return { affected, pins };
|
|
1065
|
+
}
|
|
1066
|
+
const handlers = {
|
|
1067
|
+
state: handler("state", ({ sessionID }) => state(sessionID)),
|
|
1068
|
+
capture: handler("capture", ({ sessionID }) => capture(sessionID)),
|
|
1069
|
+
activate: handler("activate", activate),
|
|
1070
|
+
clear: handler("clear", activate),
|
|
1071
|
+
create: handler("create", ({ scope, name, profile }) => {
|
|
1072
|
+
writable(name);
|
|
1073
|
+
store.create(scope, name, profile);
|
|
1074
|
+
return { revision: revision(current(scope, name)) };
|
|
1075
|
+
}),
|
|
1076
|
+
save: handler("save", ({ scope, name, profile, baseRevision }) => {
|
|
1077
|
+
writable(name);
|
|
1078
|
+
const expected = current(scope, name);
|
|
1079
|
+
if (revision(expected) !== baseRevision)
|
|
1080
|
+
throw new ProfileConflictError(scope, name);
|
|
1081
|
+
store.save(scope, name, profile, { expected });
|
|
1082
|
+
return { revision: revision(current(scope, name)) };
|
|
1083
|
+
}),
|
|
1084
|
+
rename: handler("rename", async ({ scope, name, newName }) => {
|
|
1085
|
+
writable(name);
|
|
1086
|
+
writable(newName);
|
|
1087
|
+
const { affected, pins } = await maintenance(scope, name, newName);
|
|
1088
|
+
store.rename(scope, name, newName);
|
|
1089
|
+
for (const project of pins) project.setProjectPin(newName);
|
|
1090
|
+
for (const selection of affected)
|
|
1091
|
+
await sessions.set(selection.sessionID, newName, selection.projectDir);
|
|
1092
|
+
return {};
|
|
1093
|
+
}),
|
|
1094
|
+
delete: handler("delete", async ({ scope, name }) => {
|
|
1095
|
+
writable(name);
|
|
1096
|
+
const { affected, pins } = await maintenance(scope, name);
|
|
1097
|
+
store.delete(scope, name);
|
|
1098
|
+
for (const project of pins) project.setProjectPin();
|
|
1099
|
+
for (const selection of affected)
|
|
1100
|
+
await sessions.set(selection.sessionID, void 0);
|
|
1101
|
+
return {};
|
|
1102
|
+
})
|
|
1103
|
+
};
|
|
1104
|
+
await ctx.rpc.register(profileRpc, handlers);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/opencode/profile-runtime.ts
|
|
1108
|
+
async function registerProfileRuntime(ctx, deps) {
|
|
1109
|
+
const sessions = new SessionProfileSelections(ctx.storage, deps.directory);
|
|
1110
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
1111
|
+
if (event.tool !== "subagent") return;
|
|
1112
|
+
const input = event.input;
|
|
1113
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return;
|
|
1114
|
+
const args = input;
|
|
1115
|
+
if (args.model !== void 0) return;
|
|
1116
|
+
const role = PROFILE_ROLES.find((role2) => role2 === args.agent);
|
|
1117
|
+
if (!role) return;
|
|
1118
|
+
const { store, roster } = deps.profileState;
|
|
1119
|
+
const selection = store.resolve(
|
|
1120
|
+
(await sessions.get(event.sessionID))?.name
|
|
1121
|
+
);
|
|
1122
|
+
const model = formatModelString(
|
|
1123
|
+
resolveRoleProfile(role, selection.profile, roster)
|
|
1124
|
+
);
|
|
1125
|
+
if (model !== void 0) args.model = model;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/utils/logger.ts
|
|
1130
|
+
import { createRequire } from "module";
|
|
1131
|
+
var require2 = createRequire(import.meta.url);
|
|
1132
|
+
function getLogFile() {
|
|
1133
|
+
const os2 = require2("node:os");
|
|
1134
|
+
const path3 = require2("node:path");
|
|
1135
|
+
const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
|
|
1136
|
+
return configuredPath || path3.join(os2.tmpdir(), "thoth-agents.log");
|
|
1137
|
+
}
|
|
1138
|
+
function log(message, data) {
|
|
1139
|
+
try {
|
|
1140
|
+
const fs2 = require2("node:fs");
|
|
1141
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1142
|
+
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
1143
|
+
`;
|
|
1144
|
+
fs2.appendFileSync(getLogFile(), logEntry);
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/hooks/foreground-fallback/index.ts
|
|
1150
|
+
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
1151
|
+
"provider.rate-limit",
|
|
1152
|
+
"provider.quota",
|
|
1153
|
+
"provider.free-limit",
|
|
1154
|
+
"provider.chatgpt-limit",
|
|
1155
|
+
"provider.go-limit",
|
|
1156
|
+
"provider.budget-limit",
|
|
1157
|
+
"provider.model-budget-limit",
|
|
1158
|
+
"provider.transport",
|
|
1159
|
+
"provider.timeout",
|
|
1160
|
+
"provider.internal",
|
|
1161
|
+
"provider.invalid-output"
|
|
1162
|
+
]);
|
|
1163
|
+
function isRateLimitError(error) {
|
|
1164
|
+
if (!error || typeof error !== "object") return false;
|
|
1165
|
+
const value = error;
|
|
1166
|
+
return value.type !== void 0 && RETRYABLE.has(value.type);
|
|
1167
|
+
}
|
|
1168
|
+
function modelRef(value) {
|
|
1169
|
+
const slash = value.indexOf("/");
|
|
1170
|
+
if (slash <= 0 || slash === value.length - 1) return;
|
|
1171
|
+
const [id, variant] = value.slice(slash + 1).split("#");
|
|
1172
|
+
if (!id) return;
|
|
1173
|
+
return {
|
|
1174
|
+
providerID: value.slice(0, slash),
|
|
1175
|
+
id,
|
|
1176
|
+
...variant ? { variant } : {}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
var ForegroundFallbackManager = class {
|
|
1180
|
+
constructor(session, chains, models = {}) {
|
|
1181
|
+
this.session = session;
|
|
1182
|
+
this.chains = chains;
|
|
1183
|
+
this.models = models;
|
|
841
1184
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1185
|
+
session;
|
|
1186
|
+
chains;
|
|
1187
|
+
models;
|
|
1188
|
+
tried = /* @__PURE__ */ new Map();
|
|
1189
|
+
switching = /* @__PURE__ */ new Set();
|
|
1190
|
+
forget(sessionID) {
|
|
1191
|
+
this.tried.delete(sessionID);
|
|
1192
|
+
}
|
|
1193
|
+
async retry(input) {
|
|
1194
|
+
if (!Object.hasOwn(this.chains, input.agent)) return;
|
|
1195
|
+
const chain = this.chains[input.agent];
|
|
1196
|
+
const current = `${input.model.providerID}/${input.model.id}`;
|
|
1197
|
+
const inChain = chain.some((entry) => {
|
|
1198
|
+
const ref = modelRef(entry);
|
|
1199
|
+
return ref && `${ref.providerID}/${ref.id}` === current;
|
|
1200
|
+
});
|
|
1201
|
+
if (!inChain || !isRateLimitError(input.error)) return;
|
|
1202
|
+
const lock = JSON.stringify([input.sessionID, input.agent]);
|
|
1203
|
+
if (this.switching.has(lock)) return;
|
|
1204
|
+
this.switching.add(lock);
|
|
849
1205
|
try {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1206
|
+
let agents = this.tried.get(input.sessionID);
|
|
1207
|
+
if (!agents) {
|
|
1208
|
+
agents = /* @__PURE__ */ new Map();
|
|
1209
|
+
this.tried.set(input.sessionID, agents);
|
|
1210
|
+
}
|
|
1211
|
+
let tried = agents.get(input.agent);
|
|
1212
|
+
if (!tried) {
|
|
1213
|
+
tried = /* @__PURE__ */ new Set();
|
|
1214
|
+
agents.set(input.agent, tried);
|
|
1215
|
+
}
|
|
1216
|
+
tried.add(current);
|
|
1217
|
+
for (const entry of chain) {
|
|
1218
|
+
const ref = modelRef(entry);
|
|
1219
|
+
if (!ref) continue;
|
|
1220
|
+
const id = `${ref.providerID}/${ref.id}`;
|
|
1221
|
+
if (tried.has(id)) continue;
|
|
1222
|
+
tried.add(id);
|
|
1223
|
+
const variant = this.models[input.agent]?.find(
|
|
1224
|
+
(model) => model.id === entry
|
|
1225
|
+
)?.variant;
|
|
1226
|
+
await this.session.switchModel({
|
|
1227
|
+
sessionID: input.sessionID,
|
|
1228
|
+
model: {
|
|
1229
|
+
...ref,
|
|
1230
|
+
...variant ? { variant } : {}
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
input.decision = { retry: true, delay: 0 };
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
log("[foreground-fallback] model switch failed", {
|
|
1238
|
+
sessionID: input.sessionID,
|
|
1239
|
+
error: String(error)
|
|
1240
|
+
});
|
|
853
1241
|
} finally {
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
const available = response?.ok ?? false;
|
|
857
|
-
if (available) {
|
|
858
|
-
serverCheckUrl = serverUrl;
|
|
859
|
-
serverAvailable = true;
|
|
860
|
-
log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
|
|
861
|
-
return true;
|
|
862
|
-
}
|
|
863
|
-
if (attempt < maxAttempts) {
|
|
864
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1242
|
+
this.switching.delete(lock);
|
|
865
1243
|
}
|
|
866
1244
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/opencode/session-state.ts
|
|
1248
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
1249
|
+
import { join as join2 } from "path";
|
|
1250
|
+
function missing(error) {
|
|
1251
|
+
return error.code === "ENOENT";
|
|
1252
|
+
}
|
|
1253
|
+
var SessionState = class {
|
|
1254
|
+
constructor(directory, instructions) {
|
|
1255
|
+
this.directory = directory;
|
|
1256
|
+
this.instructions = instructions;
|
|
1257
|
+
}
|
|
1258
|
+
directory;
|
|
1259
|
+
instructions;
|
|
1260
|
+
parents = /* @__PURE__ */ new Map();
|
|
1261
|
+
running = /* @__PURE__ */ new Set();
|
|
1262
|
+
published = /* @__PURE__ */ new Map();
|
|
1263
|
+
records = /* @__PURE__ */ new Map();
|
|
1264
|
+
queue = Promise.resolve();
|
|
1265
|
+
serialize(run) {
|
|
1266
|
+
const result = this.queue.then(run);
|
|
1267
|
+
this.queue = result.catch(() => {
|
|
877
1268
|
});
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1269
|
+
return result;
|
|
1270
|
+
}
|
|
1271
|
+
refresh(sessionID) {
|
|
1272
|
+
return this.serialize(() => this.publish(sessionID));
|
|
1273
|
+
}
|
|
1274
|
+
event(event) {
|
|
1275
|
+
return this.serialize(async () => {
|
|
1276
|
+
const { sessionID } = event.data;
|
|
1277
|
+
if (event.type === "session.created") {
|
|
1278
|
+
if (event.data.parentID) {
|
|
1279
|
+
this.parents.set(sessionID, event.data.parentID);
|
|
1280
|
+
this.running.add(sessionID);
|
|
1281
|
+
await this.publish(event.data.parentID);
|
|
1282
|
+
}
|
|
1283
|
+
await this.publish(sessionID);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const parent = this.parents.get(sessionID);
|
|
1287
|
+
if (event.type === "session.status") {
|
|
1288
|
+
if (event.data.status.type === "idle") this.running.delete(sessionID);
|
|
1289
|
+
else if (parent) this.running.add(sessionID);
|
|
1290
|
+
if (parent) await this.publish(parent);
|
|
1291
|
+
if (this.published.has(sessionID)) await this.publish(sessionID);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.parents.delete(sessionID);
|
|
1295
|
+
this.running.delete(sessionID);
|
|
1296
|
+
this.published.delete(sessionID);
|
|
1297
|
+
for (const [child, owner] of this.parents) {
|
|
1298
|
+
if (owner !== sessionID) continue;
|
|
1299
|
+
this.parents.delete(child);
|
|
1300
|
+
this.running.delete(child);
|
|
1301
|
+
}
|
|
1302
|
+
if (parent) await this.publish(parent);
|
|
892
1303
|
});
|
|
893
|
-
const verifyExit = await verifyProc.exited;
|
|
894
|
-
if (verifyExit !== 0) {
|
|
895
|
-
log("[tmux] findTmuxPath: tmux -V failed", { path: path3, verifyExit });
|
|
896
|
-
return null;
|
|
897
|
-
}
|
|
898
|
-
log("[tmux] findTmuxPath: found tmux", { path: path3 });
|
|
899
|
-
return path3;
|
|
900
|
-
} catch (err) {
|
|
901
|
-
log("[tmux] findTmuxPath: exception", { error: String(err) });
|
|
902
|
-
return null;
|
|
903
1304
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1305
|
+
async inventory() {
|
|
1306
|
+
const root = join2(this.directory, ".thoth", "changes");
|
|
1307
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(
|
|
1308
|
+
(error) => {
|
|
1309
|
+
if (missing(error)) return [];
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
);
|
|
1313
|
+
const active = /* @__PURE__ */ new Set();
|
|
1314
|
+
for (const entry of entries) {
|
|
1315
|
+
if (!entry.isDirectory() || entry.name === "archive") continue;
|
|
1316
|
+
const id = entry.name;
|
|
1317
|
+
const path3 = join2(root, id, `${id}.md`);
|
|
1318
|
+
try {
|
|
1319
|
+
const info = await stat(path3);
|
|
1320
|
+
if (!info.isFile()) continue;
|
|
1321
|
+
active.add(id);
|
|
1322
|
+
const fingerprint = `${info.mtimeMs}:${info.ctimeMs}:${info.size}`;
|
|
1323
|
+
if (this.records.get(id)?.fingerprint === fingerprint) continue;
|
|
1324
|
+
const content = await readFile(path3, "utf8");
|
|
1325
|
+
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim() ?? id;
|
|
1326
|
+
const status = content.match(/^\*\*(?:Status|Phase)\*\*:\s*.+$/gm) ?? [];
|
|
1327
|
+
this.records.set(id, {
|
|
1328
|
+
fingerprint,
|
|
1329
|
+
label: `${title}${status.length ? ` (${status.join("; ")})` : ""}`
|
|
1330
|
+
});
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
if (!missing(error)) throw error;
|
|
1333
|
+
active.delete(id);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
for (const id of this.records.keys())
|
|
1337
|
+
if (!active.has(id)) this.records.delete(id);
|
|
1338
|
+
return [...this.records].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(
|
|
1339
|
+
([id, record]) => `- .thoth/changes/${id}/${id}.md \u2014 ${record.label}`
|
|
1340
|
+
);
|
|
908
1341
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const sizeProc = spawn(
|
|
927
|
-
[tmux, "set-window-option", sizeOption, `${mainPaneSize}%`],
|
|
928
|
-
{
|
|
929
|
-
stdout: "pipe",
|
|
930
|
-
stderr: "pipe"
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
await sizeProc.exited;
|
|
934
|
-
const reapplyProc = spawn([tmux, "select-layout", layout], {
|
|
935
|
-
stdout: "pipe",
|
|
936
|
-
stderr: "pipe"
|
|
1342
|
+
async publish(sessionID) {
|
|
1343
|
+
const records = await this.inventory();
|
|
1344
|
+
const children = [...this.parents].filter(
|
|
1345
|
+
([child, parent]) => parent === sessionID && this.running.has(child)
|
|
1346
|
+
).map(([child]) => child).sort();
|
|
1347
|
+
const value = [
|
|
1348
|
+
"Thoth state",
|
|
1349
|
+
"Active SDD records:",
|
|
1350
|
+
...records.length ? records : ["- none"],
|
|
1351
|
+
"Running delegated sessions:",
|
|
1352
|
+
...children.length ? children.map((id) => `- ${id}`) : ["- none"]
|
|
1353
|
+
].join("\n");
|
|
1354
|
+
if (this.published.get(sessionID) !== value) {
|
|
1355
|
+
await this.instructions.entry.put({
|
|
1356
|
+
sessionID,
|
|
1357
|
+
key: "thoth.state",
|
|
1358
|
+
value
|
|
937
1359
|
});
|
|
938
|
-
|
|
1360
|
+
this.published.set(sessionID, value);
|
|
939
1361
|
}
|
|
940
|
-
|
|
941
|
-
} catch (err) {
|
|
942
|
-
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
1362
|
+
return value;
|
|
943
1363
|
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/opencode/session-hooks.ts
|
|
1367
|
+
async function registerSessionHooks(ctx, deps) {
|
|
1368
|
+
const names = new Set(deps.agentDefs.map((agent) => agent.name));
|
|
1369
|
+
const fallback = new ForegroundFallbackManager(
|
|
1370
|
+
ctx.session,
|
|
1371
|
+
deps.runtimeChains,
|
|
1372
|
+
deps.effectiveModelArrays
|
|
1373
|
+
);
|
|
1374
|
+
const state = new SessionState(deps.directory, ctx.session.instructions);
|
|
1375
|
+
await ctx.session.hook("retry", (input) => {
|
|
1376
|
+
if (names.has(input.agent)) return fallback.retry(input);
|
|
951
1377
|
});
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
return { success: false };
|
|
973
|
-
}
|
|
974
|
-
storedConfig = config;
|
|
975
|
-
try {
|
|
976
|
-
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
|
|
977
|
-
const args = [
|
|
978
|
-
"split-window",
|
|
979
|
-
"-h",
|
|
980
|
-
"-d",
|
|
981
|
-
// Don't switch focus to new pane
|
|
982
|
-
"-P",
|
|
983
|
-
// Print pane info
|
|
984
|
-
"-F",
|
|
985
|
-
"#{pane_id}",
|
|
986
|
-
// Format: just the pane ID
|
|
987
|
-
opencodeCmd
|
|
988
|
-
];
|
|
989
|
-
log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
|
|
990
|
-
const proc = spawn([tmux, ...args], {
|
|
991
|
-
stdout: "pipe",
|
|
992
|
-
stderr: "pipe"
|
|
993
|
-
});
|
|
994
|
-
const exitCode = await proc.exited;
|
|
995
|
-
const stdout = await new Response(proc.stdout).text();
|
|
996
|
-
const stderr = await new Response(proc.stderr).text();
|
|
997
|
-
const paneId = stdout.trim();
|
|
998
|
-
log("[tmux] spawnTmuxPane: split result", {
|
|
999
|
-
exitCode,
|
|
1000
|
-
paneId,
|
|
1001
|
-
stderr: stderr.trim()
|
|
1378
|
+
await ctx.session.hook("prompt", async (input) => {
|
|
1379
|
+
await state.refresh(input.sessionID);
|
|
1380
|
+
});
|
|
1381
|
+
await ctx.session.hook("compaction", async (input) => {
|
|
1382
|
+
const value = await state.refresh(input.sessionID);
|
|
1383
|
+
if (input.result) {
|
|
1384
|
+
input.result.summary += `
|
|
1385
|
+
|
|
1386
|
+
${value}`;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
input.messages.push({
|
|
1390
|
+
role: "user",
|
|
1391
|
+
content: [
|
|
1392
|
+
{
|
|
1393
|
+
type: "text",
|
|
1394
|
+
text: `Preserve this current Thoth state in the compaction summary:
|
|
1395
|
+
${value}`
|
|
1396
|
+
}
|
|
1397
|
+
]
|
|
1002
1398
|
});
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1399
|
+
});
|
|
1400
|
+
const signal = new AbortController();
|
|
1401
|
+
const events = ctx.event.subscribe({
|
|
1402
|
+
types: ["session.created", "session.status", "session.deleted"],
|
|
1403
|
+
signal: signal.signal
|
|
1404
|
+
});
|
|
1405
|
+
void (async () => {
|
|
1406
|
+
try {
|
|
1407
|
+
for await (const event of events) {
|
|
1408
|
+
if (event.type === "session.deleted")
|
|
1409
|
+
fallback.forget(event.data.sessionID);
|
|
1410
|
+
try {
|
|
1411
|
+
await state.event(event);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
log("[session-state] update failed", { error: String(error) });
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
log("[session-state] subscription ended", { error: String(error) });
|
|
1418
|
+
} finally {
|
|
1419
|
+
signal.abort();
|
|
1420
|
+
}
|
|
1421
|
+
})();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/opencode/skills.ts
|
|
1425
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1426
|
+
import { resolve } from "path";
|
|
1427
|
+
import { parse } from "yaml";
|
|
1428
|
+
var ownedSkills = [
|
|
1429
|
+
"thoth-init",
|
|
1430
|
+
"thoth-sdd",
|
|
1431
|
+
"thoth-constitution",
|
|
1432
|
+
"thoth-archive",
|
|
1433
|
+
"plan-reviewer"
|
|
1434
|
+
];
|
|
1435
|
+
async function registerSkills(ctx, deps) {
|
|
1436
|
+
const skills = [];
|
|
1437
|
+
for (const id of ownedSkills) {
|
|
1438
|
+
const path3 = resolve(deps.packageRoot, "skills", id, "SKILL.md");
|
|
1439
|
+
try {
|
|
1440
|
+
const source = await readFile2(path3, "utf8");
|
|
1441
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(
|
|
1442
|
+
source
|
|
1007
1443
|
);
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1444
|
+
if (!match) throw new Error("Missing YAML frontmatter");
|
|
1445
|
+
const metadata = parse(match[1]);
|
|
1446
|
+
if (metadata?.name !== id || typeof metadata?.description !== "string" || !metadata.description.trim()) {
|
|
1447
|
+
throw new Error(`Expected name ${id} and a non-empty description`);
|
|
1448
|
+
}
|
|
1449
|
+
skills.push({
|
|
1450
|
+
id,
|
|
1451
|
+
name: metadata.name,
|
|
1452
|
+
description: metadata.description,
|
|
1453
|
+
path: path3,
|
|
1454
|
+
content: match[2].trim()
|
|
1015
1455
|
});
|
|
1016
|
-
|
|
1456
|
+
} catch (cause) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`Cannot load bundled skill ${path3}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
1459
|
+
{ cause }
|
|
1460
|
+
);
|
|
1017
1461
|
}
|
|
1018
|
-
return { success: false };
|
|
1019
|
-
} catch (err) {
|
|
1020
|
-
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
1021
|
-
return { success: false };
|
|
1022
1462
|
}
|
|
1463
|
+
await ctx.skill.transform((editor) => {
|
|
1464
|
+
for (const skill of skills) {
|
|
1465
|
+
if (editor.get(skill.id))
|
|
1466
|
+
editor.update(skill.id, (existing) => {
|
|
1467
|
+
Object.assign(existing, skill);
|
|
1468
|
+
});
|
|
1469
|
+
else editor.add(skill);
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1023
1472
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1473
|
+
|
|
1474
|
+
// src/opencode/tool-rpc-handlers.ts
|
|
1475
|
+
async function registerToolRpc(ctx, deps) {
|
|
1476
|
+
const { store } = deps.toolState;
|
|
1477
|
+
async function state() {
|
|
1478
|
+
const snapshot = store.resolve();
|
|
1479
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1480
|
+
for (const tool of await ctx.tool.list()) {
|
|
1481
|
+
const action = tool.options?.permission ?? tool.id;
|
|
1482
|
+
const members = grouped.get(action) ?? [];
|
|
1483
|
+
members.push(tool.id);
|
|
1484
|
+
grouped.set(action, members);
|
|
1485
|
+
}
|
|
1486
|
+
if (!grouped.has("subagent")) grouped.set("subagent", []);
|
|
1487
|
+
const inventory = [...grouped].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([action, tools]) => ({
|
|
1488
|
+
action,
|
|
1489
|
+
tools: tools.sort(),
|
|
1490
|
+
locked: action === "subagent"
|
|
1491
|
+
}));
|
|
1492
|
+
const actions = inventory.map(({ action }) => action);
|
|
1493
|
+
return {
|
|
1494
|
+
specialists: [...SPECIALIST_ROLES],
|
|
1495
|
+
inventory,
|
|
1496
|
+
roles: SPECIALIST_ROLES.map((role) => {
|
|
1497
|
+
const scopeState = (scope) => ({
|
|
1498
|
+
present: Object.hasOwn(snapshot[scope].document.roles, role),
|
|
1499
|
+
revision: snapshot[scope].revision,
|
|
1500
|
+
overrides: (snapshot[scope].document.roles[role]?.overrides ?? []).map((entry) => ({
|
|
1501
|
+
...entry,
|
|
1502
|
+
matchCount: countActionMatches(entry.action, actions)
|
|
1503
|
+
}))
|
|
1504
|
+
});
|
|
1505
|
+
const base = translatePermissionRules(deps.agents[role].permission);
|
|
1506
|
+
return {
|
|
1507
|
+
role,
|
|
1508
|
+
effectiveScope: snapshot.effective[role]?.origin ?? "default",
|
|
1509
|
+
global: scopeState("global"),
|
|
1510
|
+
project: scopeState("project"),
|
|
1511
|
+
actions: inventory.map(({ action, locked }) => ({
|
|
1512
|
+
action,
|
|
1513
|
+
locked,
|
|
1514
|
+
baseEffect: evaluateAction(base, void 0, action).effect,
|
|
1515
|
+
...evaluateAction(base, snapshot.effective[role], action)
|
|
1516
|
+
}))
|
|
1517
|
+
};
|
|
1518
|
+
}),
|
|
1519
|
+
warnings: snapshot.warnings
|
|
1520
|
+
};
|
|
1029
1521
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1522
|
+
let pending = Promise.resolve();
|
|
1523
|
+
function handler(method, run) {
|
|
1524
|
+
return (raw) => {
|
|
1525
|
+
const operation = pending.then(async () => {
|
|
1526
|
+
const parsed = toolRpc.methods[method].input.safeParse(raw);
|
|
1527
|
+
if (!parsed.success)
|
|
1528
|
+
return {
|
|
1529
|
+
ok: false,
|
|
1530
|
+
error: { code: "INVALID_REQUEST", message: parsed.error.message }
|
|
1531
|
+
};
|
|
1532
|
+
try {
|
|
1533
|
+
return { ok: true, value: await run(parsed.data) };
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
return {
|
|
1536
|
+
ok: false,
|
|
1537
|
+
error: {
|
|
1538
|
+
code: error instanceof ToolsConflictError ? "TOOLS_CONFLICT" : "OPERATION_FAILED",
|
|
1539
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
pending = operation;
|
|
1545
|
+
return operation;
|
|
1546
|
+
};
|
|
1034
1547
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1548
|
+
async function reload() {
|
|
1549
|
+
deps.toolState.refresh();
|
|
1550
|
+
await ctx.agent.reload();
|
|
1551
|
+
return state();
|
|
1552
|
+
}
|
|
1553
|
+
const handlers = {
|
|
1554
|
+
state: handler("state", state),
|
|
1555
|
+
save: handler("save", async ({ scope, role, overrides, baseRevision }) => {
|
|
1556
|
+
store.save(scope, role, overrides, baseRevision);
|
|
1557
|
+
return reload();
|
|
1558
|
+
}),
|
|
1559
|
+
reset: handler("reset", async ({ scope, role, baseRevision }) => {
|
|
1560
|
+
store.reset(scope, role, baseRevision);
|
|
1561
|
+
return reload();
|
|
1562
|
+
})
|
|
1563
|
+
};
|
|
1564
|
+
await ctx.rpc.register(toolRpc, handlers);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/tools/ast-grep/tools.ts
|
|
1568
|
+
import { z } from "zod";
|
|
1569
|
+
|
|
1570
|
+
// src/tools/define-tool.ts
|
|
1571
|
+
function defineTool(definition) {
|
|
1572
|
+
return {
|
|
1573
|
+
name: definition.name,
|
|
1574
|
+
description: definition.description,
|
|
1575
|
+
input: definition.input,
|
|
1576
|
+
async execute(input, context) {
|
|
1577
|
+
context.signal.throwIfAborted();
|
|
1578
|
+
let onAbort = () => {
|
|
1579
|
+
};
|
|
1580
|
+
const aborted = new Promise((_, reject) => {
|
|
1581
|
+
onAbort = () => reject(context.signal.reason);
|
|
1582
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
1583
|
+
});
|
|
1584
|
+
try {
|
|
1585
|
+
const content = await Promise.race([
|
|
1586
|
+
definition.execute(input, context),
|
|
1587
|
+
aborted
|
|
1588
|
+
]);
|
|
1589
|
+
context.signal.throwIfAborted();
|
|
1590
|
+
if (definition.showOutput) {
|
|
1591
|
+
await context.progress({ output: content });
|
|
1592
|
+
context.signal.throwIfAborted();
|
|
1593
|
+
return { content, metadata: { output: content } };
|
|
1594
|
+
}
|
|
1595
|
+
return { content };
|
|
1596
|
+
} finally {
|
|
1597
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
1060
1598
|
}
|
|
1061
|
-
return true;
|
|
1062
1599
|
}
|
|
1063
|
-
|
|
1064
|
-
paneId
|
|
1065
|
-
});
|
|
1066
|
-
return false;
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
log("[tmux] closeTmuxPane: exception", { error: String(err) });
|
|
1069
|
-
return false;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
function startTmuxCheck() {
|
|
1073
|
-
if (!tmuxChecked) {
|
|
1074
|
-
getTmuxPath().catch(() => {
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1600
|
+
};
|
|
1077
1601
|
}
|
|
1078
1602
|
|
|
1603
|
+
// src/tools/ast-grep/cli.ts
|
|
1604
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1605
|
+
|
|
1606
|
+
// src/tools/ast-grep/constants.ts
|
|
1607
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1608
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
1609
|
+
import { createRequire as createRequire3 } from "module";
|
|
1610
|
+
import { dirname, join as join4 } from "path";
|
|
1611
|
+
|
|
1612
|
+
// src/tools/ast-grep/downloader.ts
|
|
1613
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
|
|
1614
|
+
import { createRequire as createRequire2 } from "module";
|
|
1615
|
+
import { homedir } from "os";
|
|
1616
|
+
import { join as join3 } from "path";
|
|
1617
|
+
|
|
1079
1618
|
// src/utils/zip-extractor.ts
|
|
1080
1619
|
import { release } from "os";
|
|
1081
1620
|
var WINDOWS_BUILD_WITH_TAR = 17134;
|
|
@@ -1166,7 +1705,7 @@ async function extractZip(archivePath, destDir) {
|
|
|
1166
1705
|
}
|
|
1167
1706
|
|
|
1168
1707
|
// src/utils/file-io.ts
|
|
1169
|
-
import { readFile, writeFile } from "fs/promises";
|
|
1708
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
1170
1709
|
async function writeTextFile(filePath, contents) {
|
|
1171
1710
|
await writeFile(
|
|
1172
1711
|
filePath,
|
|
@@ -1195,22 +1734,22 @@ var PLATFORM_MAP = {
|
|
|
1195
1734
|
"win32-arm64": { arch: "aarch64", os: "pc-windows-msvc" },
|
|
1196
1735
|
"win32-ia32": { arch: "i686", os: "pc-windows-msvc" }
|
|
1197
1736
|
};
|
|
1198
|
-
function
|
|
1737
|
+
function getCacheDir() {
|
|
1199
1738
|
if (process.platform === "win32") {
|
|
1200
1739
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
1201
|
-
const base2 = localAppData ||
|
|
1202
|
-
return
|
|
1740
|
+
const base2 = localAppData || join3(homedir(), "AppData", "Local");
|
|
1741
|
+
return join3(base2, "thoth-agents", "bin");
|
|
1203
1742
|
}
|
|
1204
1743
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
1205
|
-
const base = xdgCache ||
|
|
1206
|
-
return
|
|
1744
|
+
const base = xdgCache || join3(homedir(), ".cache");
|
|
1745
|
+
return join3(base, "thoth-agents", "bin");
|
|
1207
1746
|
}
|
|
1208
1747
|
function getBinaryName() {
|
|
1209
1748
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
1210
1749
|
}
|
|
1211
1750
|
function getCachedBinaryPath() {
|
|
1212
|
-
const binaryPath =
|
|
1213
|
-
return
|
|
1751
|
+
const binaryPath = join3(getCacheDir(), getBinaryName());
|
|
1752
|
+
return existsSync2(binaryPath) ? binaryPath : null;
|
|
1214
1753
|
}
|
|
1215
1754
|
async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
1216
1755
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
@@ -1221,10 +1760,10 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
1221
1760
|
);
|
|
1222
1761
|
return null;
|
|
1223
1762
|
}
|
|
1224
|
-
const cacheDir =
|
|
1763
|
+
const cacheDir = getCacheDir();
|
|
1225
1764
|
const binaryName = getBinaryName();
|
|
1226
|
-
const binaryPath =
|
|
1227
|
-
if (
|
|
1765
|
+
const binaryPath = join3(cacheDir, binaryName);
|
|
1766
|
+
if (existsSync2(binaryPath)) {
|
|
1228
1767
|
return binaryPath;
|
|
1229
1768
|
}
|
|
1230
1769
|
const { arch, os: os2 } = platformInfo;
|
|
@@ -1232,21 +1771,21 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
1232
1771
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
1233
1772
|
console.log(`[thoth-agents] Downloading ast-grep binary...`);
|
|
1234
1773
|
try {
|
|
1235
|
-
if (!
|
|
1774
|
+
if (!existsSync2(cacheDir)) {
|
|
1236
1775
|
mkdirSync(cacheDir, { recursive: true });
|
|
1237
1776
|
}
|
|
1238
1777
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
1239
1778
|
if (!response.ok) {
|
|
1240
1779
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1241
1780
|
}
|
|
1242
|
-
const archivePath =
|
|
1781
|
+
const archivePath = join3(cacheDir, assetName);
|
|
1243
1782
|
const arrayBuffer = await response.arrayBuffer();
|
|
1244
1783
|
await writeTextFile(archivePath, arrayBuffer);
|
|
1245
1784
|
await extractZip(archivePath, cacheDir);
|
|
1246
|
-
if (
|
|
1785
|
+
if (existsSync2(archivePath)) {
|
|
1247
1786
|
unlinkSync(archivePath);
|
|
1248
1787
|
}
|
|
1249
|
-
if (process.platform !== "win32" &&
|
|
1788
|
+
if (process.platform !== "win32" && existsSync2(binaryPath)) {
|
|
1250
1789
|
chmodSync(binaryPath, 493);
|
|
1251
1790
|
}
|
|
1252
1791
|
console.log(`[thoth-agents] ast-grep binary ready.`);
|
|
@@ -1300,7 +1839,7 @@ var CLI_LANGUAGES = [
|
|
|
1300
1839
|
var MIN_BINARY_SIZE = 1e4;
|
|
1301
1840
|
function isValidBinary(filePath) {
|
|
1302
1841
|
try {
|
|
1303
|
-
return
|
|
1842
|
+
return statSync(filePath).size > MIN_BINARY_SIZE;
|
|
1304
1843
|
} catch {
|
|
1305
1844
|
return false;
|
|
1306
1845
|
}
|
|
@@ -1329,9 +1868,9 @@ function findSgCliPathSync() {
|
|
|
1329
1868
|
try {
|
|
1330
1869
|
const require3 = createRequire3(import.meta.url);
|
|
1331
1870
|
const cliPkgPath = require3.resolve("@ast-grep/cli/package.json");
|
|
1332
|
-
const cliDir =
|
|
1333
|
-
const sgPath =
|
|
1334
|
-
if (
|
|
1871
|
+
const cliDir = dirname(cliPkgPath);
|
|
1872
|
+
const sgPath = join4(cliDir, binaryName);
|
|
1873
|
+
if (existsSync3(sgPath) && isValidBinary(sgPath)) {
|
|
1335
1874
|
return sgPath;
|
|
1336
1875
|
}
|
|
1337
1876
|
} catch {
|
|
@@ -1341,10 +1880,10 @@ function findSgCliPathSync() {
|
|
|
1341
1880
|
try {
|
|
1342
1881
|
const require3 = createRequire3(import.meta.url);
|
|
1343
1882
|
const pkgPath = require3.resolve(`${platformPkg}/package.json`);
|
|
1344
|
-
const pkgDir =
|
|
1883
|
+
const pkgDir = dirname(pkgPath);
|
|
1345
1884
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
1346
|
-
const binaryPath =
|
|
1347
|
-
if (
|
|
1885
|
+
const binaryPath = join4(pkgDir, astGrepName);
|
|
1886
|
+
if (existsSync3(binaryPath) && isValidBinary(binaryPath)) {
|
|
1348
1887
|
return binaryPath;
|
|
1349
1888
|
}
|
|
1350
1889
|
} catch {
|
|
@@ -1353,7 +1892,7 @@ function findSgCliPathSync() {
|
|
|
1353
1892
|
if (process.platform === "darwin") {
|
|
1354
1893
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
1355
1894
|
for (const path3 of homebrewPaths) {
|
|
1356
|
-
if (
|
|
1895
|
+
if (existsSync3(path3) && isValidBinary(path3)) {
|
|
1357
1896
|
return path3;
|
|
1358
1897
|
}
|
|
1359
1898
|
}
|
|
@@ -1382,7 +1921,7 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
1382
1921
|
var initPromise = null;
|
|
1383
1922
|
async function getAstGrepPath() {
|
|
1384
1923
|
const currentPath = getSgCliPath();
|
|
1385
|
-
if (currentPath !== "sg" &&
|
|
1924
|
+
if (currentPath !== "sg" && existsSync4(currentPath)) {
|
|
1386
1925
|
return currentPath;
|
|
1387
1926
|
}
|
|
1388
1927
|
if (initPromise) {
|
|
@@ -1390,7 +1929,7 @@ async function getAstGrepPath() {
|
|
|
1390
1929
|
}
|
|
1391
1930
|
initPromise = (async () => {
|
|
1392
1931
|
const syncPath = findSgCliPathSync();
|
|
1393
|
-
if (syncPath &&
|
|
1932
|
+
if (syncPath && existsSync4(syncPath)) {
|
|
1394
1933
|
setSgCliPath(syncPath);
|
|
1395
1934
|
return syncPath;
|
|
1396
1935
|
}
|
|
@@ -1404,6 +1943,7 @@ async function getAstGrepPath() {
|
|
|
1404
1943
|
return initPromise;
|
|
1405
1944
|
}
|
|
1406
1945
|
async function runSg(options) {
|
|
1946
|
+
options.signal?.throwIfAborted();
|
|
1407
1947
|
const args = [
|
|
1408
1948
|
"run",
|
|
1409
1949
|
"-p",
|
|
@@ -1429,18 +1969,21 @@ async function runSg(options) {
|
|
|
1429
1969
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
1430
1970
|
args.push(...paths);
|
|
1431
1971
|
let cliPath = getSgCliPath();
|
|
1432
|
-
if (!
|
|
1972
|
+
if (!existsSync4(cliPath) && cliPath !== "sg") {
|
|
1433
1973
|
const downloadedPath = await getAstGrepPath();
|
|
1434
1974
|
if (downloadedPath) {
|
|
1435
1975
|
cliPath = downloadedPath;
|
|
1436
1976
|
}
|
|
1437
1977
|
}
|
|
1978
|
+
options.signal?.throwIfAborted();
|
|
1438
1979
|
const timeout = DEFAULT_TIMEOUT_MS;
|
|
1439
1980
|
const proc = spawn([cliPath, ...args], {
|
|
1440
1981
|
windowsHide: true,
|
|
1441
1982
|
stdout: "pipe",
|
|
1442
1983
|
stderr: "pipe"
|
|
1443
1984
|
});
|
|
1985
|
+
const onAbort = () => proc.kill();
|
|
1986
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1444
1987
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1445
1988
|
const id = setTimeout(() => {
|
|
1446
1989
|
proc.kill();
|
|
@@ -1459,6 +2002,7 @@ async function runSg(options) {
|
|
|
1459
2002
|
stderr = await new Response(proc.stderr).text();
|
|
1460
2003
|
exitCode = await proc.exited;
|
|
1461
2004
|
} catch (e) {
|
|
2005
|
+
options.signal?.throwIfAborted();
|
|
1462
2006
|
const error = e;
|
|
1463
2007
|
if (error.message?.includes("timeout")) {
|
|
1464
2008
|
return {
|
|
@@ -1495,7 +2039,10 @@ Auto-download failed. Manual install options:
|
|
|
1495
2039
|
truncated: false,
|
|
1496
2040
|
error: `Failed to spawn ast-grep: ${error.message}`
|
|
1497
2041
|
};
|
|
2042
|
+
} finally {
|
|
2043
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1498
2044
|
}
|
|
2045
|
+
options.signal?.throwIfAborted();
|
|
1499
2046
|
if (exitCode !== 0 && stdout.trim() === "") {
|
|
1500
2047
|
if (stderr.includes("No files found")) {
|
|
1501
2048
|
return { matches: [], totalMatches: 0, truncated: false };
|
|
@@ -1649,24 +2196,23 @@ function getEmptyResultHint(pattern, lang) {
|
|
|
1649
2196
|
}
|
|
1650
2197
|
|
|
1651
2198
|
// src/tools/ast-grep/tools.ts
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
}
|
|
1656
|
-
var ast_grep_search = tool({
|
|
2199
|
+
var ast_grep_search = defineTool({
|
|
2200
|
+
showOutput: true,
|
|
2201
|
+
name: "ast_grep_search",
|
|
1657
2202
|
description: "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. Use meta-variables: $VAR (single node), $$$ (multiple nodes). IMPORTANT: Patterns must be complete AST nodes (valid code). For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
|
|
1658
|
-
|
|
1659
|
-
pattern:
|
|
2203
|
+
input: z.object({
|
|
2204
|
+
pattern: z.string().describe(
|
|
1660
2205
|
"AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."
|
|
1661
2206
|
),
|
|
1662
|
-
lang:
|
|
1663
|
-
paths:
|
|
1664
|
-
globs:
|
|
1665
|
-
context:
|
|
1666
|
-
},
|
|
2207
|
+
lang: z.enum(CLI_LANGUAGES).describe("Target language"),
|
|
2208
|
+
paths: z.array(z.string()).optional().describe("Paths to search (default: ['.'])"),
|
|
2209
|
+
globs: z.array(z.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
|
2210
|
+
context: z.number().optional().describe("Context lines around match")
|
|
2211
|
+
}),
|
|
1667
2212
|
execute: async (args, context) => {
|
|
1668
2213
|
try {
|
|
1669
2214
|
const result = await runSg({
|
|
2215
|
+
signal: context.signal,
|
|
1670
2216
|
pattern: args.pattern,
|
|
1671
2217
|
lang: args.lang,
|
|
1672
2218
|
paths: args.paths,
|
|
@@ -1682,28 +2228,28 @@ var ast_grep_search = tool({
|
|
|
1682
2228
|
${hint}`;
|
|
1683
2229
|
}
|
|
1684
2230
|
}
|
|
1685
|
-
showOutputToUser(context, output);
|
|
1686
2231
|
return output;
|
|
1687
2232
|
} catch (e) {
|
|
1688
|
-
|
|
1689
|
-
showOutputToUser(context, output);
|
|
1690
|
-
return output;
|
|
2233
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1691
2234
|
}
|
|
1692
2235
|
}
|
|
1693
2236
|
});
|
|
1694
|
-
var ast_grep_replace =
|
|
2237
|
+
var ast_grep_replace = defineTool({
|
|
2238
|
+
showOutput: true,
|
|
2239
|
+
name: "ast_grep_replace",
|
|
1695
2240
|
description: "Replace code patterns across filesystem with AST-aware rewriting. Dry-run by default. Use meta-variables in rewrite to preserve matched content. Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
|
|
1696
|
-
|
|
1697
|
-
pattern:
|
|
1698
|
-
rewrite:
|
|
1699
|
-
lang:
|
|
1700
|
-
paths:
|
|
1701
|
-
globs:
|
|
1702
|
-
dryRun:
|
|
1703
|
-
},
|
|
2241
|
+
input: z.object({
|
|
2242
|
+
pattern: z.string().describe("AST pattern to match"),
|
|
2243
|
+
rewrite: z.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
|
2244
|
+
lang: z.enum(CLI_LANGUAGES).describe("Target language"),
|
|
2245
|
+
paths: z.array(z.string()).optional().describe("Paths to search"),
|
|
2246
|
+
globs: z.array(z.string()).optional().describe("Include/exclude globs"),
|
|
2247
|
+
dryRun: z.boolean().optional().describe("Preview changes without applying (default: true)")
|
|
2248
|
+
}),
|
|
1704
2249
|
execute: async (args, context) => {
|
|
1705
2250
|
try {
|
|
1706
2251
|
const result = await runSg({
|
|
2252
|
+
signal: context.signal,
|
|
1707
2253
|
pattern: args.pattern,
|
|
1708
2254
|
rewrite: args.rewrite,
|
|
1709
2255
|
lang: args.lang,
|
|
@@ -1711,20 +2257,16 @@ var ast_grep_replace = tool({
|
|
|
1711
2257
|
globs: args.globs,
|
|
1712
2258
|
updateAll: args.dryRun === false
|
|
1713
2259
|
});
|
|
1714
|
-
|
|
1715
|
-
showOutputToUser(context, output);
|
|
1716
|
-
return output;
|
|
2260
|
+
return formatReplaceResult(result, args.dryRun !== false);
|
|
1717
2261
|
} catch (e) {
|
|
1718
|
-
|
|
1719
|
-
showOutputToUser(context, output);
|
|
1720
|
-
return output;
|
|
2262
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1721
2263
|
}
|
|
1722
2264
|
}
|
|
1723
2265
|
});
|
|
1724
2266
|
|
|
1725
2267
|
// src/tools/lsp/client.ts
|
|
1726
|
-
import { readFileSync as
|
|
1727
|
-
import { extname, resolve as
|
|
2268
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2269
|
+
import { extname, resolve as resolve3 } from "path";
|
|
1728
2270
|
import { Readable, Writable } from "stream";
|
|
1729
2271
|
import { pathToFileURL } from "url";
|
|
1730
2272
|
import {
|
|
@@ -1734,31 +2276,13 @@ import {
|
|
|
1734
2276
|
} from "vscode-jsonrpc/node.js";
|
|
1735
2277
|
|
|
1736
2278
|
// src/tools/lsp/config.ts
|
|
1737
|
-
import { existsSync as
|
|
1738
|
-
import { homedir as
|
|
1739
|
-
import { join as
|
|
2279
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2280
|
+
import { homedir as homedir2 } from "os";
|
|
2281
|
+
import { join as join5 } from "path";
|
|
1740
2282
|
import whichSync from "which";
|
|
1741
2283
|
|
|
1742
2284
|
// src/tools/lsp/config-store.ts
|
|
1743
2285
|
var userConfig = /* @__PURE__ */ new Map();
|
|
1744
|
-
function setUserLspConfig(config) {
|
|
1745
|
-
userConfig.clear();
|
|
1746
|
-
if (config) {
|
|
1747
|
-
for (const [id, server] of Object.entries(config)) {
|
|
1748
|
-
if (server && typeof server === "object") {
|
|
1749
|
-
const s = server;
|
|
1750
|
-
userConfig.set(id, {
|
|
1751
|
-
id,
|
|
1752
|
-
command: s.command,
|
|
1753
|
-
extensions: s.extensions,
|
|
1754
|
-
disabled: s.disabled,
|
|
1755
|
-
env: s.env,
|
|
1756
|
-
initialization: s.initialization
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
2286
|
function getAllUserLspConfigs() {
|
|
1763
2287
|
return new Map(userConfig);
|
|
1764
2288
|
}
|
|
@@ -1767,8 +2291,8 @@ function hasUserLspConfig() {
|
|
|
1767
2291
|
}
|
|
1768
2292
|
|
|
1769
2293
|
// src/tools/lsp/constants.ts
|
|
1770
|
-
import { existsSync as
|
|
1771
|
-
import { dirname as
|
|
2294
|
+
import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
2295
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1772
2296
|
var SEVERITY_MAP = {
|
|
1773
2297
|
1: "error",
|
|
1774
2298
|
2: "warning",
|
|
@@ -1785,20 +2309,20 @@ var LOCK_FILE_PATTERNS = [
|
|
|
1785
2309
|
"bun.lock"
|
|
1786
2310
|
];
|
|
1787
2311
|
function* walkUpDirectories(start, stop) {
|
|
1788
|
-
let dir =
|
|
2312
|
+
let dir = resolve2(start);
|
|
1789
2313
|
try {
|
|
1790
|
-
if (!
|
|
1791
|
-
dir =
|
|
2314
|
+
if (!statSync2(dir).isDirectory()) {
|
|
2315
|
+
dir = dirname2(dir);
|
|
1792
2316
|
}
|
|
1793
2317
|
} catch {
|
|
1794
|
-
dir =
|
|
2318
|
+
dir = dirname2(dir);
|
|
1795
2319
|
}
|
|
1796
2320
|
let prevDir = "";
|
|
1797
2321
|
while (dir !== prevDir && dir !== "/") {
|
|
1798
2322
|
yield dir;
|
|
1799
2323
|
prevDir = dir;
|
|
1800
2324
|
if (dir === stop) break;
|
|
1801
|
-
dir =
|
|
2325
|
+
dir = dirname2(dir);
|
|
1802
2326
|
}
|
|
1803
2327
|
}
|
|
1804
2328
|
function NearestRoot(includePatterns, excludePatterns) {
|
|
@@ -1807,7 +2331,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
1807
2331
|
if (excludePatterns) {
|
|
1808
2332
|
for (const dir of walkUpDirectories(file, cwd)) {
|
|
1809
2333
|
for (const pattern of excludePatterns) {
|
|
1810
|
-
if (
|
|
2334
|
+
if (existsSync5(`${dir}/${pattern}`)) {
|
|
1811
2335
|
return void 0;
|
|
1812
2336
|
}
|
|
1813
2337
|
}
|
|
@@ -1826,7 +2350,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
1826
2350
|
}
|
|
1827
2351
|
} catch {
|
|
1828
2352
|
}
|
|
1829
|
-
} else if (
|
|
2353
|
+
} else if (existsSync5(`${dir}/${pattern}`)) {
|
|
1830
2354
|
return dir;
|
|
1831
2355
|
}
|
|
1832
2356
|
}
|
|
@@ -2448,11 +2972,11 @@ function isServerInstalled(command) {
|
|
|
2448
2972
|
if (command.length === 0) return false;
|
|
2449
2973
|
const cmd = command[0];
|
|
2450
2974
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2451
|
-
return
|
|
2975
|
+
return existsSync6(cmd);
|
|
2452
2976
|
}
|
|
2453
2977
|
const isWindows = process.platform === "win32";
|
|
2454
2978
|
const ext = isWindows ? ".exe" : "";
|
|
2455
|
-
const opencodeBin =
|
|
2979
|
+
const opencodeBin = join5(homedir2(), ".config", "opencode", "bin");
|
|
2456
2980
|
const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
|
|
2457
2981
|
const result = whichSync.sync(cmd, {
|
|
2458
2982
|
path: searchPath,
|
|
@@ -2463,8 +2987,8 @@ function isServerInstalled(command) {
|
|
|
2463
2987
|
return true;
|
|
2464
2988
|
}
|
|
2465
2989
|
const cwd = process.cwd();
|
|
2466
|
-
const localBin =
|
|
2467
|
-
if (
|
|
2990
|
+
const localBin = join5(cwd, "node_modules", ".bin", cmd);
|
|
2991
|
+
if (existsSync6(localBin) || existsSync6(localBin + ext)) {
|
|
2468
2992
|
return true;
|
|
2469
2993
|
}
|
|
2470
2994
|
return false;
|
|
@@ -2725,7 +3249,7 @@ var LSPClient = class {
|
|
|
2725
3249
|
this.processExited = true;
|
|
2726
3250
|
});
|
|
2727
3251
|
this.connection.listen();
|
|
2728
|
-
await new Promise((
|
|
3252
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
2729
3253
|
if (this.proc.exitCode !== null) {
|
|
2730
3254
|
const stderr = this.stderrBuffer.join("\n");
|
|
2731
3255
|
log("[lsp] LSPClient.start: server exited immediately", {
|
|
@@ -2800,12 +3324,12 @@ stderr: ${stderr}` : "")
|
|
|
2800
3324
|
log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
|
|
2801
3325
|
}
|
|
2802
3326
|
async openFile(filePath) {
|
|
2803
|
-
const absPath =
|
|
3327
|
+
const absPath = resolve3(filePath);
|
|
2804
3328
|
if (this.openedFiles.has(absPath)) {
|
|
2805
3329
|
log("[lsp] openFile: already open, skipping", { filePath: absPath });
|
|
2806
3330
|
return;
|
|
2807
3331
|
}
|
|
2808
|
-
const text =
|
|
3332
|
+
const text = readFileSync2(absPath, "utf-8");
|
|
2809
3333
|
const ext = extname(absPath);
|
|
2810
3334
|
const languageId = getLanguageId(ext);
|
|
2811
3335
|
log("[lsp] openFile: opening document", {
|
|
@@ -2825,7 +3349,7 @@ stderr: ${stderr}` : "")
|
|
|
2825
3349
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
2826
3350
|
}
|
|
2827
3351
|
async definition(filePath, line, character) {
|
|
2828
|
-
const absPath =
|
|
3352
|
+
const absPath = resolve3(filePath);
|
|
2829
3353
|
await this.openFile(absPath);
|
|
2830
3354
|
return this.connection?.sendRequest("textDocument/definition", {
|
|
2831
3355
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2833,7 +3357,7 @@ stderr: ${stderr}` : "")
|
|
|
2833
3357
|
});
|
|
2834
3358
|
}
|
|
2835
3359
|
async references(filePath, line, character, includeDeclaration = true) {
|
|
2836
|
-
const absPath =
|
|
3360
|
+
const absPath = resolve3(filePath);
|
|
2837
3361
|
await this.openFile(absPath);
|
|
2838
3362
|
return this.connection?.sendRequest("textDocument/references", {
|
|
2839
3363
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2842,7 +3366,7 @@ stderr: ${stderr}` : "")
|
|
|
2842
3366
|
});
|
|
2843
3367
|
}
|
|
2844
3368
|
async diagnostics(filePath) {
|
|
2845
|
-
const absPath =
|
|
3369
|
+
const absPath = resolve3(filePath);
|
|
2846
3370
|
const uri = pathToFileURL(absPath).href;
|
|
2847
3371
|
await this.openFile(absPath);
|
|
2848
3372
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -2861,7 +3385,7 @@ stderr: ${stderr}` : "")
|
|
|
2861
3385
|
return { items: this.diagnosticsStore.get(uri) ?? [] };
|
|
2862
3386
|
}
|
|
2863
3387
|
async rename(filePath, line, character, newName) {
|
|
2864
|
-
const absPath =
|
|
3388
|
+
const absPath = resolve3(filePath);
|
|
2865
3389
|
await this.openFile(absPath);
|
|
2866
3390
|
return this.connection?.sendRequest("textDocument/rename", {
|
|
2867
3391
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2892,23 +3416,23 @@ stderr: ${stderr}` : "")
|
|
|
2892
3416
|
};
|
|
2893
3417
|
|
|
2894
3418
|
// src/tools/lsp/tools.ts
|
|
2895
|
-
import {
|
|
3419
|
+
import { z as z2 } from "zod";
|
|
2896
3420
|
|
|
2897
3421
|
// src/tools/lsp/utils.ts
|
|
2898
3422
|
import {
|
|
2899
|
-
existsSync as
|
|
2900
|
-
readFileSync as
|
|
2901
|
-
statSync as
|
|
3423
|
+
existsSync as existsSync7,
|
|
3424
|
+
readFileSync as readFileSync3,
|
|
3425
|
+
statSync as statSync3,
|
|
2902
3426
|
unlinkSync as unlinkSync2,
|
|
2903
3427
|
writeFileSync
|
|
2904
3428
|
} from "fs";
|
|
2905
|
-
import { dirname as
|
|
3429
|
+
import { dirname as dirname3, extname as extname2, join as join6, resolve as resolve4 } from "path";
|
|
2906
3430
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2907
3431
|
function findServerProjectRoot(filePath, server) {
|
|
2908
3432
|
if (server.root) {
|
|
2909
|
-
return server.root(filePath) ??
|
|
3433
|
+
return server.root(filePath) ?? dirname3(resolve4(filePath));
|
|
2910
3434
|
}
|
|
2911
|
-
return
|
|
3435
|
+
return dirname3(resolve4(filePath));
|
|
2912
3436
|
}
|
|
2913
3437
|
function uriToPath(uri) {
|
|
2914
3438
|
return fileURLToPath2(uri);
|
|
@@ -2926,7 +3450,7 @@ function formatServerLookupError(result) {
|
|
|
2926
3450
|
return `No LSP server configured for extension: ${result.extension}`;
|
|
2927
3451
|
}
|
|
2928
3452
|
async function withLspClient(filePath, fn) {
|
|
2929
|
-
const absPath =
|
|
3453
|
+
const absPath = resolve4(filePath);
|
|
2930
3454
|
const ext = extname2(absPath);
|
|
2931
3455
|
const result = findServerForExtension(ext);
|
|
2932
3456
|
if (result.status !== "found") {
|
|
@@ -2937,7 +3461,7 @@ async function withLspClient(filePath, fn) {
|
|
|
2937
3461
|
throw new Error(formatServerLookupError(result));
|
|
2938
3462
|
}
|
|
2939
3463
|
const server = result.server;
|
|
2940
|
-
const root = findServerProjectRoot(absPath, server) ??
|
|
3464
|
+
const root = findServerProjectRoot(absPath, server) ?? dirname3(absPath);
|
|
2941
3465
|
log("[lsp] withLspClient: acquiring client", {
|
|
2942
3466
|
filePath: absPath,
|
|
2943
3467
|
server: server.id,
|
|
@@ -3008,7 +3532,7 @@ function filterDiagnosticsBySeverity(diagnostics, severityFilter) {
|
|
|
3008
3532
|
}
|
|
3009
3533
|
function applyTextEditsToFile(filePath, edits) {
|
|
3010
3534
|
try {
|
|
3011
|
-
const content =
|
|
3535
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
3012
3536
|
const lines = content.split("\n");
|
|
3013
3537
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
3014
3538
|
if (b.range.start.line !== a.range.start.line) {
|
|
@@ -3092,7 +3616,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
3092
3616
|
try {
|
|
3093
3617
|
const oldPath = uriToPath(change.oldUri);
|
|
3094
3618
|
const newPath = uriToPath(change.newUri);
|
|
3095
|
-
const content =
|
|
3619
|
+
const content = readFileSync3(oldPath, "utf-8");
|
|
3096
3620
|
writeFileSync(newPath, content, "utf-8");
|
|
3097
3621
|
unlinkSync2(oldPath);
|
|
3098
3622
|
result.filesModified.push(newPath);
|
|
@@ -3154,16 +3678,18 @@ function formatApplyResult(result) {
|
|
|
3154
3678
|
|
|
3155
3679
|
// src/tools/lsp/tools.ts
|
|
3156
3680
|
var formatError = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
3157
|
-
var lsp_goto_definition =
|
|
3681
|
+
var lsp_goto_definition = defineTool({
|
|
3682
|
+
name: "lsp_goto_definition",
|
|
3158
3683
|
description: "Jump to symbol definition. Find WHERE something is defined.",
|
|
3159
|
-
|
|
3160
|
-
filePath:
|
|
3161
|
-
line:
|
|
3162
|
-
character:
|
|
3163
|
-
},
|
|
3164
|
-
execute: async (args) => {
|
|
3684
|
+
input: z2.object({
|
|
3685
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
3686
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
3687
|
+
character: z2.number().min(0).describe("0-based character offset")
|
|
3688
|
+
}),
|
|
3689
|
+
execute: async (args, context) => {
|
|
3165
3690
|
try {
|
|
3166
3691
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
3692
|
+
context.signal.throwIfAborted();
|
|
3167
3693
|
return await client.definition(
|
|
3168
3694
|
args.filePath,
|
|
3169
3695
|
args.line,
|
|
@@ -3183,17 +3709,19 @@ var lsp_goto_definition = tool2({
|
|
|
3183
3709
|
}
|
|
3184
3710
|
}
|
|
3185
3711
|
});
|
|
3186
|
-
var lsp_find_references =
|
|
3712
|
+
var lsp_find_references = defineTool({
|
|
3713
|
+
name: "lsp_find_references",
|
|
3187
3714
|
description: "Find ALL usages/references of a symbol across the entire workspace.",
|
|
3188
|
-
|
|
3189
|
-
filePath:
|
|
3190
|
-
line:
|
|
3191
|
-
character:
|
|
3192
|
-
includeDeclaration:
|
|
3193
|
-
},
|
|
3194
|
-
execute: async (args) => {
|
|
3715
|
+
input: z2.object({
|
|
3716
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
3717
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
3718
|
+
character: z2.number().min(0).describe("0-based character offset"),
|
|
3719
|
+
includeDeclaration: z2.boolean().optional().describe("Include the declaration itself")
|
|
3720
|
+
}),
|
|
3721
|
+
execute: async (args, context) => {
|
|
3195
3722
|
try {
|
|
3196
3723
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
3724
|
+
context.signal.throwIfAborted();
|
|
3197
3725
|
return await client.references(
|
|
3198
3726
|
args.filePath,
|
|
3199
3727
|
args.line,
|
|
@@ -3219,15 +3747,17 @@ var lsp_find_references = tool2({
|
|
|
3219
3747
|
}
|
|
3220
3748
|
}
|
|
3221
3749
|
});
|
|
3222
|
-
var lsp_diagnostics =
|
|
3750
|
+
var lsp_diagnostics = defineTool({
|
|
3751
|
+
name: "lsp_diagnostics",
|
|
3223
3752
|
description: "Get errors, warnings, hints from language server BEFORE running build.",
|
|
3224
|
-
|
|
3225
|
-
filePath:
|
|
3226
|
-
severity:
|
|
3227
|
-
},
|
|
3228
|
-
execute: async (args) => {
|
|
3753
|
+
input: z2.object({
|
|
3754
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
3755
|
+
severity: z2.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity level")
|
|
3756
|
+
}),
|
|
3757
|
+
execute: async (args, context) => {
|
|
3229
3758
|
try {
|
|
3230
3759
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
3760
|
+
context.signal.throwIfAborted();
|
|
3231
3761
|
return await client.diagnostics(args.filePath);
|
|
3232
3762
|
});
|
|
3233
3763
|
let diagnostics = [];
|
|
@@ -3257,17 +3787,19 @@ var lsp_diagnostics = tool2({
|
|
|
3257
3787
|
}
|
|
3258
3788
|
}
|
|
3259
3789
|
});
|
|
3260
|
-
var lsp_rename =
|
|
3790
|
+
var lsp_rename = defineTool({
|
|
3791
|
+
name: "lsp_rename",
|
|
3261
3792
|
description: "Rename symbol across entire workspace. APPLIES changes to all files.",
|
|
3262
|
-
|
|
3263
|
-
filePath:
|
|
3264
|
-
line:
|
|
3265
|
-
character:
|
|
3266
|
-
newName:
|
|
3267
|
-
},
|
|
3268
|
-
execute: async (args) => {
|
|
3793
|
+
input: z2.object({
|
|
3794
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
3795
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
3796
|
+
character: z2.number().min(0).describe("0-based character offset"),
|
|
3797
|
+
newName: z2.string().describe("New symbol name")
|
|
3798
|
+
}),
|
|
3799
|
+
execute: async (args, context) => {
|
|
3269
3800
|
try {
|
|
3270
3801
|
const edit = await withLspClient(args.filePath, async (client) => {
|
|
3802
|
+
context.signal.throwIfAborted();
|
|
3271
3803
|
return await client.rename(
|
|
3272
3804
|
args.filePath,
|
|
3273
3805
|
args.line,
|
|
@@ -3275,6 +3807,7 @@ var lsp_rename = tool2({
|
|
|
3275
3807
|
args.newName
|
|
3276
3808
|
);
|
|
3277
3809
|
});
|
|
3810
|
+
context.signal.throwIfAborted();
|
|
3278
3811
|
const result = applyWorkspaceEdit(edit);
|
|
3279
3812
|
return formatApplyResult(result);
|
|
3280
3813
|
} catch (e) {
|
|
@@ -3283,375 +3816,37 @@ var lsp_rename = tool2({
|
|
|
3283
3816
|
}
|
|
3284
3817
|
});
|
|
3285
3818
|
|
|
3286
|
-
// src/
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
this.client = ctx.client;
|
|
3298
|
-
this.tmuxConfig = tmuxConfig;
|
|
3299
|
-
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
3300
|
-
this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
|
|
3301
|
-
this.enabled = tmuxConfig.enabled && isInsideTmux();
|
|
3302
|
-
log("[tmux-session-manager] initialized", {
|
|
3303
|
-
enabled: this.enabled,
|
|
3304
|
-
tmuxConfig: this.tmuxConfig,
|
|
3305
|
-
serverUrl: this.serverUrl
|
|
3306
|
-
});
|
|
3307
|
-
}
|
|
3308
|
-
/**
|
|
3309
|
-
* Handle session.created events.
|
|
3310
|
-
* Spawns a tmux pane for child sessions (those with parentID).
|
|
3311
|
-
*/
|
|
3312
|
-
async onSessionCreated(event) {
|
|
3313
|
-
if (!this.enabled) return;
|
|
3314
|
-
if (event.type !== "session.created") return;
|
|
3315
|
-
const info = event.properties?.info;
|
|
3316
|
-
if (!info?.id || !info?.parentID) {
|
|
3317
|
-
return;
|
|
3318
|
-
}
|
|
3319
|
-
const sessionId = info.id;
|
|
3320
|
-
const parentId = info.parentID;
|
|
3321
|
-
const title = info.title ?? "Subagent";
|
|
3322
|
-
if (this.sessions.has(sessionId)) {
|
|
3323
|
-
log("[tmux-session-manager] session already tracked", { sessionId });
|
|
3324
|
-
return;
|
|
3325
|
-
}
|
|
3326
|
-
log("[tmux-session-manager] child session created, spawning pane", {
|
|
3327
|
-
sessionId,
|
|
3328
|
-
parentId,
|
|
3329
|
-
title
|
|
3330
|
-
});
|
|
3331
|
-
const paneResult = await spawnTmuxPane(
|
|
3332
|
-
sessionId,
|
|
3333
|
-
title,
|
|
3334
|
-
this.tmuxConfig,
|
|
3335
|
-
this.serverUrl
|
|
3336
|
-
).catch((err) => {
|
|
3337
|
-
log("[tmux-session-manager] failed to spawn pane", {
|
|
3338
|
-
error: String(err)
|
|
3339
|
-
});
|
|
3340
|
-
return { success: false, paneId: void 0 };
|
|
3341
|
-
});
|
|
3342
|
-
if (paneResult.success && paneResult.paneId) {
|
|
3343
|
-
const now = Date.now();
|
|
3344
|
-
this.sessions.set(sessionId, {
|
|
3345
|
-
sessionId,
|
|
3346
|
-
paneId: paneResult.paneId,
|
|
3347
|
-
parentId,
|
|
3348
|
-
title,
|
|
3349
|
-
createdAt: now,
|
|
3350
|
-
lastSeenAt: now
|
|
3351
|
-
});
|
|
3352
|
-
log("[tmux-session-manager] pane spawned", {
|
|
3353
|
-
sessionId,
|
|
3354
|
-
paneId: paneResult.paneId
|
|
3355
|
-
});
|
|
3356
|
-
this.startPolling();
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
/**
|
|
3360
|
-
* Handle session.status events for completion detection.
|
|
3361
|
-
* Uses session.status instead of deprecated session.idle.
|
|
3362
|
-
*
|
|
3363
|
-
* When a session becomes idle (completed), close its pane.
|
|
3364
|
-
*/
|
|
3365
|
-
async onSessionStatus(event) {
|
|
3366
|
-
if (!this.enabled) return;
|
|
3367
|
-
if (event.type !== "session.status") return;
|
|
3368
|
-
const sessionId = event.properties?.sessionID;
|
|
3369
|
-
if (!sessionId) return;
|
|
3370
|
-
if (event.properties?.status?.type === "idle") {
|
|
3371
|
-
await this.closeSession(sessionId);
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
/**
|
|
3375
|
-
* Handle session.deleted events.
|
|
3376
|
-
* When a session is deleted, close its tmux pane immediately.
|
|
3377
|
-
*/
|
|
3378
|
-
async onSessionDeleted(event) {
|
|
3379
|
-
if (!this.enabled) return;
|
|
3380
|
-
if (event.type !== "session.deleted") return;
|
|
3381
|
-
const sessionId = event.properties?.sessionID;
|
|
3382
|
-
if (!sessionId) return;
|
|
3383
|
-
log("[tmux-session-manager] session deleted, closing pane", {
|
|
3384
|
-
sessionId
|
|
3385
|
-
});
|
|
3386
|
-
await this.closeSession(sessionId);
|
|
3387
|
-
}
|
|
3388
|
-
startPolling() {
|
|
3389
|
-
if (this.pollInterval) return;
|
|
3390
|
-
this.pollInterval = setInterval(
|
|
3391
|
-
() => this.pollSessions(),
|
|
3392
|
-
POLL_INTERVAL_BACKGROUND_MS
|
|
3393
|
-
);
|
|
3394
|
-
log("[tmux-session-manager] polling started");
|
|
3395
|
-
}
|
|
3396
|
-
stopPolling() {
|
|
3397
|
-
if (this.pollInterval) {
|
|
3398
|
-
clearInterval(this.pollInterval);
|
|
3399
|
-
this.pollInterval = void 0;
|
|
3400
|
-
log("[tmux-session-manager] polling stopped");
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
/**
|
|
3404
|
-
* Poll sessions for status updates (fallback for reliability).
|
|
3405
|
-
* Also handles timeout and missing session detection.
|
|
3406
|
-
*/
|
|
3407
|
-
async pollSessions() {
|
|
3408
|
-
if (this.sessions.size === 0) {
|
|
3409
|
-
this.stopPolling();
|
|
3410
|
-
return;
|
|
3411
|
-
}
|
|
3412
|
-
try {
|
|
3413
|
-
const statusResult = await this.client.session.status();
|
|
3414
|
-
const allStatuses = statusResult.data ?? {};
|
|
3415
|
-
const now = Date.now();
|
|
3416
|
-
const sessionsToClose = [];
|
|
3417
|
-
for (const [sessionId, tracked] of this.sessions.entries()) {
|
|
3418
|
-
const status = allStatuses[sessionId];
|
|
3419
|
-
const isIdle = status?.type === "idle";
|
|
3420
|
-
if (status) {
|
|
3421
|
-
tracked.lastSeenAt = now;
|
|
3422
|
-
tracked.missingSince = void 0;
|
|
3423
|
-
} else if (!tracked.missingSince) {
|
|
3424
|
-
tracked.missingSince = now;
|
|
3425
|
-
}
|
|
3426
|
-
const missingTooLong = !!tracked.missingSince && now - tracked.missingSince >= SESSION_MISSING_GRACE_MS;
|
|
3427
|
-
const isTimedOut = now - tracked.createdAt > SESSION_TIMEOUT_MS;
|
|
3428
|
-
if (isIdle || missingTooLong || isTimedOut) {
|
|
3429
|
-
sessionsToClose.push(sessionId);
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
for (const sessionId of sessionsToClose) {
|
|
3433
|
-
await this.closeSession(sessionId);
|
|
3434
|
-
}
|
|
3435
|
-
} catch (err) {
|
|
3436
|
-
log("[tmux-session-manager] poll error", { error: String(err) });
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
async closeSession(sessionId) {
|
|
3440
|
-
const tracked = this.sessions.get(sessionId);
|
|
3441
|
-
if (!tracked) return;
|
|
3442
|
-
log("[tmux-session-manager] closing session pane", {
|
|
3443
|
-
sessionId,
|
|
3444
|
-
paneId: tracked.paneId
|
|
3445
|
-
});
|
|
3446
|
-
await closeTmuxPane(tracked.paneId);
|
|
3447
|
-
this.sessions.delete(sessionId);
|
|
3448
|
-
if (this.sessions.size === 0) {
|
|
3449
|
-
this.stopPolling();
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
/**
|
|
3453
|
-
* Clean up all tracked sessions.
|
|
3454
|
-
*/
|
|
3455
|
-
async cleanup() {
|
|
3456
|
-
this.stopPolling();
|
|
3457
|
-
if (this.sessions.size > 0) {
|
|
3458
|
-
log("[tmux-session-manager] closing all panes", {
|
|
3459
|
-
count: this.sessions.size
|
|
3460
|
-
});
|
|
3461
|
-
const closePromises = Array.from(this.sessions.values()).map(
|
|
3462
|
-
(s) => closeTmuxPane(s.paneId).catch(
|
|
3463
|
-
(err) => log("[tmux-session-manager] cleanup error for pane", {
|
|
3464
|
-
paneId: s.paneId,
|
|
3465
|
-
error: String(err)
|
|
3466
|
-
})
|
|
3467
|
-
)
|
|
3468
|
-
);
|
|
3469
|
-
await Promise.all(closePromises);
|
|
3470
|
-
this.sessions.clear();
|
|
3471
|
-
}
|
|
3472
|
-
log("[tmux-session-manager] cleanup complete");
|
|
3473
|
-
}
|
|
3474
|
-
};
|
|
3819
|
+
// src/opencode/tools.ts
|
|
3820
|
+
async function registerTools(ctx, _deps) {
|
|
3821
|
+
await ctx.tool.transform((editor) => {
|
|
3822
|
+
editor.add(lsp_goto_definition);
|
|
3823
|
+
editor.add(lsp_find_references);
|
|
3824
|
+
editor.add(lsp_diagnostics);
|
|
3825
|
+
editor.add(lsp_rename);
|
|
3826
|
+
editor.add(ast_grep_search);
|
|
3827
|
+
editor.add(ast_grep_replace);
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3475
3830
|
|
|
3476
3831
|
// src/index.ts
|
|
3477
|
-
var
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
}
|
|
3493
|
-
const runtimeChains = {};
|
|
3494
|
-
for (const agentDef of agentDefs) {
|
|
3495
|
-
if (agentDef._modelArray?.length) {
|
|
3496
|
-
runtimeChains[agentDef.name] = agentDef._modelArray.map((m) => m.id);
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
if (config.fallback?.enabled !== false) {
|
|
3500
|
-
const chains = config.fallback?.chains ?? {};
|
|
3501
|
-
for (const [agentName, chainModels] of Object.entries(chains)) {
|
|
3502
|
-
if (!chainModels?.length) continue;
|
|
3503
|
-
const existing = runtimeChains[agentName] ?? [];
|
|
3504
|
-
const seen = new Set(existing);
|
|
3505
|
-
for (const m of chainModels) {
|
|
3506
|
-
if (!seen.has(m)) {
|
|
3507
|
-
seen.add(m);
|
|
3508
|
-
existing.push(m);
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
runtimeChains[agentName] = existing;
|
|
3512
|
-
}
|
|
3513
|
-
}
|
|
3514
|
-
const tmuxConfig = {
|
|
3515
|
-
enabled: config.tmux?.enabled ?? false,
|
|
3516
|
-
layout: config.tmux?.layout ?? "main-vertical",
|
|
3517
|
-
main_pane_size: config.tmux?.main_pane_size ?? 60
|
|
3518
|
-
};
|
|
3519
|
-
log("[plugin] initialized with tmux config", {
|
|
3520
|
-
tmuxConfig,
|
|
3521
|
-
rawTmuxConfig: config.tmux,
|
|
3522
|
-
directory
|
|
3523
|
-
});
|
|
3524
|
-
if (tmuxConfig.enabled) {
|
|
3525
|
-
startTmuxCheck();
|
|
3832
|
+
var plugin = {
|
|
3833
|
+
id: "thoth-agents",
|
|
3834
|
+
async setup(ctx) {
|
|
3835
|
+
const deps = loadOpenCodeDependencies(ctx);
|
|
3836
|
+
await registerAgents(ctx, deps);
|
|
3837
|
+
await registerTools(ctx, deps);
|
|
3838
|
+
await registerMcp(ctx, deps);
|
|
3839
|
+
await registerCommand(ctx, deps);
|
|
3840
|
+
await registerSkills(ctx, deps);
|
|
3841
|
+
await registerPermissions(ctx, deps);
|
|
3842
|
+
await registerProfileRuntime(ctx, deps);
|
|
3843
|
+
await registerProfileRpc(ctx, deps);
|
|
3844
|
+
await registerToolRpc(ctx, deps);
|
|
3845
|
+
await registerDelegationHooks(ctx, deps);
|
|
3846
|
+
await registerSessionHooks(ctx, deps);
|
|
3526
3847
|
}
|
|
3527
|
-
const mcps = createBuiltinMcps(config.disabled_mcps);
|
|
3528
|
-
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
|
|
3529
|
-
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
|
|
3530
|
-
showStartupToast: true
|
|
3531
|
-
});
|
|
3532
|
-
const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
|
|
3533
|
-
const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
3534
|
-
const oracleGateVerdictHook = createOracleGateVerdictHook(ctx);
|
|
3535
|
-
const foregroundFallback = new ForegroundFallbackManager(
|
|
3536
|
-
client,
|
|
3537
|
-
runtimeChains,
|
|
3538
|
-
config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0
|
|
3539
|
-
);
|
|
3540
|
-
return {
|
|
3541
|
-
name: "thoth-agents",
|
|
3542
|
-
agent: agents,
|
|
3543
|
-
tool: {
|
|
3544
|
-
lsp_goto_definition,
|
|
3545
|
-
lsp_find_references,
|
|
3546
|
-
lsp_diagnostics,
|
|
3547
|
-
lsp_rename,
|
|
3548
|
-
ast_grep_search,
|
|
3549
|
-
ast_grep_replace
|
|
3550
|
-
},
|
|
3551
|
-
mcp: mcps,
|
|
3552
|
-
config: async (opencodeConfig) => {
|
|
3553
|
-
const lspConfig = opencodeConfig.lsp;
|
|
3554
|
-
setUserLspConfig(lspConfig);
|
|
3555
|
-
const commands = opencodeConfig.command ?? {};
|
|
3556
|
-
if (!commands["thoth-init"]) {
|
|
3557
|
-
commands["thoth-init"] = thothInitCommand;
|
|
3558
|
-
}
|
|
3559
|
-
opencodeConfig.command = commands;
|
|
3560
|
-
if (config.setDefaultAgent !== false && !opencodeConfig.default_agent) {
|
|
3561
|
-
opencodeConfig.default_agent = "orchestrator";
|
|
3562
|
-
}
|
|
3563
|
-
if (!opencodeConfig.agent) {
|
|
3564
|
-
opencodeConfig.agent = { ...agents };
|
|
3565
|
-
} else {
|
|
3566
|
-
for (const [name, pluginAgent] of Object.entries(agents)) {
|
|
3567
|
-
const existing = opencodeConfig.agent[name];
|
|
3568
|
-
if (existing) {
|
|
3569
|
-
opencodeConfig.agent[name] = {
|
|
3570
|
-
...pluginAgent,
|
|
3571
|
-
...existing
|
|
3572
|
-
};
|
|
3573
|
-
} else {
|
|
3574
|
-
opencodeConfig.agent[name] = {
|
|
3575
|
-
...pluginAgent
|
|
3576
|
-
};
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
const configAgent = opencodeConfig.agent;
|
|
3581
|
-
const fallbackChainsEnabled = config.fallback?.enabled !== false;
|
|
3582
|
-
const fallbackChains = fallbackChainsEnabled ? config.fallback?.chains ?? {} : {};
|
|
3583
|
-
const effectiveArrays = {};
|
|
3584
|
-
for (const [agentName, models] of Object.entries(modelArrayMap)) {
|
|
3585
|
-
effectiveArrays[agentName] = [...models];
|
|
3586
|
-
}
|
|
3587
|
-
for (const [agentName, chainModels] of Object.entries(fallbackChains)) {
|
|
3588
|
-
if (!chainModels || chainModels.length === 0) continue;
|
|
3589
|
-
if (!effectiveArrays[agentName]) {
|
|
3590
|
-
const entry = configAgent[agentName];
|
|
3591
|
-
const currentModel = typeof entry?.model === "string" ? entry.model : void 0;
|
|
3592
|
-
effectiveArrays[agentName] = currentModel ? [{ id: currentModel }] : [];
|
|
3593
|
-
}
|
|
3594
|
-
const seen = new Set(effectiveArrays[agentName].map((m) => m.id));
|
|
3595
|
-
for (const chainModel of chainModels) {
|
|
3596
|
-
if (!seen.has(chainModel)) {
|
|
3597
|
-
seen.add(chainModel);
|
|
3598
|
-
effectiveArrays[agentName].push({ id: chainModel });
|
|
3599
|
-
}
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
if (Object.keys(effectiveArrays).length > 0) {
|
|
3603
|
-
for (const [agentName, modelArray] of Object.entries(effectiveArrays)) {
|
|
3604
|
-
if (modelArray.length === 0) continue;
|
|
3605
|
-
const chosen = modelArray[0];
|
|
3606
|
-
const entry = configAgent[agentName];
|
|
3607
|
-
if (entry) {
|
|
3608
|
-
entry.model = chosen.id;
|
|
3609
|
-
if (chosen.variant) {
|
|
3610
|
-
entry.variant = chosen.variant;
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
log("[plugin] resolved model from array", {
|
|
3614
|
-
agent: agentName,
|
|
3615
|
-
model: chosen.id,
|
|
3616
|
-
variant: chosen.variant
|
|
3617
|
-
});
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
const configMcp = opencodeConfig.mcp;
|
|
3621
|
-
if (!configMcp) {
|
|
3622
|
-
opencodeConfig.mcp = { ...mcps };
|
|
3623
|
-
} else {
|
|
3624
|
-
Object.assign(configMcp, mcps);
|
|
3625
|
-
}
|
|
3626
|
-
},
|
|
3627
|
-
event: async (input) => {
|
|
3628
|
-
await foregroundFallback.handleEvent(input.event);
|
|
3629
|
-
await autoUpdateChecker.event(input);
|
|
3630
|
-
await tmuxSessionManager.onSessionCreated(
|
|
3631
|
-
input.event
|
|
3632
|
-
);
|
|
3633
|
-
await tmuxSessionManager.onSessionStatus(
|
|
3634
|
-
input.event
|
|
3635
|
-
);
|
|
3636
|
-
await tmuxSessionManager.onSessionDeleted(
|
|
3637
|
-
input.event
|
|
3638
|
-
);
|
|
3639
|
-
},
|
|
3640
|
-
// Post-tool hooks: delegation retry, JSON recovery, then Oracle gate verdicts.
|
|
3641
|
-
"tool.execute.after": async (input, output) => {
|
|
3642
|
-
await delegateTaskRetryHook["tool.execute.after"](
|
|
3643
|
-
input,
|
|
3644
|
-
output
|
|
3645
|
-
);
|
|
3646
|
-
await jsonErrorRecoveryHook["tool.execute.after"](
|
|
3647
|
-
input,
|
|
3648
|
-
output
|
|
3649
|
-
);
|
|
3650
|
-
await oracleGateVerdictHook["tool.execute.after"](input, output);
|
|
3651
|
-
}
|
|
3652
|
-
};
|
|
3653
3848
|
};
|
|
3654
|
-
var src_default =
|
|
3849
|
+
var src_default = plugin;
|
|
3655
3850
|
export {
|
|
3656
3851
|
src_default as default
|
|
3657
3852
|
};
|