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.
Files changed (43) hide show
  1. package/README.md +11 -2
  2. package/dist/{chunk-SEF6PFM6.js → chunk-5MYXVRZZ.js} +132 -157
  3. package/dist/chunk-6O4GKDS7.js +207 -0
  4. package/dist/{chunk-QOA2AETX.js → chunk-NOLDFT7O.js} +3 -3
  5. package/dist/{chunk-7F33WUSD.js → chunk-VVYAQTYE.js} +2 -2
  6. package/dist/{chunk-NY2IPJ7V.js → chunk-YCWGTDFE.js} +531 -233
  7. package/dist/cli/index.js +6 -6
  8. package/dist/cli/opencode-profiles.d.ts +10 -0
  9. package/dist/cli/tui/index.js +3 -3
  10. package/dist/config/loader.d.ts +1 -4
  11. package/dist/config/schema.d.ts +0 -22
  12. package/dist/index.js +824 -30
  13. package/dist/opencode/context.d.ts +16 -0
  14. package/dist/opencode/profile-rpc-handlers.d.ts +3 -0
  15. package/dist/opencode/profile-rpc.d.ts +688 -0
  16. package/dist/opencode/profile-runtime.d.ts +3 -0
  17. package/dist/opencode/profile-sessions.d.ts +18 -0
  18. package/dist/opencode/profiles/editor.d.ts +44 -0
  19. package/dist/opencode/profiles/index.d.ts +18 -0
  20. package/dist/opencode/profiles/resolver.d.ts +12 -0
  21. package/dist/opencode/profiles/store.d.ts +63 -0
  22. package/dist/opencode/profiles/types.d.ts +26 -0
  23. package/dist/opencode/tool-overrides/evaluator.d.ts +13 -0
  24. package/dist/opencode/tool-overrides/index.d.ts +5 -0
  25. package/dist/opencode/tool-overrides/store.d.ts +38 -0
  26. package/dist/opencode/tool-overrides/types.d.ts +32 -0
  27. package/dist/opencode/tool-overrides/validation.d.ts +5 -0
  28. package/dist/opencode/tool-overrides/wildcard.d.ts +3 -0
  29. package/dist/opencode/tool-rpc-handlers.d.ts +3 -0
  30. package/dist/opencode/tool-rpc.d.ts +428 -0
  31. package/dist/pi.js +2 -2
  32. package/dist/tui/fixtures.d.ts +2 -0
  33. package/dist/tui/flow.d.ts +16 -0
  34. package/dist/tui/index.d.ts +7 -0
  35. package/dist/tui/index.js +1384 -0
  36. package/dist/tui/model.d.ts +84 -0
  37. package/dist/tui/tools/fixtures.d.ts +3 -0
  38. package/dist/tui/tools/flow.d.ts +11 -0
  39. package/dist/tui/tools/model.d.ts +97 -0
  40. package/package.json +5 -1
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/.codex-plugin/plugin.json +1 -1
  43. 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-QOA2AETX.js";
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
- loadPluginConfig
18
- } from "./chunk-NY2IPJ7V.js";
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 os = require2("node:os");
435
- const path = require2("node:path");
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 || path.join(os.tmpdir(), "thoth-agents.log");
1136
+ return configuredPath || path3.join(os2.tmpdir(), "thoth-agents.log");
438
1137
  }
439
1138
  function log(message, data) {
440
1139
  try {
441
- const fs = require2("node:fs");
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
- fs.appendFileSync(getLogFile(), logEntry);
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 path = join2(root, id, `${id}.md`);
1317
+ const path3 = join2(root, id, `${id}.md`);
619
1318
  try {
620
- const info = await stat(path);
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(path, "utf8");
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
- chains,
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) => fallback.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 path = resolve(deps.packageRoot, "skills", id, "SKILL.md");
1438
+ const path3 = resolve(deps.packageRoot, "skills", id, "SKILL.md");
741
1439
  try {
742
- const source = await readFile2(path, "utf8");
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 ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
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(path) {
849
- return path.replace(/'/g, "''");
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}-${os}.zip`;
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 path of homebrewPaths) {
1104
- if (existsSync3(path) && isValidBinary(path)) {
1105
- return path;
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(path) {
1123
- resolvedCliPath = path;
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
  }