thoth-agents 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/{chunk-SEF6PFM6.js → chunk-5MYXVRZZ.js} +132 -157
- package/dist/chunk-6O4GKDS7.js +207 -0
- package/dist/{chunk-QOA2AETX.js → chunk-NOLDFT7O.js} +3 -3
- package/dist/{chunk-7F33WUSD.js → chunk-VVYAQTYE.js} +2 -2
- package/dist/{chunk-NY2IPJ7V.js → chunk-YCWGTDFE.js} +531 -233
- package/dist/cli/index.js +6 -6
- package/dist/cli/opencode-profiles.d.ts +10 -0
- package/dist/cli/tui/index.js +3 -3
- package/dist/config/loader.d.ts +1 -4
- package/dist/config/schema.d.ts +0 -22
- package/dist/index.js +824 -30
- package/dist/opencode/context.d.ts +16 -0
- package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
- package/dist/opencode/profile-rpc.d.ts +688 -0
- package/dist/opencode/profile-runtime.d.ts +3 -0
- package/dist/opencode/profile-sessions.d.ts +18 -0
- package/dist/opencode/profiles/editor.d.ts +44 -0
- package/dist/opencode/profiles/index.d.ts +18 -0
- package/dist/opencode/profiles/resolver.d.ts +12 -0
- package/dist/opencode/profiles/store.d.ts +63 -0
- package/dist/opencode/profiles/types.d.ts +26 -0
- package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
- package/dist/opencode/tool-overrides/index.d.ts +5 -0
- package/dist/opencode/tool-overrides/store.d.ts +38 -0
- package/dist/opencode/tool-overrides/types.d.ts +32 -0
- package/dist/opencode/tool-overrides/validation.d.ts +5 -0
- package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
- package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
- package/dist/opencode/tool-rpc.d.ts +428 -0
- package/dist/pi.js +2 -2
- package/dist/tui/fixtures.d.ts +2 -0
- package/dist/tui/flow.d.ts +16 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +1384 -0
- package/dist/tui/model.d.ts +84 -0
- package/dist/tui/tools/fixtures.d.ts +3 -0
- package/dist/tui/tools/flow.d.ts +11 -0
- package/dist/tui/tools/model.d.ts +97 -0
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/thoth-agents.schema.json +0 -70
package/dist/index.js
CHANGED
|
@@ -2,10 +2,14 @@ import {
|
|
|
2
2
|
formatGateVerdict,
|
|
3
3
|
summarizeGateResult
|
|
4
4
|
} from "./chunk-JLU3FSBT.js";
|
|
5
|
+
import {
|
|
6
|
+
profileRpc,
|
|
7
|
+
toolRpc
|
|
8
|
+
} from "./chunk-6O4GKDS7.js";
|
|
5
9
|
import {
|
|
6
10
|
createAgents,
|
|
7
11
|
renderOpenCodeAgentConfigs
|
|
8
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NOLDFT7O.js";
|
|
9
13
|
import {
|
|
10
14
|
context7,
|
|
11
15
|
exa,
|
|
@@ -14,8 +18,15 @@ import {
|
|
|
14
18
|
spawnSync
|
|
15
19
|
} from "./chunk-MIDGZE34.js";
|
|
16
20
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
ModelProfileStore,
|
|
22
|
+
PROFILE_ROLES,
|
|
23
|
+
ProfileConflictError,
|
|
24
|
+
formatModelString,
|
|
25
|
+
loadPluginConfig,
|
|
26
|
+
mergeProfileModelChain,
|
|
27
|
+
resolveRoleProfile,
|
|
28
|
+
validateProfileName
|
|
29
|
+
} from "./chunk-YCWGTDFE.js";
|
|
19
30
|
|
|
20
31
|
// src/opencode/permission-rules.ts
|
|
21
32
|
var ACTIONS = {
|
|
@@ -65,6 +76,202 @@ function translatePermissionRules(permission) {
|
|
|
65
76
|
];
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
// src/opencode/tool-overrides/wildcard.ts
|
|
80
|
+
function matchAction(input, pattern, platform = process.platform) {
|
|
81
|
+
const normalized = input.replaceAll("\\", "/");
|
|
82
|
+
let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
83
|
+
if (escaped.endsWith(" .*")) escaped = `${escaped.slice(0, -3)}( .*)?`;
|
|
84
|
+
return new RegExp(`^${escaped}$`, platform === "win32" ? "si" : "s").test(
|
|
85
|
+
normalized
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function countActionMatches(pattern, actions, platform = process.platform) {
|
|
89
|
+
return actions.filter((action) => matchAction(action, pattern, platform)).length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/opencode/tool-overrides/evaluator.ts
|
|
93
|
+
function appendOverrideRules(overrides) {
|
|
94
|
+
if (!overrides.length) return [];
|
|
95
|
+
return [
|
|
96
|
+
...overrides.map((override) => ({ ...override, resource: "*" })),
|
|
97
|
+
{ action: "subagent", resource: "*", effect: "deny" }
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
function evaluateAction(base, effective, action, platform = process.platform) {
|
|
101
|
+
let result = {
|
|
102
|
+
effect: "ask",
|
|
103
|
+
origin: "default"
|
|
104
|
+
};
|
|
105
|
+
for (const rule of base) {
|
|
106
|
+
if (matchAction(action, rule.action, platform) && matchAction("*", rule.resource, platform))
|
|
107
|
+
result = { effect: rule.effect, origin: "default" };
|
|
108
|
+
}
|
|
109
|
+
if (effective) {
|
|
110
|
+
const rules = appendOverrideRules(effective.overrides);
|
|
111
|
+
for (const [index, rule] of rules.entries()) {
|
|
112
|
+
if (matchAction(action, rule.action, platform))
|
|
113
|
+
result = {
|
|
114
|
+
effect: rule.effect,
|
|
115
|
+
origin: index === rules.length - 1 ? "default" : effective.origin
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/opencode/tool-overrides/store.ts
|
|
123
|
+
import { createHash, randomUUID } from "crypto";
|
|
124
|
+
import fs from "fs";
|
|
125
|
+
import os from "os";
|
|
126
|
+
import path from "path";
|
|
127
|
+
|
|
128
|
+
// src/opencode/tool-overrides/types.ts
|
|
129
|
+
var SPECIALIST_ROLES = PROFILE_ROLES.filter(
|
|
130
|
+
(role) => role !== "orchestrator"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// src/opencode/tool-overrides/validation.ts
|
|
134
|
+
function object(value) {
|
|
135
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
function parseActionOverrides(value) {
|
|
138
|
+
if (!Array.isArray(value)) throw new Error("Expected an overrides array");
|
|
139
|
+
return value.map((entry) => {
|
|
140
|
+
if (!object(entry) || typeof entry.action !== "string" || !entry.action.trim())
|
|
141
|
+
throw new Error("Action must be a non-empty pattern");
|
|
142
|
+
if (entry.effect !== "allow" && entry.effect !== "ask" && entry.effect !== "deny")
|
|
143
|
+
throw new Error("Effect must be allow, ask or deny");
|
|
144
|
+
return { action: entry.action.trim(), effect: entry.effect };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function validateSpecialistRole(role) {
|
|
148
|
+
const canonical = SPECIALIST_ROLES.find((name) => name === role);
|
|
149
|
+
if (!canonical) throw new Error(`Unknown specialist role: ${role}`);
|
|
150
|
+
return canonical;
|
|
151
|
+
}
|
|
152
|
+
function parseToolsDocument(value, warnings = []) {
|
|
153
|
+
if (!object(value) || value.version !== 1 || !object(value.roles))
|
|
154
|
+
throw new Error("Expected version 1 and a roles object");
|
|
155
|
+
const roles = {};
|
|
156
|
+
for (const [role, entry] of Object.entries(value.roles)) {
|
|
157
|
+
const canonical = SPECIALIST_ROLES.find((name) => name === role);
|
|
158
|
+
if (!canonical) {
|
|
159
|
+
warnings.push(`Unknown specialist role ignored: ${role}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!object(entry)) throw new Error(`Invalid overrides for ${role}`);
|
|
163
|
+
roles[canonical] = { overrides: parseActionOverrides(entry.overrides) };
|
|
164
|
+
}
|
|
165
|
+
return { version: 1, roles };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/opencode/tool-overrides/store.ts
|
|
169
|
+
function toolsOverridePaths(options) {
|
|
170
|
+
if (options.paths) return { ...options.paths };
|
|
171
|
+
const env = options.env ?? process.env;
|
|
172
|
+
const configDir = options.configDir ?? (env.OPENCODE_CONFIG_DIR?.trim() || path.join(
|
|
173
|
+
env.XDG_CONFIG_HOME || path.join(options.homeDir ?? os.homedir(), ".config"),
|
|
174
|
+
"opencode"
|
|
175
|
+
));
|
|
176
|
+
return {
|
|
177
|
+
global: path.join(configDir, "thoth", "tools.json"),
|
|
178
|
+
project: path.join(options.cwd, ".opencode", "thoth", "tools.json")
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
var emptyDocument = () => ({ version: 1, roles: {} });
|
|
182
|
+
var revisionOf = (raw) => raw === null ? "missing" : createHash("sha256").update(raw).digest("hex");
|
|
183
|
+
var ToolsConflictError = class extends Error {
|
|
184
|
+
constructor(scope) {
|
|
185
|
+
super(`Tools changed on disk: ${scope}; reload before saving.`);
|
|
186
|
+
this.scope = scope;
|
|
187
|
+
this.name = "ToolsConflictError";
|
|
188
|
+
}
|
|
189
|
+
scope;
|
|
190
|
+
code = "TOOLS_CONFLICT";
|
|
191
|
+
};
|
|
192
|
+
var ToolsOverrideStore = class {
|
|
193
|
+
constructor(options) {
|
|
194
|
+
this.options = options;
|
|
195
|
+
this.paths = toolsOverridePaths(options);
|
|
196
|
+
}
|
|
197
|
+
options;
|
|
198
|
+
paths;
|
|
199
|
+
raw(scope) {
|
|
200
|
+
try {
|
|
201
|
+
return fs.readFileSync(this.paths[scope], "utf8");
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === "ENOENT") return null;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
read(scope) {
|
|
208
|
+
const warnings = [];
|
|
209
|
+
let raw = null;
|
|
210
|
+
let document = emptyDocument();
|
|
211
|
+
try {
|
|
212
|
+
raw = this.raw(scope);
|
|
213
|
+
if (raw !== null)
|
|
214
|
+
document = parseToolsDocument(JSON.parse(raw), warnings);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
warnings.push(`Cannot read tools ${this.paths[scope]}: ${String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
for (const warning of warnings) this.options.warn?.(warning);
|
|
219
|
+
return { document, revision: revisionOf(raw), warnings };
|
|
220
|
+
}
|
|
221
|
+
resolve() {
|
|
222
|
+
const global = this.read("global");
|
|
223
|
+
const project = this.read("project");
|
|
224
|
+
const effective = {};
|
|
225
|
+
for (const role of SPECIALIST_ROLES) {
|
|
226
|
+
const origin = Object.hasOwn(project.document.roles, role) ? "project" : "global";
|
|
227
|
+
const entry = (origin === "project" ? project : global).document.roles[role];
|
|
228
|
+
if (entry) effective[role] = { ...entry, origin };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
global,
|
|
232
|
+
project,
|
|
233
|
+
effective,
|
|
234
|
+
warnings: [...global.warnings, ...project.warnings]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
mutate(scope, role, baseRevision, update) {
|
|
238
|
+
validateSpecialistRole(role);
|
|
239
|
+
const raw = this.raw(scope);
|
|
240
|
+
if (revisionOf(raw) !== baseRevision) throw new ToolsConflictError(scope);
|
|
241
|
+
const document = raw === null ? emptyDocument() : parseToolsDocument(JSON.parse(raw));
|
|
242
|
+
update(document);
|
|
243
|
+
const file = this.paths[scope];
|
|
244
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
245
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
246
|
+
try {
|
|
247
|
+
fs.writeFileSync(temp, `${JSON.stringify(document, null, 2)}
|
|
248
|
+
`, {
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
flag: "wx"
|
|
251
|
+
});
|
|
252
|
+
if (revisionOf(this.raw(scope)) !== baseRevision)
|
|
253
|
+
throw new ToolsConflictError(scope);
|
|
254
|
+
fs.renameSync(temp, file);
|
|
255
|
+
} finally {
|
|
256
|
+
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
257
|
+
}
|
|
258
|
+
return this.read(scope);
|
|
259
|
+
}
|
|
260
|
+
/** Save ordered rules losslessly, including duplicates; other roles survive. */
|
|
261
|
+
save(scope, role, overrides, baseRevision) {
|
|
262
|
+
const next = parseActionOverrides(overrides);
|
|
263
|
+
return this.mutate(scope, role, baseRevision, (document) => {
|
|
264
|
+
document.roles[role] = { overrides: next };
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/** Remove a scope's role entry (project reset resumes global inheritance). */
|
|
268
|
+
reset(scope, role, baseRevision) {
|
|
269
|
+
return this.mutate(scope, role, baseRevision, (document) => {
|
|
270
|
+
delete document.roles[role];
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
68
275
|
// src/opencode/agents.ts
|
|
69
276
|
var ROLE_COLORS = {
|
|
70
277
|
orchestrator: "#7c3aed",
|
|
@@ -89,6 +296,12 @@ async function registerAgents(ctx, deps) {
|
|
|
89
296
|
agent.color = /^#[0-9a-f]{6}$/i.test(config.color ?? "") ? config.color : ROLE_COLORS[name];
|
|
90
297
|
agent.steps = config.steps;
|
|
91
298
|
agent.permissions = translatePermissionRules(config.permission);
|
|
299
|
+
if (name !== "orchestrator") {
|
|
300
|
+
const overrides = deps.toolState.state.effective[name]?.overrides;
|
|
301
|
+
if (overrides?.length) {
|
|
302
|
+
agent.permissions.push(...appendOverrideRules(overrides));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
92
305
|
const separator = model?.indexOf("/") ?? -1;
|
|
93
306
|
agent.model = model && separator > 0 && separator < model.length - 1 ? {
|
|
94
307
|
providerID: model.slice(0, separator),
|
|
@@ -193,6 +406,68 @@ function loadOpenCodeDependencies(ctx) {
|
|
|
193
406
|
effectiveModelArrays[name] = effective;
|
|
194
407
|
}
|
|
195
408
|
}
|
|
409
|
+
const roster = {};
|
|
410
|
+
const configuredChains = {};
|
|
411
|
+
const baseArrays = structuredClone(effectiveModelArrays);
|
|
412
|
+
const baseRuntimeChains = structuredClone(runtimeChains);
|
|
413
|
+
for (const { name } of agentDefs) {
|
|
414
|
+
const role = name;
|
|
415
|
+
const first = effectiveModelArrays[name]?.[0];
|
|
416
|
+
roster[role] = first ? { model: first.id, variant: first.variant } : { model: agents[name]?.model, variant: agents[name]?.variant };
|
|
417
|
+
configuredChains[name] = effectiveModelArrays[name] ?? (agents[name]?.model ? [{ id: agents[name].model, variant: agents[name].variant }] : []);
|
|
418
|
+
}
|
|
419
|
+
const store = new ModelProfileStore({
|
|
420
|
+
cwd: directory,
|
|
421
|
+
roster,
|
|
422
|
+
warn: (message) => console.warn(`[thoth-agents] ${message}`)
|
|
423
|
+
});
|
|
424
|
+
const profileState = {
|
|
425
|
+
store,
|
|
426
|
+
roster,
|
|
427
|
+
selection: store.resolve(),
|
|
428
|
+
roles: {},
|
|
429
|
+
refresh() {
|
|
430
|
+
this.selection = store.resolve();
|
|
431
|
+
applyProfile();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
function applyProfile() {
|
|
435
|
+
for (const { name } of agentDefs) {
|
|
436
|
+
const role = name;
|
|
437
|
+
profileState.roles[role] = resolveRoleProfile(
|
|
438
|
+
role,
|
|
439
|
+
profileState.selection.profile,
|
|
440
|
+
roster
|
|
441
|
+
);
|
|
442
|
+
if (profileState.selection.name === "default") {
|
|
443
|
+
if (baseArrays[name])
|
|
444
|
+
effectiveModelArrays[name] = structuredClone(baseArrays[name]);
|
|
445
|
+
else delete effectiveModelArrays[name];
|
|
446
|
+
if (baseRuntimeChains[name])
|
|
447
|
+
runtimeChains[name] = [...baseRuntimeChains[name]];
|
|
448
|
+
else delete runtimeChains[name];
|
|
449
|
+
} else {
|
|
450
|
+
const chain = mergeProfileModelChain(
|
|
451
|
+
profileState.roles[role],
|
|
452
|
+
configuredChains[name]
|
|
453
|
+
);
|
|
454
|
+
effectiveModelArrays[name] = chain;
|
|
455
|
+
runtimeChains[name] = chain.map(({ id }) => id);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
applyProfile();
|
|
460
|
+
const toolStore = new ToolsOverrideStore({
|
|
461
|
+
cwd: directory,
|
|
462
|
+
warn: (message) => console.warn(`[thoth-agents] ${message}`)
|
|
463
|
+
});
|
|
464
|
+
const toolState = {
|
|
465
|
+
store: toolStore,
|
|
466
|
+
state: toolStore.resolve(),
|
|
467
|
+
refresh() {
|
|
468
|
+
this.state = toolStore.resolve();
|
|
469
|
+
}
|
|
470
|
+
};
|
|
196
471
|
return {
|
|
197
472
|
directory,
|
|
198
473
|
// Both src/opencode/context.ts and dist/index.js (or a root chunk) resolve
|
|
@@ -208,7 +483,9 @@ function loadOpenCodeDependencies(ctx) {
|
|
|
208
483
|
agents,
|
|
209
484
|
modelArrayMap,
|
|
210
485
|
effectiveModelArrays,
|
|
211
|
-
runtimeChains
|
|
486
|
+
runtimeChains,
|
|
487
|
+
profileState,
|
|
488
|
+
toolState
|
|
212
489
|
};
|
|
213
490
|
}
|
|
214
491
|
|
|
@@ -427,22 +704,444 @@ async function registerPermissions(ctx, deps) {
|
|
|
427
704
|
});
|
|
428
705
|
}
|
|
429
706
|
|
|
707
|
+
// src/opencode/profile-rpc-handlers.ts
|
|
708
|
+
import { createHash as createHash2 } from "crypto";
|
|
709
|
+
import path2 from "path";
|
|
710
|
+
|
|
711
|
+
// src/opencode/profile-sessions.ts
|
|
712
|
+
var SessionProfileSelections = class {
|
|
713
|
+
constructor(storage, projectDir) {
|
|
714
|
+
this.storage = storage;
|
|
715
|
+
this.projectDir = projectDir;
|
|
716
|
+
}
|
|
717
|
+
storage;
|
|
718
|
+
projectDir;
|
|
719
|
+
parse(value) {
|
|
720
|
+
if (typeof value === "string")
|
|
721
|
+
return { name: value, projectDir: this.projectDir };
|
|
722
|
+
if (value && typeof value === "object" && "name" in value && "projectDir" in value && typeof value.name === "string" && typeof value.projectDir === "string")
|
|
723
|
+
return { name: value.name, projectDir: value.projectDir };
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
async set(sessionID, name, projectDir = this.projectDir) {
|
|
727
|
+
const key = `model-profiles/session/${sessionID}`;
|
|
728
|
+
if (name === void 0) await this.storage.remove(key);
|
|
729
|
+
else await this.storage.set(key, { name, projectDir });
|
|
730
|
+
}
|
|
731
|
+
/** Collect before mutating storage so pagination remains stable. */
|
|
732
|
+
async all() {
|
|
733
|
+
const prefix = "model-profiles/session/";
|
|
734
|
+
const selections = [];
|
|
735
|
+
let after;
|
|
736
|
+
do {
|
|
737
|
+
const page = await this.storage.scan({ prefix, after });
|
|
738
|
+
for (const entry of page.entries) {
|
|
739
|
+
const value = this.parse(entry.value);
|
|
740
|
+
if (entry.key.startsWith(prefix) && value)
|
|
741
|
+
selections.push({
|
|
742
|
+
...value,
|
|
743
|
+
sessionID: entry.key.slice(prefix.length)
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
after = page.next;
|
|
747
|
+
} while (after !== void 0);
|
|
748
|
+
return selections;
|
|
749
|
+
}
|
|
750
|
+
async get(sessionID) {
|
|
751
|
+
return this.parse(
|
|
752
|
+
await this.storage.get(`model-profiles/session/${sessionID}`)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// src/opencode/profile-rpc-handlers.ts
|
|
758
|
+
function revision(profile) {
|
|
759
|
+
const canonical = profile === null ? null : {
|
|
760
|
+
agents: Object.fromEntries(
|
|
761
|
+
PROFILE_ROLES.filter((role) => profile.agents[role]).map((role) => [
|
|
762
|
+
role,
|
|
763
|
+
{
|
|
764
|
+
model: profile.agents[role]?.model,
|
|
765
|
+
variant: profile.agents[role]?.variant
|
|
766
|
+
}
|
|
767
|
+
])
|
|
768
|
+
)
|
|
769
|
+
};
|
|
770
|
+
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
771
|
+
}
|
|
772
|
+
var RequestError = class extends Error {
|
|
773
|
+
constructor(code, message) {
|
|
774
|
+
super(message);
|
|
775
|
+
this.code = code;
|
|
776
|
+
}
|
|
777
|
+
code;
|
|
778
|
+
};
|
|
779
|
+
async function registerProfileRpc(ctx, deps) {
|
|
780
|
+
const { store, roster } = deps.profileState;
|
|
781
|
+
const sessions = new SessionProfileSelections(ctx.storage, deps.directory);
|
|
782
|
+
async function capture(sessionID) {
|
|
783
|
+
const session = await ctx.session.get({ sessionID });
|
|
784
|
+
const ref = session.model ?? (await ctx.agent.get({ agentID: session.agent ?? "orchestrator" })).data.model;
|
|
785
|
+
return ref ? {
|
|
786
|
+
model: `${ref.providerID}/${ref.id}`,
|
|
787
|
+
...ref.variant ? { variant: ref.variant } : {}
|
|
788
|
+
} : {};
|
|
789
|
+
}
|
|
790
|
+
function snapshot(sessionName, change) {
|
|
791
|
+
const warnings = [];
|
|
792
|
+
const global = store.read("global", warnings);
|
|
793
|
+
const project = store.read("project", warnings);
|
|
794
|
+
const selections = {
|
|
795
|
+
...sessionName === void 0 ? {} : { session: sessionName },
|
|
796
|
+
...project.active === void 0 ? {} : { project: project.active },
|
|
797
|
+
...global.active === void 0 ? {} : { global: global.active }
|
|
798
|
+
};
|
|
799
|
+
if (change) {
|
|
800
|
+
if (change.name === void 0) delete selections[change.scope];
|
|
801
|
+
else selections[change.scope] = change.name;
|
|
802
|
+
}
|
|
803
|
+
let effective = {
|
|
804
|
+
name: "default",
|
|
805
|
+
selectedBy: "default",
|
|
806
|
+
profile: store.defaultProfile()
|
|
807
|
+
};
|
|
808
|
+
for (const selectedBy of ["session", "project", "global"]) {
|
|
809
|
+
const name = selections[selectedBy];
|
|
810
|
+
if (!name) continue;
|
|
811
|
+
if (name === "default") {
|
|
812
|
+
effective = { name, selectedBy, profile: store.defaultProfile() };
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
const source = Object.hasOwn(project.profiles, name) ? "project" : "global";
|
|
816
|
+
const profile = (source === "project" ? project : global).profiles[name];
|
|
817
|
+
if (Object.hasOwn(
|
|
818
|
+
(source === "project" ? project : global).profiles,
|
|
819
|
+
name
|
|
820
|
+
) && (selectedBy !== "global" || Object.hasOwn(global.profiles, name))) {
|
|
821
|
+
effective = { name, source, selectedBy, profile };
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
warnings.push(
|
|
825
|
+
`Missing ${selectedBy} model profile "${name}"; falling back.`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
const profiles = [];
|
|
829
|
+
for (const [source, document] of [
|
|
830
|
+
["global", global],
|
|
831
|
+
["project", project]
|
|
832
|
+
]) {
|
|
833
|
+
for (const [name, profile] of Object.entries(document.profiles))
|
|
834
|
+
profiles.push({
|
|
835
|
+
name,
|
|
836
|
+
scope: source,
|
|
837
|
+
source,
|
|
838
|
+
profile,
|
|
839
|
+
revision: revision(profile),
|
|
840
|
+
shadowed: source === "global" && Object.hasOwn(project.profiles, name)
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
profiles,
|
|
845
|
+
defaultProfile: store.defaultProfile(),
|
|
846
|
+
roles: [...PROFILE_ROLES],
|
|
847
|
+
selections,
|
|
848
|
+
effective,
|
|
849
|
+
warnings
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async function catalog() {
|
|
853
|
+
const [providers, models] = await Promise.all([
|
|
854
|
+
ctx.provider.list(),
|
|
855
|
+
ctx.model.list({ types: ["chat"] })
|
|
856
|
+
]);
|
|
857
|
+
return providers.data.map((provider) => ({
|
|
858
|
+
id: provider.id,
|
|
859
|
+
name: provider.name,
|
|
860
|
+
models: models.data.filter((model) => model.providerID === provider.id && model.enabled).map((model) => ({
|
|
861
|
+
id: model.id,
|
|
862
|
+
name: model.name,
|
|
863
|
+
variants: model.variants.map((variant) => variant.id)
|
|
864
|
+
}))
|
|
865
|
+
}));
|
|
866
|
+
}
|
|
867
|
+
async function state(sessionID) {
|
|
868
|
+
return {
|
|
869
|
+
...snapshot(
|
|
870
|
+
sessionID ? (await sessions.get(sessionID))?.name : void 0
|
|
871
|
+
),
|
|
872
|
+
...sessionID ? { orchestrator: await capture(sessionID) } : {},
|
|
873
|
+
providers: await catalog()
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function available(profile, providers, name) {
|
|
877
|
+
for (const role of PROFILE_ROLES) {
|
|
878
|
+
const entry = resolveRoleProfile(role, profile, roster);
|
|
879
|
+
if (!entry.model) continue;
|
|
880
|
+
const slash = entry.model.indexOf("/");
|
|
881
|
+
const providerID = entry.model.slice(0, slash);
|
|
882
|
+
const modelID = entry.model.slice(slash + 1);
|
|
883
|
+
const model = providers.find((provider) => provider.id === providerID)?.models.find((candidate) => candidate.id === modelID);
|
|
884
|
+
if (!model || entry.variant && !model.variants.includes(entry.variant))
|
|
885
|
+
throw new RequestError(
|
|
886
|
+
"MODEL_UNAVAILABLE",
|
|
887
|
+
`Unavailable model or variant in profile "${name}" for ${role}: ${entry.model}${entry.variant ? `#${entry.variant}` : ""}. Nothing changed.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function activate(input) {
|
|
892
|
+
if (input.scope === "session" && !input.sessionID)
|
|
893
|
+
throw new RequestError(
|
|
894
|
+
"INVALID_REQUEST",
|
|
895
|
+
"Open a session to change its profile selection."
|
|
896
|
+
);
|
|
897
|
+
const previous = input.sessionID ? (await sessions.get(input.sessionID))?.name : void 0;
|
|
898
|
+
const providers = await catalog();
|
|
899
|
+
if (input.name !== void 0) {
|
|
900
|
+
const target = store.lookup(
|
|
901
|
+
input.name,
|
|
902
|
+
input.scope === "global" ? "global" : void 0
|
|
903
|
+
);
|
|
904
|
+
if (!target)
|
|
905
|
+
throw new RequestError(
|
|
906
|
+
"INVALID_REQUEST",
|
|
907
|
+
`Profile not found: ${input.name}`
|
|
908
|
+
);
|
|
909
|
+
available(target.profile, providers, input.name);
|
|
910
|
+
}
|
|
911
|
+
if (input.scope !== "session") {
|
|
912
|
+
const defaults = snapshot(void 0, input);
|
|
913
|
+
available(defaults.effective.profile, providers, defaults.effective.name);
|
|
914
|
+
}
|
|
915
|
+
const next = snapshot(previous, input);
|
|
916
|
+
available(next.effective.profile, providers, next.effective.name);
|
|
917
|
+
if (input.scope === "session" && input.sessionID)
|
|
918
|
+
await sessions.set(input.sessionID, input.name);
|
|
919
|
+
else {
|
|
920
|
+
if (input.scope === "global") store.setGlobalActive(input.name);
|
|
921
|
+
else store.setProjectPin(input.name);
|
|
922
|
+
deps.profileState.refresh();
|
|
923
|
+
await ctx.agent.reload();
|
|
924
|
+
}
|
|
925
|
+
const orchestrator = resolveRoleProfile(
|
|
926
|
+
"orchestrator",
|
|
927
|
+
next.effective.profile,
|
|
928
|
+
roster
|
|
929
|
+
);
|
|
930
|
+
if (input.sessionID && orchestrator.model) {
|
|
931
|
+
const slash = orchestrator.model.indexOf("/");
|
|
932
|
+
await ctx.session.switchModel({
|
|
933
|
+
sessionID: input.sessionID,
|
|
934
|
+
model: {
|
|
935
|
+
providerID: orchestrator.model.slice(0, slash),
|
|
936
|
+
id: orchestrator.model.slice(slash + 1),
|
|
937
|
+
...orchestrator.variant ? { variant: orchestrator.variant } : {}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return state(input.sessionID);
|
|
942
|
+
}
|
|
943
|
+
let pending = Promise.resolve();
|
|
944
|
+
function handler(method, run) {
|
|
945
|
+
return (raw) => {
|
|
946
|
+
const operation = pending.then(async () => {
|
|
947
|
+
const parsed = profileRpc.methods[method].input.safeParse(raw);
|
|
948
|
+
if (!parsed.success)
|
|
949
|
+
return {
|
|
950
|
+
ok: false,
|
|
951
|
+
error: { code: "INVALID_REQUEST", message: parsed.error.message }
|
|
952
|
+
};
|
|
953
|
+
try {
|
|
954
|
+
return {
|
|
955
|
+
ok: true,
|
|
956
|
+
value: await run(parsed.data)
|
|
957
|
+
};
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const code = error instanceof ProfileConflictError ? "PROFILE_CONFLICT" : error instanceof RequestError ? error.code : "OPERATION_FAILED";
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
error: {
|
|
963
|
+
code,
|
|
964
|
+
message: error instanceof Error ? error.message : String(error)
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
pending = operation;
|
|
970
|
+
return operation;
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function writable(name) {
|
|
974
|
+
try {
|
|
975
|
+
validateProfileName(name, true);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
throw new RequestError("INVALID_REQUEST", String(error));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function current(scope, name) {
|
|
981
|
+
const document = store.read(scope);
|
|
982
|
+
return Object.hasOwn(document.profiles, name) ? document.profiles[name] : null;
|
|
983
|
+
}
|
|
984
|
+
function projectStore(projectDir) {
|
|
985
|
+
return projectDir === deps.directory ? store : new ModelProfileStore({
|
|
986
|
+
cwd: projectDir,
|
|
987
|
+
roster,
|
|
988
|
+
paths: {
|
|
989
|
+
global: store.paths.global,
|
|
990
|
+
project: path2.join(
|
|
991
|
+
projectDir,
|
|
992
|
+
".opencode",
|
|
993
|
+
"thoth",
|
|
994
|
+
"profiles.json"
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
function identity(project, name) {
|
|
1000
|
+
const found = project.lookup(name);
|
|
1001
|
+
return found ? found.scope ? `${project.paths[found.scope]}:${name}` : "default" : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
async function maintenance(scope, name, newName) {
|
|
1004
|
+
const selections = await sessions.all();
|
|
1005
|
+
const target = `${store.paths[scope]}:${name}`;
|
|
1006
|
+
const affected = selections.filter(
|
|
1007
|
+
(selection) => identity(projectStore(selection.projectDir), selection.name) === target
|
|
1008
|
+
);
|
|
1009
|
+
const projects = new Map(
|
|
1010
|
+
[
|
|
1011
|
+
.../* @__PURE__ */ new Set([deps.directory, ...selections.map((s) => s.projectDir)])
|
|
1012
|
+
].map((dir) => [dir, projectStore(dir)])
|
|
1013
|
+
);
|
|
1014
|
+
const pins = [];
|
|
1015
|
+
for (const project of projects.values()) {
|
|
1016
|
+
const pin = project.read("project").active;
|
|
1017
|
+
if (pin && identity(project, pin) === target) pins.push(project);
|
|
1018
|
+
}
|
|
1019
|
+
if (newName !== void 0) {
|
|
1020
|
+
for (const [dir, project] of projects) {
|
|
1021
|
+
let after2 = function(pointer, globalPointer = false) {
|
|
1022
|
+
if (!pointer) return void 0;
|
|
1023
|
+
if (pointer === "default") return "default";
|
|
1024
|
+
if (globalPointer && !Object.hasOwn(global.profiles, pointer))
|
|
1025
|
+
return void 0;
|
|
1026
|
+
const source = Object.hasOwn(local.profiles, pointer) ? "project" : "global";
|
|
1027
|
+
if (!Object.hasOwn(
|
|
1028
|
+
(source === "project" ? local : global).profiles,
|
|
1029
|
+
pointer
|
|
1030
|
+
))
|
|
1031
|
+
return void 0;
|
|
1032
|
+
return `${project.paths[source]}:${pointer}`;
|
|
1033
|
+
}, guard2 = function(before, next) {
|
|
1034
|
+
const expected = before === target ? `${store.paths[scope]}:${newName}` : before;
|
|
1035
|
+
if (next !== expected)
|
|
1036
|
+
throw new RequestError(
|
|
1037
|
+
"PROFILE_SHADOWED",
|
|
1038
|
+
`Renaming to "${newName}" collides with a shadowing profile in ${dir}. Nothing changed.`
|
|
1039
|
+
);
|
|
1040
|
+
};
|
|
1041
|
+
var after = after2, guard = guard2;
|
|
1042
|
+
const global = structuredClone(project.read("global"));
|
|
1043
|
+
const local = structuredClone(project.read("project"));
|
|
1044
|
+
const changed = scope === "global" ? global : project.paths.project === store.paths.project ? local : void 0;
|
|
1045
|
+
if (changed && Object.hasOwn(changed.profiles, name)) {
|
|
1046
|
+
changed.profiles[newName] = changed.profiles[name];
|
|
1047
|
+
delete changed.profiles[name];
|
|
1048
|
+
if (changed.active === name) changed.active = newName;
|
|
1049
|
+
}
|
|
1050
|
+
if (pins.includes(project)) local.active = newName;
|
|
1051
|
+
for (const selection of selections.filter(
|
|
1052
|
+
(s) => s.projectDir === dir
|
|
1053
|
+
)) {
|
|
1054
|
+
const before = identity(project, selection.name);
|
|
1055
|
+
guard2(before, after2(before === target ? newName : selection.name));
|
|
1056
|
+
}
|
|
1057
|
+
const pin = project.read("project").active;
|
|
1058
|
+
guard2(pin ? identity(project, pin) : void 0, after2(local.active));
|
|
1059
|
+
const active = project.read("global").active;
|
|
1060
|
+
const beforeGlobal = active && project.lookup(active, "global") ? identity(project, active) : void 0;
|
|
1061
|
+
guard2(beforeGlobal, after2(global.active, true));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return { affected, pins };
|
|
1065
|
+
}
|
|
1066
|
+
const handlers = {
|
|
1067
|
+
state: handler("state", ({ sessionID }) => state(sessionID)),
|
|
1068
|
+
capture: handler("capture", ({ sessionID }) => capture(sessionID)),
|
|
1069
|
+
activate: handler("activate", activate),
|
|
1070
|
+
clear: handler("clear", activate),
|
|
1071
|
+
create: handler("create", ({ scope, name, profile }) => {
|
|
1072
|
+
writable(name);
|
|
1073
|
+
store.create(scope, name, profile);
|
|
1074
|
+
return { revision: revision(current(scope, name)) };
|
|
1075
|
+
}),
|
|
1076
|
+
save: handler("save", ({ scope, name, profile, baseRevision }) => {
|
|
1077
|
+
writable(name);
|
|
1078
|
+
const expected = current(scope, name);
|
|
1079
|
+
if (revision(expected) !== baseRevision)
|
|
1080
|
+
throw new ProfileConflictError(scope, name);
|
|
1081
|
+
store.save(scope, name, profile, { expected });
|
|
1082
|
+
return { revision: revision(current(scope, name)) };
|
|
1083
|
+
}),
|
|
1084
|
+
rename: handler("rename", async ({ scope, name, newName }) => {
|
|
1085
|
+
writable(name);
|
|
1086
|
+
writable(newName);
|
|
1087
|
+
const { affected, pins } = await maintenance(scope, name, newName);
|
|
1088
|
+
store.rename(scope, name, newName);
|
|
1089
|
+
for (const project of pins) project.setProjectPin(newName);
|
|
1090
|
+
for (const selection of affected)
|
|
1091
|
+
await sessions.set(selection.sessionID, newName, selection.projectDir);
|
|
1092
|
+
return {};
|
|
1093
|
+
}),
|
|
1094
|
+
delete: handler("delete", async ({ scope, name }) => {
|
|
1095
|
+
writable(name);
|
|
1096
|
+
const { affected, pins } = await maintenance(scope, name);
|
|
1097
|
+
store.delete(scope, name);
|
|
1098
|
+
for (const project of pins) project.setProjectPin();
|
|
1099
|
+
for (const selection of affected)
|
|
1100
|
+
await sessions.set(selection.sessionID, void 0);
|
|
1101
|
+
return {};
|
|
1102
|
+
})
|
|
1103
|
+
};
|
|
1104
|
+
await ctx.rpc.register(profileRpc, handlers);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/opencode/profile-runtime.ts
|
|
1108
|
+
async function registerProfileRuntime(ctx, deps) {
|
|
1109
|
+
const sessions = new SessionProfileSelections(ctx.storage, deps.directory);
|
|
1110
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
1111
|
+
if (event.tool !== "subagent") return;
|
|
1112
|
+
const input = event.input;
|
|
1113
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return;
|
|
1114
|
+
const args = input;
|
|
1115
|
+
if (args.model !== void 0) return;
|
|
1116
|
+
const role = PROFILE_ROLES.find((role2) => role2 === args.agent);
|
|
1117
|
+
if (!role) return;
|
|
1118
|
+
const { store, roster } = deps.profileState;
|
|
1119
|
+
const selection = store.resolve(
|
|
1120
|
+
(await sessions.get(event.sessionID))?.name
|
|
1121
|
+
);
|
|
1122
|
+
const model = formatModelString(
|
|
1123
|
+
resolveRoleProfile(role, selection.profile, roster)
|
|
1124
|
+
);
|
|
1125
|
+
if (model !== void 0) args.model = model;
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
430
1129
|
// src/utils/logger.ts
|
|
431
1130
|
import { createRequire } from "module";
|
|
432
1131
|
var require2 = createRequire(import.meta.url);
|
|
433
1132
|
function getLogFile() {
|
|
434
|
-
const
|
|
435
|
-
const
|
|
1133
|
+
const os2 = require2("node:os");
|
|
1134
|
+
const path3 = require2("node:path");
|
|
436
1135
|
const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
|
|
437
|
-
return configuredPath ||
|
|
1136
|
+
return configuredPath || path3.join(os2.tmpdir(), "thoth-agents.log");
|
|
438
1137
|
}
|
|
439
1138
|
function log(message, data) {
|
|
440
1139
|
try {
|
|
441
|
-
const
|
|
1140
|
+
const fs2 = require2("node:fs");
|
|
442
1141
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
443
1142
|
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
444
1143
|
`;
|
|
445
|
-
|
|
1144
|
+
fs2.appendFileSync(getLogFile(), logEntry);
|
|
446
1145
|
} catch {
|
|
447
1146
|
}
|
|
448
1147
|
}
|
|
@@ -615,14 +1314,14 @@ var SessionState = class {
|
|
|
615
1314
|
for (const entry of entries) {
|
|
616
1315
|
if (!entry.isDirectory() || entry.name === "archive") continue;
|
|
617
1316
|
const id = entry.name;
|
|
618
|
-
const
|
|
1317
|
+
const path3 = join2(root, id, `${id}.md`);
|
|
619
1318
|
try {
|
|
620
|
-
const info = await stat(
|
|
1319
|
+
const info = await stat(path3);
|
|
621
1320
|
if (!info.isFile()) continue;
|
|
622
1321
|
active.add(id);
|
|
623
1322
|
const fingerprint = `${info.mtimeMs}:${info.ctimeMs}:${info.size}`;
|
|
624
1323
|
if (this.records.get(id)?.fingerprint === fingerprint) continue;
|
|
625
|
-
const content = await readFile(
|
|
1324
|
+
const content = await readFile(path3, "utf8");
|
|
626
1325
|
const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim() ?? id;
|
|
627
1326
|
const status = content.match(/^\*\*(?:Status|Phase)\*\*:\s*.+$/gm) ?? [];
|
|
628
1327
|
this.records.set(id, {
|
|
@@ -667,16 +1366,15 @@ var SessionState = class {
|
|
|
667
1366
|
// src/opencode/session-hooks.ts
|
|
668
1367
|
async function registerSessionHooks(ctx, deps) {
|
|
669
1368
|
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
1369
|
const fallback = new ForegroundFallbackManager(
|
|
674
1370
|
ctx.session,
|
|
675
|
-
|
|
1371
|
+
deps.runtimeChains,
|
|
676
1372
|
deps.effectiveModelArrays
|
|
677
1373
|
);
|
|
678
1374
|
const state = new SessionState(deps.directory, ctx.session.instructions);
|
|
679
|
-
await ctx.session.hook("retry", (input) =>
|
|
1375
|
+
await ctx.session.hook("retry", (input) => {
|
|
1376
|
+
if (names.has(input.agent)) return fallback.retry(input);
|
|
1377
|
+
});
|
|
680
1378
|
await ctx.session.hook("prompt", async (input) => {
|
|
681
1379
|
await state.refresh(input.sessionID);
|
|
682
1380
|
});
|
|
@@ -737,9 +1435,9 @@ var ownedSkills = [
|
|
|
737
1435
|
async function registerSkills(ctx, deps) {
|
|
738
1436
|
const skills = [];
|
|
739
1437
|
for (const id of ownedSkills) {
|
|
740
|
-
const
|
|
1438
|
+
const path3 = resolve(deps.packageRoot, "skills", id, "SKILL.md");
|
|
741
1439
|
try {
|
|
742
|
-
const source = await readFile2(
|
|
1440
|
+
const source = await readFile2(path3, "utf8");
|
|
743
1441
|
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(
|
|
744
1442
|
source
|
|
745
1443
|
);
|
|
@@ -752,12 +1450,12 @@ async function registerSkills(ctx, deps) {
|
|
|
752
1450
|
id,
|
|
753
1451
|
name: metadata.name,
|
|
754
1452
|
description: metadata.description,
|
|
755
|
-
path,
|
|
1453
|
+
path: path3,
|
|
756
1454
|
content: match[2].trim()
|
|
757
1455
|
});
|
|
758
1456
|
} catch (cause) {
|
|
759
1457
|
throw new Error(
|
|
760
|
-
`Cannot load bundled skill ${
|
|
1458
|
+
`Cannot load bundled skill ${path3}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
761
1459
|
{ cause }
|
|
762
1460
|
);
|
|
763
1461
|
}
|
|
@@ -773,6 +1471,99 @@ async function registerSkills(ctx, deps) {
|
|
|
773
1471
|
});
|
|
774
1472
|
}
|
|
775
1473
|
|
|
1474
|
+
// src/opencode/tool-rpc-handlers.ts
|
|
1475
|
+
async function registerToolRpc(ctx, deps) {
|
|
1476
|
+
const { store } = deps.toolState;
|
|
1477
|
+
async function state() {
|
|
1478
|
+
const snapshot = store.resolve();
|
|
1479
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1480
|
+
for (const tool of await ctx.tool.list()) {
|
|
1481
|
+
const action = tool.options?.permission ?? tool.id;
|
|
1482
|
+
const members = grouped.get(action) ?? [];
|
|
1483
|
+
members.push(tool.id);
|
|
1484
|
+
grouped.set(action, members);
|
|
1485
|
+
}
|
|
1486
|
+
if (!grouped.has("subagent")) grouped.set("subagent", []);
|
|
1487
|
+
const inventory = [...grouped].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([action, tools]) => ({
|
|
1488
|
+
action,
|
|
1489
|
+
tools: tools.sort(),
|
|
1490
|
+
locked: action === "subagent"
|
|
1491
|
+
}));
|
|
1492
|
+
const actions = inventory.map(({ action }) => action);
|
|
1493
|
+
return {
|
|
1494
|
+
specialists: [...SPECIALIST_ROLES],
|
|
1495
|
+
inventory,
|
|
1496
|
+
roles: SPECIALIST_ROLES.map((role) => {
|
|
1497
|
+
const scopeState = (scope) => ({
|
|
1498
|
+
present: Object.hasOwn(snapshot[scope].document.roles, role),
|
|
1499
|
+
revision: snapshot[scope].revision,
|
|
1500
|
+
overrides: (snapshot[scope].document.roles[role]?.overrides ?? []).map((entry) => ({
|
|
1501
|
+
...entry,
|
|
1502
|
+
matchCount: countActionMatches(entry.action, actions)
|
|
1503
|
+
}))
|
|
1504
|
+
});
|
|
1505
|
+
const base = translatePermissionRules(deps.agents[role].permission);
|
|
1506
|
+
return {
|
|
1507
|
+
role,
|
|
1508
|
+
effectiveScope: snapshot.effective[role]?.origin ?? "default",
|
|
1509
|
+
global: scopeState("global"),
|
|
1510
|
+
project: scopeState("project"),
|
|
1511
|
+
actions: inventory.map(({ action, locked }) => ({
|
|
1512
|
+
action,
|
|
1513
|
+
locked,
|
|
1514
|
+
baseEffect: evaluateAction(base, void 0, action).effect,
|
|
1515
|
+
...evaluateAction(base, snapshot.effective[role], action)
|
|
1516
|
+
}))
|
|
1517
|
+
};
|
|
1518
|
+
}),
|
|
1519
|
+
warnings: snapshot.warnings
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
let pending = Promise.resolve();
|
|
1523
|
+
function handler(method, run) {
|
|
1524
|
+
return (raw) => {
|
|
1525
|
+
const operation = pending.then(async () => {
|
|
1526
|
+
const parsed = toolRpc.methods[method].input.safeParse(raw);
|
|
1527
|
+
if (!parsed.success)
|
|
1528
|
+
return {
|
|
1529
|
+
ok: false,
|
|
1530
|
+
error: { code: "INVALID_REQUEST", message: parsed.error.message }
|
|
1531
|
+
};
|
|
1532
|
+
try {
|
|
1533
|
+
return { ok: true, value: await run(parsed.data) };
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
return {
|
|
1536
|
+
ok: false,
|
|
1537
|
+
error: {
|
|
1538
|
+
code: error instanceof ToolsConflictError ? "TOOLS_CONFLICT" : "OPERATION_FAILED",
|
|
1539
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
pending = operation;
|
|
1545
|
+
return operation;
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
async function reload() {
|
|
1549
|
+
deps.toolState.refresh();
|
|
1550
|
+
await ctx.agent.reload();
|
|
1551
|
+
return state();
|
|
1552
|
+
}
|
|
1553
|
+
const handlers = {
|
|
1554
|
+
state: handler("state", state),
|
|
1555
|
+
save: handler("save", async ({ scope, role, overrides, baseRevision }) => {
|
|
1556
|
+
store.save(scope, role, overrides, baseRevision);
|
|
1557
|
+
return reload();
|
|
1558
|
+
}),
|
|
1559
|
+
reset: handler("reset", async ({ scope, role, baseRevision }) => {
|
|
1560
|
+
store.reset(scope, role, baseRevision);
|
|
1561
|
+
return reload();
|
|
1562
|
+
})
|
|
1563
|
+
};
|
|
1564
|
+
await ctx.rpc.register(toolRpc, handlers);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
776
1567
|
// src/tools/ast-grep/tools.ts
|
|
777
1568
|
import { z } from "zod";
|
|
778
1569
|
|
|
@@ -845,8 +1636,8 @@ function isPwshAvailable() {
|
|
|
845
1636
|
});
|
|
846
1637
|
return result.exitCode === 0;
|
|
847
1638
|
}
|
|
848
|
-
function escapePowerShellPath(
|
|
849
|
-
return
|
|
1639
|
+
function escapePowerShellPath(path3) {
|
|
1640
|
+
return path3.replace(/'/g, "''");
|
|
850
1641
|
}
|
|
851
1642
|
function getWindowsZipExtractor() {
|
|
852
1643
|
const buildNumber = getWindowsBuildNumber();
|
|
@@ -975,8 +1766,8 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
975
1766
|
if (existsSync2(binaryPath)) {
|
|
976
1767
|
return binaryPath;
|
|
977
1768
|
}
|
|
978
|
-
const { arch, os } = platformInfo;
|
|
979
|
-
const assetName = `app-${arch}-${
|
|
1769
|
+
const { arch, os: os2 } = platformInfo;
|
|
1770
|
+
const assetName = `app-${arch}-${os2}.zip`;
|
|
980
1771
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
981
1772
|
console.log(`[thoth-agents] Downloading ast-grep binary...`);
|
|
982
1773
|
try {
|
|
@@ -1100,9 +1891,9 @@ function findSgCliPathSync() {
|
|
|
1100
1891
|
}
|
|
1101
1892
|
if (process.platform === "darwin") {
|
|
1102
1893
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
1103
|
-
for (const
|
|
1104
|
-
if (existsSync3(
|
|
1105
|
-
return
|
|
1894
|
+
for (const path3 of homebrewPaths) {
|
|
1895
|
+
if (existsSync3(path3) && isValidBinary(path3)) {
|
|
1896
|
+
return path3;
|
|
1106
1897
|
}
|
|
1107
1898
|
}
|
|
1108
1899
|
}
|
|
@@ -1119,8 +1910,8 @@ function getSgCliPath() {
|
|
|
1119
1910
|
}
|
|
1120
1911
|
return "sg";
|
|
1121
1912
|
}
|
|
1122
|
-
function setSgCliPath(
|
|
1123
|
-
resolvedCliPath =
|
|
1913
|
+
function setSgCliPath(path3) {
|
|
1914
|
+
resolvedCliPath = path3;
|
|
1124
1915
|
}
|
|
1125
1916
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
1126
1917
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -3048,6 +3839,9 @@ var plugin = {
|
|
|
3048
3839
|
await registerCommand(ctx, deps);
|
|
3049
3840
|
await registerSkills(ctx, deps);
|
|
3050
3841
|
await registerPermissions(ctx, deps);
|
|
3842
|
+
await registerProfileRuntime(ctx, deps);
|
|
3843
|
+
await registerProfileRpc(ctx, deps);
|
|
3844
|
+
await registerToolRpc(ctx, deps);
|
|
3051
3845
|
await registerDelegationHooks(ctx, deps);
|
|
3052
3846
|
await registerSessionHooks(ctx, deps);
|
|
3053
3847
|
}
|