@aron98/load-lock 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +115 -2
- package/demo/demo.sh +112 -0
- package/demo/sim-agent.mjs +149 -0
- package/dist/adapters/opencode/plugin.d.ts +18 -0
- package/dist/adapters/opencode/plugin.js +301 -0
- package/dist/cli/hook.d.ts +1 -0
- package/dist/cli/hook.js +141 -0
- package/dist/cli/install.d.ts +14 -0
- package/dist/cli/install.js +377 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +461 -0
- package/dist/core/daemon-main.d.ts +2 -0
- package/dist/core/daemon-main.js +27 -0
- package/dist/core/daemon.d.ts +79 -0
- package/dist/core/daemon.js +526 -0
- package/dist/core/persist.d.ts +11 -0
- package/dist/core/persist.js +66 -0
- package/dist/core/state.d.ts +203 -0
- package/dist/core/state.js +909 -0
- package/dist/core/sysinfo.d.ts +2 -0
- package/dist/core/sysinfo.js +123 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +4 -0
- package/dist/mcp/server.d.ts +27 -0
- package/dist/mcp/server.js +352 -0
- package/dist/policy/text.d.ts +30 -0
- package/dist/policy/text.js +58 -0
- package/dist/shared/client.d.ts +30 -0
- package/dist/shared/client.js +227 -0
- package/dist/shared/protocol.d.ts +333 -0
- package/dist/shared/protocol.js +22 -0
- package/dist/test/core.test.d.ts +1 -0
- package/dist/test/core.test.js +281 -0
- package/dist/test/daemon.test.d.ts +1 -0
- package/dist/test/daemon.test.js +384 -0
- package/dist/test/hook.test.d.ts +1 -0
- package/dist/test/hook.test.js +126 -0
- package/dist/test/mcp.test.d.ts +1 -0
- package/dist/test/mcp.test.js +139 -0
- package/dist/test/run-helper.test.d.ts +1 -0
- package/dist/test/run-helper.test.js +84 -0
- package/dist/test/testutil.d.ts +27 -0
- package/dist/test/testutil.js +111 -0
- package/docs/ADAPTERS.md +118 -0
- package/docs/AGENT_GUIDE.md +142 -0
- package/docs/ARCHITECTURE.md +195 -0
- package/docs/COMPATIBILITY.md +116 -0
- package/docs/OPERATIONS.md +126 -0
- package/package.json +102 -4
- package/skills/load-lock/SKILL.md +73 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `load-lock install --harness <id>` — wires load-lock into a harness.
|
|
3
|
+
*
|
|
4
|
+
* Everything here MERGES into existing files; unrelated user config is never
|
|
5
|
+
* touched. Our own entries are tagged (LL marker / absolute paths into this
|
|
6
|
+
* package) so uninstall removes exactly what install added.
|
|
7
|
+
*
|
|
8
|
+
* Harnesses:
|
|
9
|
+
* - opencode : plugin dir ~/.config/opencode/plugins/load-lock/ (stub that
|
|
10
|
+
* re-exports the compiled plugin from this package, with a
|
|
11
|
+
* local package.json so `@opencode/plugin` + `load-lock`
|
|
12
|
+
* resolve), plus a `plugins` entry in opencode.json.
|
|
13
|
+
* - claude-code : user-scope MCP server in ~/.claude.json, SessionStart /
|
|
14
|
+
* Stop hooks + background await-offer watcher in
|
|
15
|
+
* ~/.claude/settings.json, CLAUDE.md section, skill.
|
|
16
|
+
* - codex : ~/.codex/hooks.json (SessionStart/SubagentStart/Stop),
|
|
17
|
+
* [mcp_servers.load-lock] in ~/.codex/config.toml,
|
|
18
|
+
* ~/.codex/AGENTS.md pointer. Trust must be granted via /hooks.
|
|
19
|
+
* - generic : prints an MCP stdio config snippet (any MCP-capable harness).
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { POLICY_BRIEF } from "../policy/text.js";
|
|
26
|
+
const HOME = os.homedir();
|
|
27
|
+
export function pkgRoot(binDir) {
|
|
28
|
+
const d = binDir ?? path.join(import.meta.dirname ?? ".", ".."); // dist/cli -> dist
|
|
29
|
+
return path.resolve(d, ".."); // dist -> package root
|
|
30
|
+
}
|
|
31
|
+
function pkgPaths(binDir) {
|
|
32
|
+
const root = pkgRoot(binDir);
|
|
33
|
+
return {
|
|
34
|
+
root,
|
|
35
|
+
cli: path.join(root, "dist", "cli", "main.js"),
|
|
36
|
+
mcp: path.join(root, "dist", "mcp", "main.js"),
|
|
37
|
+
docs: path.join(root, "docs"),
|
|
38
|
+
skill: path.join(root, "skills", "load-lock", "SKILL.md"),
|
|
39
|
+
node: process.execPath,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function say(opts, msg) {
|
|
43
|
+
if (!opts.json)
|
|
44
|
+
process.stdout.write(msg + "\n");
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------- io helpers
|
|
47
|
+
function readJson(file) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeJson(file, data) {
|
|
56
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
const tmp = file + ".tmp-ll-" + process.pid;
|
|
58
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
59
|
+
fs.renameSync(tmp, file);
|
|
60
|
+
}
|
|
61
|
+
/** Append a marked section to a markdown file once. */
|
|
62
|
+
function mdSection(file, marker, section) {
|
|
63
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
64
|
+
if (existing.includes(marker))
|
|
65
|
+
return false;
|
|
66
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
67
|
+
fs.writeFileSync(file, existing + (existing.endsWith("\n") ? "" : "\n") + section);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
function removeMdSection(file, marker) {
|
|
71
|
+
if (!fs.existsSync(file))
|
|
72
|
+
return;
|
|
73
|
+
const text = fs.readFileSync(file, "utf8");
|
|
74
|
+
const idx = text.indexOf(marker);
|
|
75
|
+
if (idx === -1)
|
|
76
|
+
return;
|
|
77
|
+
const next = text.indexOf(marker, idx + marker.length);
|
|
78
|
+
const end = next === -1 ? text.length : next;
|
|
79
|
+
fs.writeFileSync(file, text.slice(0, idx) + text.slice(end));
|
|
80
|
+
}
|
|
81
|
+
const LL_HOOK_TAG = "dist/cli/main.js hook"; // how we recognize our hook entries
|
|
82
|
+
// ------------------------------------------------------------------ opencode
|
|
83
|
+
function installOpenCode(opts, P) {
|
|
84
|
+
const actions = [];
|
|
85
|
+
const pluginDir = path.join(HOME, ".config", "opencode", "plugins", "load-lock");
|
|
86
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
87
|
+
// Stub that re-exports the compiled plugin from this package.
|
|
88
|
+
fs.writeFileSync(path.join(pluginDir, "index.js"), `// installed by load-lock; re-exports the compiled plugin\n` +
|
|
89
|
+
`export { default } from "@aron98/load-lock/dist/adapters/opencode/plugin.js";\n`);
|
|
90
|
+
actions.push(`plugin stub: ${pluginDir}/index.js`);
|
|
91
|
+
fs.writeFileSync(path.join(pluginDir, "package.json"), JSON.stringify({
|
|
92
|
+
name: "load-lock-opencode-plugin",
|
|
93
|
+
version: "0.1.0",
|
|
94
|
+
type: "module",
|
|
95
|
+
main: "index.js",
|
|
96
|
+
dependencies: {
|
|
97
|
+
"@opencode/plugin": "^2.0.29",
|
|
98
|
+
"@aron98/load-lock": "file:" + P.root,
|
|
99
|
+
},
|
|
100
|
+
}, null, 2) + "\n");
|
|
101
|
+
actions.push("plugin package.json (file: dep on this package)");
|
|
102
|
+
const npmInstall = spawnSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: pluginDir, encoding: "utf8" });
|
|
103
|
+
if (npmInstall.status !== 0) {
|
|
104
|
+
actions.push("WARNING: npm install inside plugin dir failed — see errors above");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
actions.push("plugin dependencies installed");
|
|
108
|
+
}
|
|
109
|
+
// Register in ~/.config/opencode/opencode.json (merge, preserve).
|
|
110
|
+
const cfgFile = path.join(HOME, ".config", "opencode", "opencode.json");
|
|
111
|
+
const cfg = readJson(cfgFile) ?? {};
|
|
112
|
+
if (!Array.isArray(cfg.plugins))
|
|
113
|
+
cfg.plugins = [];
|
|
114
|
+
if (!cfg.plugins.includes(pluginDir))
|
|
115
|
+
cfg.plugins.push(pluginDir);
|
|
116
|
+
writeJson(cfgFile, cfg);
|
|
117
|
+
actions.push(`plugins entry in ${cfgFile}`);
|
|
118
|
+
actions.push("restart or reload your opencode service to load the plugin");
|
|
119
|
+
return actions;
|
|
120
|
+
}
|
|
121
|
+
function uninstallOpenCode() {
|
|
122
|
+
const actions = [];
|
|
123
|
+
const pluginDir = path.join(HOME, ".config", "opencode", "plugins", "load-lock");
|
|
124
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
125
|
+
actions.push(`removed ${pluginDir}`);
|
|
126
|
+
const cfgFile = path.join(HOME, ".config", "opencode", "opencode.json");
|
|
127
|
+
const cfg = readJson(cfgFile);
|
|
128
|
+
if (cfg && Array.isArray(cfg.plugins)) {
|
|
129
|
+
cfg.plugins = cfg.plugins.filter((p) => p !== pluginDir);
|
|
130
|
+
writeJson(cfgFile, cfg);
|
|
131
|
+
actions.push(`removed plugins entry from ${cfgFile}`);
|
|
132
|
+
}
|
|
133
|
+
return actions;
|
|
134
|
+
}
|
|
135
|
+
// -------------------------------------------------------------- claude-code
|
|
136
|
+
function hookEntry(P, event, extra) {
|
|
137
|
+
const cmd = `${P.node} ${JSON.stringify(P.cli)} hook claude-code`;
|
|
138
|
+
return {
|
|
139
|
+
hooks: [
|
|
140
|
+
{
|
|
141
|
+
type: "command",
|
|
142
|
+
command: event === "session-start"
|
|
143
|
+
? `${cmd} session-start`
|
|
144
|
+
: event === "stop"
|
|
145
|
+
? `${cmd} stop`
|
|
146
|
+
: `${cmd} await-offer`,
|
|
147
|
+
...extra,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function installClaudeCode(opts, P) {
|
|
153
|
+
const actions = [];
|
|
154
|
+
// 1. user-scope MCP server in ~/.claude.json
|
|
155
|
+
const claudeJson = path.join(HOME, ".claude.json");
|
|
156
|
+
const cj = readJson(claudeJson) ?? {};
|
|
157
|
+
if (typeof cj !== "object" || cj === null)
|
|
158
|
+
throw new Error(`${claudeJson} is not a JSON object; refusing to overwrite`);
|
|
159
|
+
if (!cj.mcpServers || typeof cj.mcpServers !== "object")
|
|
160
|
+
cj.mcpServers = {};
|
|
161
|
+
cj.mcpServers["load-lock"] = { type: "stdio", command: P.node, args: [P.mcp] };
|
|
162
|
+
writeJson(claudeJson, cj);
|
|
163
|
+
actions.push(`MCP server "load-lock" in ${claudeJson}`);
|
|
164
|
+
// 2. hooks in ~/.claude/settings.json (merge; replace only our own entries)
|
|
165
|
+
const settingsFile = path.join(HOME, ".claude", "settings.json");
|
|
166
|
+
const s = readJson(settingsFile) ?? {};
|
|
167
|
+
if (!s.hooks || typeof s.hooks !== "object")
|
|
168
|
+
s.hooks = {};
|
|
169
|
+
const ours = (list) => list.filter((e) => JSON.stringify(e).includes(LL_HOOK_TAG));
|
|
170
|
+
const cc = (event, entries) => {
|
|
171
|
+
const prev = Array.isArray(s.hooks[event]) ? s.hooks[event] : [];
|
|
172
|
+
const kept = prev.filter((e) => !ours([e]).length);
|
|
173
|
+
s.hooks[event] = [...kept, ...entries];
|
|
174
|
+
};
|
|
175
|
+
cc("SessionStart", [hookEntry(P, "session-start")]);
|
|
176
|
+
cc("Stop", [hookEntry(P, "stop")]);
|
|
177
|
+
// background watcher: async + asyncRewake wakes Claude (exit 2 + stderr)
|
|
178
|
+
s.hooks.SessionStart.push(hookEntry(P, "await-offer", { async: true, asyncRewake: true }));
|
|
179
|
+
writeJson(settingsFile, s);
|
|
180
|
+
actions.push(`hooks (SessionStart policy, Stop offer wakeup, await-offer watcher) in ${settingsFile}`);
|
|
181
|
+
// 3. CLAUDE.md pointer
|
|
182
|
+
const md = path.join(HOME, ".claude", "CLAUDE.md");
|
|
183
|
+
const marker = "<!-- load-lock:begin -->";
|
|
184
|
+
if (mdSection(md, marker, `\n${marker}\n## load-lock: machine-wide heavy-work budget\n\n` +
|
|
185
|
+
`Before resource-intensive local work (full builds, big test suites), coordinate with other agents on this machine:\n\n` +
|
|
186
|
+
`1. Call the MCP tool \`mcp__load-lock__acquire\` (reason + op_key) — it grants a permit or queues you.\n` +
|
|
187
|
+
`2. While queued, keep doing light work. When an offer arrives (hook wakeup or \`mcp__load-lock__pending\`), claim it via \`mcp__load-lock__claim\` within the window (default 30s).\n` +
|
|
188
|
+
`3. Only start heavy work after acquire/claim succeeds. Use \`mcp__load-lock__run_with_permit\` for full lifecycle ownership (auto-release), or \`attach_pid\` for processes you already started.\n` +
|
|
189
|
+
`4. Release (or cancel queued work) promptly. Inspect the machine with \`mcp__load-lock__inspect_machine\`; adjust capacity via \`mcp__load-lock__configure\` when warranted.\n\n` +
|
|
190
|
+
`Full guide: \`load-lock agent-guide\`.\n<!-- load-lock:end -->\n`))
|
|
191
|
+
actions.push(`CLAUDE.md section in ${md}`);
|
|
192
|
+
// 4. skill
|
|
193
|
+
const skillDir = path.join(HOME, ".claude", "skills", "load-lock");
|
|
194
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
195
|
+
fs.copyFileSync(P.skill, path.join(skillDir, "SKILL.md"));
|
|
196
|
+
actions.push(`skill in ${skillDir}/SKILL.md`);
|
|
197
|
+
return actions;
|
|
198
|
+
}
|
|
199
|
+
function uninstallClaudeCode() {
|
|
200
|
+
const actions = [];
|
|
201
|
+
const claudeJson = path.join(HOME, ".claude.json");
|
|
202
|
+
const cj = readJson(claudeJson);
|
|
203
|
+
if (cj?.mcpServers && typeof cj.mcpServers === "object" && "load-lock" in cj.mcpServers) {
|
|
204
|
+
delete cj.mcpServers["load-lock"];
|
|
205
|
+
writeJson(claudeJson, cj);
|
|
206
|
+
actions.push(`removed MCP server from ${claudeJson}`);
|
|
207
|
+
}
|
|
208
|
+
const settingsFile = path.join(HOME, ".claude", "settings.json");
|
|
209
|
+
const s = readJson(settingsFile);
|
|
210
|
+
if (s?.hooks && typeof s.hooks === "object") {
|
|
211
|
+
for (const ev of Object.keys(s.hooks)) {
|
|
212
|
+
const list = s.hooks[ev];
|
|
213
|
+
if (Array.isArray(list)) {
|
|
214
|
+
const kept = list.filter((e) => !JSON.stringify(e).includes(LL_HOOK_TAG));
|
|
215
|
+
if (kept.length)
|
|
216
|
+
s.hooks[ev] = kept;
|
|
217
|
+
else
|
|
218
|
+
delete s.hooks[ev];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
writeJson(settingsFile, s);
|
|
222
|
+
actions.push(`removed hooks from ${settingsFile}`);
|
|
223
|
+
}
|
|
224
|
+
const md = path.join(HOME, ".claude", "CLAUDE.md");
|
|
225
|
+
removeMdSection(md, "<!-- load-lock:begin -->");
|
|
226
|
+
actions.push(`removed CLAUDE.md section`);
|
|
227
|
+
fs.rmSync(path.join(HOME, ".claude", "skills", "load-lock"), { recursive: true, force: true });
|
|
228
|
+
actions.push("removed skill");
|
|
229
|
+
return actions;
|
|
230
|
+
}
|
|
231
|
+
// --------------------------------------------------------------------- codex
|
|
232
|
+
function installCodex(opts, P) {
|
|
233
|
+
const actions = [];
|
|
234
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(HOME, ".codex");
|
|
235
|
+
// 1. hooks.json (merge by replacing only our entries)
|
|
236
|
+
const hooksFile = path.join(codexHome, "hooks.json");
|
|
237
|
+
const h = readJson(hooksFile) ?? {};
|
|
238
|
+
if (typeof h !== "object" || h === null)
|
|
239
|
+
throw new Error(`${hooksFile} is not a JSON object; refusing to overwrite`);
|
|
240
|
+
if (!h.hooks || typeof h.hooks !== "object")
|
|
241
|
+
h.hooks = {};
|
|
242
|
+
const mk = (event, sub) => [
|
|
243
|
+
{
|
|
244
|
+
hooks: [
|
|
245
|
+
{
|
|
246
|
+
type: "command",
|
|
247
|
+
command: `${P.node} ${JSON.stringify(P.cli)} hook codex ${sub}`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
const mergeEv = (event, entries) => {
|
|
253
|
+
const prev = Array.isArray(h.hooks[event]) ? h.hooks[event] : [];
|
|
254
|
+
h.hooks[event] = [...prev.filter((e) => !JSON.stringify(e).includes(LL_HOOK_TAG)), ...entries];
|
|
255
|
+
};
|
|
256
|
+
mergeEv("SessionStart", [{ matcher: "startup|resume|clear|compact|fork", hooks: [{ type: "command", command: `${P.node} ${JSON.stringify(P.cli)} hook codex session-start` }] }]);
|
|
257
|
+
mergeEv("SubagentStart", [{ matcher: "*", hooks: [{ type: "command", command: `${P.node} ${JSON.stringify(P.cli)} hook codex subagent-start` }] }]);
|
|
258
|
+
mergeEv("Stop", mk("Stop", "stop"));
|
|
259
|
+
writeJson(hooksFile, h);
|
|
260
|
+
actions.push(`hooks in ${hooksFile} (SessionStart/SubagentStart/Stop)`);
|
|
261
|
+
// 2. MCP server in config.toml (append block only if absent)
|
|
262
|
+
const tomlFile = path.join(codexHome, "config.toml");
|
|
263
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
264
|
+
const toml = fs.existsSync(tomlFile) ? fs.readFileSync(tomlFile, "utf8") : "";
|
|
265
|
+
if (!toml.includes("[mcp_servers.load-lock]")) {
|
|
266
|
+
fs.writeFileSync(tomlFile, toml +
|
|
267
|
+
(toml.endsWith("\n") ? "" : "\n") +
|
|
268
|
+
`\n# load-lock (added by \`load-lock install\`)\n[mcp_servers.load-lock]\n` +
|
|
269
|
+
`command = ${JSON.stringify(P.node)}\n` +
|
|
270
|
+
`args = [${JSON.stringify(P.mcp)}]\n`);
|
|
271
|
+
actions.push(`[mcp_servers.load-lock] appended to ${tomlFile}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
actions.push(`[mcp_servers.load-lock] already present in ${tomlFile}`);
|
|
275
|
+
}
|
|
276
|
+
// 3. AGENTS.md pointer
|
|
277
|
+
const agentsMd = path.join(codexHome, "AGENTS.md");
|
|
278
|
+
if (mdSection(agentsMd, "<!-- load-lock:begin -->", `\n<!-- load-lock:begin -->\n## load-lock: machine-wide heavy-work budget\n\n` +
|
|
279
|
+
`Before resource-intensive local work (full builds, big test suites), coordinate with other agents on this machine via the \`load-lock\` MCP tools (mcp__load-lock__acquire / claim / release / run_with_permit / pending / status / inspect_machine / configure). ` +
|
|
280
|
+
`Acquire before heavy work, keep doing light work while queued, claim offers promptly (they expire), release when done. Full guide: \`load-lock agent-guide\`.\n<!-- load-lock:end -->\n`))
|
|
281
|
+
actions.push(`AGENTS.md section in ${agentsMd}`);
|
|
282
|
+
actions.push("IMPORTANT: run `/hooks` inside Codex and grant trust to the new hooks (they are inert until trusted)");
|
|
283
|
+
return actions;
|
|
284
|
+
}
|
|
285
|
+
function uninstallCodex() {
|
|
286
|
+
const actions = [];
|
|
287
|
+
const codexHome = process.env.CODEX_HOME ?? path.join(HOME, ".codex");
|
|
288
|
+
const hooksFile = path.join(codexHome, "hooks.json");
|
|
289
|
+
const h = readJson(hooksFile);
|
|
290
|
+
if (h?.hooks && typeof h.hooks === "object") {
|
|
291
|
+
for (const ev of Object.keys(h.hooks)) {
|
|
292
|
+
const list = h.hooks[ev];
|
|
293
|
+
if (Array.isArray(list)) {
|
|
294
|
+
const kept = list.filter((e) => !JSON.stringify(e).includes(LL_HOOK_TAG));
|
|
295
|
+
if (kept.length)
|
|
296
|
+
h.hooks[ev] = kept;
|
|
297
|
+
else
|
|
298
|
+
delete h.hooks[ev];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
writeJson(hooksFile, h);
|
|
302
|
+
actions.push(`removed hooks from ${hooksFile}`);
|
|
303
|
+
}
|
|
304
|
+
const tomlFile = path.join(codexHome, "config.toml");
|
|
305
|
+
if (fs.existsSync(tomlFile)) {
|
|
306
|
+
const toml = fs.readFileSync(tomlFile, "utf8");
|
|
307
|
+
const re = /\n?# load-lock \(added by `load-lock install`\)\n\[mcp_servers\.load-lock\]\n[^\n]*\n[^\n]*\n/;
|
|
308
|
+
if (re.test(toml)) {
|
|
309
|
+
fs.writeFileSync(tomlFile, toml.replace(re, ""));
|
|
310
|
+
actions.push(`removed [mcp_servers.load-lock] from ${tomlFile}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const agentsMd = path.join(codexHome, "AGENTS.md");
|
|
314
|
+
removeMdSection(agentsMd, "<!-- load-lock:begin -->");
|
|
315
|
+
actions.push("removed AGENTS.md section");
|
|
316
|
+
return actions;
|
|
317
|
+
}
|
|
318
|
+
// -------------------------------------------------------------------- public
|
|
319
|
+
export async function installCommand(opts) {
|
|
320
|
+
const P = pkgPaths(opts.binDir);
|
|
321
|
+
if (!fs.existsSync(P.mcp)) {
|
|
322
|
+
throw new Error(`compiled entry not found at ${P.mcp} — run \`npm run build\` inside the load-lock package first`);
|
|
323
|
+
}
|
|
324
|
+
const harness = opts.harness;
|
|
325
|
+
if (!harness)
|
|
326
|
+
throw new Error("install: --harness <opencode|claude-code|codex|generic> is required");
|
|
327
|
+
let actions;
|
|
328
|
+
switch (harness) {
|
|
329
|
+
case "opencode":
|
|
330
|
+
actions = installOpenCode(opts, P);
|
|
331
|
+
break;
|
|
332
|
+
case "claude-code":
|
|
333
|
+
case "claude":
|
|
334
|
+
actions = installClaudeCode(opts, P);
|
|
335
|
+
break;
|
|
336
|
+
case "codex":
|
|
337
|
+
actions = installCodex(opts, P);
|
|
338
|
+
break;
|
|
339
|
+
case "generic":
|
|
340
|
+
case "all":
|
|
341
|
+
default:
|
|
342
|
+
// generic: print an MCP stdio snippet for any MCP-capable harness
|
|
343
|
+
process.stdout.write(`\nAdd this MCP stdio server to your harness's MCP config:\n\n` +
|
|
344
|
+
` name: load-lock\n command: ${P.node}\n args: [${JSON.stringify(P.mcp)}]\n\n` +
|
|
345
|
+
`Tools: acquire, claim, release, cancel, rejoin, pending, status, inspect_machine,\n` +
|
|
346
|
+
`get_config, configure, run_with_permit, job_status, job_stop, attach_pid, mark_started.\n` +
|
|
347
|
+
`Then make sure your harness instructions tell agents to use it (see docs/AGENT_GUIDE.md).\n\n`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
say(opts, `\nload-lock installed for ${harness}:`);
|
|
351
|
+
for (const a of actions)
|
|
352
|
+
say(opts, " - " + a);
|
|
353
|
+
say(opts, "");
|
|
354
|
+
}
|
|
355
|
+
export async function uninstallCommand(opts) {
|
|
356
|
+
let actions;
|
|
357
|
+
switch (opts.harness) {
|
|
358
|
+
case "opencode":
|
|
359
|
+
actions = uninstallOpenCode();
|
|
360
|
+
break;
|
|
361
|
+
case "claude-code":
|
|
362
|
+
case "claude":
|
|
363
|
+
actions = uninstallClaudeCode();
|
|
364
|
+
break;
|
|
365
|
+
case "codex":
|
|
366
|
+
actions = uninstallCodex();
|
|
367
|
+
break;
|
|
368
|
+
default:
|
|
369
|
+
throw new Error(`uninstall: unknown harness "${opts.harness}"`);
|
|
370
|
+
}
|
|
371
|
+
process.stdout.write(`\nload-lock uninstalled for ${opts.harness}:\n`);
|
|
372
|
+
for (const a of actions)
|
|
373
|
+
process.stdout.write(" - " + a + "\n");
|
|
374
|
+
process.stdout.write("\n");
|
|
375
|
+
}
|
|
376
|
+
// keep POLICY_BRIEF referenced for potential future use in generic snippets
|
|
377
|
+
void POLICY_BRIEF;
|