@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,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
claudeConfigDirs,
|
|
4
|
+
claudeLabel,
|
|
5
|
+
codexConfigDir,
|
|
6
|
+
codexLabel,
|
|
7
|
+
profileKeys
|
|
8
|
+
} from "./chunk-NCRQGIJY.js";
|
|
9
|
+
|
|
10
|
+
// src/osinfo.ts
|
|
11
|
+
import os from "os";
|
|
12
|
+
var cliVersionOs = () => `${os.platform()} ${os.release()}`.slice(0, 100);
|
|
13
|
+
|
|
14
|
+
// src/service/discovery.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
var BY_EXE = { claude: "claude", codex: "codex", "cursor-agent": "cursor", gemini: "gemini", grok: "grok" };
|
|
18
|
+
var BY_DIR = { "claude-code": "claude", codex: "codex", "cursor-agent": "cursor", "gemini-cli": "gemini", "grok-cli": "grok" };
|
|
19
|
+
var RUNNERS = /* @__PURE__ */ new Set(["node", "bun", "deno", "npx"]);
|
|
20
|
+
var HELPER_ARGS = /* @__PURE__ */ new Set(["mcp", "update", "doctor", "--version"]);
|
|
21
|
+
var exeName = (tok) => (tok.replace(/^"|"$/g, "").split(/[\\/]/).pop() ?? "").replace(/\.(?:js|mjs|cjs|exe|cmd)$/, "");
|
|
22
|
+
var dirTool = (script) => {
|
|
23
|
+
for (const part of script.split(/[\\/]/).reverse()) {
|
|
24
|
+
const tool = BY_DIR[part];
|
|
25
|
+
if (tool) return tool;
|
|
26
|
+
}
|
|
27
|
+
return void 0;
|
|
28
|
+
};
|
|
29
|
+
function splitCommandLine(line) {
|
|
30
|
+
const toks = [];
|
|
31
|
+
let cur = "";
|
|
32
|
+
let quoted = false;
|
|
33
|
+
let started = false;
|
|
34
|
+
for (const c of line) {
|
|
35
|
+
if (c === '"') {
|
|
36
|
+
quoted = !quoted;
|
|
37
|
+
started = true;
|
|
38
|
+
} else if (!quoted && /\s/.test(c)) {
|
|
39
|
+
if (started) toks.push(cur);
|
|
40
|
+
cur = "";
|
|
41
|
+
started = false;
|
|
42
|
+
} else {
|
|
43
|
+
cur += c;
|
|
44
|
+
started = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (started) toks.push(cur);
|
|
48
|
+
return toks;
|
|
49
|
+
}
|
|
50
|
+
function fromTokens(pid, toks) {
|
|
51
|
+
const first = exeName(toks[0] ?? "");
|
|
52
|
+
let rest = toks.slice(1);
|
|
53
|
+
let tool;
|
|
54
|
+
if (RUNNERS.has(first) && toks[1] && !toks[1].startsWith("-")) {
|
|
55
|
+
tool = BY_EXE[exeName(toks[1])] ?? dirTool(toks[1]);
|
|
56
|
+
rest = toks.slice(2);
|
|
57
|
+
} else {
|
|
58
|
+
tool = BY_EXE[first];
|
|
59
|
+
}
|
|
60
|
+
if (!tool || pid === process.pid) return void 0;
|
|
61
|
+
if (HELPER_ARGS.has(rest[0] ?? "")) return void 0;
|
|
62
|
+
const mi = rest.findIndex((t) => t === "--model" || t === "-m");
|
|
63
|
+
const eq = rest.find((t) => t.startsWith("--model="));
|
|
64
|
+
const model = eq ? eq.slice(8) : mi >= 0 ? rest[mi + 1] : void 0;
|
|
65
|
+
return { pid, tool, ...model && /^[\w.:\-/[\]]{1,100}$/.test(model) ? { model } : {} };
|
|
66
|
+
}
|
|
67
|
+
function parsePs(text, uid) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const line of text.split("\n")) {
|
|
70
|
+
const m = /^\s*(\d+)\s+(\d+)\s+\S+\s+(.+)$/.exec(line);
|
|
71
|
+
if (!m) continue;
|
|
72
|
+
const pid = Number(m[1]);
|
|
73
|
+
if (uid !== void 0 && Number(m[2]) !== uid) continue;
|
|
74
|
+
const p = fromTokens(pid, m[3].trim().split(/\s+/));
|
|
75
|
+
if (p) out.push(p);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function parseWindowsProcesses(text) {
|
|
80
|
+
const raw = text.replace(/^\uFEFF/, "").trim();
|
|
81
|
+
if (raw === "" || raw === "null") return [];
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
data = JSON.parse(raw);
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const item of Array.isArray(data) ? data : [data]) {
|
|
90
|
+
if (typeof item !== "object" || item === null) continue;
|
|
91
|
+
const p = item;
|
|
92
|
+
const pid = typeof p.ProcessId === "number" ? p.ProcessId : typeof p.ProcessId === "string" ? Number(p.ProcessId) : NaN;
|
|
93
|
+
if (!Number.isInteger(pid) || pid <= 0 || typeof p.CommandLine !== "string" || p.CommandLine.trim() === "") continue;
|
|
94
|
+
const found = fromTokens(pid, splitCommandLine(p.CommandLine));
|
|
95
|
+
if (found) out.push(found);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function makeEnvReader(platform, run) {
|
|
100
|
+
if (platform === "win32") return async () => void 0;
|
|
101
|
+
return async (pid, name) => {
|
|
102
|
+
try {
|
|
103
|
+
if (platform === "linux") {
|
|
104
|
+
const raw = fs.readFileSync(`/proc/${pid}/environ`, "utf8");
|
|
105
|
+
for (const kv of raw.split("\0")) if (kv.startsWith(`${name}=`)) return kv.slice(name.length + 1) || void 0;
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
if (platform === "darwin") {
|
|
109
|
+
const r = await run("ps", ["-E", "-ww", "-p", String(pid), "-o", "command="], { timeoutMs: 5e3 });
|
|
110
|
+
const m = new RegExp(`(?:^|\\s)${name}=(\\S+)`).exec(r.stdout);
|
|
111
|
+
return m?.[1];
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
return void 0;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
var WINDOWS_PROCESS_QUERY = [
|
|
119
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
120
|
+
"$me = [Security.Principal.WindowsIdentity]::GetCurrent().Name",
|
|
121
|
+
"$names = @('claude.exe', 'claude', 'codex.exe', 'codex', 'cursor-agent.exe', 'cursor-agent', 'gemini.exe', 'gemini', 'grok.exe', 'grok', 'node.exe', 'node', 'bun.exe', 'bun', 'deno.exe', 'deno', 'npx.cmd', 'npx')",
|
|
122
|
+
'$rows = @(Get-CimInstance Win32_Process | Where-Object { $names -contains $_.Name.ToLowerInvariant() } | ForEach-Object { $p = $_; $o = Invoke-CimMethod -InputObject $p -MethodName GetOwner; if ($o.ReturnValue -eq 0) { $owner = if ($o.Domain) { "$($o.Domain)\\$($o.User)" } else { $o.User }; if ($owner -ieq $me) { [pscustomobject]@{ ProcessId = $p.ProcessId; CommandLine = $p.CommandLine } } } })',
|
|
123
|
+
"ConvertTo-Json -InputObject $rows -Compress"
|
|
124
|
+
].join("; ");
|
|
125
|
+
async function scanProcesses(d) {
|
|
126
|
+
if (d.platform === "win32") {
|
|
127
|
+
const r2 = await d.run("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_QUERY], { timeoutMs: 1e4 });
|
|
128
|
+
if (r2.code !== 0) return { procs: [] };
|
|
129
|
+
return { procs: parseWindowsProcesses(r2.stdout) };
|
|
130
|
+
}
|
|
131
|
+
const r = await d.run("ps", ["-axo", "pid=,uid=,etime=,command="], { timeoutMs: 1e4 });
|
|
132
|
+
if (r.code !== 0) return { procs: [] };
|
|
133
|
+
const readEnv = d.readEnv ?? makeEnvReader(d.platform, d.run);
|
|
134
|
+
const procs = [];
|
|
135
|
+
const uid = d.uid ?? (typeof process.getuid === "function" ? process.getuid() : void 0);
|
|
136
|
+
for (const p of parsePs(r.stdout, uid)) {
|
|
137
|
+
const dir = p.tool === "claude" ? await readEnv(p.pid, "CLAUDE_CONFIG_DIR") : p.tool === "codex" ? await readEnv(p.pid, "CODEX_HOME") : void 0;
|
|
138
|
+
procs.push({ ...p, ...dir ? { dir } : {} });
|
|
139
|
+
}
|
|
140
|
+
return { procs };
|
|
141
|
+
}
|
|
142
|
+
var claudeDirsOfScan = (s) => s.procs.filter((p) => p.tool === "claude" && p.dir).map((p) => p.dir);
|
|
143
|
+
function buildProfiles(scan, env, homeDir) {
|
|
144
|
+
const claudeDirs = claudeConfigDirs(claudeDirsOfScan(scan), homeDir);
|
|
145
|
+
const ckeys = profileKeys("claude", claudeDirs);
|
|
146
|
+
const out = [];
|
|
147
|
+
const defaultDir = claudeDirs[0];
|
|
148
|
+
for (const dir of claudeDirs) {
|
|
149
|
+
const key = ckeys.get(dir);
|
|
150
|
+
const here = scan.procs.filter((p) => p.tool === "claude" && (p.dir ? path.resolve(p.dir) : defaultDir) === dir);
|
|
151
|
+
if (here.length === 0 && !fs.existsSync(dir)) continue;
|
|
152
|
+
const model = latestModel(here);
|
|
153
|
+
out.push({ tool: "claude", key, label: claudeLabel(dir, key), ...model ? { model } : {}, running: here.length });
|
|
154
|
+
}
|
|
155
|
+
const cdir = codexConfigDir(env);
|
|
156
|
+
const codexProcs = scan.procs.filter((p) => p.tool === "codex");
|
|
157
|
+
const codexKeys = profileKeys("codex", [cdir, ...codexProcs.flatMap((p) => p.dir ? [path.resolve(p.dir)] : [])]);
|
|
158
|
+
const seenCodex = /* @__PURE__ */ new Set();
|
|
159
|
+
for (const [dir, key] of codexKeys) {
|
|
160
|
+
if (seenCodex.has(dir)) continue;
|
|
161
|
+
seenCodex.add(dir);
|
|
162
|
+
const here = codexProcs.filter((p) => p.dir ? path.resolve(p.dir) === dir : dir === cdir);
|
|
163
|
+
if (here.length === 0 && !fs.existsSync(dir)) continue;
|
|
164
|
+
const model = latestModel(here);
|
|
165
|
+
out.push({ tool: "codex", key, label: codexLabel(dir), ...model ? { model } : {}, running: here.length });
|
|
166
|
+
}
|
|
167
|
+
for (const tool of ["cursor", "gemini", "grok"]) {
|
|
168
|
+
const here = scan.procs.filter((p) => p.tool === tool);
|
|
169
|
+
if (here.length === 0) continue;
|
|
170
|
+
const model = latestModel(here);
|
|
171
|
+
out.push({ tool, key: "default", label: { cursor: "Cursor", gemini: "Gemini CLI", grok: "Grok CLI" }[tool], ...model ? { model } : {}, running: here.length });
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
var latestModel = (ps) => ps.map((p) => p.model).filter((m) => Boolean(m)).pop();
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
cliVersionOs,
|
|
179
|
+
scanProcesses,
|
|
180
|
+
claudeDirsOfScan,
|
|
181
|
+
buildProfiles
|
|
182
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLAUDE_EVENTS,
|
|
4
|
+
CODEX_EVENTS,
|
|
5
|
+
addCreated,
|
|
6
|
+
addHooks,
|
|
7
|
+
editJson,
|
|
8
|
+
enableFeatureHooks,
|
|
9
|
+
forgetCreated,
|
|
10
|
+
removeHooks,
|
|
11
|
+
restoreFeatureHooks,
|
|
12
|
+
wasCreated
|
|
13
|
+
} from "./chunk-YCZ7XJJF.js";
|
|
14
|
+
import {
|
|
15
|
+
claudeConfigDirs,
|
|
16
|
+
codexConfigDir,
|
|
17
|
+
profileKeys
|
|
18
|
+
} from "./chunk-NCRQGIJY.js";
|
|
19
|
+
import {
|
|
20
|
+
atomicWrite,
|
|
21
|
+
backupOnce,
|
|
22
|
+
cliDir,
|
|
23
|
+
cliEntry,
|
|
24
|
+
dropBackupIfSame,
|
|
25
|
+
readText,
|
|
26
|
+
removeFile
|
|
27
|
+
} from "./chunk-WNDUGQDX.js";
|
|
28
|
+
|
|
29
|
+
// src/machine-hooks.ts
|
|
30
|
+
import fs from "fs";
|
|
31
|
+
import path from "path";
|
|
32
|
+
function installCliCopy(srcDir) {
|
|
33
|
+
if (!fs.existsSync(path.join(srcDir, "index.js"))) throw new Error(`cannot find the CLI bundle in ${srcDir}`);
|
|
34
|
+
const dest = cliDir();
|
|
35
|
+
fs.mkdirSync(dest, { recursive: true, mode: 448 });
|
|
36
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
37
|
+
if (!/\.(?:js|mjs)$/.test(f)) continue;
|
|
38
|
+
const tmp = path.join(dest, `${f}.${process.pid}.tmp`);
|
|
39
|
+
fs.copyFileSync(path.join(srcDir, f), tmp);
|
|
40
|
+
fs.renameSync(tmp, path.join(dest, f));
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(path.join(dest, "package.json"), '{"type":"module"}\n');
|
|
43
|
+
}
|
|
44
|
+
var removeCliCopy = () => fs.rmSync(cliDir(), { recursive: true, force: true });
|
|
45
|
+
function claudeMachineSpecs(node, profile) {
|
|
46
|
+
return CLAUDE_EVENTS.map((event) => ({
|
|
47
|
+
event,
|
|
48
|
+
...event === "Notification" ? { matcher: "permission_prompt|idle_prompt" } : {},
|
|
49
|
+
// Exec form: no shell, no quoting. async: the hook never slows the agent down.
|
|
50
|
+
entry: { type: "command", command: node, args: [cliEntry(), "hook", "claude", event, "--profile", profile], async: true }
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
var q = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
54
|
+
function codexMachineSpecs(node, profile) {
|
|
55
|
+
return CODEX_EVENTS.map((event) => ({ event, entry: { type: "command", command: `${q(node)} ${q(cliEntry())} hook codex ${event} --profile ${profile}` } }));
|
|
56
|
+
}
|
|
57
|
+
var isDir = (p) => {
|
|
58
|
+
try {
|
|
59
|
+
return fs.statSync(p).isDirectory();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function installMachineHooks(node, opts) {
|
|
65
|
+
const res = { claude_dirs: [], lines: [], warnings: [] };
|
|
66
|
+
const dirs = claudeConfigDirs(opts.processDirs).filter(isDir);
|
|
67
|
+
const keys = profileKeys("claude", dirs);
|
|
68
|
+
for (const dir of dirs) {
|
|
69
|
+
const file = path.join(dir, "settings.json");
|
|
70
|
+
try {
|
|
71
|
+
editJson(file, (d) => (addHooks(d, "nested", claudeMachineSpecs(node, keys.get(dir)), "machine"), true));
|
|
72
|
+
res.claude_dirs.push(dir);
|
|
73
|
+
res.lines.push(`Claude Code hooks (${keys.get(dir)}): ${file}`);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
res.warnings.push(e.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (opts.codex && isDir(codexConfigDir(opts.env))) {
|
|
79
|
+
const dir = codexConfigDir(opts.env);
|
|
80
|
+
const key = profileKeys("codex", [dir]).get(dir);
|
|
81
|
+
try {
|
|
82
|
+
editJson(path.join(dir, "hooks.json"), (d) => (addHooks(d, "nested", codexMachineSpecs(node, key), "machine"), true));
|
|
83
|
+
editToml(path.join(dir, "config.toml"), (c) => enableFeatureHooks(c));
|
|
84
|
+
res.codex_dir = dir;
|
|
85
|
+
res.lines.push(`Codex hooks (${key}): ${path.join(dir, "hooks.json")}`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
res.warnings.push(e.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
function editToml(file, fn) {
|
|
93
|
+
const before = readText(file);
|
|
94
|
+
const next = fn(before ?? "");
|
|
95
|
+
if (next === (before ?? "")) return;
|
|
96
|
+
if (before === void 0) addCreated(file);
|
|
97
|
+
else if (!wasCreated(file)) backupOnce(file);
|
|
98
|
+
atomicWrite(file, next);
|
|
99
|
+
}
|
|
100
|
+
function removeMachineHooks(inst) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const dir of inst.claude_dirs) {
|
|
103
|
+
const file = path.join(dir, "settings.json");
|
|
104
|
+
try {
|
|
105
|
+
if (editJson(file, (d) => removeHooks(d, "nested", "machine") > 0, { drop: true })) lines.push(`Claude Code hooks: removed from ${file}`);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (inst.codex_dir) {
|
|
110
|
+
const file = path.join(inst.codex_dir, "hooks.json");
|
|
111
|
+
try {
|
|
112
|
+
if (editJson(file, (d) => removeHooks(d, "nested", "machine") > 0, { drop: true })) lines.push(`Codex hooks: removed from ${file}`);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
const cfg = path.join(inst.codex_dir, "config.toml");
|
|
116
|
+
const before = readText(cfg);
|
|
117
|
+
if (before !== void 0) {
|
|
118
|
+
const next = restoreFeatureHooks(before);
|
|
119
|
+
if (next !== before) {
|
|
120
|
+
if (wasCreated(cfg) && next.trim() === "") {
|
|
121
|
+
removeFile(cfg);
|
|
122
|
+
forgetCreated(cfg);
|
|
123
|
+
} else {
|
|
124
|
+
atomicWrite(cfg, next);
|
|
125
|
+
dropBackupIfSame(cfg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export {
|
|
134
|
+
installCliCopy,
|
|
135
|
+
removeCliCopy,
|
|
136
|
+
installMachineHooks,
|
|
137
|
+
removeMachineHooks
|
|
138
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "@hivefloor/cli",
|
|
6
|
+
version: "0.1.1",
|
|
7
|
+
description: "Connect your coding agents (Claude Code, Codex, Cursor, Gemini, Grok) to Hivefloor: live presence, task coordination and usage on one floor.",
|
|
8
|
+
author: {
|
|
9
|
+
name: "Errr0rr404",
|
|
10
|
+
url: "https://github.com/Errr0rr404"
|
|
11
|
+
},
|
|
12
|
+
keywords: [
|
|
13
|
+
"hivefloor",
|
|
14
|
+
"ai-agents",
|
|
15
|
+
"coding-agents",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"codex",
|
|
18
|
+
"mcp",
|
|
19
|
+
"hooks",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
homepage: "https://hivefloor.worldofz.info",
|
|
23
|
+
bugs: {
|
|
24
|
+
url: "https://github.com/Errr0rr404/hivefloor/issues"
|
|
25
|
+
},
|
|
26
|
+
repository: {
|
|
27
|
+
type: "git",
|
|
28
|
+
url: "git+https://github.com/Errr0rr404/hivefloor.git",
|
|
29
|
+
directory: "packages/cli"
|
|
30
|
+
},
|
|
31
|
+
license: "MIT",
|
|
32
|
+
type: "module",
|
|
33
|
+
bin: {
|
|
34
|
+
hivefloor: "dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
files: [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
engines: {
|
|
40
|
+
node: ">=22"
|
|
41
|
+
},
|
|
42
|
+
publishConfig: {
|
|
43
|
+
access: "public"
|
|
44
|
+
},
|
|
45
|
+
scripts: {
|
|
46
|
+
build: "tsup",
|
|
47
|
+
typecheck: "tsc -p tsconfig.json",
|
|
48
|
+
"pack-test": "bash scripts/pack-test.sh",
|
|
49
|
+
prepack: "npm run build && node scripts/pack-manifest.mjs strip",
|
|
50
|
+
postpack: "node scripts/pack-manifest.mjs restore"
|
|
51
|
+
},
|
|
52
|
+
devDependencies: {
|
|
53
|
+
"@hivefloor/shared": "workspace:*",
|
|
54
|
+
"@types/node": "^22.20.5",
|
|
55
|
+
tsup: "^8.5.1",
|
|
56
|
+
typescript: "~5.9.3"
|
|
57
|
+
},
|
|
58
|
+
dependencies: {
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.32.2"
|
|
60
|
+
},
|
|
61
|
+
optionalDependencies: {
|
|
62
|
+
"@napi-rs/keyring": "^2.1.2"
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/version.ts
|
|
67
|
+
var VERSION = package_default.version;
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
VERSION
|
|
71
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/fsutil.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
var home = () => os.homedir();
|
|
8
|
+
function configDirFor(platform, env, homeDir) {
|
|
9
|
+
if (platform === "win32") return path.win32.join(env.APPDATA || path.win32.join(homeDir, "AppData", "Roaming"), "hivefloor");
|
|
10
|
+
return path.join(homeDir, ".config", "hivefloor");
|
|
11
|
+
}
|
|
12
|
+
var configDir = () => configDirFor(process.platform, process.env, home());
|
|
13
|
+
var credentialsFile = () => path.join(configDir(), "credentials.json");
|
|
14
|
+
var spoolFile = () => path.join(configDir(), "spool.jsonl");
|
|
15
|
+
var manifestFile = () => path.join(configDir(), "manifest.json");
|
|
16
|
+
var machineFile = () => path.join(configDir(), "machine.json");
|
|
17
|
+
var machineSpoolFile = () => path.join(configDir(), "machine-spool.jsonl");
|
|
18
|
+
var serviceStateFile = () => path.join(configDir(), "service.json");
|
|
19
|
+
var serviceLogFile = () => path.join(configDir(), "service.log");
|
|
20
|
+
var cliDir = () => path.join(configDir(), "cli");
|
|
21
|
+
var cliEntry = () => path.join(cliDir(), "index.js");
|
|
22
|
+
function readText(file) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync(file, "utf8");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e.code === "ENOENT") return void 0;
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function resolveLink(file) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.realpathSync(file);
|
|
33
|
+
} catch {
|
|
34
|
+
try {
|
|
35
|
+
return path.resolve(path.dirname(file), fs.readlinkSync(file));
|
|
36
|
+
} catch {
|
|
37
|
+
return file;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function atomicWrite(link, content, mode) {
|
|
42
|
+
atomicWriteBytes(link, Buffer.from(content, "utf8"), mode);
|
|
43
|
+
}
|
|
44
|
+
function atomicWriteBytes(link, content, mode) {
|
|
45
|
+
const file = resolveLink(link);
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
let m = mode;
|
|
48
|
+
if (m === void 0) {
|
|
49
|
+
try {
|
|
50
|
+
m = fs.statSync(file).mode & 511;
|
|
51
|
+
} catch {
|
|
52
|
+
m = 420;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
try {
|
|
57
|
+
fs.writeFileSync(tmp, content, { mode: m });
|
|
58
|
+
fs.chmodSync(tmp, m);
|
|
59
|
+
fs.renameSync(tmp, file);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
try {
|
|
62
|
+
fs.unlinkSync(tmp);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function atomicWriteUtf16(link, content, mode) {
|
|
69
|
+
atomicWriteBytes(link, Buffer.concat([Buffer.from([255, 254]), Buffer.from(content, "utf16le")]), mode);
|
|
70
|
+
}
|
|
71
|
+
function backupOnce(file) {
|
|
72
|
+
const bak = `${file}.hivefloor.bak`;
|
|
73
|
+
if (fs.existsSync(file) && !fs.existsSync(bak)) fs.copyFileSync(file, bak);
|
|
74
|
+
}
|
|
75
|
+
function dropBackupIfSame(file) {
|
|
76
|
+
const bak = `${file}.hivefloor.bak`;
|
|
77
|
+
const a = readText(file);
|
|
78
|
+
const b = readText(bak);
|
|
79
|
+
if (b !== void 0 && a === b) fs.unlinkSync(bak);
|
|
80
|
+
}
|
|
81
|
+
function removeFile(file) {
|
|
82
|
+
try {
|
|
83
|
+
fs.unlinkSync(file);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
var ConfigError = class extends Error {
|
|
88
|
+
};
|
|
89
|
+
function readJsonFile(file) {
|
|
90
|
+
const text = readText(file);
|
|
91
|
+
if (text === void 0 || text.trim() === "") {
|
|
92
|
+
return { data: {}, style: { indent: 2, eol: "\n", finalNewline: true }, existed: text !== void 0 };
|
|
93
|
+
}
|
|
94
|
+
let data;
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(text);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new ConfigError(`${file} is not plain JSON (comments or a syntax error), so it was left untouched`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) throw new ConfigError(`${file} is not a JSON object`);
|
|
101
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
102
|
+
const m = /\n([ \t]+)\S/.exec(text);
|
|
103
|
+
const indent = m ? m[1].startsWith(" ") ? " " : m[1].length : text.trim().includes("\n") ? 2 : 0;
|
|
104
|
+
return { data, style: { indent, eol, finalNewline: text.endsWith("\n") }, existed: true };
|
|
105
|
+
}
|
|
106
|
+
function serializeJson(data, style) {
|
|
107
|
+
let s = JSON.stringify(data, null, style.indent);
|
|
108
|
+
if (style.eol === "\r\n") s = s.replace(/\n/g, "\r\n");
|
|
109
|
+
return style.finalNewline ? s + style.eol : s;
|
|
110
|
+
}
|
|
111
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
home,
|
|
115
|
+
configDir,
|
|
116
|
+
credentialsFile,
|
|
117
|
+
spoolFile,
|
|
118
|
+
manifestFile,
|
|
119
|
+
machineFile,
|
|
120
|
+
machineSpoolFile,
|
|
121
|
+
serviceStateFile,
|
|
122
|
+
serviceLogFile,
|
|
123
|
+
cliDir,
|
|
124
|
+
cliEntry,
|
|
125
|
+
readText,
|
|
126
|
+
atomicWrite,
|
|
127
|
+
atomicWriteUtf16,
|
|
128
|
+
backupOnce,
|
|
129
|
+
dropBackupIfSame,
|
|
130
|
+
removeFile,
|
|
131
|
+
ConfigError,
|
|
132
|
+
readJsonFile,
|
|
133
|
+
serializeJson,
|
|
134
|
+
isRecord
|
|
135
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
apiRequest,
|
|
5
|
+
errorOf
|
|
6
|
+
} from "./chunk-PY74LT2W.js";
|
|
7
|
+
import {
|
|
8
|
+
deviceCodeResponseSchema,
|
|
9
|
+
deviceTokenErrorSchema,
|
|
10
|
+
deviceTokenSuccessSchema
|
|
11
|
+
} from "./chunk-ZYRPQPGL.js";
|
|
12
|
+
|
|
13
|
+
// src/device.ts
|
|
14
|
+
var printable = (s) => [...s].filter((c) => c.charCodeAt(0) >= 32 && c.charCodeAt(0) !== 127).join("");
|
|
15
|
+
async function deviceLogin(ctx, opts) {
|
|
16
|
+
const started = await apiRequest(ctx, opts.host, "/api/v1/device/code", { method: "POST", body: opts.request });
|
|
17
|
+
if (started.status !== 200) throw errorOf(started);
|
|
18
|
+
const code = deviceCodeResponseSchema.parse(started.json);
|
|
19
|
+
const userCode = printable(code.user_code);
|
|
20
|
+
const url = printable(code.verification_uri_complete);
|
|
21
|
+
ctx.out(`
|
|
22
|
+
Your code is ${userCode}
|
|
23
|
+
Confirm it at ${url}
|
|
24
|
+
`);
|
|
25
|
+
if (opts.openBrowser) {
|
|
26
|
+
try {
|
|
27
|
+
await ctx.open(url);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
ctx.out("Waiting for you to confirm in the browser...\n");
|
|
32
|
+
const deadline = ctx.now() + code.expires_in * 1e3;
|
|
33
|
+
let interval = code.interval;
|
|
34
|
+
let failures = 0;
|
|
35
|
+
for (; ; ) {
|
|
36
|
+
await ctx.sleep(interval * 1e3);
|
|
37
|
+
if (ctx.now() >= deadline) throw new ApiError(400, "expired_token", "The code expired before it was confirmed. Run the command again.");
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await apiRequest(ctx, opts.host, "/api/v1/device/token", { method: "POST", body: { device_code: code.device_code } });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
failures += 1;
|
|
43
|
+
if (failures >= 5) throw new ApiError(0, "network", `Could not reach ${opts.host}: ${e.message}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (res.status === 200) return opts.parse ? opts.parse(res.json) : deviceTokenSuccessSchema.parse(res.json);
|
|
47
|
+
const err = deviceTokenErrorSchema.safeParse(res.json);
|
|
48
|
+
if (res.status === 429) {
|
|
49
|
+
interval += 5;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!err.success) {
|
|
53
|
+
failures += 1;
|
|
54
|
+
if (failures >= 5 || res.status >= 400 && res.status < 500) throw errorOf(res);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
failures = 0;
|
|
58
|
+
switch (err.data.error) {
|
|
59
|
+
case "authorization_pending":
|
|
60
|
+
break;
|
|
61
|
+
case "slow_down":
|
|
62
|
+
interval += 5;
|
|
63
|
+
break;
|
|
64
|
+
case "access_denied":
|
|
65
|
+
throw new ApiError(400, "access_denied", "The request was denied in the browser.");
|
|
66
|
+
case "expired_token":
|
|
67
|
+
throw new ApiError(400, "expired_token", "The code expired before it was confirmed. Run the command again.");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
deviceLogin
|
|
74
|
+
};
|