@kisev/agentomatic 11.0.2-dev.86.g5908817e39ee → 11.0.2-dev.89.g40768ae03b15

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/dist/index.js CHANGED
@@ -1,191 +1,115 @@
1
1
  import { z } from "zod";
2
- import { registerV2Hooks } from "./plugins/compatibility.js";
3
2
  import { CATEGORIES, RoutingGate, } from "./routing.js";
4
3
  import { rulesInjector } from "./plugins/rules-injector.js";
5
4
  import { rtk } from "./plugins/rtk.js";
6
5
  import { zedBell } from "./plugins/zed-bell.js";
6
+ import { resultText, toolInput } from "./plugins/events.js";
7
7
  import { digest } from "./lifecycle.js";
8
+ import { matchesWildcard } from "./opencode-config.js";
8
9
  export { rulesInjector, rtk, zedBell };
9
- const plugin = (async (input) => {
10
- const gate = new RoutingGate();
11
- const hostInventory = async () => {
12
- if (!input.client?.app?.agents)
13
- throw new Error("Resolved OpenCode agent inventory is unavailable");
14
- const response = await input.client.app.agents({ query: { directory: input.directory } });
15
- const raw = response.data;
16
- if (!Array.isArray(raw))
17
- throw new Error("Resolved OpenCode agent inventory is malformed");
18
- const capabilities = {
19
- mapper: ["read", "search"],
20
- architect: ["read", "architecture"],
21
- worker: ["read", "write", "verify"],
22
- review: ["read", "review"],
23
- critic: ["read", "review"],
24
- };
25
- const agents = raw.map((value) => {
26
- const agent = value;
27
- if (typeof agent.name !== "string" || !agent.name)
28
- throw new Error("Resolved OpenCode agent has no name");
29
- return {
30
- agent: agent.name,
31
- available: true,
32
- capabilities: capabilities[agent.name] ??
33
- (/^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent.name) ? ["read", "review"] : ["read"]),
34
- tools: [
35
- "read",
36
- ...(agent.permission?.edit !== "deny" ? ["edit"] : []),
37
- ...Object.entries(agent.tools ?? {})
38
- .filter(([, enabled]) => enabled)
39
- .map(([name]) => name),
40
- ...Object.entries(agent.permission?.bash ?? {})
41
- .filter(([, mode]) => mode !== "deny")
42
- .map(() => "bash"),
43
- ].filter((name, index, list) => list.indexOf(name) === index),
44
- };
45
- });
46
- return { agents, revision: digest(raw) };
47
- };
48
- const route = {
49
- description: "Resolve a capability category and dispatch one eligible agent through a one-use Task receipt gate.",
50
- args: {
51
- action: z.enum(["preview", "dispatch"]),
52
- category: z.enum(CATEGORIES),
53
- task: z.string(),
54
- requirements: z.array(z.string()).default([]),
55
- execution_card: z.any().optional(),
56
- override: z.string().optional(),
57
- trusted_override: z.boolean().optional(),
58
- budget: z
59
- .object({
60
- cost_class: z.string().optional(),
61
- latency_class: z.string().optional(),
62
- })
63
- .optional(),
64
- decision: z.any().optional(),
65
- },
66
- async execute(args, context) {
67
- const inventory = await hostInventory();
68
- const input = {
69
- category: args.category,
70
- task: args.task,
71
- requirements: args.requirements,
72
- agents: inventory.agents,
73
- inventory_revision: inventory.revision,
74
- execution_card: args.execution_card,
75
- override: args.override,
76
- trusted_override: args.trusted_override,
77
- budget: args.budget,
10
+ export default {
11
+ id: "agentomatic",
12
+ async setup(ctx) {
13
+ const gate = new RoutingGate();
14
+ const hostInventory = async () => {
15
+ const { data: raw } = await ctx.agent.list({
16
+ location: { directory: ctx.location.directory },
17
+ });
18
+ const capabilities = {
19
+ mapper: ["read", "search"],
20
+ architect: ["read", "architecture"],
21
+ worker: ["read", "write", "verify"],
22
+ review: ["read", "review"],
23
+ critic: ["read", "review"],
78
24
  };
79
- if (args.action === "preview")
80
- return JSON.stringify(gate.preview(input));
81
- const decision = gate.dispatch(input, args.decision);
82
- if (decision.agent === "worker" && args.execution_card === undefined)
83
- throw new Error("implementation dispatch requires a confirmed execution card");
84
- const receipt = gate.grant(context.sessionID, decision, {
85
- task: args.task,
86
- requirements: args.requirements,
87
- card: args.execution_card,
25
+ const agents = raw.map((agent) => ({
26
+ agent: agent.id,
27
+ available: true,
28
+ capabilities: capabilities[agent.id] ??
29
+ (/^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent.id) ? ["read", "review"] : ["read"]),
30
+ tools: ["read", "glob", "grep", "edit", "shell"].filter((action) => {
31
+ const rules = agent.permissions.filter((rule) => matchesWildcard(rule.action, action));
32
+ const broad = rules.reduce((last, rule, index) => (rule.resource === "*" ? index : last), -1);
33
+ return rules.slice(Math.max(0, broad)).some((rule) => rule.effect !== "deny");
34
+ }),
35
+ }));
36
+ return { agents, revision: digest(raw) };
37
+ };
38
+ await ctx.tool.transform((editor) => {
39
+ editor.add({
40
+ name: "route",
41
+ description: "Resolve a capability category and dispatch one eligible agent through a one-use subagent receipt gate.",
42
+ input: z.object({
43
+ action: z.enum(["preview", "dispatch"]),
44
+ category: z.enum(CATEGORIES),
45
+ task: z.string(),
46
+ requirements: z.array(z.string()).default([]),
47
+ execution_card: z.any().optional(),
48
+ override: z.string().optional(),
49
+ trusted_override: z.boolean().optional(),
50
+ budget: z
51
+ .object({ cost_class: z.string().optional(), latency_class: z.string().optional() })
52
+ .optional(),
53
+ decision: z.any().optional(),
54
+ }),
55
+ async execute(args, context) {
56
+ const inventory = await hostInventory();
57
+ const input = {
58
+ category: args.category,
59
+ task: args.task,
60
+ requirements: args.requirements,
61
+ agents: inventory.agents,
62
+ inventory_revision: inventory.revision,
63
+ execution_card: args.execution_card,
64
+ override: args.override,
65
+ trusted_override: args.trusted_override,
66
+ budget: args.budget,
67
+ };
68
+ if (args.action === "preview")
69
+ return { content: JSON.stringify(gate.preview(input)) };
70
+ const decision = gate.dispatch(input, args.decision);
71
+ if (decision.agent === "worker" && args.execution_card === undefined)
72
+ throw new Error("implementation dispatch requires a confirmed execution card");
73
+ const receipt = gate.grant(context.sessionID, decision, {
74
+ task: args.task,
75
+ requirements: args.requirements,
76
+ card: args.execution_card,
77
+ });
78
+ return { content: JSON.stringify({ decision, receipt, status: "routed" }) };
79
+ },
88
80
  });
89
- return JSON.stringify({ decision, receipt, status: "routed" });
90
- },
91
- };
92
- return {
93
- tool: { route },
94
- "tool.execute.before": async (input, output) => {
95
- if (input.tool !== "task")
81
+ });
82
+ await ctx.tool.hook("execute.before", async (event) => {
83
+ if (event.tool !== "subagent")
96
84
  return;
97
- const args = output.args && typeof output.args === "object"
98
- ? output.args
99
- : {};
100
- const agent = typeof args.agent === "string"
101
- ? args.agent
102
- : typeof args.subagent_type === "string"
103
- ? args.subagent_type
104
- : undefined;
85
+ const args = toolInput(event.input);
86
+ const agent = typeof args.agent === "string" ? args.agent : undefined;
105
87
  if (!agent)
106
- throw new Error("Native Task requires an explicit agent and an active routing receipt");
88
+ throw new Error("Native subagent requires an explicit agent and an active routing receipt");
107
89
  const hasBinding = "task" in args || "requirements" in args || "execution_card" in args;
108
90
  if (!hasBinding)
109
- gate.consume(input.sessionID, agent);
110
- else {
111
- const task = typeof args.task === "string" ? args.task : "";
112
- const requirements = Array.isArray(args.requirements)
113
- ? args.requirements
114
- : [];
115
- gate.consume(input.sessionID, agent, { task, requirements, card: args.execution_card });
116
- }
117
- },
118
- "tool.execute.after": async (input, output) => {
119
- if (input.tool !== "task")
91
+ gate.consume(event.sessionID, agent);
92
+ else
93
+ gate.consume(event.sessionID, agent, {
94
+ task: typeof args.task === "string" ? args.task : "",
95
+ requirements: Array.isArray(args.requirements) ? args.requirements : [],
96
+ card: args.execution_card,
97
+ });
98
+ });
99
+ await ctx.tool.hook("execute.after", async (event) => {
100
+ if (event.tool !== "subagent" || event.status !== "completed")
120
101
  return;
121
- const args = input.args && typeof input.args === "object" ? input.args : {};
122
- const agent = typeof args.agent === "string"
123
- ? args.agent
124
- : typeof args.subagent_type === "string"
125
- ? args.subagent_type
126
- : undefined;
127
- if (!agent)
128
- throw new Error("Task result requires an explicit agent");
102
+ const args = toolInput(event.input);
103
+ if (typeof args.agent !== "string")
104
+ throw new Error("Subagent result requires an explicit agent");
129
105
  let result;
130
106
  try {
131
- result = JSON.parse(output.output);
107
+ result = JSON.parse(resultText(event.result) ?? "");
132
108
  }
133
109
  catch {
134
- throw new Error("Task result must be JSON structured report");
110
+ throw new Error("Subagent result must be JSON structured report");
135
111
  }
136
- gate.complete(input.sessionID, agent, result);
137
- },
138
- };
139
- });
140
- export const server = plugin;
141
- export default {
142
- id: "agentomatic",
143
- server,
144
- async setup(ctx) {
145
- const hooks = await plugin({
146
- directory: ctx.location.directory,
147
- client: {
148
- app: {
149
- agents: async () => {
150
- const { data: agents } = await ctx.agent.list({
151
- location: { directory: ctx.location.directory },
152
- });
153
- return {
154
- data: agents.map((agent) => ({
155
- name: agent.id,
156
- tools: Object.fromEntries(["read", "glob", "grep", "edit", "bash"].map((tool) => [
157
- tool,
158
- [...agent.permissions]
159
- .reverse()
160
- .find((rule) => (rule.action === (tool === "bash" ? "shell" : tool) ||
161
- rule.action === "*") &&
162
- rule.resource === "*")?.effect !== "deny",
163
- ])),
164
- permission: {
165
- edit: agent.permissions.some((rule) => rule.action === "edit" && rule.resource === "*" && rule.effect === "deny")
166
- ? "deny"
167
- : "allow",
168
- bash: Object.fromEntries(agent.permissions
169
- .filter((rule) => rule.action === "shell")
170
- .map((rule) => [rule.resource, rule.effect])),
171
- },
172
- })),
173
- };
174
- },
175
- },
176
- },
177
- });
178
- await ctx.tool.transform((editor) => {
179
- const route = hooks.tool.route;
180
- editor.add({
181
- name: "route",
182
- description: route.description,
183
- input: z.object(route.args),
184
- async execute(args, context) {
185
- return { content: await route.execute(args, context) };
186
- },
187
- });
112
+ gate.complete(event.sessionID, args.agent, result);
188
113
  });
189
- return registerV2Hooks(ctx, hooks);
190
114
  },
191
115
  };
package/dist/jsonc.d.ts CHANGED
@@ -30,7 +30,18 @@ export type JsoncPath = Array<string | number>;
30
30
  export type JsoncEdit = {
31
31
  kind: "append-unique";
32
32
  path: JsoncPath;
33
- value: string;
33
+ value: unknown;
34
+ } | {
35
+ kind: "set-value";
36
+ path: JsoncPath;
37
+ value: unknown;
38
+ } | {
39
+ kind: "rename-key";
40
+ path: JsoncPath;
41
+ to: string;
42
+ } | {
43
+ kind: "remove-key";
44
+ path: JsoncPath;
34
45
  } | {
35
46
  kind: "replace-array-value";
36
47
  path: JsoncPath;
package/dist/jsonc.js CHANGED
@@ -143,6 +143,9 @@ function parseObject(parser) {
143
143
  parser.cursor += 1;
144
144
  trailingComma = true;
145
145
  }
146
+ else if (next && !(next.type === "punct" && next.raw === "}")) {
147
+ throw new JsoncError("invalid_jsonc", `Expected comma at offset ${next.start}`);
148
+ }
146
149
  }
147
150
  if (!closed)
148
151
  throw new JsoncError("invalid_jsonc", `Unterminated object at offset ${open.start}`);
@@ -180,6 +183,9 @@ function parseArray(parser) {
180
183
  parser.cursor += 1;
181
184
  trailingComma = true;
182
185
  }
186
+ else if (next && !(next.type === "punct" && next.raw === "]")) {
187
+ throw new JsoncError("invalid_jsonc", `Expected comma at offset ${next.start}`);
188
+ }
183
189
  }
184
190
  if (!closed)
185
191
  throw new JsoncError("invalid_jsonc", `Unterminated array at offset ${open.start}`);
@@ -200,7 +206,12 @@ export function nodeToValue(node) {
200
206
  return node.items.map(nodeToValue);
201
207
  const value = {};
202
208
  for (const key of node.propOrder)
203
- value[key] = nodeToValue(node.props.get(key));
209
+ Object.defineProperty(value, key, {
210
+ value: nodeToValue(node.props.get(key)),
211
+ writable: true,
212
+ configurable: true,
213
+ enumerable: true,
214
+ });
204
215
  return value;
205
216
  }
206
217
  function parseDocument(text) {
@@ -278,11 +289,62 @@ function requireObjectPath(text, root, path) {
278
289
  return { text: currentText, node };
279
290
  }
280
291
  function applyEdit(text, root, edit) {
292
+ if (edit.kind === "rename-key" || edit.kind === "remove-key") {
293
+ const key = edit.path.at(-1);
294
+ const parent = resolve(root, edit.path.slice(0, -1));
295
+ if (typeof key !== "string" || !parent || parent.kind !== "object")
296
+ throw new JsoncError("conflict", "Key edit requires an object parent");
297
+ const target = parent.props.get(key);
298
+ if (!target)
299
+ return { text, result: "present" };
300
+ const index = parent.propOrder.indexOf(key);
301
+ const previous = index ? parent.props.get(parent.propOrder[index - 1]) : undefined;
302
+ const tokens = tokenize(text).filter((token) => token.start >= (previous?.end ?? parent.start + 1) && token.end <= target.start);
303
+ const keyToken = tokens.find((token) => token.type === "string");
304
+ if (!keyToken)
305
+ throw new JsoncError("conflict", "Object key token is unavailable");
306
+ if (edit.kind === "rename-key") {
307
+ if (parent.props.has(edit.to))
308
+ throw new JsoncError("conflict", `Destination key already exists: ${edit.to}`);
309
+ return {
310
+ text: text.slice(0, keyToken.start) + JSON.stringify(edit.to) + text.slice(keyToken.end),
311
+ result: "replaced",
312
+ };
313
+ }
314
+ const following = tokenize(text).find((token) => token.start >= target.end && token.type !== "ws" && token.type !== "comment");
315
+ const start = following?.raw === "," || !previous ? keyToken.start : previous.end;
316
+ const end = following?.raw === "," ? following.end : target.end;
317
+ const comments = tokenize(text.slice(start, end))
318
+ .filter((token) => token.type === "comment")
319
+ .map((token) => `${token.raw}\n`)
320
+ .join("");
321
+ return { text: text.slice(0, start) + comments + text.slice(end), result: "replaced" };
322
+ }
323
+ if (edit.kind === "set-value") {
324
+ const target = resolve(root, edit.path);
325
+ if (!target)
326
+ return applyEdit(text, root, { ...edit, kind: "set-if-absent" });
327
+ if (JSON.stringify(nodeToValue(target)) === JSON.stringify(edit.value))
328
+ return { text, result: "present" };
329
+ const comments = tokenize(text.slice(target.start, target.end))
330
+ .filter((token) => token.type === "comment")
331
+ .map((token) => `${token.raw}\n${lineIndent(text, target.start)}`)
332
+ .join("");
333
+ return {
334
+ text: text.slice(0, target.start) +
335
+ comments +
336
+ serialize(edit.value, lineIndent(text, target.start)) +
337
+ text.slice(target.end),
338
+ result: "replaced",
339
+ };
340
+ }
281
341
  if (edit.kind === "append-unique") {
282
342
  const target = resolve(root, edit.path);
283
343
  if (!target || target.kind !== "array")
284
344
  throw new JsoncError("conflict", "append-unique target is missing or not an array");
285
- if (target.items.map(nodeToValue).includes(edit.value))
345
+ if (target.items
346
+ .map(nodeToValue)
347
+ .some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
286
348
  return { text, result: "present" };
287
349
  const insertion = target.empty ? JSON.stringify(edit.value) : `, ${JSON.stringify(edit.value)}`;
288
350
  const at = target.empty ? target.start + 1 : target.items.at(-1).end;
@@ -369,9 +431,24 @@ function at(value, path) {
369
431
  : undefined, value);
370
432
  }
371
433
  function verifyEdit(before, after, edit) {
434
+ if (edit.kind === "set-value") {
435
+ if (JSON.stringify(at(after, edit.path)) !== JSON.stringify(edit.value))
436
+ throw new JsoncError("merge_validation_failed", "set-value postcondition failed");
437
+ return;
438
+ }
439
+ if (edit.kind === "rename-key" || edit.kind === "remove-key") {
440
+ if (at(after, edit.path) !== undefined)
441
+ throw new JsoncError("merge_validation_failed", "Old key remains after key edit");
442
+ if (edit.kind === "rename-key" &&
443
+ JSON.stringify(at(before, edit.path)) !==
444
+ JSON.stringify(at(after, [...edit.path.slice(0, -1), edit.to])))
445
+ throw new JsoncError("merge_validation_failed", "Renamed key lost its value");
446
+ return;
447
+ }
372
448
  if (edit.kind === "append-unique") {
373
449
  const target = at(after, edit.path);
374
- if (!Array.isArray(target) || !target.includes(edit.value))
450
+ if (!Array.isArray(target) ||
451
+ !target.some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
375
452
  throw new JsoncError("merge_validation_failed", "append-unique postcondition failed");
376
453
  return;
377
454
  }
@@ -419,12 +496,12 @@ function verifyEdit(before, after, edit) {
419
496
  throw new JsoncError("merge_validation_failed", "widened map lost the original scalar value");
420
497
  }
421
498
  export function applyJsoncEdits(text, edits) {
422
- const before = parseJsonc(text);
499
+ parseJsonc(text);
423
500
  let current = text;
424
501
  const results = [];
425
502
  for (const edit of edits) {
426
503
  const applied = applyEdit(current, parseDocument(current), edit);
427
- verifyEdit(before, parseJsonc(applied.text), edit);
504
+ verifyEdit(parseJsonc(current), parseJsonc(applied.text), edit);
428
505
  results.push(applied.result);
429
506
  current = applied.text;
430
507
  }
@@ -0,0 +1,9 @@
1
+ import { type JsoncEdit } from "./jsonc.js";
2
+ export type PermissionRule = {
3
+ action: string;
4
+ resource: string;
5
+ effect: "allow" | "ask" | "deny";
6
+ };
7
+ export declare function permissionEdits(config: Record<string, unknown>, additions: PermissionRule[]): JsoncEdit[];
8
+ export declare function matchesWildcard(pattern: string, value: string): boolean;
9
+ export declare function corePluginEdits(config: Record<string, unknown>): JsoncEdit[];
@@ -0,0 +1,158 @@
1
+ import { JsoncError } from "./jsonc.js";
2
+ import { requirePackageVersion } from "./package-metadata.js";
3
+ const effects = new Set(["allow", "ask", "deny"]);
4
+ const aliases = {
5
+ bash: "shell",
6
+ task: "subagent",
7
+ write: "edit",
8
+ patch: "edit",
9
+ };
10
+ function object(value) {
11
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
12
+ }
13
+ function legacyPermissions(value) {
14
+ if (typeof value === "string" && effects.has(value))
15
+ return [{ action: "*", resource: "*", effect: value }];
16
+ if (!object(value))
17
+ throw new JsoncError("conflict", "Legacy permission must be an effect or a tool map");
18
+ return Object.entries(value).flatMap(([name, entry]) => {
19
+ if (name === "lsp" || name === "doom_loop")
20
+ throw new JsoncError("conflict", `Permission action has no V2 equivalent: ${name}`);
21
+ const action = aliases[name] ?? name;
22
+ const resources = typeof entry === "string" ? { "*": entry } : entry;
23
+ if (!object(resources))
24
+ throw new JsoncError("conflict", `Invalid permission map: ${name}`);
25
+ return Object.entries(resources).map(([resource, effect]) => {
26
+ if (typeof effect !== "string" || !effects.has(effect))
27
+ throw new JsoncError("conflict", `Invalid permission effect: ${name}`);
28
+ return { action, resource, effect: effect };
29
+ });
30
+ });
31
+ }
32
+ export function permissionEdits(config, additions) {
33
+ const edits = [];
34
+ let rules = [];
35
+ if ("permissions" in config) {
36
+ if (!Array.isArray(config.permissions) ||
37
+ !config.permissions.every((rule) => object(rule) &&
38
+ typeof rule.action === "string" &&
39
+ typeof rule.resource === "string" &&
40
+ typeof rule.effect === "string" &&
41
+ effects.has(rule.effect)))
42
+ throw new JsoncError("conflict", "V2 permissions must be an ordered array of action/resource/effect rules");
43
+ rules = config.permissions;
44
+ }
45
+ let tools = [];
46
+ if ("tools" in config) {
47
+ if (!object(config.tools) ||
48
+ !Object.values(config.tools).every((value) => typeof value === "boolean"))
49
+ throw new JsoncError("conflict", "Legacy tools must be a boolean map");
50
+ tools = Object.entries(config.tools).map(([name, enabled]) => ({
51
+ action: aliases[name] ?? name,
52
+ resource: "*",
53
+ effect: enabled ? "allow" : "deny",
54
+ }));
55
+ }
56
+ const legacyKeys = ["tools", "permission"].filter((key) => key in config);
57
+ if (legacyKeys.length) {
58
+ // V2 normalizes tools first, legacy permission next, and native rules last.
59
+ rules = [
60
+ ...tools,
61
+ ...("permission" in config ? legacyPermissions(config.permission) : []),
62
+ ...rules,
63
+ ];
64
+ const renamed = "permissions" in config ? undefined : legacyKeys[0];
65
+ for (const key of legacyKeys)
66
+ edits.push(key === renamed
67
+ ? { kind: "rename-key", path: [key], to: "permissions" }
68
+ : { kind: "remove-key", path: [key] });
69
+ edits.push({ kind: "set-value", path: ["permissions"], value: rules });
70
+ }
71
+ else if (!("permissions" in config))
72
+ edits.push({ kind: "set-if-absent", path: ["permissions"], value: [] });
73
+ for (const rule of additions) {
74
+ const exact = rules.reduce((last, existing, index) => existing.action === rule.action && existing.resource === rule.resource ? index : last, -1);
75
+ if (exact >= 0) {
76
+ if (rules[exact].effect !== rule.effect ||
77
+ rules
78
+ .slice(exact + 1)
79
+ .some((later) => matchesWildcard(later.action, rule.action) &&
80
+ matchesWildcard(later.resource, rule.resource) &&
81
+ later.effect !== rule.effect))
82
+ throw new JsoncError("conflict", `Existing permission conflicts with preset: ${rule.action} ${rule.resource}`);
83
+ continue;
84
+ }
85
+ edits.push({ kind: "append-unique", path: ["permissions"], value: rule });
86
+ rules = [...rules, rule];
87
+ }
88
+ return edits;
89
+ }
90
+ export function matchesWildcard(pattern, value) {
91
+ return new RegExp(`^${pattern
92
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
93
+ .replace(/\\\*/g, ".*")
94
+ .replace(/\\\?/g, ".")}$`).test(value);
95
+ }
96
+ function plugins(value, legacy) {
97
+ if (!Array.isArray(value))
98
+ throw new JsoncError("conflict", "Plugin configuration must be an array");
99
+ return value.map((entry) => {
100
+ if (typeof entry === "string")
101
+ return entry === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry;
102
+ if (legacy &&
103
+ Array.isArray(entry) &&
104
+ entry.length === 2 &&
105
+ typeof entry[0] === "string" &&
106
+ object(entry[1]))
107
+ return {
108
+ package: entry[0] === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry[0],
109
+ options: entry[1],
110
+ };
111
+ if (object(entry) && typeof entry.package === "string")
112
+ return {
113
+ ...entry,
114
+ package: entry.package === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry.package,
115
+ };
116
+ throw new JsoncError("conflict", "Invalid plugin entry; use a package string or V2 package/options object");
117
+ });
118
+ }
119
+ const OUR_PACKAGE = /^@kisev\/(?:agentomatic|skills-opencode)(?:@|$)/;
120
+ function ourEntry(entry) {
121
+ if (typeof entry === "string")
122
+ return OUR_PACKAGE.test(entry);
123
+ return object(entry) && typeof entry.package === "string" && OUR_PACKAGE.test(entry.package);
124
+ }
125
+ export function corePluginEdits(config) {
126
+ const edits = [
127
+ { kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
128
+ ];
129
+ // V2 resolves a bare package name through the registry "latest" dist-tag, which
130
+ // can differ from the installed build; pin the exact running version instead.
131
+ const pinned = `@kisev/agentomatic@${requirePackageVersion()}`;
132
+ const native = "plugins" in config ? plugins(config.plugins, false) : undefined;
133
+ const legacy = "plugin" in config ? plugins(config.plugin, true) : undefined;
134
+ if (native && legacy) {
135
+ if (JSON.stringify(native) !== JSON.stringify(legacy))
136
+ throw new JsoncError("conflict", "Conflicting plugin and plugins sections; migrate explicitly before config setup");
137
+ edits.push({ kind: "remove-key", path: ["plugin"] });
138
+ }
139
+ else if (legacy)
140
+ edits.push({ kind: "rename-key", path: ["plugin"], to: "plugins" });
141
+ const normalized = [];
142
+ let pinnedPresent = false;
143
+ for (const entry of native ?? legacy ?? []) {
144
+ if (!ourEntry(entry)) {
145
+ normalized.push(entry);
146
+ continue;
147
+ }
148
+ if (pinnedPresent)
149
+ continue;
150
+ normalized.push(object(entry) ? { ...entry, package: pinned } : pinned);
151
+ pinnedPresent = true;
152
+ }
153
+ const entries = normalized;
154
+ if (!pinnedPresent)
155
+ entries.push(pinned);
156
+ edits.push({ kind: "set-value", path: ["plugins"], value: entries });
157
+ return edits;
158
+ }
@@ -0,0 +1,20 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import type { Result } from "@opencode/plugin/promise/tool";
3
+ export type ToolAfter = {
4
+ tool: string;
5
+ sessionID: string;
6
+ input: unknown;
7
+ } & ({
8
+ status: "completed";
9
+ result: Result;
10
+ } | {
11
+ status: "error";
12
+ error: unknown;
13
+ });
14
+ export declare function toolInput(value: unknown): Record<string, unknown>;
15
+ export declare function resultText(result: Result): string | undefined;
16
+ export declare function replaceResultText(result: Result, text: string): Result;
17
+ export declare function subscribeEvents(ctx: Plugin.Context, receive: (event: {
18
+ type: string;
19
+ data?: unknown;
20
+ }) => Promise<void>): () => Promise<void>;