thoth-agents 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- 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-VHQMTZ5O.js → chunk-7F33WUSD.js} +1 -1
- package/dist/{chunk-RWQPMWEM.js → chunk-JLU3FSBT.js} +17 -2
- package/dist/chunk-MIDGZE34.js +107 -0
- package/dist/{chunk-RVQ6IIBR.js → chunk-NY2IPJ7V.js} +361 -118
- package/dist/{chunk-RN2ERZLZ.js → chunk-QOA2AETX.js} +2 -228
- package/dist/{chunk-6NZTNWI4.js → chunk-SEF6PFM6.js} +504 -211
- package/dist/cli/config-io.d.ts +2 -2
- package/dist/cli/index.js +92 -39
- package/dist/cli/install.d.ts +1 -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 +1 -1
- package/dist/config/schema.d.ts +0 -31
- package/dist/harness/adapters/opencode.d.ts +1 -1
- package/dist/harness/core/agent-pack.d.ts +1 -0
- package/dist/harness/core/sdd.d.ts +6 -6
- 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 +822 -1421
- package/dist/opencode/agents.d.ts +3 -0
- package/dist/opencode/command.d.ts +3 -0
- package/dist/opencode/context.d.ts +20 -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/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/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/utils/index.d.ts +0 -1
- package/package.json +14 -3
- package/pi/.thoth-agents-assets.json +2 -2
- package/pi/agents/thoth-designer.md +1 -1
- package/pi/agents/thoth-worker.md +1 -1
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +3 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +3 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/agents/designer.md +1 -1
- package/plugin/agents/orchestrator.md +32 -32
- package/plugin/agents/worker.md +1 -1
- package/plugin/codex-hooks/payloads/designer.md +1 -1
- package/plugin/codex-hooks/payloads/root.md +32 -32
- package/plugin/codex-hooks/payloads/worker.md +1 -1
- package/plugin/skills/plan-reviewer/SKILL.md +5 -5
- package/plugin/skills/thoth-sdd/SKILL.md +33 -9
- package/plugin/skills/thoth-sdd/references/phases/checklist.md +1 -1
- package/plugin/skills/thoth-sdd/references/phases/clarify.md +1 -1
- package/plugin/skills/thoth-sdd/references/phases/converge.md +8 -1
- package/plugin/skills/thoth-sdd/references/phases/implement.md +5 -1
- package/plugin/skills/thoth-sdd/references/phases/plan.md +1 -1
- package/plugin/skills/thoth-sdd/references/phases/specify.md +1 -1
- package/plugin/skills/thoth-sdd/references/phases/tasks.md +1 -1
- package/plugin/skills/thoth-sdd/references/phases/verify.md +10 -3
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/plugin/skills/thoth-sdd/templates/change.md +2 -1
- package/skills/plan-reviewer/SKILL.md +5 -5
- package/skills/thoth-sdd/SKILL.md +33 -9
- package/skills/thoth-sdd/references/phases/checklist.md +1 -1
- package/skills/thoth-sdd/references/phases/clarify.md +1 -1
- package/skills/thoth-sdd/references/phases/converge.md +8 -1
- package/skills/thoth-sdd/references/phases/implement.md +5 -1
- package/skills/thoth-sdd/references/phases/plan.md +1 -1
- package/skills/thoth-sdd/references/phases/specify.md +1 -1
- package/skills/thoth-sdd/references/phases/tasks.md +1 -1
- package/skills/thoth-sdd/references/phases/verify.md +10 -3
- package/skills/thoth-sdd/scripts/validate-result.mjs +28 -2
- package/skills/thoth-sdd/templates/change.md +2 -1
- package/thoth-agents.schema.json +0 -26
- 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,107 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatGateVerdict,
|
|
3
3
|
summarizeGateResult
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JLU3FSBT.js";
|
|
5
5
|
import {
|
|
6
6
|
createAgents,
|
|
7
|
-
renderOpenCodeAgentConfigs
|
|
8
|
-
|
|
9
|
-
spawnSync
|
|
10
|
-
} from "./chunk-RN2ERZLZ.js";
|
|
7
|
+
renderOpenCodeAgentConfigs
|
|
8
|
+
} from "./chunk-QOA2AETX.js";
|
|
11
9
|
import {
|
|
12
10
|
context7,
|
|
13
11
|
exa,
|
|
14
|
-
grep_app
|
|
15
|
-
|
|
12
|
+
grep_app,
|
|
13
|
+
spawn,
|
|
14
|
+
spawnSync
|
|
15
|
+
} from "./chunk-MIDGZE34.js";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
getOpenCodeConfigPaths,
|
|
20
|
-
loadPluginConfig,
|
|
21
|
-
stripJsonComments
|
|
22
|
-
} from "./chunk-RVQ6IIBR.js";
|
|
17
|
+
loadPluginConfig
|
|
18
|
+
} from "./chunk-NY2IPJ7V.js";
|
|
23
19
|
|
|
24
|
-
// src/
|
|
25
|
-
|
|
20
|
+
// src/opencode/permission-rules.ts
|
|
21
|
+
var ACTIONS = {
|
|
22
|
+
read: "read",
|
|
23
|
+
glob: "glob",
|
|
24
|
+
grep: "grep",
|
|
25
|
+
list: "read",
|
|
26
|
+
codesearch: "read",
|
|
27
|
+
lsp: "read",
|
|
28
|
+
edit: "edit",
|
|
29
|
+
write: "edit",
|
|
30
|
+
patch: "edit",
|
|
31
|
+
bash: "shell",
|
|
32
|
+
task: "subagent",
|
|
33
|
+
webfetch: "webfetch",
|
|
34
|
+
exa: "exa_*",
|
|
35
|
+
external_directory: "external_directory",
|
|
36
|
+
skill: "skill",
|
|
37
|
+
question: "question",
|
|
38
|
+
todowrite: "todowrite"
|
|
39
|
+
};
|
|
40
|
+
var RESTRICTION = { allow: 0, ask: 1, deny: 2 };
|
|
41
|
+
function specificity(rule) {
|
|
42
|
+
return Number(rule.action !== "*") + 2 * Number(rule.resource !== "*");
|
|
43
|
+
}
|
|
44
|
+
function translatePermissionRules(permission) {
|
|
45
|
+
if (typeof permission === "string") {
|
|
46
|
+
return [{ action: "*", resource: "*", effect: permission }];
|
|
47
|
+
}
|
|
48
|
+
const rules = [];
|
|
49
|
+
for (const [key, value] of Object.entries(permission ?? {})) {
|
|
50
|
+
if (!value) continue;
|
|
51
|
+
const action = ACTIONS[key] ?? key;
|
|
52
|
+
const entries = typeof value === "string" ? [["*", value]] : Object.entries(value);
|
|
53
|
+
for (const [resource, effect] of entries) {
|
|
54
|
+
const existing = rules.find(
|
|
55
|
+
(rule) => rule.action === action && rule.resource === resource
|
|
56
|
+
);
|
|
57
|
+
if (!existing) rules.push({ action, resource, effect });
|
|
58
|
+
else if (RESTRICTION[effect] > RESTRICTION[existing.effect])
|
|
59
|
+
existing.effect = effect;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
{ action: "*", resource: "*", effect: "ask" },
|
|
64
|
+
...rules.sort((a, b) => specificity(a) - specificity(b))
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/opencode/agents.ts
|
|
69
|
+
var ROLE_COLORS = {
|
|
70
|
+
orchestrator: "#7c3aed",
|
|
71
|
+
explorer: "#0ea5e9",
|
|
72
|
+
librarian: "#0284c7",
|
|
73
|
+
oracle: "#f59e0b",
|
|
74
|
+
designer: "#ec4899",
|
|
75
|
+
worker: "#10b981"
|
|
76
|
+
};
|
|
77
|
+
async function registerAgents(ctx, deps) {
|
|
78
|
+
await ctx.agent.transform((editor) => {
|
|
79
|
+
for (const { name } of deps.agentDefs) {
|
|
80
|
+
const config = deps.agents[name];
|
|
81
|
+
const first = deps.effectiveModelArrays[name]?.[0];
|
|
82
|
+
const model = first?.id ?? config.model;
|
|
83
|
+
const variant = first ? first.variant : config.variant;
|
|
84
|
+
editor.update(name, (agent) => {
|
|
85
|
+
agent.description = config.description;
|
|
86
|
+
agent.system = config.prompt;
|
|
87
|
+
agent.mode = name === "orchestrator" ? "primary" : "subagent";
|
|
88
|
+
agent.hidden = config.hidden ?? false;
|
|
89
|
+
agent.color = /^#[0-9a-f]{6}$/i.test(config.color ?? "") ? config.color : ROLE_COLORS[name];
|
|
90
|
+
agent.steps = config.steps;
|
|
91
|
+
agent.permissions = translatePermissionRules(config.permission);
|
|
92
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
93
|
+
agent.model = model && separator > 0 && separator < model.length - 1 ? {
|
|
94
|
+
providerID: model.slice(0, separator),
|
|
95
|
+
id: model.slice(separator + 1),
|
|
96
|
+
...variant ? { variant } : {}
|
|
97
|
+
} : void 0;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
editor.default("orchestrator");
|
|
101
|
+
editor.remove("explore");
|
|
102
|
+
editor.remove("general");
|
|
103
|
+
});
|
|
104
|
+
}
|
|
26
105
|
|
|
27
106
|
// src/harness/opencode-init-command.ts
|
|
28
107
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -62,664 +141,160 @@ ${contract}`
|
|
|
62
141
|
};
|
|
63
142
|
}
|
|
64
143
|
|
|
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];
|
|
144
|
+
// src/opencode/command.ts
|
|
145
|
+
async function registerCommand(ctx, deps) {
|
|
146
|
+
const command = createOpenCodeInitCommand({
|
|
147
|
+
projectRoot: deps.directory,
|
|
148
|
+
packageRoot: deps.packageRoot
|
|
149
|
+
});
|
|
150
|
+
await ctx.command.transform((editor) => {
|
|
151
|
+
editor.add({
|
|
152
|
+
name: "thoth-init",
|
|
153
|
+
description: command.description,
|
|
154
|
+
async execute({ sessionID, prompt, delivery }) {
|
|
155
|
+
const args = prompt.text.trim();
|
|
156
|
+
const text = command.template + (args ? `
|
|
112
157
|
|
|
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
|
-
}
|
|
158
|
+
${args}` : "");
|
|
159
|
+
await ctx.session.switchAgent({ sessionID, agent: "orchestrator" });
|
|
160
|
+
await ctx.session.prompt({ sessionID, text, delivery });
|
|
155
161
|
}
|
|
156
|
-
}
|
|
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
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const parent = path2.dirname(dir);
|
|
176
|
-
if (parent === dir) break;
|
|
177
|
-
dir = parent;
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
function getLocalDevVersion(directory) {
|
|
184
|
-
const localPath = getLocalDevPath(directory);
|
|
185
|
-
if (!localPath) return null;
|
|
186
|
-
try {
|
|
187
|
-
const pkgPath = findPackageJsonUp(localPath);
|
|
188
|
-
if (!pkgPath) return null;
|
|
189
|
-
const content = fs.readFileSync(pkgPath, "utf-8");
|
|
190
|
-
const pkg = JSON.parse(content);
|
|
191
|
-
return pkg.version ?? null;
|
|
192
|
-
} catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function findPluginEntry(directory) {
|
|
197
|
-
for (const configPath of getConfigPaths(directory)) {
|
|
198
|
-
try {
|
|
199
|
-
if (!fs.existsSync(configPath)) continue;
|
|
200
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
201
|
-
const config = JSON.parse(stripJsonComments(content));
|
|
202
|
-
const plugins = config.plugin ?? [];
|
|
203
|
-
for (const entry of plugins) {
|
|
204
|
-
if (entry === PACKAGE_NAME) {
|
|
205
|
-
return { entry, isPinned: false, pinnedVersion: null, configPath };
|
|
206
|
-
}
|
|
207
|
-
if (entry.startsWith(`${PACKAGE_NAME}@`)) {
|
|
208
|
-
const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1);
|
|
209
|
-
const isPinned = pinnedVersion !== "latest";
|
|
210
|
-
return {
|
|
211
|
-
entry,
|
|
212
|
-
isPinned,
|
|
213
|
-
pinnedVersion: isPinned ? pinnedVersion : null,
|
|
214
|
-
configPath
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
162
|
+
});
|
|
163
|
+
});
|
|
222
164
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
165
|
+
|
|
166
|
+
// src/opencode/context.ts
|
|
167
|
+
import { fileURLToPath } from "url";
|
|
168
|
+
function loadOpenCodeDependencies(ctx) {
|
|
169
|
+
const directory = ctx.location.directory;
|
|
170
|
+
const config = loadPluginConfig(directory);
|
|
171
|
+
const agentDefs = createAgents(config);
|
|
172
|
+
const agents = renderOpenCodeAgentConfigs(config);
|
|
173
|
+
const modelArrayMap = {};
|
|
174
|
+
const runtimeChains = {};
|
|
175
|
+
const effectiveModelArrays = {};
|
|
176
|
+
for (const agent of agentDefs) {
|
|
177
|
+
if (!agent._modelArray?.length) continue;
|
|
178
|
+
modelArrayMap[agent.name] = [...agent._modelArray];
|
|
179
|
+
effectiveModelArrays[agent.name] = [...agent._modelArray];
|
|
180
|
+
runtimeChains[agent.name] = agent._modelArray.map((model) => model.id);
|
|
236
181
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
182
|
+
if (config.fallback?.enabled !== false) {
|
|
183
|
+
for (const [name, models] of Object.entries(
|
|
184
|
+
config.fallback?.chains ?? {}
|
|
185
|
+
)) {
|
|
186
|
+
if (!models?.length) continue;
|
|
187
|
+
const currentModel = agents[name]?.model;
|
|
188
|
+
const effective = effectiveModelArrays[name] ?? (currentModel ? [{ id: currentModel, variant: agents[name]?.variant }] : []);
|
|
189
|
+
for (const id of models) {
|
|
190
|
+
if (!effective.some((model) => model.id === id)) effective.push({ id });
|
|
246
191
|
}
|
|
192
|
+
runtimeChains[name] = effective.map((model) => model.id);
|
|
193
|
+
effectiveModelArrays[name] = effective;
|
|
247
194
|
}
|
|
248
|
-
} catch (err) {
|
|
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
195
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// src/hooks/auto-update-checker/index.ts
|
|
275
|
-
function createAutoUpdateCheckerHook(ctx, options = {}) {
|
|
276
|
-
const { showStartupToast = true } = options;
|
|
277
|
-
let hasChecked = false;
|
|
278
196
|
return {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"Running in local development mode.",
|
|
295
|
-
"info"
|
|
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
|
-
}
|
|
197
|
+
directory,
|
|
198
|
+
// Both src/opencode/context.ts and dist/index.js (or a root chunk) resolve
|
|
199
|
+
// the package root here; nested source modules need one extra parent.
|
|
200
|
+
packageRoot: fileURLToPath(
|
|
201
|
+
new URL(
|
|
202
|
+
import.meta.url.endsWith(".ts") ? "../.." : "..",
|
|
203
|
+
import.meta.url
|
|
204
|
+
)
|
|
205
|
+
),
|
|
206
|
+
config,
|
|
207
|
+
agentDefs,
|
|
208
|
+
agents,
|
|
209
|
+
modelArrayMap,
|
|
210
|
+
effectiveModelArrays,
|
|
211
|
+
runtimeChains
|
|
314
212
|
};
|
|
315
213
|
}
|
|
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
214
|
|
|
363
215
|
// src/hooks/delegate-task-retry/patterns.ts
|
|
364
216
|
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
365
217
|
{
|
|
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".'
|
|
218
|
+
pattern: "Unknown agent:",
|
|
219
|
+
errorType: "unknown_agent",
|
|
220
|
+
fixHint: "Use an available agent name in the agent argument."
|
|
379
221
|
},
|
|
380
222
|
{
|
|
381
|
-
pattern: "
|
|
382
|
-
errorType: "
|
|
383
|
-
fixHint: "
|
|
223
|
+
pattern: "cannot run as a subagent",
|
|
224
|
+
errorType: "primary_agent",
|
|
225
|
+
fixHint: "Choose a specialist agent, not a primary agent."
|
|
384
226
|
},
|
|
385
227
|
{
|
|
386
|
-
pattern: "
|
|
387
|
-
errorType: "
|
|
388
|
-
fixHint: "
|
|
228
|
+
pattern: "Subagent depth limit reached",
|
|
229
|
+
errorType: "depth_limit",
|
|
230
|
+
fixHint: "Return to the root coordinator; nesting depth is 1. Do not retry nested delegation."
|
|
389
231
|
},
|
|
390
232
|
{
|
|
391
|
-
pattern: "
|
|
392
|
-
errorType: "
|
|
393
|
-
fixHint: "
|
|
233
|
+
pattern: "Subagent denied:",
|
|
234
|
+
errorType: "permission_denied",
|
|
235
|
+
fixHint: "Return the permission blocker to the root coordinator; do not bypass the denial."
|
|
394
236
|
},
|
|
395
237
|
{
|
|
396
|
-
pattern: "
|
|
397
|
-
errorType: "
|
|
398
|
-
fixHint: "
|
|
238
|
+
pattern: "Invalid arguments",
|
|
239
|
+
errorType: "invalid_arguments",
|
|
240
|
+
fixHint: "Supply agent, description and prompt; optional model, sessionID and background must have the native types."
|
|
399
241
|
}
|
|
400
242
|
];
|
|
401
243
|
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;
|
|
244
|
+
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
245
|
+
(entry) => output.includes(entry.pattern)
|
|
246
|
+
);
|
|
247
|
+
return pattern ? { errorType: pattern.errorType, originalOutput: output } : null;
|
|
414
248
|
}
|
|
415
249
|
|
|
416
250
|
// 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
251
|
function buildRetryGuidance(errorInfo) {
|
|
425
252
|
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
426
|
-
(
|
|
253
|
+
(entry) => entry.errorType === errorInfo.errorType
|
|
427
254
|
);
|
|
428
|
-
if (!pattern) {
|
|
429
|
-
return "\n[delegate-task retry] Fix parameters and retry with corrected arguments.";
|
|
430
|
-
}
|
|
431
|
-
const available = extractAvailableList(errorInfo.originalOutput);
|
|
432
255
|
const lines = [
|
|
433
|
-
"",
|
|
434
|
-
"[delegate-task retry suggestion]",
|
|
256
|
+
"[subagent retry suggestion]",
|
|
435
257
|
`Error type: ${errorInfo.errorType}`,
|
|
436
|
-
`Fix: ${pattern.
|
|
258
|
+
`Fix: ${pattern?.fixHint ?? "Correct the native subagent arguments before retrying."}`
|
|
437
259
|
];
|
|
438
|
-
if (
|
|
439
|
-
lines.push(`Available: ${available}`);
|
|
440
|
-
}
|
|
441
|
-
if (errorInfo.errorType === "background_agent_not_allowed") {
|
|
260
|
+
if (errorInfo.errorType !== "depth_limit" && errorInfo.errorType !== "permission_denied") {
|
|
442
261
|
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)'
|
|
262
|
+
'Example: subagent({agent:"explorer", description:"Locate entrypoint", prompt:"Find the bounded entrypoint"})',
|
|
263
|
+
"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
264
|
);
|
|
450
|
-
return lines.join("\n");
|
|
451
265
|
}
|
|
452
|
-
lines.push(
|
|
453
|
-
"Retry now with corrected parameters. Example:",
|
|
454
|
-
'task(description="...", prompt="...", subagent_type="explorer")'
|
|
455
|
-
);
|
|
456
266
|
return lines.join("\n");
|
|
457
267
|
}
|
|
458
268
|
|
|
459
269
|
// src/hooks/delegate-task-retry/hook.ts
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
270
|
+
function appendErrorGuidance(event, guidance) {
|
|
271
|
+
const properties = Object.getOwnPropertyDescriptors(event.error);
|
|
272
|
+
properties.message = {
|
|
273
|
+
value: `${event.error.message}
|
|
274
|
+
${guidance}`,
|
|
275
|
+
configurable: true,
|
|
276
|
+
writable: true,
|
|
277
|
+
enumerable: true
|
|
278
|
+
};
|
|
279
|
+
properties.stack = {
|
|
280
|
+
value: event.error.stack,
|
|
281
|
+
configurable: true,
|
|
282
|
+
writable: true,
|
|
283
|
+
enumerable: false
|
|
284
|
+
};
|
|
285
|
+
event.error = Object.create(Object.getPrototypeOf(event.error), properties);
|
|
286
|
+
}
|
|
287
|
+
function createDelegateTaskRetryHook() {
|
|
288
|
+
return async (event) => {
|
|
289
|
+
if (event.tool !== "subagent" || event.status !== "error") return;
|
|
290
|
+
if (event.error.message.includes("[subagent retry suggestion]")) return;
|
|
291
|
+
const detected = detectDelegateTaskError(event.error.message);
|
|
292
|
+
if (!detected) return;
|
|
293
|
+
appendErrorGuidance(event, buildRetryGuidance(detected));
|
|
472
294
|
};
|
|
473
295
|
}
|
|
474
296
|
|
|
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
297
|
// 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
298
|
var JSON_ERROR_PATTERNS = [
|
|
724
299
|
/json parse error/i,
|
|
725
300
|
/failed to parse json/i,
|
|
@@ -731,7 +306,6 @@ var JSON_ERROR_PATTERNS = [
|
|
|
731
306
|
/json[^\n]*unexpected eof/i
|
|
732
307
|
];
|
|
733
308
|
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
309
|
var JSON_ERROR_REMINDER = `
|
|
736
310
|
[JSON PARSE ERROR - IMMEDIATE ACTION REQUIRED]
|
|
737
311
|
|
|
@@ -744,56 +318,74 @@ STOP and do this NOW:
|
|
|
744
318
|
|
|
745
319
|
DO NOT repeat the exact same invalid call.
|
|
746
320
|
`;
|
|
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
|
-
}
|
|
321
|
+
function createJsonErrorRecoveryHook() {
|
|
322
|
+
return async (event) => {
|
|
323
|
+
if (event.tool !== "subagent" || event.status !== "error") return;
|
|
324
|
+
const text = event.error.message;
|
|
325
|
+
if (text.includes(JSON_ERROR_REMINDER_MARKER)) return;
|
|
326
|
+
if (!JSON_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return;
|
|
327
|
+
appendErrorGuidance(event, JSON_ERROR_REMINDER);
|
|
762
328
|
};
|
|
763
329
|
}
|
|
764
330
|
|
|
765
331
|
// src/hooks/oracle-gate-verdict/hook.ts
|
|
766
|
-
var ORACLE_AGENT = getAgentRole("oracle").name;
|
|
767
332
|
function createOracleGateVerdictHook(ctx) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
333
|
+
const delivered = /* @__PURE__ */ new WeakSet();
|
|
334
|
+
return async (event) => {
|
|
335
|
+
if (event.tool !== "subagent" || event.status !== "completed") return;
|
|
336
|
+
if (delivered.has(event.result)) return;
|
|
337
|
+
if (!event.input || typeof event.input !== "object" || !("agent" in event.input) || event.input.agent !== "oracle")
|
|
338
|
+
return;
|
|
339
|
+
const output = event.result.output;
|
|
340
|
+
if (!output || typeof output !== "object" || !("status" in output) || output.status !== "completed")
|
|
341
|
+
return;
|
|
342
|
+
const content = event.result.content;
|
|
343
|
+
if (content === void 0) return;
|
|
344
|
+
const text = typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
345
|
+
const raw = "output" in output && typeof output.output === "string" && text.includes(output.output) ? output.output : void 0;
|
|
346
|
+
const message = raw ?? text;
|
|
347
|
+
if (!message.trim().split(/\r\n|\r|\n/).includes("```thoth-result"))
|
|
348
|
+
return;
|
|
349
|
+
const end = text.lastIndexOf("</subagent>");
|
|
350
|
+
const tail = end < 0 ? "" : text.slice(end + "</subagent>".length);
|
|
351
|
+
if (tail.split(/\r\n|\r|\n/).some((line) => line.startsWith("thoth-gate-verdict ")))
|
|
352
|
+
return;
|
|
353
|
+
if (Array.isArray(content) && content.some((part) => {
|
|
354
|
+
if (part.type !== "text") return false;
|
|
355
|
+
const lines = part.text.split(/\r\n|\r|\n/);
|
|
356
|
+
return lines.length === 3 && lines[0] === "" && lines[1] === "" && lines[2].startsWith("thoth-gate-verdict ");
|
|
357
|
+
}))
|
|
358
|
+
return;
|
|
359
|
+
const bare = raw === void 0 || !raw.trimStart().startsWith("<subagent ");
|
|
360
|
+
const summary = {
|
|
361
|
+
...summarizeGateResult(bare ? message : "", { cwd: ctx.directory }),
|
|
362
|
+
agent: "oracle"
|
|
363
|
+
};
|
|
364
|
+
const verdict = formatGateVerdict(summary);
|
|
365
|
+
event.result = {
|
|
366
|
+
...event.result,
|
|
367
|
+
content: typeof content === "string" ? `${content}
|
|
787
368
|
|
|
788
|
-
${verdict}`
|
|
789
|
-
return;
|
|
790
|
-
output.output += `
|
|
369
|
+
${verdict}` : [...content, { type: "text", text: `
|
|
791
370
|
|
|
792
|
-
${verdict}
|
|
793
|
-
}
|
|
371
|
+
${verdict}` }]
|
|
372
|
+
};
|
|
373
|
+
delivered.add(event.result);
|
|
794
374
|
};
|
|
795
375
|
}
|
|
796
376
|
|
|
377
|
+
// src/opencode/delegation-hooks.ts
|
|
378
|
+
async function registerDelegationHooks(ctx, deps) {
|
|
379
|
+
const retry = createDelegateTaskRetryHook();
|
|
380
|
+
const recover = createJsonErrorRecoveryHook();
|
|
381
|
+
const verdict = createOracleGateVerdictHook({ directory: deps.directory });
|
|
382
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
383
|
+
await retry(event);
|
|
384
|
+
await recover(event);
|
|
385
|
+
await verdict(event);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
797
389
|
// src/mcp/index.ts
|
|
798
390
|
function createAllBuiltinMcps() {
|
|
799
391
|
return {
|
|
@@ -811,271 +403,427 @@ function createBuiltinMcps(disabledMcps = []) {
|
|
|
811
403
|
);
|
|
812
404
|
}
|
|
813
405
|
|
|
814
|
-
// src/
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
406
|
+
// src/opencode/mcp.ts
|
|
407
|
+
async function registerMcp(ctx, deps) {
|
|
408
|
+
await ctx.mcp.transform((editor) => {
|
|
409
|
+
for (const [name, config] of Object.entries(
|
|
410
|
+
createBuiltinMcps(deps.config.disabled_mcps)
|
|
411
|
+
)) {
|
|
412
|
+
editor.set(name, config);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
819
416
|
|
|
820
|
-
// src/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
417
|
+
// src/opencode/permissions.ts
|
|
418
|
+
async function registerPermissions(ctx, deps) {
|
|
419
|
+
const specialists = new Set(
|
|
420
|
+
deps.agentDefs.map(({ name }) => name).filter((name) => name !== "orchestrator")
|
|
421
|
+
);
|
|
422
|
+
await ctx.permission.hook("evaluate", (event) => {
|
|
423
|
+
if (event.action !== "subagent" || !event.agent || !specialists.has(event.agent))
|
|
424
|
+
return;
|
|
425
|
+
event.effect = "deny";
|
|
426
|
+
event.message = `Thoth role "${event.agent}" cannot delegate: only orchestrator may call subagent.`;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
825
429
|
|
|
826
|
-
// src/
|
|
827
|
-
import {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
430
|
+
// src/utils/logger.ts
|
|
431
|
+
import { createRequire } from "module";
|
|
432
|
+
var require2 = createRequire(import.meta.url);
|
|
433
|
+
function getLogFile() {
|
|
434
|
+
const os = require2("node:os");
|
|
435
|
+
const path = require2("node:path");
|
|
436
|
+
const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
|
|
437
|
+
return configuredPath || path.join(os.tmpdir(), "thoth-agents.log");
|
|
438
|
+
}
|
|
439
|
+
function log(message, data) {
|
|
440
|
+
try {
|
|
441
|
+
const fs = require2("node:fs");
|
|
442
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
443
|
+
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
444
|
+
`;
|
|
445
|
+
fs.appendFileSync(getLogFile(), logEntry);
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
831
449
|
|
|
832
|
-
// src/
|
|
833
|
-
var
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
450
|
+
// src/hooks/foreground-fallback/index.ts
|
|
451
|
+
var RETRYABLE = /* @__PURE__ */ new Set([
|
|
452
|
+
"provider.rate-limit",
|
|
453
|
+
"provider.quota",
|
|
454
|
+
"provider.free-limit",
|
|
455
|
+
"provider.chatgpt-limit",
|
|
456
|
+
"provider.go-limit",
|
|
457
|
+
"provider.budget-limit",
|
|
458
|
+
"provider.model-budget-limit",
|
|
459
|
+
"provider.transport",
|
|
460
|
+
"provider.timeout",
|
|
461
|
+
"provider.internal",
|
|
462
|
+
"provider.invalid-output"
|
|
463
|
+
]);
|
|
464
|
+
function isRateLimitError(error) {
|
|
465
|
+
if (!error || typeof error !== "object") return false;
|
|
466
|
+
const value = error;
|
|
467
|
+
return value.type !== void 0 && RETRYABLE.has(value.type);
|
|
468
|
+
}
|
|
469
|
+
function modelRef(value) {
|
|
470
|
+
const slash = value.indexOf("/");
|
|
471
|
+
if (slash <= 0 || slash === value.length - 1) return;
|
|
472
|
+
const [id, variant] = value.slice(slash + 1).split("#");
|
|
473
|
+
if (!id) return;
|
|
474
|
+
return {
|
|
475
|
+
providerID: value.slice(0, slash),
|
|
476
|
+
id,
|
|
477
|
+
...variant ? { variant } : {}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
var ForegroundFallbackManager = class {
|
|
481
|
+
constructor(session, chains, models = {}) {
|
|
482
|
+
this.session = session;
|
|
483
|
+
this.chains = chains;
|
|
484
|
+
this.models = models;
|
|
841
485
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
486
|
+
session;
|
|
487
|
+
chains;
|
|
488
|
+
models;
|
|
489
|
+
tried = /* @__PURE__ */ new Map();
|
|
490
|
+
switching = /* @__PURE__ */ new Set();
|
|
491
|
+
forget(sessionID) {
|
|
492
|
+
this.tried.delete(sessionID);
|
|
493
|
+
}
|
|
494
|
+
async retry(input) {
|
|
495
|
+
if (!Object.hasOwn(this.chains, input.agent)) return;
|
|
496
|
+
const chain = this.chains[input.agent];
|
|
497
|
+
const current = `${input.model.providerID}/${input.model.id}`;
|
|
498
|
+
const inChain = chain.some((entry) => {
|
|
499
|
+
const ref = modelRef(entry);
|
|
500
|
+
return ref && `${ref.providerID}/${ref.id}` === current;
|
|
501
|
+
});
|
|
502
|
+
if (!inChain || !isRateLimitError(input.error)) return;
|
|
503
|
+
const lock = JSON.stringify([input.sessionID, input.agent]);
|
|
504
|
+
if (this.switching.has(lock)) return;
|
|
505
|
+
this.switching.add(lock);
|
|
849
506
|
try {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
507
|
+
let agents = this.tried.get(input.sessionID);
|
|
508
|
+
if (!agents) {
|
|
509
|
+
agents = /* @__PURE__ */ new Map();
|
|
510
|
+
this.tried.set(input.sessionID, agents);
|
|
511
|
+
}
|
|
512
|
+
let tried = agents.get(input.agent);
|
|
513
|
+
if (!tried) {
|
|
514
|
+
tried = /* @__PURE__ */ new Set();
|
|
515
|
+
agents.set(input.agent, tried);
|
|
516
|
+
}
|
|
517
|
+
tried.add(current);
|
|
518
|
+
for (const entry of chain) {
|
|
519
|
+
const ref = modelRef(entry);
|
|
520
|
+
if (!ref) continue;
|
|
521
|
+
const id = `${ref.providerID}/${ref.id}`;
|
|
522
|
+
if (tried.has(id)) continue;
|
|
523
|
+
tried.add(id);
|
|
524
|
+
const variant = this.models[input.agent]?.find(
|
|
525
|
+
(model) => model.id === entry
|
|
526
|
+
)?.variant;
|
|
527
|
+
await this.session.switchModel({
|
|
528
|
+
sessionID: input.sessionID,
|
|
529
|
+
model: {
|
|
530
|
+
...ref,
|
|
531
|
+
...variant ? { variant } : {}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
input.decision = { retry: true, delay: 0 };
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
} catch (error) {
|
|
538
|
+
log("[foreground-fallback] model switch failed", {
|
|
539
|
+
sessionID: input.sessionID,
|
|
540
|
+
error: String(error)
|
|
541
|
+
});
|
|
853
542
|
} 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));
|
|
543
|
+
this.switching.delete(lock);
|
|
865
544
|
}
|
|
866
545
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/opencode/session-state.ts
|
|
549
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
550
|
+
import { join as join2 } from "path";
|
|
551
|
+
function missing(error) {
|
|
552
|
+
return error.code === "ENOENT";
|
|
553
|
+
}
|
|
554
|
+
var SessionState = class {
|
|
555
|
+
constructor(directory, instructions) {
|
|
556
|
+
this.directory = directory;
|
|
557
|
+
this.instructions = instructions;
|
|
558
|
+
}
|
|
559
|
+
directory;
|
|
560
|
+
instructions;
|
|
561
|
+
parents = /* @__PURE__ */ new Map();
|
|
562
|
+
running = /* @__PURE__ */ new Set();
|
|
563
|
+
published = /* @__PURE__ */ new Map();
|
|
564
|
+
records = /* @__PURE__ */ new Map();
|
|
565
|
+
queue = Promise.resolve();
|
|
566
|
+
serialize(run) {
|
|
567
|
+
const result = this.queue.then(run);
|
|
568
|
+
this.queue = result.catch(() => {
|
|
877
569
|
});
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
refresh(sessionID) {
|
|
573
|
+
return this.serialize(() => this.publish(sessionID));
|
|
574
|
+
}
|
|
575
|
+
event(event) {
|
|
576
|
+
return this.serialize(async () => {
|
|
577
|
+
const { sessionID } = event.data;
|
|
578
|
+
if (event.type === "session.created") {
|
|
579
|
+
if (event.data.parentID) {
|
|
580
|
+
this.parents.set(sessionID, event.data.parentID);
|
|
581
|
+
this.running.add(sessionID);
|
|
582
|
+
await this.publish(event.data.parentID);
|
|
583
|
+
}
|
|
584
|
+
await this.publish(sessionID);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const parent = this.parents.get(sessionID);
|
|
588
|
+
if (event.type === "session.status") {
|
|
589
|
+
if (event.data.status.type === "idle") this.running.delete(sessionID);
|
|
590
|
+
else if (parent) this.running.add(sessionID);
|
|
591
|
+
if (parent) await this.publish(parent);
|
|
592
|
+
if (this.published.has(sessionID)) await this.publish(sessionID);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this.parents.delete(sessionID);
|
|
596
|
+
this.running.delete(sessionID);
|
|
597
|
+
this.published.delete(sessionID);
|
|
598
|
+
for (const [child, owner] of this.parents) {
|
|
599
|
+
if (owner !== sessionID) continue;
|
|
600
|
+
this.parents.delete(child);
|
|
601
|
+
this.running.delete(child);
|
|
602
|
+
}
|
|
603
|
+
if (parent) await this.publish(parent);
|
|
892
604
|
});
|
|
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
605
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
606
|
+
async inventory() {
|
|
607
|
+
const root = join2(this.directory, ".thoth", "changes");
|
|
608
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(
|
|
609
|
+
(error) => {
|
|
610
|
+
if (missing(error)) return [];
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
const active = /* @__PURE__ */ new Set();
|
|
615
|
+
for (const entry of entries) {
|
|
616
|
+
if (!entry.isDirectory() || entry.name === "archive") continue;
|
|
617
|
+
const id = entry.name;
|
|
618
|
+
const path = join2(root, id, `${id}.md`);
|
|
619
|
+
try {
|
|
620
|
+
const info = await stat(path);
|
|
621
|
+
if (!info.isFile()) continue;
|
|
622
|
+
active.add(id);
|
|
623
|
+
const fingerprint = `${info.mtimeMs}:${info.ctimeMs}:${info.size}`;
|
|
624
|
+
if (this.records.get(id)?.fingerprint === fingerprint) continue;
|
|
625
|
+
const content = await readFile(path, "utf8");
|
|
626
|
+
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim() ?? id;
|
|
627
|
+
const status = content.match(/^\*\*(?:Status|Phase)\*\*:\s*.+$/gm) ?? [];
|
|
628
|
+
this.records.set(id, {
|
|
629
|
+
fingerprint,
|
|
630
|
+
label: `${title}${status.length ? ` (${status.join("; ")})` : ""}`
|
|
631
|
+
});
|
|
632
|
+
} catch (error) {
|
|
633
|
+
if (!missing(error)) throw error;
|
|
634
|
+
active.delete(id);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const id of this.records.keys())
|
|
638
|
+
if (!active.has(id)) this.records.delete(id);
|
|
639
|
+
return [...this.records].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(
|
|
640
|
+
([id, record]) => `- .thoth/changes/${id}/${id}.md \u2014 ${record.label}`
|
|
641
|
+
);
|
|
908
642
|
}
|
|
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"
|
|
643
|
+
async publish(sessionID) {
|
|
644
|
+
const records = await this.inventory();
|
|
645
|
+
const children = [...this.parents].filter(
|
|
646
|
+
([child, parent]) => parent === sessionID && this.running.has(child)
|
|
647
|
+
).map(([child]) => child).sort();
|
|
648
|
+
const value = [
|
|
649
|
+
"Thoth state",
|
|
650
|
+
"Active SDD records:",
|
|
651
|
+
...records.length ? records : ["- none"],
|
|
652
|
+
"Running delegated sessions:",
|
|
653
|
+
...children.length ? children.map((id) => `- ${id}`) : ["- none"]
|
|
654
|
+
].join("\n");
|
|
655
|
+
if (this.published.get(sessionID) !== value) {
|
|
656
|
+
await this.instructions.entry.put({
|
|
657
|
+
sessionID,
|
|
658
|
+
key: "thoth.state",
|
|
659
|
+
value
|
|
937
660
|
});
|
|
938
|
-
|
|
661
|
+
this.published.set(sessionID, value);
|
|
939
662
|
}
|
|
940
|
-
|
|
941
|
-
} catch (err) {
|
|
942
|
-
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
663
|
+
return value;
|
|
943
664
|
}
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// src/opencode/session-hooks.ts
|
|
668
|
+
async function registerSessionHooks(ctx, deps) {
|
|
669
|
+
const names = new Set(deps.agentDefs.map((agent) => agent.name));
|
|
670
|
+
const chains = Object.fromEntries(
|
|
671
|
+
Object.entries(deps.runtimeChains).filter(([name]) => names.has(name))
|
|
672
|
+
);
|
|
673
|
+
const fallback = new ForegroundFallbackManager(
|
|
674
|
+
ctx.session,
|
|
675
|
+
chains,
|
|
676
|
+
deps.effectiveModelArrays
|
|
677
|
+
);
|
|
678
|
+
const state = new SessionState(deps.directory, ctx.session.instructions);
|
|
679
|
+
await ctx.session.hook("retry", (input) => fallback.retry(input));
|
|
680
|
+
await ctx.session.hook("prompt", async (input) => {
|
|
681
|
+
await state.refresh(input.sessionID);
|
|
951
682
|
});
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const tmux = await getTmuxPath();
|
|
970
|
-
if (!tmux) {
|
|
971
|
-
log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
|
|
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()
|
|
683
|
+
await ctx.session.hook("compaction", async (input) => {
|
|
684
|
+
const value = await state.refresh(input.sessionID);
|
|
685
|
+
if (input.result) {
|
|
686
|
+
input.result.summary += `
|
|
687
|
+
|
|
688
|
+
${value}`;
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
input.messages.push({
|
|
692
|
+
role: "user",
|
|
693
|
+
content: [
|
|
694
|
+
{
|
|
695
|
+
type: "text",
|
|
696
|
+
text: `Preserve this current Thoth state in the compaction summary:
|
|
697
|
+
${value}`
|
|
698
|
+
}
|
|
699
|
+
]
|
|
1002
700
|
});
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
701
|
+
});
|
|
702
|
+
const signal = new AbortController();
|
|
703
|
+
const events = ctx.event.subscribe({
|
|
704
|
+
types: ["session.created", "session.status", "session.deleted"],
|
|
705
|
+
signal: signal.signal
|
|
706
|
+
});
|
|
707
|
+
void (async () => {
|
|
708
|
+
try {
|
|
709
|
+
for await (const event of events) {
|
|
710
|
+
if (event.type === "session.deleted")
|
|
711
|
+
fallback.forget(event.data.sessionID);
|
|
712
|
+
try {
|
|
713
|
+
await state.event(event);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
log("[session-state] update failed", { error: String(error) });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch (error) {
|
|
719
|
+
log("[session-state] subscription ended", { error: String(error) });
|
|
720
|
+
} finally {
|
|
721
|
+
signal.abort();
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/opencode/skills.ts
|
|
727
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
728
|
+
import { resolve } from "path";
|
|
729
|
+
import { parse } from "yaml";
|
|
730
|
+
var ownedSkills = [
|
|
731
|
+
"thoth-init",
|
|
732
|
+
"thoth-sdd",
|
|
733
|
+
"thoth-constitution",
|
|
734
|
+
"thoth-archive",
|
|
735
|
+
"plan-reviewer"
|
|
736
|
+
];
|
|
737
|
+
async function registerSkills(ctx, deps) {
|
|
738
|
+
const skills = [];
|
|
739
|
+
for (const id of ownedSkills) {
|
|
740
|
+
const path = resolve(deps.packageRoot, "skills", id, "SKILL.md");
|
|
741
|
+
try {
|
|
742
|
+
const source = await readFile2(path, "utf8");
|
|
743
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(
|
|
744
|
+
source
|
|
1007
745
|
);
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
746
|
+
if (!match) throw new Error("Missing YAML frontmatter");
|
|
747
|
+
const metadata = parse(match[1]);
|
|
748
|
+
if (metadata?.name !== id || typeof metadata?.description !== "string" || !metadata.description.trim()) {
|
|
749
|
+
throw new Error(`Expected name ${id} and a non-empty description`);
|
|
750
|
+
}
|
|
751
|
+
skills.push({
|
|
752
|
+
id,
|
|
753
|
+
name: metadata.name,
|
|
754
|
+
description: metadata.description,
|
|
755
|
+
path,
|
|
756
|
+
content: match[2].trim()
|
|
1015
757
|
});
|
|
1016
|
-
|
|
758
|
+
} catch (cause) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`Cannot load bundled skill ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
761
|
+
{ cause }
|
|
762
|
+
);
|
|
1017
763
|
}
|
|
1018
|
-
return { success: false };
|
|
1019
|
-
} catch (err) {
|
|
1020
|
-
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
1021
|
-
return { success: false };
|
|
1022
764
|
}
|
|
765
|
+
await ctx.skill.transform((editor) => {
|
|
766
|
+
for (const skill of skills) {
|
|
767
|
+
if (editor.get(skill.id))
|
|
768
|
+
editor.update(skill.id, (existing) => {
|
|
769
|
+
Object.assign(existing, skill);
|
|
770
|
+
});
|
|
771
|
+
else editor.add(skill);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
1023
774
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const layout = storedConfig.layout ?? "main-vertical";
|
|
1057
|
-
const mainPaneSize = storedConfig.main_pane_size ?? 60;
|
|
1058
|
-
await applyLayout(tmux, layout, mainPaneSize);
|
|
1059
|
-
log("[tmux] closeTmuxPane: layout reapplied", { layout });
|
|
775
|
+
|
|
776
|
+
// src/tools/ast-grep/tools.ts
|
|
777
|
+
import { z } from "zod";
|
|
778
|
+
|
|
779
|
+
// src/tools/define-tool.ts
|
|
780
|
+
function defineTool(definition) {
|
|
781
|
+
return {
|
|
782
|
+
name: definition.name,
|
|
783
|
+
description: definition.description,
|
|
784
|
+
input: definition.input,
|
|
785
|
+
async execute(input, context) {
|
|
786
|
+
context.signal.throwIfAborted();
|
|
787
|
+
let onAbort = () => {
|
|
788
|
+
};
|
|
789
|
+
const aborted = new Promise((_, reject) => {
|
|
790
|
+
onAbort = () => reject(context.signal.reason);
|
|
791
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
792
|
+
});
|
|
793
|
+
try {
|
|
794
|
+
const content = await Promise.race([
|
|
795
|
+
definition.execute(input, context),
|
|
796
|
+
aborted
|
|
797
|
+
]);
|
|
798
|
+
context.signal.throwIfAborted();
|
|
799
|
+
if (definition.showOutput) {
|
|
800
|
+
await context.progress({ output: content });
|
|
801
|
+
context.signal.throwIfAborted();
|
|
802
|
+
return { content, metadata: { output: content } };
|
|
803
|
+
}
|
|
804
|
+
return { content };
|
|
805
|
+
} finally {
|
|
806
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
1060
807
|
}
|
|
1061
|
-
return true;
|
|
1062
808
|
}
|
|
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
|
-
}
|
|
809
|
+
};
|
|
1077
810
|
}
|
|
1078
811
|
|
|
812
|
+
// src/tools/ast-grep/cli.ts
|
|
813
|
+
import { existsSync as existsSync4 } from "fs";
|
|
814
|
+
|
|
815
|
+
// src/tools/ast-grep/constants.ts
|
|
816
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
817
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
818
|
+
import { createRequire as createRequire3 } from "module";
|
|
819
|
+
import { dirname, join as join4 } from "path";
|
|
820
|
+
|
|
821
|
+
// src/tools/ast-grep/downloader.ts
|
|
822
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
|
|
823
|
+
import { createRequire as createRequire2 } from "module";
|
|
824
|
+
import { homedir } from "os";
|
|
825
|
+
import { join as join3 } from "path";
|
|
826
|
+
|
|
1079
827
|
// src/utils/zip-extractor.ts
|
|
1080
828
|
import { release } from "os";
|
|
1081
829
|
var WINDOWS_BUILD_WITH_TAR = 17134;
|
|
@@ -1097,8 +845,8 @@ function isPwshAvailable() {
|
|
|
1097
845
|
});
|
|
1098
846
|
return result.exitCode === 0;
|
|
1099
847
|
}
|
|
1100
|
-
function escapePowerShellPath(
|
|
1101
|
-
return
|
|
848
|
+
function escapePowerShellPath(path) {
|
|
849
|
+
return path.replace(/'/g, "''");
|
|
1102
850
|
}
|
|
1103
851
|
function getWindowsZipExtractor() {
|
|
1104
852
|
const buildNumber = getWindowsBuildNumber();
|
|
@@ -1166,7 +914,7 @@ async function extractZip(archivePath, destDir) {
|
|
|
1166
914
|
}
|
|
1167
915
|
|
|
1168
916
|
// src/utils/file-io.ts
|
|
1169
|
-
import { readFile, writeFile } from "fs/promises";
|
|
917
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
1170
918
|
async function writeTextFile(filePath, contents) {
|
|
1171
919
|
await writeFile(
|
|
1172
920
|
filePath,
|
|
@@ -1195,22 +943,22 @@ var PLATFORM_MAP = {
|
|
|
1195
943
|
"win32-arm64": { arch: "aarch64", os: "pc-windows-msvc" },
|
|
1196
944
|
"win32-ia32": { arch: "i686", os: "pc-windows-msvc" }
|
|
1197
945
|
};
|
|
1198
|
-
function
|
|
946
|
+
function getCacheDir() {
|
|
1199
947
|
if (process.platform === "win32") {
|
|
1200
948
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
1201
|
-
const base2 = localAppData ||
|
|
1202
|
-
return
|
|
949
|
+
const base2 = localAppData || join3(homedir(), "AppData", "Local");
|
|
950
|
+
return join3(base2, "thoth-agents", "bin");
|
|
1203
951
|
}
|
|
1204
952
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
1205
|
-
const base = xdgCache ||
|
|
1206
|
-
return
|
|
953
|
+
const base = xdgCache || join3(homedir(), ".cache");
|
|
954
|
+
return join3(base, "thoth-agents", "bin");
|
|
1207
955
|
}
|
|
1208
956
|
function getBinaryName() {
|
|
1209
957
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
1210
958
|
}
|
|
1211
959
|
function getCachedBinaryPath() {
|
|
1212
|
-
const binaryPath =
|
|
1213
|
-
return
|
|
960
|
+
const binaryPath = join3(getCacheDir(), getBinaryName());
|
|
961
|
+
return existsSync2(binaryPath) ? binaryPath : null;
|
|
1214
962
|
}
|
|
1215
963
|
async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
1216
964
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
@@ -1221,32 +969,32 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
1221
969
|
);
|
|
1222
970
|
return null;
|
|
1223
971
|
}
|
|
1224
|
-
const cacheDir =
|
|
972
|
+
const cacheDir = getCacheDir();
|
|
1225
973
|
const binaryName = getBinaryName();
|
|
1226
|
-
const binaryPath =
|
|
1227
|
-
if (
|
|
974
|
+
const binaryPath = join3(cacheDir, binaryName);
|
|
975
|
+
if (existsSync2(binaryPath)) {
|
|
1228
976
|
return binaryPath;
|
|
1229
977
|
}
|
|
1230
|
-
const { arch, os
|
|
1231
|
-
const assetName = `app-${arch}-${
|
|
978
|
+
const { arch, os } = platformInfo;
|
|
979
|
+
const assetName = `app-${arch}-${os}.zip`;
|
|
1232
980
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
1233
981
|
console.log(`[thoth-agents] Downloading ast-grep binary...`);
|
|
1234
982
|
try {
|
|
1235
|
-
if (!
|
|
983
|
+
if (!existsSync2(cacheDir)) {
|
|
1236
984
|
mkdirSync(cacheDir, { recursive: true });
|
|
1237
985
|
}
|
|
1238
986
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
1239
987
|
if (!response.ok) {
|
|
1240
988
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1241
989
|
}
|
|
1242
|
-
const archivePath =
|
|
990
|
+
const archivePath = join3(cacheDir, assetName);
|
|
1243
991
|
const arrayBuffer = await response.arrayBuffer();
|
|
1244
992
|
await writeTextFile(archivePath, arrayBuffer);
|
|
1245
993
|
await extractZip(archivePath, cacheDir);
|
|
1246
|
-
if (
|
|
994
|
+
if (existsSync2(archivePath)) {
|
|
1247
995
|
unlinkSync(archivePath);
|
|
1248
996
|
}
|
|
1249
|
-
if (process.platform !== "win32" &&
|
|
997
|
+
if (process.platform !== "win32" && existsSync2(binaryPath)) {
|
|
1250
998
|
chmodSync(binaryPath, 493);
|
|
1251
999
|
}
|
|
1252
1000
|
console.log(`[thoth-agents] ast-grep binary ready.`);
|
|
@@ -1300,7 +1048,7 @@ var CLI_LANGUAGES = [
|
|
|
1300
1048
|
var MIN_BINARY_SIZE = 1e4;
|
|
1301
1049
|
function isValidBinary(filePath) {
|
|
1302
1050
|
try {
|
|
1303
|
-
return
|
|
1051
|
+
return statSync(filePath).size > MIN_BINARY_SIZE;
|
|
1304
1052
|
} catch {
|
|
1305
1053
|
return false;
|
|
1306
1054
|
}
|
|
@@ -1329,9 +1077,9 @@ function findSgCliPathSync() {
|
|
|
1329
1077
|
try {
|
|
1330
1078
|
const require3 = createRequire3(import.meta.url);
|
|
1331
1079
|
const cliPkgPath = require3.resolve("@ast-grep/cli/package.json");
|
|
1332
|
-
const cliDir =
|
|
1333
|
-
const sgPath =
|
|
1334
|
-
if (
|
|
1080
|
+
const cliDir = dirname(cliPkgPath);
|
|
1081
|
+
const sgPath = join4(cliDir, binaryName);
|
|
1082
|
+
if (existsSync3(sgPath) && isValidBinary(sgPath)) {
|
|
1335
1083
|
return sgPath;
|
|
1336
1084
|
}
|
|
1337
1085
|
} catch {
|
|
@@ -1341,10 +1089,10 @@ function findSgCliPathSync() {
|
|
|
1341
1089
|
try {
|
|
1342
1090
|
const require3 = createRequire3(import.meta.url);
|
|
1343
1091
|
const pkgPath = require3.resolve(`${platformPkg}/package.json`);
|
|
1344
|
-
const pkgDir =
|
|
1092
|
+
const pkgDir = dirname(pkgPath);
|
|
1345
1093
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
1346
|
-
const binaryPath =
|
|
1347
|
-
if (
|
|
1094
|
+
const binaryPath = join4(pkgDir, astGrepName);
|
|
1095
|
+
if (existsSync3(binaryPath) && isValidBinary(binaryPath)) {
|
|
1348
1096
|
return binaryPath;
|
|
1349
1097
|
}
|
|
1350
1098
|
} catch {
|
|
@@ -1352,9 +1100,9 @@ function findSgCliPathSync() {
|
|
|
1352
1100
|
}
|
|
1353
1101
|
if (process.platform === "darwin") {
|
|
1354
1102
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
1355
|
-
for (const
|
|
1356
|
-
if (
|
|
1357
|
-
return
|
|
1103
|
+
for (const path of homebrewPaths) {
|
|
1104
|
+
if (existsSync3(path) && isValidBinary(path)) {
|
|
1105
|
+
return path;
|
|
1358
1106
|
}
|
|
1359
1107
|
}
|
|
1360
1108
|
}
|
|
@@ -1371,8 +1119,8 @@ function getSgCliPath() {
|
|
|
1371
1119
|
}
|
|
1372
1120
|
return "sg";
|
|
1373
1121
|
}
|
|
1374
|
-
function setSgCliPath(
|
|
1375
|
-
resolvedCliPath =
|
|
1122
|
+
function setSgCliPath(path) {
|
|
1123
|
+
resolvedCliPath = path;
|
|
1376
1124
|
}
|
|
1377
1125
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1378
1126
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -1382,7 +1130,7 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
1382
1130
|
var initPromise = null;
|
|
1383
1131
|
async function getAstGrepPath() {
|
|
1384
1132
|
const currentPath = getSgCliPath();
|
|
1385
|
-
if (currentPath !== "sg" &&
|
|
1133
|
+
if (currentPath !== "sg" && existsSync4(currentPath)) {
|
|
1386
1134
|
return currentPath;
|
|
1387
1135
|
}
|
|
1388
1136
|
if (initPromise) {
|
|
@@ -1390,7 +1138,7 @@ async function getAstGrepPath() {
|
|
|
1390
1138
|
}
|
|
1391
1139
|
initPromise = (async () => {
|
|
1392
1140
|
const syncPath = findSgCliPathSync();
|
|
1393
|
-
if (syncPath &&
|
|
1141
|
+
if (syncPath && existsSync4(syncPath)) {
|
|
1394
1142
|
setSgCliPath(syncPath);
|
|
1395
1143
|
return syncPath;
|
|
1396
1144
|
}
|
|
@@ -1404,6 +1152,7 @@ async function getAstGrepPath() {
|
|
|
1404
1152
|
return initPromise;
|
|
1405
1153
|
}
|
|
1406
1154
|
async function runSg(options) {
|
|
1155
|
+
options.signal?.throwIfAborted();
|
|
1407
1156
|
const args = [
|
|
1408
1157
|
"run",
|
|
1409
1158
|
"-p",
|
|
@@ -1429,18 +1178,21 @@ async function runSg(options) {
|
|
|
1429
1178
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
1430
1179
|
args.push(...paths);
|
|
1431
1180
|
let cliPath = getSgCliPath();
|
|
1432
|
-
if (!
|
|
1181
|
+
if (!existsSync4(cliPath) && cliPath !== "sg") {
|
|
1433
1182
|
const downloadedPath = await getAstGrepPath();
|
|
1434
1183
|
if (downloadedPath) {
|
|
1435
1184
|
cliPath = downloadedPath;
|
|
1436
1185
|
}
|
|
1437
1186
|
}
|
|
1187
|
+
options.signal?.throwIfAborted();
|
|
1438
1188
|
const timeout = DEFAULT_TIMEOUT_MS;
|
|
1439
1189
|
const proc = spawn([cliPath, ...args], {
|
|
1440
1190
|
windowsHide: true,
|
|
1441
1191
|
stdout: "pipe",
|
|
1442
1192
|
stderr: "pipe"
|
|
1443
1193
|
});
|
|
1194
|
+
const onAbort = () => proc.kill();
|
|
1195
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1444
1196
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1445
1197
|
const id = setTimeout(() => {
|
|
1446
1198
|
proc.kill();
|
|
@@ -1459,6 +1211,7 @@ async function runSg(options) {
|
|
|
1459
1211
|
stderr = await new Response(proc.stderr).text();
|
|
1460
1212
|
exitCode = await proc.exited;
|
|
1461
1213
|
} catch (e) {
|
|
1214
|
+
options.signal?.throwIfAborted();
|
|
1462
1215
|
const error = e;
|
|
1463
1216
|
if (error.message?.includes("timeout")) {
|
|
1464
1217
|
return {
|
|
@@ -1495,7 +1248,10 @@ Auto-download failed. Manual install options:
|
|
|
1495
1248
|
truncated: false,
|
|
1496
1249
|
error: `Failed to spawn ast-grep: ${error.message}`
|
|
1497
1250
|
};
|
|
1251
|
+
} finally {
|
|
1252
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1498
1253
|
}
|
|
1254
|
+
options.signal?.throwIfAborted();
|
|
1499
1255
|
if (exitCode !== 0 && stdout.trim() === "") {
|
|
1500
1256
|
if (stderr.includes("No files found")) {
|
|
1501
1257
|
return { matches: [], totalMatches: 0, truncated: false };
|
|
@@ -1649,24 +1405,23 @@ function getEmptyResultHint(pattern, lang) {
|
|
|
1649
1405
|
}
|
|
1650
1406
|
|
|
1651
1407
|
// src/tools/ast-grep/tools.ts
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
}
|
|
1656
|
-
var ast_grep_search = tool({
|
|
1408
|
+
var ast_grep_search = defineTool({
|
|
1409
|
+
showOutput: true,
|
|
1410
|
+
name: "ast_grep_search",
|
|
1657
1411
|
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:
|
|
1412
|
+
input: z.object({
|
|
1413
|
+
pattern: z.string().describe(
|
|
1660
1414
|
"AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."
|
|
1661
1415
|
),
|
|
1662
|
-
lang:
|
|
1663
|
-
paths:
|
|
1664
|
-
globs:
|
|
1665
|
-
context:
|
|
1666
|
-
},
|
|
1416
|
+
lang: z.enum(CLI_LANGUAGES).describe("Target language"),
|
|
1417
|
+
paths: z.array(z.string()).optional().describe("Paths to search (default: ['.'])"),
|
|
1418
|
+
globs: z.array(z.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
|
1419
|
+
context: z.number().optional().describe("Context lines around match")
|
|
1420
|
+
}),
|
|
1667
1421
|
execute: async (args, context) => {
|
|
1668
1422
|
try {
|
|
1669
1423
|
const result = await runSg({
|
|
1424
|
+
signal: context.signal,
|
|
1670
1425
|
pattern: args.pattern,
|
|
1671
1426
|
lang: args.lang,
|
|
1672
1427
|
paths: args.paths,
|
|
@@ -1682,28 +1437,28 @@ var ast_grep_search = tool({
|
|
|
1682
1437
|
${hint}`;
|
|
1683
1438
|
}
|
|
1684
1439
|
}
|
|
1685
|
-
showOutputToUser(context, output);
|
|
1686
1440
|
return output;
|
|
1687
1441
|
} catch (e) {
|
|
1688
|
-
|
|
1689
|
-
showOutputToUser(context, output);
|
|
1690
|
-
return output;
|
|
1442
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1691
1443
|
}
|
|
1692
1444
|
}
|
|
1693
1445
|
});
|
|
1694
|
-
var ast_grep_replace =
|
|
1446
|
+
var ast_grep_replace = defineTool({
|
|
1447
|
+
showOutput: true,
|
|
1448
|
+
name: "ast_grep_replace",
|
|
1695
1449
|
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
|
-
},
|
|
1450
|
+
input: z.object({
|
|
1451
|
+
pattern: z.string().describe("AST pattern to match"),
|
|
1452
|
+
rewrite: z.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
|
1453
|
+
lang: z.enum(CLI_LANGUAGES).describe("Target language"),
|
|
1454
|
+
paths: z.array(z.string()).optional().describe("Paths to search"),
|
|
1455
|
+
globs: z.array(z.string()).optional().describe("Include/exclude globs"),
|
|
1456
|
+
dryRun: z.boolean().optional().describe("Preview changes without applying (default: true)")
|
|
1457
|
+
}),
|
|
1704
1458
|
execute: async (args, context) => {
|
|
1705
1459
|
try {
|
|
1706
1460
|
const result = await runSg({
|
|
1461
|
+
signal: context.signal,
|
|
1707
1462
|
pattern: args.pattern,
|
|
1708
1463
|
rewrite: args.rewrite,
|
|
1709
1464
|
lang: args.lang,
|
|
@@ -1711,20 +1466,16 @@ var ast_grep_replace = tool({
|
|
|
1711
1466
|
globs: args.globs,
|
|
1712
1467
|
updateAll: args.dryRun === false
|
|
1713
1468
|
});
|
|
1714
|
-
|
|
1715
|
-
showOutputToUser(context, output);
|
|
1716
|
-
return output;
|
|
1469
|
+
return formatReplaceResult(result, args.dryRun !== false);
|
|
1717
1470
|
} catch (e) {
|
|
1718
|
-
|
|
1719
|
-
showOutputToUser(context, output);
|
|
1720
|
-
return output;
|
|
1471
|
+
return `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1721
1472
|
}
|
|
1722
1473
|
}
|
|
1723
1474
|
});
|
|
1724
1475
|
|
|
1725
1476
|
// src/tools/lsp/client.ts
|
|
1726
|
-
import { readFileSync as
|
|
1727
|
-
import { extname, resolve as
|
|
1477
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1478
|
+
import { extname, resolve as resolve3 } from "path";
|
|
1728
1479
|
import { Readable, Writable } from "stream";
|
|
1729
1480
|
import { pathToFileURL } from "url";
|
|
1730
1481
|
import {
|
|
@@ -1734,31 +1485,13 @@ import {
|
|
|
1734
1485
|
} from "vscode-jsonrpc/node.js";
|
|
1735
1486
|
|
|
1736
1487
|
// src/tools/lsp/config.ts
|
|
1737
|
-
import { existsSync as
|
|
1738
|
-
import { homedir as
|
|
1739
|
-
import { join as
|
|
1488
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1489
|
+
import { homedir as homedir2 } from "os";
|
|
1490
|
+
import { join as join5 } from "path";
|
|
1740
1491
|
import whichSync from "which";
|
|
1741
1492
|
|
|
1742
1493
|
// src/tools/lsp/config-store.ts
|
|
1743
1494
|
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
1495
|
function getAllUserLspConfigs() {
|
|
1763
1496
|
return new Map(userConfig);
|
|
1764
1497
|
}
|
|
@@ -1767,8 +1500,8 @@ function hasUserLspConfig() {
|
|
|
1767
1500
|
}
|
|
1768
1501
|
|
|
1769
1502
|
// src/tools/lsp/constants.ts
|
|
1770
|
-
import { existsSync as
|
|
1771
|
-
import { dirname as
|
|
1503
|
+
import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
1504
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1772
1505
|
var SEVERITY_MAP = {
|
|
1773
1506
|
1: "error",
|
|
1774
1507
|
2: "warning",
|
|
@@ -1785,20 +1518,20 @@ var LOCK_FILE_PATTERNS = [
|
|
|
1785
1518
|
"bun.lock"
|
|
1786
1519
|
];
|
|
1787
1520
|
function* walkUpDirectories(start, stop) {
|
|
1788
|
-
let dir =
|
|
1521
|
+
let dir = resolve2(start);
|
|
1789
1522
|
try {
|
|
1790
|
-
if (!
|
|
1791
|
-
dir =
|
|
1523
|
+
if (!statSync2(dir).isDirectory()) {
|
|
1524
|
+
dir = dirname2(dir);
|
|
1792
1525
|
}
|
|
1793
1526
|
} catch {
|
|
1794
|
-
dir =
|
|
1527
|
+
dir = dirname2(dir);
|
|
1795
1528
|
}
|
|
1796
1529
|
let prevDir = "";
|
|
1797
1530
|
while (dir !== prevDir && dir !== "/") {
|
|
1798
1531
|
yield dir;
|
|
1799
1532
|
prevDir = dir;
|
|
1800
1533
|
if (dir === stop) break;
|
|
1801
|
-
dir =
|
|
1534
|
+
dir = dirname2(dir);
|
|
1802
1535
|
}
|
|
1803
1536
|
}
|
|
1804
1537
|
function NearestRoot(includePatterns, excludePatterns) {
|
|
@@ -1807,7 +1540,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
1807
1540
|
if (excludePatterns) {
|
|
1808
1541
|
for (const dir of walkUpDirectories(file, cwd)) {
|
|
1809
1542
|
for (const pattern of excludePatterns) {
|
|
1810
|
-
if (
|
|
1543
|
+
if (existsSync5(`${dir}/${pattern}`)) {
|
|
1811
1544
|
return void 0;
|
|
1812
1545
|
}
|
|
1813
1546
|
}
|
|
@@ -1826,7 +1559,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
1826
1559
|
}
|
|
1827
1560
|
} catch {
|
|
1828
1561
|
}
|
|
1829
|
-
} else if (
|
|
1562
|
+
} else if (existsSync5(`${dir}/${pattern}`)) {
|
|
1830
1563
|
return dir;
|
|
1831
1564
|
}
|
|
1832
1565
|
}
|
|
@@ -2448,11 +2181,11 @@ function isServerInstalled(command) {
|
|
|
2448
2181
|
if (command.length === 0) return false;
|
|
2449
2182
|
const cmd = command[0];
|
|
2450
2183
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2451
|
-
return
|
|
2184
|
+
return existsSync6(cmd);
|
|
2452
2185
|
}
|
|
2453
2186
|
const isWindows = process.platform === "win32";
|
|
2454
2187
|
const ext = isWindows ? ".exe" : "";
|
|
2455
|
-
const opencodeBin =
|
|
2188
|
+
const opencodeBin = join5(homedir2(), ".config", "opencode", "bin");
|
|
2456
2189
|
const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
|
|
2457
2190
|
const result = whichSync.sync(cmd, {
|
|
2458
2191
|
path: searchPath,
|
|
@@ -2463,8 +2196,8 @@ function isServerInstalled(command) {
|
|
|
2463
2196
|
return true;
|
|
2464
2197
|
}
|
|
2465
2198
|
const cwd = process.cwd();
|
|
2466
|
-
const localBin =
|
|
2467
|
-
if (
|
|
2199
|
+
const localBin = join5(cwd, "node_modules", ".bin", cmd);
|
|
2200
|
+
if (existsSync6(localBin) || existsSync6(localBin + ext)) {
|
|
2468
2201
|
return true;
|
|
2469
2202
|
}
|
|
2470
2203
|
return false;
|
|
@@ -2725,7 +2458,7 @@ var LSPClient = class {
|
|
|
2725
2458
|
this.processExited = true;
|
|
2726
2459
|
});
|
|
2727
2460
|
this.connection.listen();
|
|
2728
|
-
await new Promise((
|
|
2461
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
2729
2462
|
if (this.proc.exitCode !== null) {
|
|
2730
2463
|
const stderr = this.stderrBuffer.join("\n");
|
|
2731
2464
|
log("[lsp] LSPClient.start: server exited immediately", {
|
|
@@ -2800,12 +2533,12 @@ stderr: ${stderr}` : "")
|
|
|
2800
2533
|
log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
|
|
2801
2534
|
}
|
|
2802
2535
|
async openFile(filePath) {
|
|
2803
|
-
const absPath =
|
|
2536
|
+
const absPath = resolve3(filePath);
|
|
2804
2537
|
if (this.openedFiles.has(absPath)) {
|
|
2805
2538
|
log("[lsp] openFile: already open, skipping", { filePath: absPath });
|
|
2806
2539
|
return;
|
|
2807
2540
|
}
|
|
2808
|
-
const text =
|
|
2541
|
+
const text = readFileSync2(absPath, "utf-8");
|
|
2809
2542
|
const ext = extname(absPath);
|
|
2810
2543
|
const languageId = getLanguageId(ext);
|
|
2811
2544
|
log("[lsp] openFile: opening document", {
|
|
@@ -2825,7 +2558,7 @@ stderr: ${stderr}` : "")
|
|
|
2825
2558
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
2826
2559
|
}
|
|
2827
2560
|
async definition(filePath, line, character) {
|
|
2828
|
-
const absPath =
|
|
2561
|
+
const absPath = resolve3(filePath);
|
|
2829
2562
|
await this.openFile(absPath);
|
|
2830
2563
|
return this.connection?.sendRequest("textDocument/definition", {
|
|
2831
2564
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2833,7 +2566,7 @@ stderr: ${stderr}` : "")
|
|
|
2833
2566
|
});
|
|
2834
2567
|
}
|
|
2835
2568
|
async references(filePath, line, character, includeDeclaration = true) {
|
|
2836
|
-
const absPath =
|
|
2569
|
+
const absPath = resolve3(filePath);
|
|
2837
2570
|
await this.openFile(absPath);
|
|
2838
2571
|
return this.connection?.sendRequest("textDocument/references", {
|
|
2839
2572
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2842,7 +2575,7 @@ stderr: ${stderr}` : "")
|
|
|
2842
2575
|
});
|
|
2843
2576
|
}
|
|
2844
2577
|
async diagnostics(filePath) {
|
|
2845
|
-
const absPath =
|
|
2578
|
+
const absPath = resolve3(filePath);
|
|
2846
2579
|
const uri = pathToFileURL(absPath).href;
|
|
2847
2580
|
await this.openFile(absPath);
|
|
2848
2581
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -2861,7 +2594,7 @@ stderr: ${stderr}` : "")
|
|
|
2861
2594
|
return { items: this.diagnosticsStore.get(uri) ?? [] };
|
|
2862
2595
|
}
|
|
2863
2596
|
async rename(filePath, line, character, newName) {
|
|
2864
|
-
const absPath =
|
|
2597
|
+
const absPath = resolve3(filePath);
|
|
2865
2598
|
await this.openFile(absPath);
|
|
2866
2599
|
return this.connection?.sendRequest("textDocument/rename", {
|
|
2867
2600
|
textDocument: { uri: pathToFileURL(absPath).href },
|
|
@@ -2892,23 +2625,23 @@ stderr: ${stderr}` : "")
|
|
|
2892
2625
|
};
|
|
2893
2626
|
|
|
2894
2627
|
// src/tools/lsp/tools.ts
|
|
2895
|
-
import {
|
|
2628
|
+
import { z as z2 } from "zod";
|
|
2896
2629
|
|
|
2897
2630
|
// src/tools/lsp/utils.ts
|
|
2898
2631
|
import {
|
|
2899
|
-
existsSync as
|
|
2900
|
-
readFileSync as
|
|
2901
|
-
statSync as
|
|
2632
|
+
existsSync as existsSync7,
|
|
2633
|
+
readFileSync as readFileSync3,
|
|
2634
|
+
statSync as statSync3,
|
|
2902
2635
|
unlinkSync as unlinkSync2,
|
|
2903
2636
|
writeFileSync
|
|
2904
2637
|
} from "fs";
|
|
2905
|
-
import { dirname as
|
|
2638
|
+
import { dirname as dirname3, extname as extname2, join as join6, resolve as resolve4 } from "path";
|
|
2906
2639
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2907
2640
|
function findServerProjectRoot(filePath, server) {
|
|
2908
2641
|
if (server.root) {
|
|
2909
|
-
return server.root(filePath) ??
|
|
2642
|
+
return server.root(filePath) ?? dirname3(resolve4(filePath));
|
|
2910
2643
|
}
|
|
2911
|
-
return
|
|
2644
|
+
return dirname3(resolve4(filePath));
|
|
2912
2645
|
}
|
|
2913
2646
|
function uriToPath(uri) {
|
|
2914
2647
|
return fileURLToPath2(uri);
|
|
@@ -2926,7 +2659,7 @@ function formatServerLookupError(result) {
|
|
|
2926
2659
|
return `No LSP server configured for extension: ${result.extension}`;
|
|
2927
2660
|
}
|
|
2928
2661
|
async function withLspClient(filePath, fn) {
|
|
2929
|
-
const absPath =
|
|
2662
|
+
const absPath = resolve4(filePath);
|
|
2930
2663
|
const ext = extname2(absPath);
|
|
2931
2664
|
const result = findServerForExtension(ext);
|
|
2932
2665
|
if (result.status !== "found") {
|
|
@@ -2937,7 +2670,7 @@ async function withLspClient(filePath, fn) {
|
|
|
2937
2670
|
throw new Error(formatServerLookupError(result));
|
|
2938
2671
|
}
|
|
2939
2672
|
const server = result.server;
|
|
2940
|
-
const root = findServerProjectRoot(absPath, server) ??
|
|
2673
|
+
const root = findServerProjectRoot(absPath, server) ?? dirname3(absPath);
|
|
2941
2674
|
log("[lsp] withLspClient: acquiring client", {
|
|
2942
2675
|
filePath: absPath,
|
|
2943
2676
|
server: server.id,
|
|
@@ -3008,7 +2741,7 @@ function filterDiagnosticsBySeverity(diagnostics, severityFilter) {
|
|
|
3008
2741
|
}
|
|
3009
2742
|
function applyTextEditsToFile(filePath, edits) {
|
|
3010
2743
|
try {
|
|
3011
|
-
const content =
|
|
2744
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
3012
2745
|
const lines = content.split("\n");
|
|
3013
2746
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
3014
2747
|
if (b.range.start.line !== a.range.start.line) {
|
|
@@ -3092,7 +2825,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
3092
2825
|
try {
|
|
3093
2826
|
const oldPath = uriToPath(change.oldUri);
|
|
3094
2827
|
const newPath = uriToPath(change.newUri);
|
|
3095
|
-
const content =
|
|
2828
|
+
const content = readFileSync3(oldPath, "utf-8");
|
|
3096
2829
|
writeFileSync(newPath, content, "utf-8");
|
|
3097
2830
|
unlinkSync2(oldPath);
|
|
3098
2831
|
result.filesModified.push(newPath);
|
|
@@ -3154,16 +2887,18 @@ function formatApplyResult(result) {
|
|
|
3154
2887
|
|
|
3155
2888
|
// src/tools/lsp/tools.ts
|
|
3156
2889
|
var formatError = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
3157
|
-
var lsp_goto_definition =
|
|
2890
|
+
var lsp_goto_definition = defineTool({
|
|
2891
|
+
name: "lsp_goto_definition",
|
|
3158
2892
|
description: "Jump to symbol definition. Find WHERE something is defined.",
|
|
3159
|
-
|
|
3160
|
-
filePath:
|
|
3161
|
-
line:
|
|
3162
|
-
character:
|
|
3163
|
-
},
|
|
3164
|
-
execute: async (args) => {
|
|
2893
|
+
input: z2.object({
|
|
2894
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
2895
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
2896
|
+
character: z2.number().min(0).describe("0-based character offset")
|
|
2897
|
+
}),
|
|
2898
|
+
execute: async (args, context) => {
|
|
3165
2899
|
try {
|
|
3166
2900
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
2901
|
+
context.signal.throwIfAborted();
|
|
3167
2902
|
return await client.definition(
|
|
3168
2903
|
args.filePath,
|
|
3169
2904
|
args.line,
|
|
@@ -3183,17 +2918,19 @@ var lsp_goto_definition = tool2({
|
|
|
3183
2918
|
}
|
|
3184
2919
|
}
|
|
3185
2920
|
});
|
|
3186
|
-
var lsp_find_references =
|
|
2921
|
+
var lsp_find_references = defineTool({
|
|
2922
|
+
name: "lsp_find_references",
|
|
3187
2923
|
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) => {
|
|
2924
|
+
input: z2.object({
|
|
2925
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
2926
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
2927
|
+
character: z2.number().min(0).describe("0-based character offset"),
|
|
2928
|
+
includeDeclaration: z2.boolean().optional().describe("Include the declaration itself")
|
|
2929
|
+
}),
|
|
2930
|
+
execute: async (args, context) => {
|
|
3195
2931
|
try {
|
|
3196
2932
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
2933
|
+
context.signal.throwIfAborted();
|
|
3197
2934
|
return await client.references(
|
|
3198
2935
|
args.filePath,
|
|
3199
2936
|
args.line,
|
|
@@ -3219,15 +2956,17 @@ var lsp_find_references = tool2({
|
|
|
3219
2956
|
}
|
|
3220
2957
|
}
|
|
3221
2958
|
});
|
|
3222
|
-
var lsp_diagnostics =
|
|
2959
|
+
var lsp_diagnostics = defineTool({
|
|
2960
|
+
name: "lsp_diagnostics",
|
|
3223
2961
|
description: "Get errors, warnings, hints from language server BEFORE running build.",
|
|
3224
|
-
|
|
3225
|
-
filePath:
|
|
3226
|
-
severity:
|
|
3227
|
-
},
|
|
3228
|
-
execute: async (args) => {
|
|
2962
|
+
input: z2.object({
|
|
2963
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
2964
|
+
severity: z2.enum(["error", "warning", "information", "hint", "all"]).optional().describe("Filter by severity level")
|
|
2965
|
+
}),
|
|
2966
|
+
execute: async (args, context) => {
|
|
3229
2967
|
try {
|
|
3230
2968
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
2969
|
+
context.signal.throwIfAborted();
|
|
3231
2970
|
return await client.diagnostics(args.filePath);
|
|
3232
2971
|
});
|
|
3233
2972
|
let diagnostics = [];
|
|
@@ -3257,17 +2996,19 @@ var lsp_diagnostics = tool2({
|
|
|
3257
2996
|
}
|
|
3258
2997
|
}
|
|
3259
2998
|
});
|
|
3260
|
-
var lsp_rename =
|
|
2999
|
+
var lsp_rename = defineTool({
|
|
3000
|
+
name: "lsp_rename",
|
|
3261
3001
|
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) => {
|
|
3002
|
+
input: z2.object({
|
|
3003
|
+
filePath: z2.string().describe("Absolute path to the file"),
|
|
3004
|
+
line: z2.number().min(1).describe("1-based line number"),
|
|
3005
|
+
character: z2.number().min(0).describe("0-based character offset"),
|
|
3006
|
+
newName: z2.string().describe("New symbol name")
|
|
3007
|
+
}),
|
|
3008
|
+
execute: async (args, context) => {
|
|
3269
3009
|
try {
|
|
3270
3010
|
const edit = await withLspClient(args.filePath, async (client) => {
|
|
3011
|
+
context.signal.throwIfAborted();
|
|
3271
3012
|
return await client.rename(
|
|
3272
3013
|
args.filePath,
|
|
3273
3014
|
args.line,
|
|
@@ -3275,6 +3016,7 @@ var lsp_rename = tool2({
|
|
|
3275
3016
|
args.newName
|
|
3276
3017
|
);
|
|
3277
3018
|
});
|
|
3019
|
+
context.signal.throwIfAborted();
|
|
3278
3020
|
const result = applyWorkspaceEdit(edit);
|
|
3279
3021
|
return formatApplyResult(result);
|
|
3280
3022
|
} catch (e) {
|
|
@@ -3283,375 +3025,34 @@ var lsp_rename = tool2({
|
|
|
3283
3025
|
}
|
|
3284
3026
|
});
|
|
3285
3027
|
|
|
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
|
-
};
|
|
3028
|
+
// src/opencode/tools.ts
|
|
3029
|
+
async function registerTools(ctx, _deps) {
|
|
3030
|
+
await ctx.tool.transform((editor) => {
|
|
3031
|
+
editor.add(lsp_goto_definition);
|
|
3032
|
+
editor.add(lsp_find_references);
|
|
3033
|
+
editor.add(lsp_diagnostics);
|
|
3034
|
+
editor.add(lsp_rename);
|
|
3035
|
+
editor.add(ast_grep_search);
|
|
3036
|
+
editor.add(ast_grep_replace);
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3475
3039
|
|
|
3476
3040
|
// src/index.ts
|
|
3477
|
-
var
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
if (agentDef._modelArray && agentDef._modelArray.length > 0) {
|
|
3490
|
-
modelArrayMap[agentDef.name] = agentDef._modelArray;
|
|
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
|
-
}
|
|
3041
|
+
var plugin = {
|
|
3042
|
+
id: "thoth-agents",
|
|
3043
|
+
async setup(ctx) {
|
|
3044
|
+
const deps = loadOpenCodeDependencies(ctx);
|
|
3045
|
+
await registerAgents(ctx, deps);
|
|
3046
|
+
await registerTools(ctx, deps);
|
|
3047
|
+
await registerMcp(ctx, deps);
|
|
3048
|
+
await registerCommand(ctx, deps);
|
|
3049
|
+
await registerSkills(ctx, deps);
|
|
3050
|
+
await registerPermissions(ctx, deps);
|
|
3051
|
+
await registerDelegationHooks(ctx, deps);
|
|
3052
|
+
await registerSessionHooks(ctx, deps);
|
|
3513
3053
|
}
|
|
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();
|
|
3526
|
-
}
|
|
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
3054
|
};
|
|
3654
|
-
var src_default =
|
|
3055
|
+
var src_default = plugin;
|
|
3655
3056
|
export {
|
|
3656
3057
|
src_default as default
|
|
3657
3058
|
};
|