@kisev/agentomatic 11.0.2 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +11 -6
- package/dist/assets/commands/skill-improve.md +0 -10
- package/dist/assets/commands/stopit.md +0 -10
package/dist/config-setup.d.ts
CHANGED
|
@@ -6,20 +6,20 @@ export declare const CONFIG_TARGETS: readonly ["opencode", "kilo", "mimo"];
|
|
|
6
6
|
export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
|
|
7
7
|
export type TargetFileKind = "main" | "tui";
|
|
8
8
|
export declare const CONFIG_FRAGMENTS: readonly [{
|
|
9
|
+
readonly name: "core-disable";
|
|
10
|
+
readonly description: "Disconnect only the agentomatic plugin, preserving unrelated configuration";
|
|
11
|
+
readonly targets: readonly ["opencode"];
|
|
12
|
+
readonly file: "main";
|
|
13
|
+
}, {
|
|
9
14
|
readonly name: "core-plugin";
|
|
10
15
|
readonly description: "Register @kisev/agentomatic in the plugin array (OpenCode only)";
|
|
11
16
|
readonly targets: readonly ["opencode"];
|
|
12
17
|
readonly file: "main";
|
|
13
18
|
}, {
|
|
14
19
|
readonly name: "skills-state-permissions";
|
|
15
|
-
readonly description: "Allow the
|
|
20
|
+
readonly description: "Allow the canonical ~/.agents/skills tree, legacy skills state paths, and reviewmatic-managed review worktrees without per-run prompts";
|
|
16
21
|
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
17
22
|
readonly file: "main";
|
|
18
|
-
}, {
|
|
19
|
-
readonly name: "lsp-preset";
|
|
20
|
-
readonly description: "Add LSP servers from the shared catalog (OpenCode only)";
|
|
21
|
-
readonly targets: readonly ["opencode"];
|
|
22
|
-
readonly file: "main";
|
|
23
23
|
}, {
|
|
24
24
|
readonly name: "secrets-guard";
|
|
25
25
|
readonly description: "Deny reads and edits of common secret files";
|
|
@@ -32,7 +32,7 @@ export declare const CONFIG_FRAGMENTS: readonly [{
|
|
|
32
32
|
readonly file: "main";
|
|
33
33
|
}, {
|
|
34
34
|
readonly name: "tui-schema";
|
|
35
|
-
readonly description: "
|
|
35
|
+
readonly description: "Configure terminal settings: OpenCode V2 cli.json or Kilo/MiMo TUI files";
|
|
36
36
|
readonly targets: readonly ["opencode", "kilo", "mimo"];
|
|
37
37
|
readonly file: "tui";
|
|
38
38
|
}];
|
|
@@ -77,9 +77,15 @@ export declare function recoverConfigSetup(scope: Scope, dryRun: boolean, cwd?:
|
|
|
77
77
|
}>;
|
|
78
78
|
export declare function normalizeConfigSelection(scope: Scope, value?: Partial<ConfigSetupSelection>): ConfigSetupSelection;
|
|
79
79
|
export declare function defaultConfigSelection(scope: Scope, cwd?: string, home?: string): Promise<ConfigSetupSelection>;
|
|
80
|
-
export declare function
|
|
80
|
+
export declare function inspectIntegration(scope: Scope, cwd?: string, home?: string): Promise<{
|
|
81
|
+
path: string;
|
|
82
|
+
connected: boolean;
|
|
83
|
+
problem?: string;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, provisionDependency?: boolean, syncSelection?: boolean): Promise<ConfigSetupPlan>;
|
|
81
86
|
export declare function applyConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, options?: {
|
|
82
87
|
dependencyRunner?: DependencyRunner;
|
|
83
88
|
provisionDependency?: boolean;
|
|
84
89
|
receipt?: string;
|
|
90
|
+
syncSelection?: boolean;
|
|
85
91
|
}): Promise<ConfigSetupPlan>;
|
package/dist/config-setup.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { lstat } from "node:fs/promises";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { archiveMutations } from "./installer.js";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { archiveMutations, coreSelectionMutation } from "./installer.js";
|
|
8
6
|
import { requirePackageVersion } from "./package-metadata.js";
|
|
9
7
|
import { ensureDependency, planDependency } from "./self-install.js";
|
|
10
8
|
import { applyTransaction, archiveRoot, deploymentRoot, LifecycleError, lifecycleRoot, inspectTransaction, readRegular, recoverTransaction, sha256, withLifecycleLock, } from "./lifecycle.js";
|
|
11
9
|
import { applyJsoncEdits, parseJsonc } from "./jsonc.js";
|
|
12
|
-
|
|
13
|
-
const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
|
|
14
|
-
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
import { corePluginEdits, corePluginRemovalEdits, permissionEdits, } from "./opencode-config.js";
|
|
15
11
|
const XDG_STATE_GLOB = "~/.local/state/agent-skills/**";
|
|
16
12
|
const OPENCODE_SKILLS_GLOB = "~/.config/opencode/skills/**";
|
|
13
|
+
const AGENTS_SKILLS_GLOB = "~/.agents/skills/**";
|
|
14
|
+
// Structural pattern for reviewmatic-managed review worktrees: one managed
|
|
15
|
+
// catalog per repository as a sibling of the checkout (`<repo>.worktrees/
|
|
16
|
+
// reviewmatic/<hash>`). No per-repository or per-MR path is predetermined, and
|
|
17
|
+
// a typo'd sibling state directory such as `agent-skill` never matches this
|
|
18
|
+
// pattern or the state globs above.
|
|
19
|
+
const WORKTREE_TREE_GLOB = "*.worktrees/reviewmatic/**";
|
|
20
|
+
const WORKTREE_ROOT = "*.worktrees/reviewmatic";
|
|
21
|
+
const WORKTREE_BASE = "*.worktrees";
|
|
22
|
+
// V2 checks external paths against a canonical directory boundary that
|
|
23
|
+
// normally ends in `/*`, so the exact roots above never match an actual
|
|
24
|
+
// worktree access. This boundary pattern covers the managed reviewmatic
|
|
25
|
+
// catalog and every managed worktree inside it, without opening sibling
|
|
26
|
+
// directories of other tools under `<repo>.worktrees`.
|
|
27
|
+
const WORKTREE_BOUNDARY_GLOB = "*.worktrees/reviewmatic/*";
|
|
17
28
|
const SECRET_PATHS = [
|
|
18
29
|
"**/.env",
|
|
19
30
|
"**/.env.*",
|
|
@@ -40,6 +51,12 @@ export class ConfigSetupError extends LifecycleError {
|
|
|
40
51
|
}
|
|
41
52
|
export const CONFIG_TARGETS = ["opencode", "kilo", "mimo"];
|
|
42
53
|
export const CONFIG_FRAGMENTS = [
|
|
54
|
+
{
|
|
55
|
+
name: "core-disable",
|
|
56
|
+
description: "Disconnect only the agentomatic plugin, preserving unrelated configuration",
|
|
57
|
+
targets: ["opencode"],
|
|
58
|
+
file: "main",
|
|
59
|
+
},
|
|
43
60
|
{
|
|
44
61
|
name: "core-plugin",
|
|
45
62
|
description: "Register @kisev/agentomatic in the plugin array (OpenCode only)",
|
|
@@ -48,16 +65,10 @@ export const CONFIG_FRAGMENTS = [
|
|
|
48
65
|
},
|
|
49
66
|
{
|
|
50
67
|
name: "skills-state-permissions",
|
|
51
|
-
description: "Allow the
|
|
68
|
+
description: "Allow the canonical ~/.agents/skills tree, legacy skills state paths, and reviewmatic-managed review worktrees without per-run prompts",
|
|
52
69
|
targets: ["opencode", "kilo", "mimo"],
|
|
53
70
|
file: "main",
|
|
54
71
|
},
|
|
55
|
-
{
|
|
56
|
-
name: "lsp-preset",
|
|
57
|
-
description: "Add LSP servers from the shared catalog (OpenCode only)",
|
|
58
|
-
targets: ["opencode"],
|
|
59
|
-
file: "main",
|
|
60
|
-
},
|
|
61
72
|
{
|
|
62
73
|
name: "secrets-guard",
|
|
63
74
|
description: "Deny reads and edits of common secret files",
|
|
@@ -72,7 +83,7 @@ export const CONFIG_FRAGMENTS = [
|
|
|
72
83
|
},
|
|
73
84
|
{
|
|
74
85
|
name: "tui-schema",
|
|
75
|
-
description: "
|
|
86
|
+
description: "Configure terminal settings: OpenCode V2 cli.json or Kilo/MiMo TUI files",
|
|
76
87
|
targets: ["opencode", "kilo", "mimo"],
|
|
77
88
|
file: "tui",
|
|
78
89
|
},
|
|
@@ -94,16 +105,43 @@ async function configSnapshot(built) {
|
|
|
94
105
|
}
|
|
95
106
|
async function requireNoRecovery(stateRoot) {
|
|
96
107
|
if (await readRegular(join(stateRoot, "transaction-journal.json")))
|
|
97
|
-
throw new ConfigSetupError("recovery_required", "An interrupted transaction requires
|
|
108
|
+
throw new ConfigSetupError("recovery_required", "An interrupted transaction requires maintenance recover --dry-run, then confirmed maintenance recover before a fresh preview");
|
|
98
109
|
}
|
|
99
110
|
export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), home = homedir(), expectedDigest) {
|
|
100
111
|
const root = deploymentRoot(scope, cwd, home);
|
|
101
112
|
const state = lifecycleRoot(scope, cwd, home);
|
|
102
|
-
const built = await build({ targets: [...CONFIG_TARGETS], fragments: [...FRAGMENT_NAMES] }, scope, cwd, home);
|
|
103
|
-
const allowed = [...built.allowedRoots, archiveRoot(scope, cwd, home)];
|
|
104
|
-
const paths = await inspectTransaction(root, state, allowed);
|
|
105
113
|
const journalPath = join(state, "transaction-journal.json");
|
|
106
114
|
const journal = await readRegular(journalPath);
|
|
115
|
+
const scopeRoot = resolve(scope === "global" ? home : cwd);
|
|
116
|
+
let recoveryRoot = root;
|
|
117
|
+
if (journal) {
|
|
118
|
+
let candidate;
|
|
119
|
+
try {
|
|
120
|
+
candidate = JSON.parse(journal.toString("utf8"));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new ConfigSetupError("invalid_journal", "Transaction journal is not valid JSON");
|
|
124
|
+
}
|
|
125
|
+
if (candidate.root === scopeRoot && scopeRoot !== root) {
|
|
126
|
+
const prefix = scope === "global" ? ".config/opencode/" : ".opencode/";
|
|
127
|
+
if (!Array.isArray(candidate.operations) ||
|
|
128
|
+
candidate.operations.some((operation) => !operation.root && !operation.path?.startsWith(prefix)))
|
|
129
|
+
throw new ConfigSetupError("invalid_journal", "Cleanup journal contains paths outside the deployment");
|
|
130
|
+
recoveryRoot = scopeRoot;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const allowed = [
|
|
134
|
+
root,
|
|
135
|
+
archiveRoot(scope, cwd, home),
|
|
136
|
+
...(scope === "project"
|
|
137
|
+
? [resolve(cwd)]
|
|
138
|
+
: [
|
|
139
|
+
join(home, ".config", "opencode"),
|
|
140
|
+
join(home, ".config", "kilo"),
|
|
141
|
+
join(home, ".config", "mimocode"),
|
|
142
|
+
]),
|
|
143
|
+
];
|
|
144
|
+
const paths = await inspectTransaction(recoveryRoot, state, allowed);
|
|
107
145
|
const digest = journal ? sha256(journal) : null;
|
|
108
146
|
if (dryRun || !paths.length)
|
|
109
147
|
return { paths, recovered: false, digest };
|
|
@@ -111,7 +149,7 @@ export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), hom
|
|
|
111
149
|
const current = await readRegular(journalPath);
|
|
112
150
|
if (!current || !expectedDigest || sha256(current) !== expectedDigest)
|
|
113
151
|
throw new ConfigSetupError("stale_receipt", "Recovery journal changed; preview recovery again");
|
|
114
|
-
return recoverTransaction(
|
|
152
|
+
return recoverTransaction(recoveryRoot, state, allowed);
|
|
115
153
|
});
|
|
116
154
|
return { paths, recovered, digest };
|
|
117
155
|
}
|
|
@@ -137,23 +175,26 @@ const UNIFIED_TUI_KEYBINDS = {
|
|
|
137
175
|
messages_toggle_conceal: "<leader>h,<leader>р",
|
|
138
176
|
tips_toggle: "<leader>h,<leader>р",
|
|
139
177
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
const OPENCODE_KEYBINDS = {
|
|
179
|
+
"command.palette.show": "alt+p",
|
|
180
|
+
"app.exit": "ctrl+d,<leader>q,<leader>й",
|
|
181
|
+
"session.first": "ctrl+g",
|
|
182
|
+
"session.last": "ctrl+alt+g",
|
|
183
|
+
"prompt.editor": "<leader>e,<leader>у",
|
|
184
|
+
"theme.switch": "<leader>t,<leader>е",
|
|
185
|
+
"session.sidebar.toggle": "<leader>b,<leader>и",
|
|
186
|
+
"opencode.status": "<leader>s,<leader>ы",
|
|
187
|
+
"session.export": "<leader>x,<leader>ч",
|
|
188
|
+
"session.new": "<leader>n,<leader>т",
|
|
189
|
+
"session.list": "<leader>l,<leader>д",
|
|
190
|
+
"session.timeline": "<leader>g,<leader>п",
|
|
191
|
+
"session.compact": "<leader>c,<leader>с",
|
|
192
|
+
"model.list": "<leader>m,<leader>ь",
|
|
193
|
+
"agent.list": "<leader>a,<leader>ф",
|
|
194
|
+
"messages.copy": "<leader>y,<leader>н",
|
|
195
|
+
"session.undo": "<leader>u,<leader>г",
|
|
196
|
+
"session.redo": "<leader>r,<leader>к",
|
|
145
197
|
};
|
|
146
|
-
function lspCatalog() {
|
|
147
|
-
const raw = readFileSync(resolve(packageRoot, "dist", "assets", "lsp-catalog.json"), "utf8");
|
|
148
|
-
const catalog = JSON.parse(raw);
|
|
149
|
-
if (!Array.isArray(catalog.servers))
|
|
150
|
-
throw new ConfigSetupError("invalid_package", "LSP catalog is unavailable");
|
|
151
|
-
return catalog.servers
|
|
152
|
-
.filter((server) => typeof server.name === "string" &&
|
|
153
|
-
Array.isArray(server.extensions) &&
|
|
154
|
-
server.extensions.every((extension) => typeof extension === "string"))
|
|
155
|
-
.filter((server) => LSP_SERVER_COMMANDS[server.name]);
|
|
156
|
-
}
|
|
157
198
|
function mapAllowsAll(value) {
|
|
158
199
|
return Boolean(value &&
|
|
159
200
|
typeof value === "object" &&
|
|
@@ -171,20 +212,41 @@ function fragmentEdits(fragment, target, current) {
|
|
|
171
212
|
? value.permission
|
|
172
213
|
: {};
|
|
173
214
|
if (fragment === "core-plugin") {
|
|
174
|
-
return
|
|
175
|
-
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
|
|
176
|
-
{ kind: "set-if-absent", path: ["plugin"], value: [] },
|
|
177
|
-
{
|
|
178
|
-
kind: "replace-array-value",
|
|
179
|
-
path: ["plugin"],
|
|
180
|
-
from: LEGACY_PACKAGE_NAME,
|
|
181
|
-
to: PACKAGE_NAME,
|
|
182
|
-
},
|
|
183
|
-
{ kind: "append-unique", path: ["plugin"], value: PACKAGE_NAME },
|
|
184
|
-
];
|
|
215
|
+
return corePluginEdits(value);
|
|
185
216
|
}
|
|
217
|
+
if (fragment === "core-disable")
|
|
218
|
+
return corePluginRemovalEdits(value);
|
|
186
219
|
if (fragment === "skills-state-permissions") {
|
|
187
|
-
const statePaths = target === "opencode"
|
|
220
|
+
const statePaths = target === "opencode"
|
|
221
|
+
? [XDG_STATE_GLOB, OPENCODE_SKILLS_GLOB, AGENTS_SKILLS_GLOB]
|
|
222
|
+
: [XDG_STATE_GLOB];
|
|
223
|
+
if (target === "opencode")
|
|
224
|
+
return permissionEdits(value, [
|
|
225
|
+
...statePaths.flatMap((resource) => [
|
|
226
|
+
{ action: "read", resource, effect: "allow" },
|
|
227
|
+
{ action: "external_directory", resource, effect: "allow" },
|
|
228
|
+
]),
|
|
229
|
+
{ action: "edit", resource: XDG_STATE_GLOB, effect: "allow" },
|
|
230
|
+
// Directory enumeration (glob/list) targets the roots themselves, which
|
|
231
|
+
// the tree globs above do not match; allow those exact directories.
|
|
232
|
+
...statePaths.map((resource) => ({
|
|
233
|
+
action: "external_directory",
|
|
234
|
+
resource: resource.replace(/\/\*\*$/, ""),
|
|
235
|
+
effect: "allow",
|
|
236
|
+
})),
|
|
237
|
+
{ action: "external_directory", resource: "~/.agents", effect: "allow" },
|
|
238
|
+
// Read-only access to reviewmatic-managed review worktrees, including
|
|
239
|
+
// the directory boundaries external access is checked against. Read is
|
|
240
|
+
// scoped to the managed reviewmatic catalog, and pre-existing user or
|
|
241
|
+
// secret denies keep priority: the insertion rule in permissionEdits
|
|
242
|
+
// places each new allow before any overlapping recorded deny. No shell
|
|
243
|
+
// or edit permission is added, and sibling catalogs of other tools
|
|
244
|
+
// under `<repo>.worktrees` stay unapproved.
|
|
245
|
+
{ action: "read", resource: WORKTREE_TREE_GLOB, effect: "allow" },
|
|
246
|
+
{ action: "external_directory", resource: WORKTREE_BOUNDARY_GLOB, effect: "allow" },
|
|
247
|
+
{ action: "external_directory", resource: WORKTREE_ROOT, effect: "allow" },
|
|
248
|
+
{ action: "external_directory", resource: WORKTREE_BASE, effect: "allow" },
|
|
249
|
+
]);
|
|
188
250
|
const edits = [];
|
|
189
251
|
if (!mapAllowsAll(permission.read))
|
|
190
252
|
edits.push(...permissionMapEdits(["permission", "read"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
|
|
@@ -192,16 +254,22 @@ function fragmentEdits(fragment, target, current) {
|
|
|
192
254
|
edits.push(...permissionMapEdits(["permission", "edit"], { [XDG_STATE_GLOB]: "allow" }));
|
|
193
255
|
if (!mapAllowsAll(permission.external_directory))
|
|
194
256
|
edits.push(...permissionMapEdits(["permission", "external_directory"], Object.fromEntries(statePaths.map((glob) => [glob, "allow"]))));
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
kind: "set-if-absent",
|
|
200
|
-
path: ["lsp", server.name],
|
|
201
|
-
value: { command: LSP_SERVER_COMMANDS[server.name], extensions: server.extensions },
|
|
257
|
+
edits.push(...permissionMapEdits(["permission", "read"], { [WORKTREE_TREE_GLOB]: "allow" }), ...permissionMapEdits(["permission", "external_directory"], {
|
|
258
|
+
[WORKTREE_BOUNDARY_GLOB]: "allow",
|
|
259
|
+
[WORKTREE_ROOT]: "allow",
|
|
260
|
+
[WORKTREE_BASE]: "allow",
|
|
202
261
|
}));
|
|
262
|
+
return edits;
|
|
203
263
|
}
|
|
204
264
|
if (fragment === "secrets-guard") {
|
|
265
|
+
if (target === "opencode")
|
|
266
|
+
return permissionEdits(value, [
|
|
267
|
+
...SECRET_PATHS.flatMap((glob) => [
|
|
268
|
+
{ action: "read", resource: glob.replace(/^\*\*\//, "*"), effect: "deny" },
|
|
269
|
+
{ action: "edit", resource: glob.replace(/^\*\*\//, "*"), effect: "deny" },
|
|
270
|
+
]),
|
|
271
|
+
{ action: "read", resource: "*.env.example", effect: "allow" },
|
|
272
|
+
]);
|
|
205
273
|
const deny = Object.fromEntries(SECRET_PATHS.map((glob) => [glob, "deny"]));
|
|
206
274
|
return [
|
|
207
275
|
...permissionMapEdits(["permission", "read"], {
|
|
@@ -219,8 +287,18 @@ function fragmentEdits(fragment, target, current) {
|
|
|
219
287
|
{ kind: "set-if-absent", path: ["mcp_tool_display"], value: "expanded" },
|
|
220
288
|
];
|
|
221
289
|
}
|
|
290
|
+
if (target === "opencode")
|
|
291
|
+
return [
|
|
292
|
+
{ kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/v2/cli.json" },
|
|
293
|
+
{ kind: "set-if-absent", path: ["theme", "name"], value: "ayu" },
|
|
294
|
+
...Object.entries(OPENCODE_KEYBINDS).map(([key, value]) => ({
|
|
295
|
+
kind: "set-if-absent",
|
|
296
|
+
path: ["keybinds", key],
|
|
297
|
+
value,
|
|
298
|
+
})),
|
|
299
|
+
];
|
|
222
300
|
const tuiSchemas = {
|
|
223
|
-
opencode:
|
|
301
|
+
opencode: undefined,
|
|
224
302
|
kilo: undefined,
|
|
225
303
|
mimo: "https://mimo.xiaomi.com/mimocode/tui.json",
|
|
226
304
|
};
|
|
@@ -255,6 +333,8 @@ export function normalizeConfigSelection(scope, value = {}) {
|
|
|
255
333
|
if (scope === "project" && (targets.length !== 1 || targets[0] !== "opencode"))
|
|
256
334
|
throw new ConfigSetupError("invalid_selection", "Project scope supports only the opencode target");
|
|
257
335
|
const requestedFragments = value.fragments ?? [];
|
|
336
|
+
if (requestedFragments.includes("core-plugin") && requestedFragments.includes("core-disable"))
|
|
337
|
+
throw new ConfigSetupError("invalid_selection", "Choose either core-plugin or core-disable");
|
|
258
338
|
const fragments = FRAGMENT_NAMES.filter((fragment) => requestedFragments.includes(fragment));
|
|
259
339
|
if (fragments.length !== requestedFragments.length)
|
|
260
340
|
throw new ConfigSetupError("invalid_selection", "Unknown config fragment selection");
|
|
@@ -275,9 +355,26 @@ export async function defaultConfigSelection(scope, cwd = process.cwd(), home =
|
|
|
275
355
|
}
|
|
276
356
|
return normalizeConfigSelection(scope, {
|
|
277
357
|
targets: [...new Set(targets)],
|
|
278
|
-
fragments:
|
|
358
|
+
fragments: FRAGMENT_NAMES.filter((name) => name !== "core-disable"),
|
|
279
359
|
});
|
|
280
360
|
}
|
|
361
|
+
export async function inspectIntegration(scope, cwd = process.cwd(), home = homedir()) {
|
|
362
|
+
const file = await resolveTargetFile("opencode", scope, cwd, home);
|
|
363
|
+
const raw = await readRegular(file.absolute);
|
|
364
|
+
if (!raw)
|
|
365
|
+
return { path: file.absolute, connected: false };
|
|
366
|
+
try {
|
|
367
|
+
const current = parseJsonc(raw.toString("utf8"));
|
|
368
|
+
return { path: file.absolute, connected: corePluginRemovalEdits(current).length > 0 };
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return {
|
|
372
|
+
path: file.absolute,
|
|
373
|
+
connected: false,
|
|
374
|
+
problem: error instanceof Error ? error.message : "Invalid configuration",
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
281
378
|
async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
|
|
282
379
|
const existing = async (root, path) => Boolean(await readRegular(join(root, path)).catch(() => undefined));
|
|
283
380
|
const globalRoot = deploymentRoot("global", cwd, home);
|
|
@@ -308,9 +405,9 @@ async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
|
|
|
308
405
|
return {
|
|
309
406
|
target,
|
|
310
407
|
root: globalRoot,
|
|
311
|
-
path: "
|
|
312
|
-
absolute: join(globalRoot, "
|
|
313
|
-
exists: await existing(globalRoot, "
|
|
408
|
+
path: "cli.json",
|
|
409
|
+
absolute: join(globalRoot, "cli.json"),
|
|
410
|
+
exists: await existing(globalRoot, "cli.json"),
|
|
314
411
|
};
|
|
315
412
|
}
|
|
316
413
|
if (target === "opencode") {
|
|
@@ -362,7 +459,7 @@ async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
|
|
|
362
459
|
exists: false,
|
|
363
460
|
};
|
|
364
461
|
}
|
|
365
|
-
async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
462
|
+
async function build(selection, scope, cwd = process.cwd(), home = homedir(), syncSelection = true) {
|
|
366
463
|
const root = deploymentRoot(scope, cwd, home);
|
|
367
464
|
const operations = [];
|
|
368
465
|
const mutations = [];
|
|
@@ -404,6 +501,20 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
404
501
|
for (const fragment of selection.fragments) {
|
|
405
502
|
if (!applicable(fragment, target) || fragmentFileKind(fragment) !== kind)
|
|
406
503
|
continue;
|
|
504
|
+
if (target === "opencode" &&
|
|
505
|
+
kind === "tui" &&
|
|
506
|
+
!file.exists &&
|
|
507
|
+
((await readRegular(join(file.root, "tui.json"))) ||
|
|
508
|
+
(await readRegular(join(file.root, "tui.jsonc"))))) {
|
|
509
|
+
operations.push({
|
|
510
|
+
target,
|
|
511
|
+
path: file.absolute,
|
|
512
|
+
fragment,
|
|
513
|
+
operation: "conflict",
|
|
514
|
+
reason: "Start OpenCode V2 once to migrate existing TUI preferences into cli.json before applying terminal presets",
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
407
518
|
let parsed = undefined;
|
|
408
519
|
try {
|
|
409
520
|
parsed = parseJsonc(text);
|
|
@@ -464,6 +575,23 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
464
575
|
? "TUI files are global-only"
|
|
465
576
|
: "no selected target supports this fragment",
|
|
466
577
|
}));
|
|
578
|
+
const core = selection.fragments.find((fragment) => fragment === "core-plugin" || fragment === "core-disable");
|
|
579
|
+
if (syncSelection &&
|
|
580
|
+
core &&
|
|
581
|
+
selection.targets.includes("opencode") &&
|
|
582
|
+
!operations.some((operation) => operation.fragment === core && operation.operation === "conflict")) {
|
|
583
|
+
const mutation = await coreSelectionMutation(root, core === "core-plugin");
|
|
584
|
+
if (mutation) {
|
|
585
|
+
mutations.push(mutation);
|
|
586
|
+
operations.push({
|
|
587
|
+
target: "opencode",
|
|
588
|
+
path: join(root, mutation.path),
|
|
589
|
+
fragment: core,
|
|
590
|
+
operation: "update",
|
|
591
|
+
reason: "Save connection choice for repeat install and repair",
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
467
595
|
const sorted = operations.sort((left, right) => CONFIG_TARGETS.indexOf(left.target) - CONFIG_TARGETS.indexOf(right.target) ||
|
|
468
596
|
FRAGMENT_NAMES.indexOf(left.fragment) -
|
|
469
597
|
FRAGMENT_NAMES.indexOf(right.fragment) ||
|
|
@@ -491,11 +619,11 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
|
|
|
491
619
|
allowedRoots: [...allowedRoots],
|
|
492
620
|
};
|
|
493
621
|
}
|
|
494
|
-
export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true) {
|
|
622
|
+
export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true, syncSelection = true) {
|
|
495
623
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
496
624
|
try {
|
|
497
625
|
await requireNoRecovery(stateRoot);
|
|
498
|
-
const built = await build(selection, scope, cwd, home);
|
|
626
|
+
const built = await build(selection, scope, cwd, home, syncSelection);
|
|
499
627
|
if (provisionDependency &&
|
|
500
628
|
selection.fragments.includes("core-plugin") &&
|
|
501
629
|
selection.targets.includes("opencode"))
|
|
@@ -507,6 +635,7 @@ export async function previewConfigSetup(selection, scope, cwd = process.cwd(),
|
|
|
507
635
|
resolve(cwd),
|
|
508
636
|
resolve(home),
|
|
509
637
|
provisionDependency,
|
|
638
|
+
syncSelection,
|
|
510
639
|
]);
|
|
511
640
|
for (const [id, prior] of configReceipts)
|
|
512
641
|
if (prior.key === key || prior.expires < Date.now())
|
|
@@ -532,7 +661,8 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
|
|
|
532
661
|
try {
|
|
533
662
|
return await withLifecycleLock(stateRoot, async () => {
|
|
534
663
|
await requireNoRecovery(stateRoot);
|
|
535
|
-
const
|
|
664
|
+
const syncSelection = options.syncSelection !== false;
|
|
665
|
+
const built = await build(selection, scope, cwd, home, syncSelection);
|
|
536
666
|
const provisionDependency = options.provisionDependency !== false;
|
|
537
667
|
if (provisionDependency &&
|
|
538
668
|
selection.fragments.includes("core-plugin") &&
|
|
@@ -544,6 +674,7 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
|
|
|
544
674
|
resolve(cwd),
|
|
545
675
|
resolve(home),
|
|
546
676
|
provisionDependency,
|
|
677
|
+
syncSelection,
|
|
547
678
|
]);
|
|
548
679
|
const id = options.receipt ?? [...configReceipts].find(([, entry]) => entry.key === key)?.[0];
|
|
549
680
|
const receipt = id ? configReceipts.get(id) : undefined;
|
|
@@ -574,6 +705,8 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
|
|
|
574
705
|
await provision();
|
|
575
706
|
return built.plan;
|
|
576
707
|
}
|
|
708
|
+
if (!built.plan.operations.some((operation) => operation.operation === "conflict"))
|
|
709
|
+
return built.plan;
|
|
577
710
|
throw new ConfigSetupError("invalid_state", "Config setup plan has no applicable changes");
|
|
578
711
|
}
|
|
579
712
|
const backups = [];
|
package/dist/contracts.js
CHANGED
|
@@ -9,6 +9,32 @@ function strings(value) {
|
|
|
9
9
|
export function validateAgentReport(agent, report, card) {
|
|
10
10
|
if (!object(report))
|
|
11
11
|
throw new Error("agent report must be an object");
|
|
12
|
+
if ((agent === "review" || agent === "critic") &&
|
|
13
|
+
!card &&
|
|
14
|
+
report.schema === "portable-gitlab/critic-receipt/v2") {
|
|
15
|
+
const required = [
|
|
16
|
+
"schema",
|
|
17
|
+
"evidence_digest",
|
|
18
|
+
"run_id",
|
|
19
|
+
"session_id",
|
|
20
|
+
"findings",
|
|
21
|
+
"external_mutations",
|
|
22
|
+
];
|
|
23
|
+
const allowed = [...required, "scope_digest", "target_finding_ids"];
|
|
24
|
+
if (!required.every((key) => key in report) ||
|
|
25
|
+
Object.keys(report).some((key) => !allowed.includes(key)) ||
|
|
26
|
+
!/^[0-9a-f]{64}$/.test(String(report.evidence_digest)) ||
|
|
27
|
+
typeof report.run_id !== "string" ||
|
|
28
|
+
!report.run_id ||
|
|
29
|
+
typeof report.session_id !== "string" ||
|
|
30
|
+
!report.session_id ||
|
|
31
|
+
!Array.isArray(report.findings) ||
|
|
32
|
+
report.external_mutations !== false ||
|
|
33
|
+
("scope_digest" in report && !/^[0-9a-f]{64}$/.test(String(report.scope_digest))) ||
|
|
34
|
+
("target_finding_ids" in report && !strings(report.target_finding_ids)))
|
|
35
|
+
throw new Error("code-review critic receipt has an invalid transport envelope");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
12
38
|
if (agent === "critic" && !card && "review_report" in report) {
|
|
13
39
|
validateAgentReport("review", report);
|
|
14
40
|
return;
|