@kisev/agentomatic 11.0.2-dev.85.gb47f85ffc092 → 11.0.2-dev.87.g5a92e74c6754
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 +10 -8
- package/README.ru.md +11 -8
- package/dist/agent-profiles.js +33 -48
- package/dist/assets/agents/architect.md +4 -4
- package/dist/assets/agents/critic.md +10 -11
- package/dist/assets/agents/manager.md +11 -12
- package/dist/assets/agents/mapper.md +4 -4
- package/dist/assets/agents/review.md +11 -12
- package/dist/assets/agents/worker.md +29 -32
- package/dist/assets/commands/rtk-stats.md +1 -1
- package/dist/cli.js +5 -4
- package/dist/config-setup.d.ts +1 -6
- package/dist/config-setup.js +69 -51
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +32 -52
- package/dist/index.d.ts +2 -128
- package/dist/index.js +93 -169
- package/dist/jsonc.d.ts +12 -1
- package/dist/jsonc.js +82 -5
- package/dist/opencode-config.d.ts +9 -0
- package/dist/opencode-config.js +136 -0
- package/dist/plugins/events.d.ts +20 -0
- package/dist/plugins/events.js +48 -0
- package/dist/plugins/rtk.d.ts +4 -17
- package/dist/plugins/rtk.js +15 -7
- package/dist/plugins/rules-injector.d.ts +15 -28
- package/dist/plugins/rules-injector.js +38 -33
- package/dist/plugins/zed-bell.d.ts +2 -13
- package/dist/plugins/zed-bell.js +5 -4
- package/dist/registry.js +1 -1
- package/dist/routing.js +1 -1
- package/package.json +2 -7
- package/dist/plugins/compatibility.d.ts +0 -30
- package/dist/plugins/compatibility.js +0 -87
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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 =
|
|
98
|
-
|
|
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
|
|
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(
|
|
110
|
-
else
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
? args.requirements
|
|
114
|
-
:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 =
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
107
|
+
result = JSON.parse(resultText(event.result) ?? "");
|
|
132
108
|
}
|
|
133
109
|
catch {
|
|
134
|
-
throw new Error("
|
|
110
|
+
throw new Error("Subagent result must be JSON structured report");
|
|
135
111
|
}
|
|
136
|
-
gate.complete(
|
|
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:
|
|
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
|
|
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
|
|
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) ||
|
|
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
|
-
|
|
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(
|
|
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,136 @@
|
|
|
1
|
+
import { JsoncError } from "./jsonc.js";
|
|
2
|
+
const effects = new Set(["allow", "ask", "deny"]);
|
|
3
|
+
const aliases = {
|
|
4
|
+
bash: "shell",
|
|
5
|
+
task: "subagent",
|
|
6
|
+
write: "edit",
|
|
7
|
+
patch: "edit",
|
|
8
|
+
};
|
|
9
|
+
function object(value) {
|
|
10
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
11
|
+
}
|
|
12
|
+
function legacyPermissions(value) {
|
|
13
|
+
if (typeof value === "string" && effects.has(value))
|
|
14
|
+
return [{ action: "*", resource: "*", effect: value }];
|
|
15
|
+
if (!object(value))
|
|
16
|
+
throw new JsoncError("conflict", "Legacy permission must be an effect or a tool map");
|
|
17
|
+
return Object.entries(value).flatMap(([name, entry]) => {
|
|
18
|
+
if (name === "lsp" || name === "doom_loop")
|
|
19
|
+
throw new JsoncError("conflict", `Permission action has no V2 equivalent: ${name}`);
|
|
20
|
+
const action = aliases[name] ?? name;
|
|
21
|
+
const resources = typeof entry === "string" ? { "*": entry } : entry;
|
|
22
|
+
if (!object(resources))
|
|
23
|
+
throw new JsoncError("conflict", `Invalid permission map: ${name}`);
|
|
24
|
+
return Object.entries(resources).map(([resource, effect]) => {
|
|
25
|
+
if (typeof effect !== "string" || !effects.has(effect))
|
|
26
|
+
throw new JsoncError("conflict", `Invalid permission effect: ${name}`);
|
|
27
|
+
return { action, resource, effect: effect };
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function permissionEdits(config, additions) {
|
|
32
|
+
const edits = [];
|
|
33
|
+
let rules = [];
|
|
34
|
+
if ("permissions" in config) {
|
|
35
|
+
if (!Array.isArray(config.permissions) ||
|
|
36
|
+
!config.permissions.every((rule) => object(rule) &&
|
|
37
|
+
typeof rule.action === "string" &&
|
|
38
|
+
typeof rule.resource === "string" &&
|
|
39
|
+
typeof rule.effect === "string" &&
|
|
40
|
+
effects.has(rule.effect)))
|
|
41
|
+
throw new JsoncError("conflict", "V2 permissions must be an ordered array of action/resource/effect rules");
|
|
42
|
+
rules = config.permissions;
|
|
43
|
+
}
|
|
44
|
+
let tools = [];
|
|
45
|
+
if ("tools" in config) {
|
|
46
|
+
if (!object(config.tools) ||
|
|
47
|
+
!Object.values(config.tools).every((value) => typeof value === "boolean"))
|
|
48
|
+
throw new JsoncError("conflict", "Legacy tools must be a boolean map");
|
|
49
|
+
tools = Object.entries(config.tools).map(([name, enabled]) => ({
|
|
50
|
+
action: aliases[name] ?? name,
|
|
51
|
+
resource: "*",
|
|
52
|
+
effect: enabled ? "allow" : "deny",
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const legacyKeys = ["tools", "permission"].filter((key) => key in config);
|
|
56
|
+
if (legacyKeys.length) {
|
|
57
|
+
// V2 normalizes tools first, legacy permission next, and native rules last.
|
|
58
|
+
rules = [
|
|
59
|
+
...tools,
|
|
60
|
+
...("permission" in config ? legacyPermissions(config.permission) : []),
|
|
61
|
+
...rules,
|
|
62
|
+
];
|
|
63
|
+
const renamed = "permissions" in config ? undefined : legacyKeys[0];
|
|
64
|
+
for (const key of legacyKeys)
|
|
65
|
+
edits.push(key === renamed
|
|
66
|
+
? { kind: "rename-key", path: [key], to: "permissions" }
|
|
67
|
+
: { kind: "remove-key", path: [key] });
|
|
68
|
+
edits.push({ kind: "set-value", path: ["permissions"], value: rules });
|
|
69
|
+
}
|
|
70
|
+
else if (!("permissions" in config))
|
|
71
|
+
edits.push({ kind: "set-if-absent", path: ["permissions"], value: [] });
|
|
72
|
+
for (const rule of additions) {
|
|
73
|
+
const exact = rules.reduce((last, existing, index) => existing.action === rule.action && existing.resource === rule.resource ? index : last, -1);
|
|
74
|
+
if (exact >= 0) {
|
|
75
|
+
if (rules[exact].effect !== rule.effect ||
|
|
76
|
+
rules
|
|
77
|
+
.slice(exact + 1)
|
|
78
|
+
.some((later) => matchesWildcard(later.action, rule.action) &&
|
|
79
|
+
matchesWildcard(later.resource, rule.resource) &&
|
|
80
|
+
later.effect !== rule.effect))
|
|
81
|
+
throw new JsoncError("conflict", `Existing permission conflicts with preset: ${rule.action} ${rule.resource}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
edits.push({ kind: "append-unique", path: ["permissions"], value: rule });
|
|
85
|
+
rules = [...rules, rule];
|
|
86
|
+
}
|
|
87
|
+
return edits;
|
|
88
|
+
}
|
|
89
|
+
export function matchesWildcard(pattern, value) {
|
|
90
|
+
return new RegExp(`^${pattern
|
|
91
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
92
|
+
.replace(/\\\*/g, ".*")
|
|
93
|
+
.replace(/\\\?/g, ".")}$`).test(value);
|
|
94
|
+
}
|
|
95
|
+
function plugins(value, legacy) {
|
|
96
|
+
if (!Array.isArray(value))
|
|
97
|
+
throw new JsoncError("conflict", "Plugin configuration must be an array");
|
|
98
|
+
return value.map((entry) => {
|
|
99
|
+
if (typeof entry === "string")
|
|
100
|
+
return entry === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry;
|
|
101
|
+
if (legacy &&
|
|
102
|
+
Array.isArray(entry) &&
|
|
103
|
+
entry.length === 2 &&
|
|
104
|
+
typeof entry[0] === "string" &&
|
|
105
|
+
object(entry[1]))
|
|
106
|
+
return {
|
|
107
|
+
package: entry[0] === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry[0],
|
|
108
|
+
options: entry[1],
|
|
109
|
+
};
|
|
110
|
+
if (object(entry) && typeof entry.package === "string")
|
|
111
|
+
return {
|
|
112
|
+
...entry,
|
|
113
|
+
package: entry.package === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry.package,
|
|
114
|
+
};
|
|
115
|
+
throw new JsoncError("conflict", "Invalid plugin entry; use a package string or V2 package/options object");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function corePluginEdits(config) {
|
|
119
|
+
const edits = [
|
|
120
|
+
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
|
|
121
|
+
];
|
|
122
|
+
const native = "plugins" in config ? plugins(config.plugins, false) : undefined;
|
|
123
|
+
const legacy = "plugin" in config ? plugins(config.plugin, true) : undefined;
|
|
124
|
+
if (native && legacy) {
|
|
125
|
+
if (JSON.stringify(native) !== JSON.stringify(legacy))
|
|
126
|
+
throw new JsoncError("conflict", "Conflicting plugin and plugins sections; migrate explicitly before config setup");
|
|
127
|
+
edits.push({ kind: "remove-key", path: ["plugin"] });
|
|
128
|
+
}
|
|
129
|
+
else if (legacy)
|
|
130
|
+
edits.push({ kind: "rename-key", path: ["plugin"], to: "plugins" });
|
|
131
|
+
const entries = native ?? legacy ?? [];
|
|
132
|
+
if (!entries.some((entry) => entry === "@kisev/agentomatic" || (object(entry) && entry.package === "@kisev/agentomatic")))
|
|
133
|
+
entries.push("@kisev/agentomatic");
|
|
134
|
+
edits.push({ kind: "set-value", path: ["plugins"], value: entries });
|
|
135
|
+
return edits;
|
|
136
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function toolInput(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
}
|
|
6
|
+
export function resultText(result) {
|
|
7
|
+
if (typeof result.content === "string")
|
|
8
|
+
return result.content;
|
|
9
|
+
if (Array.isArray(result.content))
|
|
10
|
+
return result.content
|
|
11
|
+
.filter((part) => part.type === "text")
|
|
12
|
+
.map((part) => part.text)
|
|
13
|
+
.join("\n");
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function replaceResultText(result, text) {
|
|
17
|
+
if (!Array.isArray(result.content))
|
|
18
|
+
return { ...result, content: text };
|
|
19
|
+
let replaced = false;
|
|
20
|
+
return {
|
|
21
|
+
...result,
|
|
22
|
+
content: result.content.flatMap((part) => {
|
|
23
|
+
if (part.type !== "text")
|
|
24
|
+
return [part];
|
|
25
|
+
if (replaced)
|
|
26
|
+
return [];
|
|
27
|
+
replaced = true;
|
|
28
|
+
return [{ type: "text", text }];
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function subscribeEvents(ctx, receive) {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const subscription = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal }))
|
|
37
|
+
await receive(event);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!controller.signal.aborted)
|
|
41
|
+
console.error("agentomatic event subscription failed", error);
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return async () => {
|
|
45
|
+
controller.abort();
|
|
46
|
+
await subscription;
|
|
47
|
+
};
|
|
48
|
+
}
|