@hivefloor/cli 0.0.0-stage → 0.1.1
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 +76 -2
- package/dist/chunk-2IMJECYD.js +71 -0
- package/dist/chunk-3IMV5EHP.js +94 -0
- package/dist/chunk-76HDATOB.js +182 -0
- package/dist/chunk-CWEEIRW2.js +38 -0
- package/dist/chunk-JCLWCMEM.js +8 -0
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/chunk-MMWX62ZU.js +30 -0
- package/dist/chunk-NCRQGIJY.js +143 -0
- package/dist/chunk-PY74LT2W.js +41 -0
- package/dist/chunk-Q2TS3GT3.js +356 -0
- package/dist/chunk-SGDM7MAT.js +237 -0
- package/dist/chunk-TKMIV7BG.js +46 -0
- package/dist/chunk-U7PMOZGI.js +182 -0
- package/dist/chunk-WHHV2NZA.js +138 -0
- package/dist/chunk-WLHRUZTR.js +71 -0
- package/dist/chunk-WNDUGQDX.js +135 -0
- package/dist/chunk-WO7NALQO.js +74 -0
- package/dist/chunk-YCZ7XJJF.js +594 -0
- package/dist/chunk-ZYRPQPGL.js +21279 -0
- package/dist/cli-FFAZRKLR.js +100 -0
- package/dist/connect-FM4BXTQQ.js +23 -0
- package/dist/disconnect-VWUOUZP6.js +140 -0
- package/dist/hook-WSJXG2XU.js +265 -0
- package/dist/index.js +22 -0
- package/dist/link-TFITMC6H.js +78 -0
- package/dist/machine-connect-I3OZCTUZ.js +139 -0
- package/dist/mcp-47WTNQNN.js +121 -0
- package/dist/run-JO4NEHWG.js +629 -0
- package/dist/status-GXZ267BD.js +113 -0
- package/dist/token-C4K23KXH.js +32 -0
- package/package.json +46 -4
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findBin
|
|
4
|
+
} from "./chunk-2IMJECYD.js";
|
|
5
|
+
import {
|
|
6
|
+
atomicWrite,
|
|
7
|
+
backupOnce,
|
|
8
|
+
dropBackupIfSame,
|
|
9
|
+
home,
|
|
10
|
+
isRecord,
|
|
11
|
+
manifestFile,
|
|
12
|
+
readJsonFile,
|
|
13
|
+
readText,
|
|
14
|
+
removeFile,
|
|
15
|
+
serializeJson
|
|
16
|
+
} from "./chunk-WNDUGQDX.js";
|
|
17
|
+
import {
|
|
18
|
+
cliNpxCommand,
|
|
19
|
+
runCmd
|
|
20
|
+
} from "./chunk-ZYRPQPGL.js";
|
|
21
|
+
|
|
22
|
+
// src/manifest.ts
|
|
23
|
+
function readManifest() {
|
|
24
|
+
const text = readText(manifestFile());
|
|
25
|
+
const base = { version: 1, created: [], tools: {}, rc: [] };
|
|
26
|
+
if (!text) return base;
|
|
27
|
+
try {
|
|
28
|
+
const j = JSON.parse(text);
|
|
29
|
+
if (isRecord(j)) {
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
created: Array.isArray(j.created) ? j.created.filter((x) => typeof x === "string") : [],
|
|
33
|
+
tools: isRecord(j.tools) ? j.tools : {},
|
|
34
|
+
rc: Array.isArray(j.rc) ? j.rc.filter((x) => typeof x === "string") : []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return base;
|
|
40
|
+
}
|
|
41
|
+
function writeManifest(m) {
|
|
42
|
+
atomicWrite(manifestFile(), JSON.stringify(m, null, 2) + "\n", 384);
|
|
43
|
+
}
|
|
44
|
+
function updateManifest(f) {
|
|
45
|
+
const m = readManifest();
|
|
46
|
+
f(m);
|
|
47
|
+
writeManifest(m);
|
|
48
|
+
}
|
|
49
|
+
var addCreated = (file) => updateManifest((m) => {
|
|
50
|
+
if (!m.created.includes(file)) m.created.push(file);
|
|
51
|
+
});
|
|
52
|
+
var wasCreated = (file) => readManifest().created.includes(file);
|
|
53
|
+
var forgetCreated = (file) => updateManifest((m) => {
|
|
54
|
+
m.created = m.created.filter((f) => f !== file);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// src/shellrc.ts
|
|
58
|
+
import fs from "fs";
|
|
59
|
+
import path from "path";
|
|
60
|
+
var RC_MARK = "# hivefloor";
|
|
61
|
+
var RC_LINE = `export HIVEFLOOR_TOKEN="$(hivefloor token)" ${RC_MARK}`;
|
|
62
|
+
function rcFileFor(env) {
|
|
63
|
+
const shell = path.basename(env.SHELL ?? "");
|
|
64
|
+
if (shell === "zsh") return path.join(home(), ".zshrc");
|
|
65
|
+
if (shell === "bash") {
|
|
66
|
+
const profile = path.join(home(), ".bash_profile");
|
|
67
|
+
return process.platform === "darwin" && fs.existsSync(profile) ? profile : path.join(home(), ".bashrc");
|
|
68
|
+
}
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
function hasRcLine(file) {
|
|
72
|
+
return (readText(file) ?? "").split("\n").some((l) => {
|
|
73
|
+
const t = l.trim();
|
|
74
|
+
return !t.startsWith("#") && t.includes("HIVEFLOOR_TOKEN") && t.includes("hivefloor token") && t.endsWith(RC_MARK);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function addRcLine(file) {
|
|
78
|
+
if (hasRcLine(file)) return false;
|
|
79
|
+
const cur = readText(file);
|
|
80
|
+
if (cur === void 0) addCreated(file);
|
|
81
|
+
const sep = cur && !cur.endsWith("\n") ? "\n" : "";
|
|
82
|
+
atomicWrite(file, `${cur ?? ""}${sep}${RC_LINE}
|
|
83
|
+
`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function removeRcLine(file) {
|
|
87
|
+
const cur = readText(file);
|
|
88
|
+
if (cur === void 0) return false;
|
|
89
|
+
const lines = cur.split("\n");
|
|
90
|
+
const kept = lines.filter((l) => l.trim() !== RC_LINE);
|
|
91
|
+
if (kept.length === lines.length) return false;
|
|
92
|
+
const next = kept.join("\n");
|
|
93
|
+
if (next.trim() === "" && wasCreated(file)) {
|
|
94
|
+
fs.rmSync(file, { force: true });
|
|
95
|
+
forgetCreated(file);
|
|
96
|
+
} else {
|
|
97
|
+
atomicWrite(file, next);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/writers/claude.ts
|
|
103
|
+
import path2 from "path";
|
|
104
|
+
|
|
105
|
+
// src/writers/common.ts
|
|
106
|
+
import fs2 from "fs";
|
|
107
|
+
var emptyReport = () => ({ lines: [], warnings: [] });
|
|
108
|
+
var CLI_COPY_RE = /hivefloor[/\\]cli[/\\]index\.js$/;
|
|
109
|
+
function isMachineHook(h) {
|
|
110
|
+
if (!isRecord(h)) return false;
|
|
111
|
+
const cmd = h.command;
|
|
112
|
+
if (typeof cmd !== "string") return false;
|
|
113
|
+
if (Array.isArray(h.args) && h.args[1] === "hook" && typeof h.args[0] === "string" && CLI_COPY_RE.test(h.args[0])) return true;
|
|
114
|
+
return /hivefloor[/\\]cli[/\\]index\.js"?\s+hook\s/.test(cmd);
|
|
115
|
+
}
|
|
116
|
+
function isAgentHook(h) {
|
|
117
|
+
if (!isRecord(h) || isMachineHook(h)) return false;
|
|
118
|
+
if (h.hivefloor === true || h.name === "hivefloor") return true;
|
|
119
|
+
const cmd = h.command;
|
|
120
|
+
if (typeof cmd !== "string") return false;
|
|
121
|
+
if (/^\s*(?:\S*[/\\])?hivefloor(?:\.cmd|\.exe)?\s+hook\s/.test(cmd)) return true;
|
|
122
|
+
return /^(?:\S*[/\\])?hivefloor(?:\.cmd|\.exe)?$/.test(cmd) && Array.isArray(h.args) && h.args[0] === "hook";
|
|
123
|
+
}
|
|
124
|
+
var ofKind = (kind) => (h) => kind === "machine" ? isMachineHook(h) : isAgentHook(h);
|
|
125
|
+
function addHooks(data, shape, specs, kind = "agent") {
|
|
126
|
+
removeHooks(data, shape, kind);
|
|
127
|
+
const hooks = isRecord(data.hooks) ? data.hooks : data.hooks = {};
|
|
128
|
+
for (const s of specs) {
|
|
129
|
+
const list = Array.isArray(hooks[s.event]) ? hooks[s.event] : hooks[s.event] = [];
|
|
130
|
+
list.push(shape === "flat" ? { ...s.entry, ...s.matcher ? { matcher: s.matcher } : {} } : { ...s.matcher ? { matcher: s.matcher } : {}, hooks: [s.entry] });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function removeHooks(data, shape, kind = "agent") {
|
|
134
|
+
const ours = ofKind(kind);
|
|
135
|
+
const hooks = data.hooks;
|
|
136
|
+
if (!isRecord(hooks)) return 0;
|
|
137
|
+
let removed = 0;
|
|
138
|
+
for (const [event, list] of Object.entries(hooks)) {
|
|
139
|
+
if (!Array.isArray(list)) continue;
|
|
140
|
+
const before = removed;
|
|
141
|
+
const kept = [];
|
|
142
|
+
for (const item of list) {
|
|
143
|
+
if (shape === "flat") {
|
|
144
|
+
if (ours(item)) removed += 1;
|
|
145
|
+
else kept.push(item);
|
|
146
|
+
} else if (isRecord(item) && Array.isArray(item.hooks)) {
|
|
147
|
+
const inner = item.hooks.filter((h) => !ours(h));
|
|
148
|
+
const gone = item.hooks.length - inner.length;
|
|
149
|
+
removed += gone;
|
|
150
|
+
if (gone === 0) kept.push(item);
|
|
151
|
+
else if (inner.length > 0) kept.push({ ...item, hooks: inner });
|
|
152
|
+
} else {
|
|
153
|
+
kept.push(item);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (removed === before) continue;
|
|
157
|
+
if (kept.length > 0) hooks[event] = kept;
|
|
158
|
+
else delete hooks[event];
|
|
159
|
+
}
|
|
160
|
+
if (removed > 0 && Object.keys(hooks).length === 0) delete data.hooks;
|
|
161
|
+
return removed;
|
|
162
|
+
}
|
|
163
|
+
function isEmptyConfig(data) {
|
|
164
|
+
return Object.keys(data).every((k) => k === "version");
|
|
165
|
+
}
|
|
166
|
+
function editJson(file, mutate, opts = {}) {
|
|
167
|
+
const f = readJsonFile(file);
|
|
168
|
+
if (!mutate(f.data)) return false;
|
|
169
|
+
if (opts.drop && wasCreated(file) && isEmptyConfig(f.data)) {
|
|
170
|
+
removeFile(file);
|
|
171
|
+
forgetCreated(file);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (!f.existed) addCreated(file);
|
|
175
|
+
else if (!wasCreated(file)) backupOnce(file);
|
|
176
|
+
atomicWrite(file, serializeJson(f.data, f.style), opts.mode);
|
|
177
|
+
if (opts.drop) dropBackupIfSame(file);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
function secureFile(file) {
|
|
181
|
+
for (const f of [file, `${file}.hivefloor.bak`]) {
|
|
182
|
+
try {
|
|
183
|
+
fs2.chmodSync(f, 384);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function dropBackupWithEntry(file, name) {
|
|
189
|
+
const bak = `${file}.hivefloor.bak`;
|
|
190
|
+
const text = readText(bak);
|
|
191
|
+
if (text === void 0) return;
|
|
192
|
+
try {
|
|
193
|
+
const data = JSON.parse(text);
|
|
194
|
+
if (isRecord(data) && isRecord(data.mcpServers) && name in data.mcpServers) removeFile(bak);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/writers/claude.ts
|
|
200
|
+
var CLAUDE_EVENTS = ["SessionStart", "SessionEnd", "UserPromptSubmit", "Stop", "Notification", "SubagentStart", "SubagentStop"];
|
|
201
|
+
var claudeJson = () => path2.join(home(), ".claude.json");
|
|
202
|
+
var claudeSettings = () => path2.join(home(), ".claude", "settings.json");
|
|
203
|
+
var MCP_NAME = "hivefloor";
|
|
204
|
+
function claudeHookSpecs() {
|
|
205
|
+
return CLAUDE_EVENTS.map((event) => ({
|
|
206
|
+
event,
|
|
207
|
+
// A permission or idle prompt marks the session "waiting on you"; other notifications are not sent.
|
|
208
|
+
...event === "Notification" ? { matcher: "permission_prompt|idle_prompt" } : {},
|
|
209
|
+
// Exec form: no shell. async: the hook never slows the agent down.
|
|
210
|
+
entry: { type: "command", command: "hivefloor", args: ["hook", "claude", event], async: true }
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
async function installClaude(i) {
|
|
214
|
+
const report = emptyReport();
|
|
215
|
+
const auth = i.useEnvToken ? "Bearer ${HIVEFLOOR_TOKEN}" : `Bearer ${i.token}`;
|
|
216
|
+
let mcp = "file";
|
|
217
|
+
const bin = i.noCli || !i.useEnvToken ? void 0 : findBin("claude", i.ctx.env);
|
|
218
|
+
if (bin) {
|
|
219
|
+
await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: i.ctx.env });
|
|
220
|
+
const r = await runCmd(bin, ["mcp", "add", "--transport", "http", MCP_NAME, i.mcpUrl, "--header", `Authorization: ${auth}`, "-s", "user"], { env: i.ctx.env });
|
|
221
|
+
if (r.code === 0) {
|
|
222
|
+
mcp = "cli";
|
|
223
|
+
report.lines.push(`Claude Code MCP: claude mcp add (user scope) -> ${i.mcpUrl}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (mcp === "file") {
|
|
227
|
+
editJson(
|
|
228
|
+
claudeJson(),
|
|
229
|
+
(d) => {
|
|
230
|
+
const servers = isRecord(d.mcpServers) ? d.mcpServers : d.mcpServers = {};
|
|
231
|
+
servers[MCP_NAME] = { type: "http", url: i.mcpUrl, headers: { Authorization: auth } };
|
|
232
|
+
return true;
|
|
233
|
+
},
|
|
234
|
+
i.useEnvToken ? {} : { mode: 384 }
|
|
235
|
+
);
|
|
236
|
+
if (!i.useEnvToken) secureFile(claudeJson());
|
|
237
|
+
report.lines.push(`Claude Code MCP: ${claudeJson()} (mcpServers.${MCP_NAME})`);
|
|
238
|
+
}
|
|
239
|
+
i.onMcpRegistered?.(mcp);
|
|
240
|
+
if (i.skipHooks) report.lines.push("Claude Code hooks: the machine connection already forwards them");
|
|
241
|
+
else {
|
|
242
|
+
editJson(claudeSettings(), (d) => (addHooks(d, "nested", claudeHookSpecs()), true));
|
|
243
|
+
report.lines.push(`Claude Code hooks: ${claudeSettings()} (${CLAUDE_EVENTS.join(", ")})`);
|
|
244
|
+
}
|
|
245
|
+
return { report, mcp };
|
|
246
|
+
}
|
|
247
|
+
async function removeClaude(ctx, viaCli, keepHooks = false, viaFile = false) {
|
|
248
|
+
const report = emptyReport();
|
|
249
|
+
if (viaCli) {
|
|
250
|
+
const bin = findBin("claude", ctx.env);
|
|
251
|
+
if (bin) {
|
|
252
|
+
const r = await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: ctx.env });
|
|
253
|
+
if (r.code === 0) report.lines.push("Claude Code MCP: claude mcp remove");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (viaFile) {
|
|
257
|
+
const changed = editJson(
|
|
258
|
+
claudeJson(),
|
|
259
|
+
(d) => {
|
|
260
|
+
if (!isRecord(d.mcpServers) || !(MCP_NAME in d.mcpServers)) return false;
|
|
261
|
+
delete d.mcpServers[MCP_NAME];
|
|
262
|
+
if (Object.keys(d.mcpServers).length === 0) delete d.mcpServers;
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
{ drop: true }
|
|
266
|
+
);
|
|
267
|
+
if (changed) report.lines.push(`Claude Code MCP: removed from ${claudeJson()}`);
|
|
268
|
+
}
|
|
269
|
+
dropBackupWithEntry(claudeJson(), MCP_NAME);
|
|
270
|
+
if (!keepHooks && editJson(claudeSettings(), (d) => removeHooks(d, "nested") > 0, { drop: true })) report.lines.push(`Claude Code hooks: removed from ${claudeSettings()}`);
|
|
271
|
+
return report;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/writers/codex.ts
|
|
275
|
+
import path3 from "path";
|
|
276
|
+
|
|
277
|
+
// src/writers/toml.ts
|
|
278
|
+
var BEGIN = (name, sep) => `# hivefloor:begin ${name} sep=${sep}`;
|
|
279
|
+
var END = (name) => `# hivefloor:end ${name}`;
|
|
280
|
+
var blockRe = (name) => new RegExp(`(?:\\r?\\n)*# hivefloor:begin ${name} sep=(\\d+)\\r?\\n[\\s\\S]*?# hivefloor:end ${name}\\r?\\n?`);
|
|
281
|
+
function esc(s) {
|
|
282
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
283
|
+
}
|
|
284
|
+
var tomlString = (s) => `"${esc(s)}"`;
|
|
285
|
+
function setBlock(content, name, body) {
|
|
286
|
+
const without = removeBlock(content, name);
|
|
287
|
+
if (without === "") return `${BEGIN(name, 0)}
|
|
288
|
+
${body}
|
|
289
|
+
${END(name)}
|
|
290
|
+
`;
|
|
291
|
+
const sep = without.endsWith("\n") ? 1 : 2;
|
|
292
|
+
return `${without}${"\n".repeat(sep)}${BEGIN(name, sep)}
|
|
293
|
+
${body}
|
|
294
|
+
${END(name)}
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
function removeBlock(content, name) {
|
|
298
|
+
const m = blockRe(name).exec(content);
|
|
299
|
+
if (!m) return content;
|
|
300
|
+
const sep = Number(m[1]);
|
|
301
|
+
const before = content.slice(0, m.index);
|
|
302
|
+
const after = content.slice(m.index + m[0].length);
|
|
303
|
+
const swallowed = m[0].length - m[0].replace(/^(?:\r?\n)*/, "").length;
|
|
304
|
+
const keep = Math.max(0, swallowed - sep);
|
|
305
|
+
return before + "\n".repeat(keep) + after;
|
|
306
|
+
}
|
|
307
|
+
function hasBlock(content, name) {
|
|
308
|
+
return blockRe(name).test(content);
|
|
309
|
+
}
|
|
310
|
+
function hasTable(content, table) {
|
|
311
|
+
const stripped = content.replace(/# hivefloor:begin[\s\S]*?# hivefloor:end [^\n]*\n?/g, "");
|
|
312
|
+
const re = new RegExp(`^\\s*\\[\\s*${table.replace(/\./g, "\\s*\\.\\s*")}\\s*\\]`, "m");
|
|
313
|
+
return re.test(stripped);
|
|
314
|
+
}
|
|
315
|
+
var FEATURE_TAG = "# hivefloor";
|
|
316
|
+
function enableFeatureHooks(content) {
|
|
317
|
+
const lines = content.split("\n");
|
|
318
|
+
const header = lines.findIndex((l) => /^\s*\[features\]\s*(#.*)?\r?$/.test(l));
|
|
319
|
+
if (header === -1) {
|
|
320
|
+
if (/^\s*features\.hooks\s*=\s*true\b/m.test(content)) return content;
|
|
321
|
+
return setBlock(content, "features", "[features]\nhooks = true");
|
|
322
|
+
}
|
|
323
|
+
let end = lines.length;
|
|
324
|
+
for (let k = header + 1; k < lines.length; k++) {
|
|
325
|
+
if (/^\s*\[/.test(lines[k])) {
|
|
326
|
+
end = k;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (let k = header + 1; k < end; k++) {
|
|
331
|
+
const m = /^(\s*hooks\s*=\s*)(true|false)(\s*(?:#.*)?)(\r?)$/.exec(lines[k]);
|
|
332
|
+
if (!m) continue;
|
|
333
|
+
if (m[2] === "true") return content;
|
|
334
|
+
lines[k] = `${m[1]}true ${FEATURE_TAG}:was=false${m[4]}`;
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
337
|
+
const cr = lines[header].endsWith("\r") ? "\r" : "";
|
|
338
|
+
lines.splice(header + 1, 0, `hooks = true ${FEATURE_TAG}${cr}`);
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
function restoreFeatureHooks(content) {
|
|
342
|
+
let out = removeBlock(content, "features");
|
|
343
|
+
out = out.split("\n").filter((l) => !/^\s*hooks\s*=\s*true\s+# hivefloor\s*\r?$/.test(l)).map((l) => l.replace(/^(\s*hooks\s*=\s*)true\s+# hivefloor:was=false(\r?)$/, "$1false$2")).join("\n");
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/writers/codex.ts
|
|
348
|
+
var CODEX_EVENTS = ["SessionStart", "SessionEnd", "UserPromptSubmit", "Stop", "SubagentStart", "SubagentStop"];
|
|
349
|
+
var codexDir = () => path3.join(process.env.CODEX_HOME || path3.join(home(), ".codex"));
|
|
350
|
+
var codexConfig = () => path3.join(codexDir(), "config.toml");
|
|
351
|
+
var codexHooks = () => path3.join(codexDir(), "hooks.json");
|
|
352
|
+
function codexHookSpecs() {
|
|
353
|
+
return CODEX_EVENTS.map((event) => ({ event, entry: { type: "command", command: `hivefloor hook codex ${event}` } }));
|
|
354
|
+
}
|
|
355
|
+
function editToml(fn, opts = {}) {
|
|
356
|
+
const file = codexConfig();
|
|
357
|
+
const before = readText(file);
|
|
358
|
+
const next = fn(before ?? "");
|
|
359
|
+
if (next === (before ?? "")) return false;
|
|
360
|
+
if (opts.drop && wasCreated(file) && next.trim() === "") {
|
|
361
|
+
removeFile(file);
|
|
362
|
+
forgetCreated(file);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (before === void 0) addCreated(file);
|
|
366
|
+
else if (!wasCreated(file)) backupOnce(file);
|
|
367
|
+
atomicWrite(file, next);
|
|
368
|
+
if (opts.drop) dropBackupIfSame(file);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
async function installCodex(i) {
|
|
372
|
+
const report = emptyReport();
|
|
373
|
+
let mcp = "file";
|
|
374
|
+
const bin = i.noCli ? void 0 : findBin("codex", i.ctx.env);
|
|
375
|
+
if (bin) {
|
|
376
|
+
await runCmd(bin, ["mcp", "remove", MCP_NAME], { env: i.ctx.env });
|
|
377
|
+
const r = await runCmd(bin, ["mcp", "add", MCP_NAME, "--url", i.mcpUrl, "--bearer-token-env-var", "HIVEFLOOR_TOKEN"], { env: i.ctx.env });
|
|
378
|
+
if (r.code === 0) {
|
|
379
|
+
mcp = "cli";
|
|
380
|
+
report.lines.push(`Codex MCP: codex mcp add -> ${i.mcpUrl}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (mcp === "file") {
|
|
384
|
+
editToml((c) => {
|
|
385
|
+
if (hasTable(c, `mcp_servers.${MCP_NAME}`)) {
|
|
386
|
+
report.warnings.push(`${codexConfig()} already defines [mcp_servers.${MCP_NAME}]; left as it is`);
|
|
387
|
+
return c;
|
|
388
|
+
}
|
|
389
|
+
return setBlock(c, "mcp", `[mcp_servers.${MCP_NAME}]
|
|
390
|
+
url = ${tomlString(i.mcpUrl)}
|
|
391
|
+
bearer_token_env_var = "HIVEFLOOR_TOKEN"`);
|
|
392
|
+
});
|
|
393
|
+
report.lines.push(`Codex MCP: ${codexConfig()} ([mcp_servers.${MCP_NAME}])`);
|
|
394
|
+
}
|
|
395
|
+
i.onMcpRegistered?.(mcp);
|
|
396
|
+
if (i.skipHooks) report.lines.push("Codex hooks: the machine connection already forwards them");
|
|
397
|
+
else {
|
|
398
|
+
editJson(codexHooks(), (d) => (addHooks(d, "nested", codexHookSpecs()), true));
|
|
399
|
+
report.lines.push(`Codex hooks: ${codexHooks()} (${CODEX_EVENTS.join(", ")})`);
|
|
400
|
+
editToml(enableFeatureHooks);
|
|
401
|
+
report.lines.push(`Codex: features.hooks = true in ${codexConfig()}`);
|
|
402
|
+
report.warnings.push("Codex asks you to trust new hooks: run codex, open /hooks and approve the Hivefloor hooks once.");
|
|
403
|
+
}
|
|
404
|
+
if (!i.useEnvToken) report.warnings.push('Codex reads the token from HIVEFLOOR_TOKEN: add export HIVEFLOOR_TOKEN="$(hivefloor token)" to your shell rc.');
|
|
405
|
+
return { report, mcp };
|
|
406
|
+
}
|
|
407
|
+
async function removeCodex(ctx, viaCli, keepHooks = false) {
|
|
408
|
+
const report = emptyReport();
|
|
409
|
+
if (viaCli) {
|
|
410
|
+
const bin = findBin("codex", ctx.env);
|
|
411
|
+
if (bin && (await runCmd(bin, ["mcp", "remove", MCP_NAME], { env: ctx.env })).code === 0) report.lines.push("Codex MCP: codex mcp remove");
|
|
412
|
+
}
|
|
413
|
+
const text = readText(codexConfig());
|
|
414
|
+
if (text !== void 0) {
|
|
415
|
+
const had = hasBlock(text, "mcp");
|
|
416
|
+
if (editToml((c) => keepHooks ? removeBlock(c, "mcp") : restoreFeatureHooks(removeBlock(c, "mcp")), { drop: true })) {
|
|
417
|
+
report.lines.push(`Codex: removed our entries from ${codexConfig()}${had ? " (MCP server, features.hooks)" : " (features.hooks)"}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!keepHooks && editJson(codexHooks(), (d) => removeHooks(d, "nested") > 0, { drop: true })) report.lines.push(`Codex hooks: removed from ${codexHooks()}`);
|
|
421
|
+
return report;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/writers/cursor.ts
|
|
425
|
+
import path4 from "path";
|
|
426
|
+
var CURSOR_EVENTS = ["sessionStart", "sessionEnd", "beforeSubmitPrompt", "stop", "subagentStart", "subagentStop"];
|
|
427
|
+
var cursorMcp = () => path4.join(home(), ".cursor", "mcp.json");
|
|
428
|
+
var cursorHooks = () => path4.join(home(), ".cursor", "hooks.json");
|
|
429
|
+
function cursorHookSpecs() {
|
|
430
|
+
return CURSOR_EVENTS.map((event) => ({ event, entry: { command: `hivefloor hook cursor ${event}` } }));
|
|
431
|
+
}
|
|
432
|
+
async function installCursor(i) {
|
|
433
|
+
const report = emptyReport();
|
|
434
|
+
const auth = i.useEnvToken ? "Bearer ${env:HIVEFLOOR_TOKEN}" : `Bearer ${i.token}`;
|
|
435
|
+
editJson(
|
|
436
|
+
cursorMcp(),
|
|
437
|
+
(d) => {
|
|
438
|
+
const servers = isRecord(d.mcpServers) ? d.mcpServers : d.mcpServers = {};
|
|
439
|
+
servers[MCP_NAME] = { url: i.mcpUrl, headers: { Authorization: auth } };
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
i.useEnvToken ? {} : { mode: 384 }
|
|
443
|
+
);
|
|
444
|
+
report.lines.push(`Cursor MCP: ${cursorMcp()} (mcpServers.${MCP_NAME})`);
|
|
445
|
+
i.onMcpRegistered?.("file");
|
|
446
|
+
editJson(cursorHooks(), (d) => {
|
|
447
|
+
if (d.version === void 0) d.version = 1;
|
|
448
|
+
addHooks(d, "flat", cursorHookSpecs());
|
|
449
|
+
return true;
|
|
450
|
+
});
|
|
451
|
+
report.lines.push(`Cursor hooks: ${cursorHooks()} (${CURSOR_EVENTS.join(", ")})`);
|
|
452
|
+
return { report, mcp: "file" };
|
|
453
|
+
}
|
|
454
|
+
async function removeCursor() {
|
|
455
|
+
const report = emptyReport();
|
|
456
|
+
const changed = editJson(
|
|
457
|
+
cursorMcp(),
|
|
458
|
+
(d) => {
|
|
459
|
+
if (!isRecord(d.mcpServers) || !(MCP_NAME in d.mcpServers)) return false;
|
|
460
|
+
delete d.mcpServers[MCP_NAME];
|
|
461
|
+
if (Object.keys(d.mcpServers).length === 0) delete d.mcpServers;
|
|
462
|
+
return true;
|
|
463
|
+
},
|
|
464
|
+
{ drop: true }
|
|
465
|
+
);
|
|
466
|
+
if (changed) report.lines.push(`Cursor MCP: removed from ${cursorMcp()}`);
|
|
467
|
+
if (editJson(cursorHooks(), (d) => removeHooks(d, "flat") > 0, { drop: true })) report.lines.push(`Cursor hooks: removed from ${cursorHooks()}`);
|
|
468
|
+
return report;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/writers/gemini.ts
|
|
472
|
+
import path5 from "path";
|
|
473
|
+
var GEMINI_EVENTS = ["SessionStart", "SessionEnd", "AfterAgent"];
|
|
474
|
+
var geminiSettings = () => path5.join(home(), ".gemini", "settings.json");
|
|
475
|
+
function geminiHookSpecs() {
|
|
476
|
+
return GEMINI_EVENTS.map((event) => ({
|
|
477
|
+
event,
|
|
478
|
+
// Gemini hook entries carry a name: ours is "hivefloor", which is also how disconnect finds them.
|
|
479
|
+
entry: { name: "hivefloor", type: "command", command: `hivefloor hook gemini ${event}` }
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
async function installGemini(i) {
|
|
483
|
+
const report = emptyReport();
|
|
484
|
+
const auth = i.useEnvToken ? "Bearer $HIVEFLOOR_TOKEN" : `Bearer ${i.token}`;
|
|
485
|
+
let mcp = "file";
|
|
486
|
+
const bin = i.noCli || !i.useEnvToken ? void 0 : findBin("gemini", i.ctx.env);
|
|
487
|
+
if (bin) {
|
|
488
|
+
await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: i.ctx.env });
|
|
489
|
+
const r = await runCmd(bin, ["mcp", "add", MCP_NAME, i.mcpUrl, "-t", "http", "-s", "user", "-H", `Authorization: ${auth}`], { env: i.ctx.env });
|
|
490
|
+
if (r.code === 0) {
|
|
491
|
+
mcp = "cli";
|
|
492
|
+
report.lines.push(`Gemini MCP: gemini mcp add (user scope) -> ${i.mcpUrl}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (mcp === "file") {
|
|
496
|
+
editJson(
|
|
497
|
+
geminiSettings(),
|
|
498
|
+
(d) => {
|
|
499
|
+
const servers = isRecord(d.mcpServers) ? d.mcpServers : d.mcpServers = {};
|
|
500
|
+
servers[MCP_NAME] = { httpUrl: i.mcpUrl, headers: { Authorization: auth } };
|
|
501
|
+
return true;
|
|
502
|
+
},
|
|
503
|
+
i.useEnvToken ? {} : { mode: 384 }
|
|
504
|
+
);
|
|
505
|
+
if (!i.useEnvToken) secureFile(geminiSettings());
|
|
506
|
+
report.lines.push(`Gemini MCP: ${geminiSettings()} (mcpServers.${MCP_NAME})`);
|
|
507
|
+
}
|
|
508
|
+
i.onMcpRegistered?.(mcp);
|
|
509
|
+
editJson(geminiSettings(), (d) => (addHooks(d, "nested", geminiHookSpecs()), true), i.useEnvToken ? {} : { mode: 384 });
|
|
510
|
+
report.lines.push(`Gemini hooks: ${geminiSettings()} (${GEMINI_EVENTS.join(", ")})`);
|
|
511
|
+
return { report, mcp };
|
|
512
|
+
}
|
|
513
|
+
async function removeGemini(ctx, viaCli, viaFile = false) {
|
|
514
|
+
const report = emptyReport();
|
|
515
|
+
if (viaCli) {
|
|
516
|
+
const bin = findBin("gemini", ctx.env);
|
|
517
|
+
if (bin && (await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: ctx.env })).code === 0) report.lines.push("Gemini MCP: gemini mcp remove");
|
|
518
|
+
}
|
|
519
|
+
const changed = editJson(
|
|
520
|
+
geminiSettings(),
|
|
521
|
+
(d) => {
|
|
522
|
+
let did = removeHooks(d, "nested") > 0;
|
|
523
|
+
if (viaFile && isRecord(d.mcpServers) && MCP_NAME in d.mcpServers) {
|
|
524
|
+
delete d.mcpServers[MCP_NAME];
|
|
525
|
+
if (Object.keys(d.mcpServers).length === 0) delete d.mcpServers;
|
|
526
|
+
did = true;
|
|
527
|
+
}
|
|
528
|
+
return did;
|
|
529
|
+
},
|
|
530
|
+
{ drop: true }
|
|
531
|
+
);
|
|
532
|
+
if (changed) report.lines.push(`Gemini: removed our entries from ${geminiSettings()}`);
|
|
533
|
+
dropBackupWithEntry(geminiSettings(), MCP_NAME);
|
|
534
|
+
return report;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/writers/grok.ts
|
|
538
|
+
async function installGrok(i) {
|
|
539
|
+
const report = emptyReport();
|
|
540
|
+
const auth = i.useEnvToken ? "Bearer ${HIVEFLOOR_TOKEN}" : `Bearer ${i.token}`;
|
|
541
|
+
const bin = i.noCli ? void 0 : findBin("grok", i.ctx.env);
|
|
542
|
+
if (bin) {
|
|
543
|
+
await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: i.ctx.env });
|
|
544
|
+
const r = await runCmd(bin, ["mcp", "add", "--transport", "http", MCP_NAME, i.mcpUrl, "-s", "user", "--header", `Authorization: ${auth}`], { env: i.ctx.env });
|
|
545
|
+
if (r.code === 0) {
|
|
546
|
+
report.lines.push(`Grok MCP: grok mcp add (user scope) -> ${i.mcpUrl}`);
|
|
547
|
+
report.lines.push("Grok hooks: none needed (Grok reads the Claude Code hooks in ~/.claude/settings.json; connect Claude Code too for live sessions)");
|
|
548
|
+
i.onMcpRegistered?.("cli");
|
|
549
|
+
return { report, mcp: "cli" };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
report.warnings.push(`Could not run the grok CLI. Add the server by hand: grok mcp add --transport http ${MCP_NAME} ${i.mcpUrl} -s user --header "Authorization: Bearer <token from ${cliNpxCommand("token --reveal")}>"`);
|
|
553
|
+
return { report, mcp: "manual" };
|
|
554
|
+
}
|
|
555
|
+
async function removeGrok(ctx, viaCli) {
|
|
556
|
+
const report = emptyReport();
|
|
557
|
+
if (viaCli) {
|
|
558
|
+
const bin = findBin("grok", ctx.env);
|
|
559
|
+
if (bin && (await runCmd(bin, ["mcp", "remove", MCP_NAME, "-s", "user"], { env: ctx.env })).code === 0) report.lines.push("Grok MCP: grok mcp remove");
|
|
560
|
+
}
|
|
561
|
+
return report;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export {
|
|
565
|
+
readManifest,
|
|
566
|
+
updateManifest,
|
|
567
|
+
addCreated,
|
|
568
|
+
wasCreated,
|
|
569
|
+
forgetCreated,
|
|
570
|
+
RC_LINE,
|
|
571
|
+
rcFileFor,
|
|
572
|
+
hasRcLine,
|
|
573
|
+
addRcLine,
|
|
574
|
+
removeRcLine,
|
|
575
|
+
addHooks,
|
|
576
|
+
removeHooks,
|
|
577
|
+
editJson,
|
|
578
|
+
CLAUDE_EVENTS,
|
|
579
|
+
claudeJson,
|
|
580
|
+
installClaude,
|
|
581
|
+
removeClaude,
|
|
582
|
+
enableFeatureHooks,
|
|
583
|
+
restoreFeatureHooks,
|
|
584
|
+
CODEX_EVENTS,
|
|
585
|
+
installCodex,
|
|
586
|
+
removeCodex,
|
|
587
|
+
installCursor,
|
|
588
|
+
removeCursor,
|
|
589
|
+
geminiSettings,
|
|
590
|
+
installGemini,
|
|
591
|
+
removeGemini,
|
|
592
|
+
installGrok,
|
|
593
|
+
removeGrok
|
|
594
|
+
};
|