@kisev/agentomatic 11.0.2 → 12.0.0-dev.113.g1bd7385234c6
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 +32 -16
- package/README.ru.md +33 -16
- package/dist/agent-profiles.d.ts +8 -2
- package/dist/agent-profiles.js +131 -82
- 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/asd-ste100.md +10 -0
- package/dist/assets/commands/code-simplify.md +12 -0
- package/dist/assets/commands/eli5.md +10 -0
- package/dist/assets/commands/handoff.md +10 -0
- package/dist/assets/commands/rtk-stats.md +2 -2
- package/dist/assets/commands/skill-doctor.md +10 -0
- package/dist/assets/migration-inventory.json +17 -6
- package/dist/assets/plugins/code-simplify.js +3 -0
- package/dist/assets/plugins/rtk.js +1 -1
- package/dist/assets/plugins/rules-injector.js +1 -1
- package/dist/assets/plugins/zed-bell.js +1 -1
- package/dist/catalog.d.ts +2 -2
- package/dist/catalog.js +9 -3
- package/dist/cli-output.js +6 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1257
- package/dist/command-cli.d.ts +1 -0
- package/dist/command-cli.js +1004 -0
- package/dist/config-setup.d.ts +14 -8
- package/dist/config-setup.js +197 -64
- package/dist/contracts.js +26 -0
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +42 -55
- package/dist/index.d.ts +13 -118
- package/dist/index.js +118 -125
- package/dist/installer.d.ts +25 -2
- package/dist/installer.js +68 -23
- package/dist/jsonc.d.ts +18 -2
- package/dist/jsonc.js +109 -5
- package/dist/opencode-config.d.ts +11 -0
- package/dist/opencode-config.js +237 -0
- package/dist/plugins/code-simplify.d.ts +25 -0
- package/dist/plugins/code-simplify.js +49 -0
- package/dist/plugins/events.d.ts +20 -0
- package/dist/plugins/events.js +48 -0
- package/dist/plugins/rtk.d.ts +9 -12
- package/dist/plugins/rtk.js +20 -5
- package/dist/plugins/rules-injector.d.ts +21 -11
- package/dist/plugins/rules-injector.js +43 -31
- package/dist/plugins/zed-bell.d.ts +8 -5
- package/dist/plugins/zed-bell.js +10 -2
- package/dist/reconcile.d.ts +4 -1
- package/dist/reconcile.js +12 -16
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +21 -5
- package/dist/routing.d.ts +5 -3
- package/dist/routing.js +57 -22
- package/dist/self-install.d.ts +7 -0
- package/dist/self-install.js +52 -1
- package/dist/terminal-wizard.d.ts +1 -1
- package/dist/terminal-wizard.js +2 -2
- package/package.json +12 -7
- package/dist/assets/commands/skill-improve.md +0 -10
- package/dist/assets/commands/stopit.md +0 -10
package/dist/installer.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { buildAgentProfilePlan, listAgentProfiles, validateBuiltAgentProfilePlan, } from "./agent-profiles.js";
|
|
7
|
-
import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular,
|
|
7
|
+
import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
8
8
|
import { CATALOG } from "./catalog.js";
|
|
9
9
|
import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
|
|
10
10
|
import { readPackageVersion } from "./package-metadata.js";
|
|
@@ -15,8 +15,11 @@ const LEGACY_MANIFEST_NAME = ".skills-opencode-manifest.json";
|
|
|
15
15
|
const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
|
|
16
16
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
17
|
const assetsRoot = resolve(packageRoot, "dist", "assets");
|
|
18
|
+
function migrationInventory() {
|
|
19
|
+
return JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
|
|
20
|
+
}
|
|
18
21
|
function retiredAssetPaths() {
|
|
19
|
-
const inventory =
|
|
22
|
+
const inventory = migrationInventory();
|
|
20
23
|
return new Set([
|
|
21
24
|
...Object.keys(inventory.retired_command_hashes ?? {}),
|
|
22
25
|
...Object.keys(inventory.retired_plugin_hashes ?? {}),
|
|
@@ -33,6 +36,49 @@ export function defaultSelection() {
|
|
|
33
36
|
core_activation: true,
|
|
34
37
|
};
|
|
35
38
|
}
|
|
39
|
+
const RENAME_CHAIN_LIMIT = 8;
|
|
40
|
+
function migrateSavedCommand(renamed, name) {
|
|
41
|
+
let current = name;
|
|
42
|
+
for (let depth = 0; depth < RENAME_CHAIN_LIMIT; depth += 1) {
|
|
43
|
+
const replacement = renamed[current];
|
|
44
|
+
if (replacement === undefined)
|
|
45
|
+
return current;
|
|
46
|
+
current = replacement;
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
export function savedSelection(manifest) {
|
|
51
|
+
const renamed = migrationInventory().renamed ?? {};
|
|
52
|
+
const requested = (manifest.commands ?? defaultSelection().commands).map((name) => migrateSavedCommand(renamed, name));
|
|
53
|
+
const commands = requested.filter((name) => SKILL_COMMANDS.includes(name));
|
|
54
|
+
const agents = (manifest.agents ?? FIXED_AGENT_ROLES).filter((role) => FIXED_AGENT_ROLES.includes(role));
|
|
55
|
+
const plugins = (manifest.plugins ?? ["rtk"]).filter((plugin) => SELECTABLE_PLUGINS.includes(plugin));
|
|
56
|
+
if (requested.join("\0") !== commands.join("\0"))
|
|
57
|
+
process.stderr.write(`Saved selection migrated to the current catalog: ${requested.join(", ")} -> ${commands.join(", ") || "none"}\n`);
|
|
58
|
+
return { commands, agents, plugins };
|
|
59
|
+
}
|
|
60
|
+
export async function installedSelection(scope, cwd = process.cwd(), home = homedir(), overrides = {}) {
|
|
61
|
+
const { manifest } = await currentManifest(deploymentRoot(scope, cwd, home));
|
|
62
|
+
return manifest
|
|
63
|
+
? normalizeSelection({
|
|
64
|
+
...savedSelection(manifest),
|
|
65
|
+
core_activation: manifest.core_activation,
|
|
66
|
+
...overrides,
|
|
67
|
+
})
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
export async function coreSelectionMutation(root, enabled) {
|
|
71
|
+
const owned = await currentManifest(root);
|
|
72
|
+
if (!owned.manifest || !owned.raw || owned.manifest.core_activation === enabled)
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
path: MANIFEST_NAME,
|
|
76
|
+
operation: "write",
|
|
77
|
+
content: Buffer.from(`${stable({ ...owned.manifest, core_activation: enabled })}\n`),
|
|
78
|
+
mode: 0o600,
|
|
79
|
+
expected: { sha256: sha256(owned.raw) },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
36
82
|
export function normalizeSelection(value = {}) {
|
|
37
83
|
const allCommands = new Set(SKILL_COMMANDS);
|
|
38
84
|
const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
|
|
@@ -325,15 +371,13 @@ function asLegacy(manifest) {
|
|
|
325
371
|
? manifest
|
|
326
372
|
: undefined;
|
|
327
373
|
}
|
|
328
|
-
async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true) {
|
|
374
|
+
async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true, profileChanges = []) {
|
|
329
375
|
const root = deploymentRoot(scope, cwd, home);
|
|
330
376
|
const owned = await currentManifest(root);
|
|
331
377
|
const selection = normalizeSelection(requestedSelection ??
|
|
332
378
|
(owned.manifest
|
|
333
379
|
? {
|
|
334
|
-
|
|
335
|
-
agents: owned.manifest.agents,
|
|
336
|
-
plugins: owned.manifest.plugins,
|
|
380
|
+
...savedSelection(owned.manifest),
|
|
337
381
|
core_activation: owned.manifest.core_activation,
|
|
338
382
|
}
|
|
339
383
|
: undefined));
|
|
@@ -346,7 +390,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
346
390
|
manifestSha256: sha256(owned.raw),
|
|
347
391
|
}
|
|
348
392
|
: undefined;
|
|
349
|
-
const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
|
|
393
|
+
const profiles = await buildAgentProfilePlan({ action, changes: profileChanges }, scope, cwd, home, legacyRecord, selection.agents);
|
|
350
394
|
const dependency = action === "install" && provisionDependency && selection.core_activation
|
|
351
395
|
? planDependency(scope, cwd, home)
|
|
352
396
|
: undefined;
|
|
@@ -628,24 +672,23 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
628
672
|
item.path.startsWith("plugins/")),
|
|
629
673
|
};
|
|
630
674
|
return {
|
|
631
|
-
plan: {
|
|
675
|
+
plan: {
|
|
676
|
+
...base,
|
|
677
|
+
digest: sha256(stable({ base, mutations, manifest: owned.raw, profiles: profiles.plan.digest })),
|
|
678
|
+
},
|
|
632
679
|
mutations,
|
|
633
680
|
expectedManifest: manifestContent,
|
|
634
681
|
profiles,
|
|
635
682
|
};
|
|
636
683
|
}
|
|
637
|
-
export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true) {
|
|
638
|
-
//
|
|
639
|
-
// preserves every legacy byte, and never touches user configuration.
|
|
640
|
-
await migrateLegacyNamespaces(home);
|
|
684
|
+
export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true, profileChanges = []) {
|
|
685
|
+
// Preview never migrates namespaces, creates locks, or recovers transactions.
|
|
641
686
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
642
|
-
const root = deploymentRoot(scope, cwd, home);
|
|
643
687
|
try {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
});
|
|
688
|
+
if (await readRegular(join(stateRoot, "transaction-journal.json")))
|
|
689
|
+
throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
|
|
690
|
+
return (await build(action, scope, cwd, home, selection, provisionDependency, profileChanges))
|
|
691
|
+
.plan;
|
|
649
692
|
}
|
|
650
693
|
catch (error) {
|
|
651
694
|
if (error instanceof InstallerError)
|
|
@@ -655,7 +698,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
|
|
|
655
698
|
throw error;
|
|
656
699
|
}
|
|
657
700
|
}
|
|
658
|
-
export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true) {
|
|
701
|
+
export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true, profileChanges = []) {
|
|
659
702
|
const { dependencyRunner, ...transactionOptions } = options;
|
|
660
703
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
661
704
|
const root = deploymentRoot(scope, cwd, home);
|
|
@@ -663,9 +706,11 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
|
|
|
663
706
|
try {
|
|
664
707
|
return await withLifecycleLock(stateRoot, async () => {
|
|
665
708
|
await migrateLegacyDeploymentNamespace(root);
|
|
666
|
-
if (await
|
|
667
|
-
throw new InstallerError("
|
|
668
|
-
const built = await build(action, scope, cwd, home, selection, provisionDependency);
|
|
709
|
+
if (await readRegular(join(stateRoot, "transaction-journal.json")))
|
|
710
|
+
throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
|
|
711
|
+
const built = await build(action, scope, cwd, home, selection, provisionDependency, profileChanges);
|
|
712
|
+
if (options.expectedDigest && built.plan.digest !== options.expectedDigest)
|
|
713
|
+
throw new InstallerError("stale_plan", "Installation changed; request a fresh preview");
|
|
669
714
|
if (built.plan.operations.some((item) => item.operation === "conflict" &&
|
|
670
715
|
(item.reason === "unmanaged_file" ||
|
|
671
716
|
item.reason === "v1.0.0_agent_ownership_mismatch" ||
|
|
@@ -689,7 +734,7 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
|
|
|
689
734
|
inventory.drift.length ||
|
|
690
735
|
inventory.profiles
|
|
691
736
|
.filter((item) => item.ownership !== "user-owned")
|
|
692
|
-
.some((item) => item.state !== "current"))) {
|
|
737
|
+
.some((item) => item.state !== "current" && item.state !== "not-installed"))) {
|
|
693
738
|
throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
|
|
694
739
|
}
|
|
695
740
|
},
|
package/dist/jsonc.d.ts
CHANGED
|
@@ -30,7 +30,23 @@ 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: "insert-before";
|
|
36
|
+
path: JsoncPath;
|
|
37
|
+
before: unknown;
|
|
38
|
+
value: unknown;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "set-value";
|
|
41
|
+
path: JsoncPath;
|
|
42
|
+
value: unknown;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "rename-key";
|
|
45
|
+
path: JsoncPath;
|
|
46
|
+
to: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "remove-key";
|
|
49
|
+
path: JsoncPath;
|
|
34
50
|
} | {
|
|
35
51
|
kind: "replace-array-value";
|
|
36
52
|
path: JsoncPath;
|
|
@@ -45,7 +61,7 @@ export type JsoncEdit = {
|
|
|
45
61
|
path: JsoncPath;
|
|
46
62
|
entries: Record<string, string>;
|
|
47
63
|
};
|
|
48
|
-
export type JsoncEditResult = "created" | "present" | "appended" | "replaced" | "widened";
|
|
64
|
+
export type JsoncEditResult = "created" | "present" | "appended" | "inserted" | "replaced" | "widened";
|
|
49
65
|
export declare function nodeToValue(node: Node): unknown;
|
|
50
66
|
export declare function parseJsonc(text: string): unknown;
|
|
51
67
|
export declare function applyJsoncEdits(text: string, edits: readonly JsoncEdit[]): {
|
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,16 +289,83 @@ 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;
|
|
289
351
|
return { text: text.slice(0, at) + insertion + text.slice(at), result: "appended" };
|
|
290
352
|
}
|
|
353
|
+
if (edit.kind === "insert-before") {
|
|
354
|
+
const target = resolve(root, edit.path);
|
|
355
|
+
if (!target || target.kind !== "array")
|
|
356
|
+
throw new JsoncError("conflict", "insert-before target is missing or not an array");
|
|
357
|
+
const items = target.items.map(nodeToValue);
|
|
358
|
+
if (items.some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
|
|
359
|
+
return { text, result: "present" };
|
|
360
|
+
const anchor = items.findIndex((value) => JSON.stringify(value) === JSON.stringify(edit.before));
|
|
361
|
+
if (anchor === -1)
|
|
362
|
+
throw new JsoncError("conflict", "insert-before anchor item is missing from the array");
|
|
363
|
+
const node = target.items[anchor];
|
|
364
|
+
return {
|
|
365
|
+
text: text.slice(0, node.start) + `${JSON.stringify(edit.value)}, ` + text.slice(node.start),
|
|
366
|
+
result: "inserted",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
291
369
|
if (edit.kind === "replace-array-value") {
|
|
292
370
|
const target = resolve(root, edit.path);
|
|
293
371
|
if (!target || target.kind !== "array")
|
|
@@ -369,12 +447,38 @@ function at(value, path) {
|
|
|
369
447
|
: undefined, value);
|
|
370
448
|
}
|
|
371
449
|
function verifyEdit(before, after, edit) {
|
|
450
|
+
if (edit.kind === "set-value") {
|
|
451
|
+
if (JSON.stringify(at(after, edit.path)) !== JSON.stringify(edit.value))
|
|
452
|
+
throw new JsoncError("merge_validation_failed", "set-value postcondition failed");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (edit.kind === "rename-key" || edit.kind === "remove-key") {
|
|
456
|
+
if (at(after, edit.path) !== undefined)
|
|
457
|
+
throw new JsoncError("merge_validation_failed", "Old key remains after key edit");
|
|
458
|
+
if (edit.kind === "rename-key" &&
|
|
459
|
+
JSON.stringify(at(before, edit.path)) !==
|
|
460
|
+
JSON.stringify(at(after, [...edit.path.slice(0, -1), edit.to])))
|
|
461
|
+
throw new JsoncError("merge_validation_failed", "Renamed key lost its value");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
372
464
|
if (edit.kind === "append-unique") {
|
|
373
465
|
const target = at(after, edit.path);
|
|
374
|
-
if (!Array.isArray(target) ||
|
|
466
|
+
if (!Array.isArray(target) ||
|
|
467
|
+
!target.some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
|
|
375
468
|
throw new JsoncError("merge_validation_failed", "append-unique postcondition failed");
|
|
376
469
|
return;
|
|
377
470
|
}
|
|
471
|
+
if (edit.kind === "insert-before") {
|
|
472
|
+
const target = at(after, edit.path);
|
|
473
|
+
if (!Array.isArray(target))
|
|
474
|
+
throw new JsoncError("merge_validation_failed", "insert-before postcondition failed");
|
|
475
|
+
const serialized = target.map((value) => JSON.stringify(value));
|
|
476
|
+
const value = serialized.indexOf(JSON.stringify(edit.value));
|
|
477
|
+
const anchor = serialized.indexOf(JSON.stringify(edit.before));
|
|
478
|
+
if (value === -1 || anchor === -1 || value > anchor)
|
|
479
|
+
throw new JsoncError("merge_validation_failed", "insert-before postcondition failed");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
378
482
|
if (edit.kind === "replace-array-value") {
|
|
379
483
|
const beforeTarget = at(before, edit.path);
|
|
380
484
|
const afterTarget = at(after, edit.path);
|
|
@@ -419,12 +523,12 @@ function verifyEdit(before, after, edit) {
|
|
|
419
523
|
throw new JsoncError("merge_validation_failed", "widened map lost the original scalar value");
|
|
420
524
|
}
|
|
421
525
|
export function applyJsoncEdits(text, edits) {
|
|
422
|
-
|
|
526
|
+
parseJsonc(text);
|
|
423
527
|
let current = text;
|
|
424
528
|
const results = [];
|
|
425
529
|
for (const edit of edits) {
|
|
426
530
|
const applied = applyEdit(current, parseDocument(current), edit);
|
|
427
|
-
verifyEdit(
|
|
531
|
+
verifyEdit(parseJsonc(current), parseJsonc(applied.text), edit);
|
|
428
532
|
results.push(applied.result);
|
|
429
533
|
current = applied.text;
|
|
430
534
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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 patternsOverlap(left: string, right: string): boolean;
|
|
10
|
+
export declare function corePluginRemovalEdits(config: Record<string, unknown>): JsoncEdit[];
|
|
11
|
+
export declare function corePluginEdits(config: Record<string, unknown>): JsoncEdit[];
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
// Rules that existed before this preset run. V2 resolves by the last
|
|
74
|
+
// matching rule, so a new allow that can overlap a pre-existing deny is
|
|
75
|
+
// inserted before that deny instead of being appended after it: the user's
|
|
76
|
+
// prohibition keeps priority for every resource it matches. An allow that
|
|
77
|
+
// overlaps a deny added earlier in the same batch is a deliberate exception
|
|
78
|
+
// to that deny (the OpenCode "specific rule follows the broad rule" order)
|
|
79
|
+
// and is appended, and denies are always appended because a later deny can
|
|
80
|
+
// only strengthen the result. Batch membership is tracked by object
|
|
81
|
+
// identity: insertions shift array positions, so a length boundary would
|
|
82
|
+
// misclassify shifted pre-existing rules as batch additions.
|
|
83
|
+
const preexisting = rules;
|
|
84
|
+
const batchRules = new Set();
|
|
85
|
+
for (const rule of additions) {
|
|
86
|
+
const exact = rules.reduce((last, existing, index) => existing.action === rule.action && existing.resource === rule.resource ? index : last, -1);
|
|
87
|
+
if (exact >= 0) {
|
|
88
|
+
if (rules[exact].effect !== rule.effect ||
|
|
89
|
+
rules
|
|
90
|
+
.slice(exact + 1)
|
|
91
|
+
.some((later) => matchesWildcard(later.action, rule.action) &&
|
|
92
|
+
matchesWildcard(later.resource, rule.resource) &&
|
|
93
|
+
later.effect !== rule.effect))
|
|
94
|
+
throw new JsoncError("conflict", `Existing permission conflicts with preset: ${rule.action} ${rule.resource}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const overlapsDeny = (candidate) => candidate.effect === "deny" && rulesOverlap(candidate, rule);
|
|
98
|
+
const anchor = rule.effect === "allow" && ![...batchRules].some((added) => overlapsDeny(added))
|
|
99
|
+
? preexisting.find((existing) => overlapsDeny(existing))
|
|
100
|
+
: undefined;
|
|
101
|
+
if (anchor !== undefined) {
|
|
102
|
+
edits.push({ kind: "insert-before", path: ["permissions"], before: anchor, value: rule });
|
|
103
|
+
const position = rules.indexOf(anchor);
|
|
104
|
+
rules = [...rules.slice(0, position), rule, ...rules.slice(position)];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
edits.push({ kind: "append-unique", path: ["permissions"], value: rule });
|
|
108
|
+
rules = [...rules, rule];
|
|
109
|
+
}
|
|
110
|
+
batchRules.add(rule);
|
|
111
|
+
}
|
|
112
|
+
return edits;
|
|
113
|
+
}
|
|
114
|
+
export function matchesWildcard(pattern, value) {
|
|
115
|
+
return new RegExp(`^${pattern
|
|
116
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
117
|
+
.replace(/\\\*/g, ".*")
|
|
118
|
+
.replace(/\\\?/g, ".")}$`).test(value);
|
|
119
|
+
}
|
|
120
|
+
function patternUnits(pattern) {
|
|
121
|
+
return [...pattern].map((character) => character === "*"
|
|
122
|
+
? { kind: "star" }
|
|
123
|
+
: character === "?"
|
|
124
|
+
? { kind: "any" }
|
|
125
|
+
: { kind: "char", char: character });
|
|
126
|
+
}
|
|
127
|
+
export function patternsOverlap(left, right) {
|
|
128
|
+
const a = patternUnits(left);
|
|
129
|
+
const b = patternUnits(right);
|
|
130
|
+
const memo = new Map();
|
|
131
|
+
const overlap = (i, j) => {
|
|
132
|
+
if (i === a.length && j === b.length)
|
|
133
|
+
return true;
|
|
134
|
+
// A trailing wildcard run can still match the empty string, so exhaustion
|
|
135
|
+
// is compatible only with an all-star remainder on the other side.
|
|
136
|
+
if (i === a.length)
|
|
137
|
+
return b.slice(j).every((unit) => unit.kind === "star");
|
|
138
|
+
if (j === b.length)
|
|
139
|
+
return a.slice(i).every((unit) => unit.kind === "star");
|
|
140
|
+
const key = `${i}:${j}`;
|
|
141
|
+
const cached = memo.get(key);
|
|
142
|
+
if (cached !== undefined)
|
|
143
|
+
return cached;
|
|
144
|
+
const unit = a[i];
|
|
145
|
+
const other = b[j];
|
|
146
|
+
let result;
|
|
147
|
+
if (unit.kind === "star" || other.kind === "star")
|
|
148
|
+
// A star consumes nothing from the other pattern or absorbs one unit of it.
|
|
149
|
+
result = overlap(i + 1, j) || overlap(i, j + 1);
|
|
150
|
+
else if (unit.kind === "any" || other.kind === "any")
|
|
151
|
+
result = overlap(i + 1, j + 1);
|
|
152
|
+
else
|
|
153
|
+
result = unit.char === other.char && overlap(i + 1, j + 1);
|
|
154
|
+
memo.set(key, result);
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
return overlap(0, 0);
|
|
158
|
+
}
|
|
159
|
+
function rulesOverlap(existing, candidate) {
|
|
160
|
+
return (patternsOverlap(existing.action, candidate.action) &&
|
|
161
|
+
patternsOverlap(existing.resource, candidate.resource));
|
|
162
|
+
}
|
|
163
|
+
function plugins(value, legacy) {
|
|
164
|
+
if (!Array.isArray(value))
|
|
165
|
+
throw new JsoncError("conflict", "Plugin configuration must be an array");
|
|
166
|
+
return value.map((entry) => {
|
|
167
|
+
if (typeof entry === "string")
|
|
168
|
+
return entry === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry;
|
|
169
|
+
if (legacy &&
|
|
170
|
+
Array.isArray(entry) &&
|
|
171
|
+
entry.length === 2 &&
|
|
172
|
+
typeof entry[0] === "string" &&
|
|
173
|
+
object(entry[1]))
|
|
174
|
+
return {
|
|
175
|
+
package: entry[0] === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry[0],
|
|
176
|
+
options: entry[1],
|
|
177
|
+
};
|
|
178
|
+
if (object(entry) && typeof entry.package === "string")
|
|
179
|
+
return {
|
|
180
|
+
...entry,
|
|
181
|
+
package: entry.package === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry.package,
|
|
182
|
+
};
|
|
183
|
+
throw new JsoncError("conflict", "Invalid plugin entry; use a package string or V2 package/options object");
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const OUR_PACKAGE = /^@kisev\/(?:agentomatic|skills-opencode)(?:@|$)/;
|
|
187
|
+
function ourEntry(entry) {
|
|
188
|
+
if (typeof entry === "string")
|
|
189
|
+
return OUR_PACKAGE.test(entry);
|
|
190
|
+
return object(entry) && typeof entry.package === "string" && OUR_PACKAGE.test(entry.package);
|
|
191
|
+
}
|
|
192
|
+
export function corePluginRemovalEdits(config) {
|
|
193
|
+
return ["plugin", "plugins"].flatMap((key) => {
|
|
194
|
+
if (!(key in config))
|
|
195
|
+
return [];
|
|
196
|
+
plugins(config[key], key === "plugin");
|
|
197
|
+
const entries = config[key];
|
|
198
|
+
const remaining = entries.filter((entry) => !ourEntry(Array.isArray(entry) ? entry[0] : entry));
|
|
199
|
+
return remaining.length === entries.length
|
|
200
|
+
? []
|
|
201
|
+
: [{ kind: "set-value", path: [key], value: remaining }];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
export function corePluginEdits(config) {
|
|
205
|
+
const edits = [
|
|
206
|
+
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
|
|
207
|
+
];
|
|
208
|
+
// V2 resolves a bare package name through the registry "latest" dist-tag, which
|
|
209
|
+
// can differ from the installed build; pin the exact running version instead.
|
|
210
|
+
const pinned = `@kisev/agentomatic@${requirePackageVersion()}`;
|
|
211
|
+
const native = "plugins" in config ? plugins(config.plugins, false) : undefined;
|
|
212
|
+
const legacy = "plugin" in config ? plugins(config.plugin, true) : undefined;
|
|
213
|
+
if (native && legacy) {
|
|
214
|
+
if (JSON.stringify(native) !== JSON.stringify(legacy))
|
|
215
|
+
throw new JsoncError("conflict", "Conflicting plugin and plugins sections; migrate explicitly before config setup");
|
|
216
|
+
edits.push({ kind: "remove-key", path: ["plugin"] });
|
|
217
|
+
}
|
|
218
|
+
else if (legacy)
|
|
219
|
+
edits.push({ kind: "rename-key", path: ["plugin"], to: "plugins" });
|
|
220
|
+
const normalized = [];
|
|
221
|
+
let pinnedPresent = false;
|
|
222
|
+
for (const entry of native ?? legacy ?? []) {
|
|
223
|
+
if (!ourEntry(entry)) {
|
|
224
|
+
normalized.push(entry);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (pinnedPresent)
|
|
228
|
+
continue;
|
|
229
|
+
normalized.push(object(entry) ? { ...entry, package: pinned } : pinned);
|
|
230
|
+
pinnedPresent = true;
|
|
231
|
+
}
|
|
232
|
+
const entries = normalized;
|
|
233
|
+
if (!pinnedPresent)
|
|
234
|
+
entries.push(pinned);
|
|
235
|
+
edits.push({ kind: "set-value", path: ["plugins"], value: entries });
|
|
236
|
+
return edits;
|
|
237
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin";
|
|
2
|
+
export type CodeSimplifyLevel = "off" | "lite" | "full" | "ultra";
|
|
3
|
+
export declare const CODE_SIMPLIFY_ROLES: readonly ["manager", "architect", "mapper", "worker", "review", "critic"];
|
|
4
|
+
export type CodeSimplifyRole = (typeof CODE_SIMPLIFY_ROLES)[number];
|
|
5
|
+
export type CodeSimplifyOptions = {
|
|
6
|
+
level?: CodeSimplifyLevel;
|
|
7
|
+
/** Agent roles that receive the injection; every role when absent. */
|
|
8
|
+
scope?: readonly CodeSimplifyRole[];
|
|
9
|
+
};
|
|
10
|
+
export declare function codeSimplify(options?: CodeSimplifyOptions): {
|
|
11
|
+
context?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
context: (input: {
|
|
14
|
+
agent?: string;
|
|
15
|
+
system: Array<{
|
|
16
|
+
type: "text";
|
|
17
|
+
text: string;
|
|
18
|
+
}>;
|
|
19
|
+
}) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
declare const _default: {
|
|
22
|
+
id: string;
|
|
23
|
+
setup(ctx: Plugin.Context): Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
export default _default;
|