@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Errr0rr404
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @hivefloor/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect your coding agents to [Hivefloor](https://hivefloor.worldofz.info): one floor that shows every agent on your machines (Claude Code, Codex, Cursor, Gemini CLI, Grok CLI), what each is working on, who is waiting for you, and what they cost in tokens. Agents also get task tools (claim, update, complete) over MCP.
4
+
5
+ The package installs one command, `hivefloor`. Node.js 22 or later.
6
+
7
+ ## Quick start
8
+
9
+ ```bash
10
+ npx @hivefloor/cli connect
11
+ ```
12
+
13
+ It prints a short code and opens your browser to approve this computer on Hivefloor (sign in there; the CLI never sees your password). Then it sets up the hooks and the background service. Your running agents show up on the Floor within a minute.
14
+
15
+ Install once instead of using `npx`:
16
+
17
+ ```bash
18
+ npm i -g @hivefloor/cli
19
+ hivefloor connect
20
+ ```
21
+
22
+ Headless box with no browser: run `connect --no-open`, then open the printed link on any device where you are signed in.
23
+
24
+ ## Commands
25
+
26
+ ```
27
+ hivefloor <command> [options]
28
+ npx @hivefloor/cli <command> [options] (no install)
29
+
30
+ connect Connect this computer once: every agent on it shows up (Claude profiles, Codex, ...)
31
+ --host URL --name NAME --no-open --no-service --yes
32
+ --agent | --tool claude|codex|cursor|gemini|grok|other connect one agent for one repo (MCP and tasks) instead
33
+ disconnect Remove everything connect added, the machine on the server included (--tool <name> for one agent tool)
34
+ status This machine and its service; the per-repo agent, its projects and live sessions
35
+ service run: the background service (started by your OS; you rarely run it by hand)
36
+ link Link this repo to another project (--project ID | --new NAME [--path PREFIX])
37
+ token Print the stored token for shell use (--reveal to print to a terminal)
38
+ mcp Stdio MCP server that proxies to Hivefloor (for tools without HTTP header support)
39
+ hook <tool> <event>: the hook runner the tools call (reads JSON on stdin)
40
+ help Show this help
41
+ --version Print the version
42
+ ```
43
+
44
+ The default server is `https://hivefloor.worldofz.info`; use `--host` or the `HIVEFLOOR_URL` environment variable for another one.
45
+
46
+ ## What `connect` changes on your computer
47
+
48
+ - `~/.config/hivefloor/` (`%APPDATA%\hivefloor\` on Windows): `machine.json` with the machine token (mode 0600, user-only ACL on Windows), a copy of this CLI that the hooks and the service run, and a small event spool and service log.
49
+ - Hooks in each Claude Code profile's `settings.json` and, if Codex is present, in its `hooks.json` and `config.toml`. Every entry is marked as Hivefloor's.
50
+ - A user-level background service: a launchd agent on macOS, a systemd user unit on Linux, a Scheduled Task on Windows. Skip it with `--no-service`.
51
+
52
+ `hivefloor disconnect` removes all of it and tells the server to delete the machine. Nothing is installed with administrator rights.
53
+
54
+ ## Privacy: what is sent to the server
55
+
56
+ Only to the Hivefloor server you connect to (the default host or `--host`), over HTTPS, with the machine token. There is no telemetry and no third-party service.
57
+
58
+ - **Connecting:** the machine name (your hostname unless you pass `--name`), OS and CLI version.
59
+ - **Hook events** (when an agent starts or ends a session, you submit a prompt, the agent stops, waits for you, or starts or stops a subagent): the tool, the event name, the session id, the model name, the git remote, branch and subfolder of the working directory, and for a stop or subagent event the first line of the agent's last message, with secrets masked and cut to 140 characters. A subagent's type and its short description (140 characters) are sent too. Your prompt text, file contents, tool inputs and outputs, and full transcripts are not sent.
60
+ - **Heartbeat** (about every 30 seconds): OS, CLI version, memory total and use, load average, and token and cost counters per agent profile. The counters are numbers read from the `usage` fields of your local Claude Code and Codex session files from the last 24 hours; message text is never read into what is sent.
61
+ - **Discovery** (about every minute): for each agent tool found running, its name, a profile key (for example `default` or `work`), a label such as `Claude Max (work)` (from the plan name in the profile's credentials file; no email, no token), the model, and how many agent processes are alive. No command lines, paths or prompts.
62
+ - **`link`, `--agent` mode and `mcp`:** the same repo details as above, and your tool's MCP calls to the server's `/mcp` endpoint.
63
+
64
+ The machine token stays on your computer (the file above, or the OS keychain in `--agent` mode) and is sent only to the host it was issued for.
65
+
66
+ ## Troubleshooting
67
+
68
+ - `hivefloor status` shows the machine, whether the service is alive and the last heartbeat.
69
+ - The service log is in the config directory above (`service.log`).
70
+ - The hooks never print and always exit 0, so they cannot break your agent.
71
+
72
+ ## Links
73
+
74
+ - Source and issues: https://github.com/Errr0rr404/hivefloor
75
+ - Docs: https://hivefloor.worldofz.info
76
+
77
+ MIT License.
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/tools.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var REAL_TOOLS = ["claude", "codex", "cursor", "gemini", "grok"];
7
+ var TOOL_LABEL = {
8
+ claude: "Claude Code",
9
+ codex: "Codex",
10
+ cursor: "Cursor",
11
+ gemini: "Gemini CLI",
12
+ grok: "Grok CLI",
13
+ other: "Other"
14
+ };
15
+ var BINARIES = {
16
+ claude: ["claude"],
17
+ codex: ["codex"],
18
+ cursor: ["cursor-agent", "agent"],
19
+ gemini: ["gemini"],
20
+ grok: ["grok"]
21
+ };
22
+ function isTool(s) {
23
+ return s === "other" || REAL_TOOLS.includes(s);
24
+ }
25
+ function findBin(name, env) {
26
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
27
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
28
+ if (!dir) continue;
29
+ for (const ext of exts) {
30
+ const p = path.join(dir, name + ext);
31
+ try {
32
+ if (fs.statSync(p).isFile()) {
33
+ fs.accessSync(p, fs.constants.X_OK);
34
+ return p;
35
+ }
36
+ } catch {
37
+ }
38
+ }
39
+ }
40
+ return void 0;
41
+ }
42
+ function detectTools(env) {
43
+ const found = [];
44
+ for (const tool of REAL_TOOLS) {
45
+ for (const name of BINARIES[tool]) {
46
+ const bin = findBin(name, env);
47
+ if (bin) {
48
+ found.push({ tool, bin });
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+ function toolFromEnv(env) {
56
+ if (env.GROK_SESSION_ID) return "grok";
57
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude";
58
+ if (env.CODEX_SANDBOX || env.CODEX_SESSION_ID || env.CODEX_THREAD_ID || env.CODEX_CI) return "codex";
59
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
60
+ if (env.GEMINI_CLI || env.GEMINI_SESSION_ID) return "gemini";
61
+ return void 0;
62
+ }
63
+
64
+ export {
65
+ REAL_TOOLS,
66
+ TOOL_LABEL,
67
+ isTool,
68
+ findBin,
69
+ detectTools,
70
+ toolFromEnv
71
+ };
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ atomicWrite,
4
+ configDir,
5
+ isRecord,
6
+ machineFile,
7
+ readText,
8
+ serviceStateFile
9
+ } from "./chunk-WNDUGQDX.js";
10
+
11
+ // src/machine.ts
12
+ import fs from "fs";
13
+ import os from "os";
14
+ function readMachine() {
15
+ const text = readText(machineFile());
16
+ if (!text) return void 0;
17
+ try {
18
+ const j = JSON.parse(text);
19
+ if (!isRecord(j) || typeof j.token !== "string" || typeof j.host !== "string" || !isRecord(j.machine)) return void 0;
20
+ const inst = isRecord(j.installed) ? j.installed : {};
21
+ return {
22
+ version: 1,
23
+ host: j.host,
24
+ machine: { id: String(j.machine.id ?? ""), name: String(j.machine.name ?? "") },
25
+ token: j.token,
26
+ connected_at: typeof j.connected_at === "string" ? j.connected_at : "",
27
+ ...j.revoked === true ? { revoked: true } : {},
28
+ installed: {
29
+ claude_dirs: Array.isArray(inst.claude_dirs) ? inst.claude_dirs.filter((x) => typeof x === "string") : [],
30
+ ...typeof inst.codex_dir === "string" ? { codex_dir: inst.codex_dir } : {},
31
+ ...isRecord(inst.service) && typeof inst.service.file === "string" && (inst.service.kind === "launchd" || inst.service.kind === "systemd" || inst.service.kind === "windows") ? { service: { kind: inst.service.kind, file: inst.service.file } } : {}
32
+ }
33
+ };
34
+ } catch {
35
+ return void 0;
36
+ }
37
+ }
38
+ function writeMachine(m) {
39
+ const dir = configDir();
40
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
41
+ fs.chmodSync(dir, 448);
42
+ try {
43
+ if (fs.lstatSync(machineFile()).isSymbolicLink()) {
44
+ throw new Error(`${machineFile()} is a symbolic link; the machine token is not written through links. Remove the link and run connect again.`);
45
+ }
46
+ } catch (e) {
47
+ if (e.code !== "ENOENT") throw e;
48
+ }
49
+ atomicWrite(machineFile(), JSON.stringify(m, null, 2) + "\n", 384);
50
+ }
51
+ var removeMachineFile = () => fs.rmSync(machineFile(), { force: true });
52
+ async function secureWindowsMachine(run, username = os.userInfo().username) {
53
+ if (!username) {
54
+ removeMachineFile();
55
+ throw new Error("could not determine the Windows user; the machine token was not kept");
56
+ }
57
+ const dir = configDir();
58
+ const d = await run("icacls", [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`]);
59
+ if (d.code === 0) {
60
+ const f = await run("icacls", [machineFile(), "/inheritance:r", "/grant:r", `${username}:F`]);
61
+ if (f.code === 0) return;
62
+ }
63
+ removeMachineFile();
64
+ throw new Error("could not restrict the Hivefloor config to your Windows user with icacls; the machine token was not kept");
65
+ }
66
+ var SERVICE_ALIVE_MS = 9e4;
67
+ function readServiceState() {
68
+ const text = readText(serviceStateFile());
69
+ if (!text) return void 0;
70
+ try {
71
+ const j = JSON.parse(text);
72
+ if (isRecord(j) && typeof j.beat === "number" && typeof j.pid === "number") return j;
73
+ } catch {
74
+ }
75
+ return void 0;
76
+ }
77
+ function writeServiceState(s) {
78
+ atomicWrite(serviceStateFile(), JSON.stringify(s) + "\n", 384);
79
+ }
80
+ var serviceAlive = (nowMs) => {
81
+ const s = readServiceState();
82
+ return s !== void 0 && nowMs - s.beat < SERVICE_ALIVE_MS;
83
+ };
84
+
85
+ export {
86
+ readMachine,
87
+ writeMachine,
88
+ removeMachineFile,
89
+ secureWindowsMachine,
90
+ SERVICE_ALIVE_MS,
91
+ readServiceState,
92
+ writeServiceState,
93
+ serviceAlive
94
+ };
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ RC_LINE,
4
+ addRcLine,
5
+ hasRcLine,
6
+ installClaude,
7
+ installCodex,
8
+ installCursor,
9
+ installGemini,
10
+ installGrok,
11
+ rcFileFor,
12
+ updateManifest
13
+ } from "./chunk-YCZ7XJJF.js";
14
+ import {
15
+ readMachine
16
+ } from "./chunk-3IMV5EHP.js";
17
+ import {
18
+ deviceLogin
19
+ } from "./chunk-WO7NALQO.js";
20
+ import {
21
+ detectRepo
22
+ } from "./chunk-CWEEIRW2.js";
23
+ import {
24
+ REAL_TOOLS,
25
+ TOOL_LABEL,
26
+ detectTools,
27
+ findBin,
28
+ isTool,
29
+ toolFromEnv
30
+ } from "./chunk-2IMJECYD.js";
31
+ import {
32
+ checkUnknownFlags,
33
+ flagStr
34
+ } from "./chunk-TKMIV7BG.js";
35
+ import {
36
+ saveToken
37
+ } from "./chunk-Q2TS3GT3.js";
38
+ import {
39
+ VERSION
40
+ } from "./chunk-WLHRUZTR.js";
41
+ import {
42
+ ApiError
43
+ } from "./chunk-PY74LT2W.js";
44
+ import {
45
+ ConfigError
46
+ } from "./chunk-WNDUGQDX.js";
47
+ import {
48
+ CLI_BIN_NAME,
49
+ CLI_INSTALL_COMMAND,
50
+ cliNpxCommand,
51
+ hostFromEnv,
52
+ machineName,
53
+ normalizeHost
54
+ } from "./chunk-ZYRPQPGL.js";
55
+
56
+ // src/connect.ts
57
+ var INSTALLERS = {
58
+ claude: installClaude,
59
+ codex: installCodex,
60
+ cursor: installCursor,
61
+ gemini: installGemini,
62
+ grok: installGrok
63
+ };
64
+ function resolveHost(p, ctx) {
65
+ const h = flagStr(p, "host");
66
+ return h ? normalizeHost(h) : hostFromEnv(ctx.env);
67
+ }
68
+ async function chooseTool(ctx, p) {
69
+ const detected = detectTools(ctx.env).map((d) => d.tool);
70
+ const flag = flagStr(p, "tool");
71
+ if (flag !== void 0) {
72
+ if (!isTool(flag)) throw new ApiError(0, "invalid", `Unknown tool "${flag}". Use one of: ${[...REAL_TOOLS, "other"].join(", ")}.`);
73
+ return { tool: flag, others: detected.filter((t) => t !== flag) };
74
+ }
75
+ const hint = toolFromEnv(ctx.env);
76
+ let tool = hint ?? detected[0] ?? "other";
77
+ if (!hint && detected.length > 1 && ctx.stdoutIsTTY && !p.flags.yes) {
78
+ const answer = (await ctx.ask(`Which tool should this agent be? (${detected.join(", ")}) [${tool}]: `)).trim().toLowerCase();
79
+ if (isTool(answer)) tool = answer;
80
+ }
81
+ return { tool, others: detected.filter((t) => t !== tool) };
82
+ }
83
+ async function connectAgent(ctx, p) {
84
+ const host = resolveHost(p, ctx);
85
+ const { tool, others } = await chooseTool(ctx, p);
86
+ const repo = await detectRepo(ctx.cwd);
87
+ ctx.out(`Connecting ${TOOL_LABEL[tool]} on ${machineName()} to ${host}
88
+ `);
89
+ if (repo) ctx.out(`Repo: ${repo.remote}${repo.branch ? ` (${repo.branch})` : ""}${repo.path ? ` in ${repo.path}` : ""}
90
+ `);
91
+ else ctx.out("No git remote here: the agent will use your default project.\n");
92
+ const request = {
93
+ client: { tool, machine: machineName(), os: process.platform, cli_version: VERSION },
94
+ ...repo ? { repo: { remote: repo.remote, ...repo.branch ? { branch: repo.branch } : {}, ...repo.path ? { path: repo.path } : {} } } : {}
95
+ };
96
+ const canOpen = !p.flags["no-open"] && (process.platform !== "linux" || Boolean(ctx.env.DISPLAY || ctx.env.WAYLAND_DISPLAY));
97
+ const result = await deviceLogin(ctx, { host, request, openBrowser: canOpen });
98
+ const storage = await saveToken(host, tool, result.token, { agent: result.agent, project: result.project });
99
+ const rc = rcFileFor(ctx.env);
100
+ let rcInstalled = rc !== void 0 && hasRcLine(rc);
101
+ let rcAdded;
102
+ if (rc && !rcInstalled && tool !== "other") {
103
+ let yes = Boolean(p.flags.yes);
104
+ if (!yes && ctx.stdoutIsTTY) yes = /^y(es)?$/i.test((await ctx.ask(`Add this line to ${rc} so your tools can read the token?
105
+ ${RC_LINE}
106
+ Add it? [y/N] `)).trim());
107
+ if (yes) {
108
+ addRcLine(rc);
109
+ rcInstalled = true;
110
+ rcAdded = rc;
111
+ updateManifest((m) => {
112
+ if (!m.rc.includes(rc)) m.rc.push(rc);
113
+ });
114
+ }
115
+ }
116
+ const machine = readMachine();
117
+ const ownsHooks = machine !== void 0 && !machine.revoked;
118
+ const lines = [];
119
+ const warnings = [];
120
+ const mcpUrl = `${host}/mcp`;
121
+ if (tool === "other") {
122
+ lines.push(`MCP server: ${mcpUrl} (streamable HTTP), header "Authorization: Bearer <token>" (run: ${cliNpxCommand("token --reveal")})`);
123
+ lines.push("The agent shows up the first time it calls a tool.");
124
+ } else {
125
+ try {
126
+ const r = await INSTALLERS[tool]({
127
+ ctx,
128
+ mcpUrl,
129
+ token: result.token,
130
+ useEnvToken: rcInstalled,
131
+ noCli: Boolean(p.flags["no-cli"]),
132
+ skipHooks: ownsHooks,
133
+ onMcpRegistered: (mcp) => {
134
+ updateManifest((m) => {
135
+ m.tools[tool] = { mcp, host };
136
+ });
137
+ }
138
+ });
139
+ lines.push(...r.report.lines);
140
+ warnings.push(...r.report.warnings);
141
+ updateManifest((m) => {
142
+ m.tools[tool] = { mcp: r.mcp, host };
143
+ });
144
+ } catch (e) {
145
+ if (!(e instanceof ConfigError)) throw e;
146
+ warnings.push(e.message);
147
+ }
148
+ }
149
+ if (rcAdded) lines.push(`Shell: added the token export to ${rcAdded}`);
150
+ if (!findBin(CLI_BIN_NAME, ctx.env) && tool !== "other") warnings.push(`"${CLI_BIN_NAME}" is not on your PATH, so the hooks cannot run it. Install it once: ${CLI_INSTALL_COMMAND}`);
151
+ ctx.out(`
152
+ Connected as ${result.agent.name} to project ${result.project.name} (${result.project.key}).
153
+ `);
154
+ ctx.out(`Token stored in ${storage === "keychain" ? "the OS keychain" : "~/.config/hivefloor/credentials.json (mode 0600)"}.
155
+ `);
156
+ for (const l of lines) ctx.out(` ${l}
157
+ `);
158
+ for (const w of warnings) ctx.out(` note: ${w}
159
+ `);
160
+ if (others.length > 0) ctx.out(`Also installed: ${others.join(", ")}. Connect each with: ${cliNpxCommand("connect --tool <name>")}
161
+ `);
162
+ ctx.out(`Undo everything with: ${cliNpxCommand("disconnect")}
163
+ `);
164
+ return 0;
165
+ }
166
+ var CONNECT_FLAGS = /* @__PURE__ */ new Set(["host", "name", "no-open", "no-service", "yes", "y", "agent", "tool", "no-cli", "help", "h"]);
167
+ async function connect(ctx, p) {
168
+ const unknown = checkUnknownFlags(p, CONNECT_FLAGS);
169
+ if (unknown) {
170
+ ctx.err(`hivefloor: unknown flag "--${unknown}". Run "hivefloor help".
171
+ `);
172
+ return 2;
173
+ }
174
+ if (p.flags.agent || p.flags.tool !== void 0) return connectAgent(ctx, p);
175
+ return (await import("./machine-connect-I3OZCTUZ.js")).connectMachine(ctx, p);
176
+ }
177
+
178
+ export {
179
+ resolveHost,
180
+ connectAgent,
181
+ connect
182
+ };
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatRemote,
4
+ normalizeRemote,
5
+ runCmd
6
+ } from "./chunk-ZYRPQPGL.js";
7
+
8
+ // src/git.ts
9
+ function remoteOf(url) {
10
+ const n = normalizeRemote(url);
11
+ return n ? formatRemote(n) : void 0;
12
+ }
13
+ async function git(cwd, args) {
14
+ const r = await runCmd("git", args, { cwd, timeoutMs: 2e3, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
15
+ return r.code === 0 ? r.stdout.trim() : void 0;
16
+ }
17
+ async function detectRepo(cwd) {
18
+ const [origin, upstream, branch, prefix] = await Promise.all([
19
+ git(cwd, ["remote", "get-url", "origin"]),
20
+ git(cwd, ["remote", "get-url", "upstream"]),
21
+ git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]),
22
+ git(cwd, ["rev-parse", "--show-prefix"])
23
+ ]);
24
+ const remote = origin ? remoteOf(origin) : void 0;
25
+ if (!remote) return void 0;
26
+ const path = prefix?.replace(/\/+$/, "");
27
+ const up = upstream ? remoteOf(upstream) : void 0;
28
+ return {
29
+ remote,
30
+ ...branch ? { branch } : {},
31
+ ...path ? { path } : {},
32
+ ...up && up.toLowerCase() !== remote.toLowerCase() ? { upstream: up } : {}
33
+ };
34
+ }
35
+
36
+ export {
37
+ detectRepo
38
+ };
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hookclock.ts
4
+ var HARD_EXIT_MS = 4e3;
5
+
6
+ export {
7
+ HARD_EXIT_MS
8
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ export {
9
+ __export
10
+ };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isTool
4
+ } from "./chunk-2IMJECYD.js";
5
+ import {
6
+ flagStr
7
+ } from "./chunk-TKMIV7BG.js";
8
+ import {
9
+ defaultHost,
10
+ loadToken
11
+ } from "./chunk-Q2TS3GT3.js";
12
+ import {
13
+ hostFromEnv,
14
+ normalizeHost
15
+ } from "./chunk-ZYRPQPGL.js";
16
+
17
+ // src/session.ts
18
+ async function resolveSession(ctx, p) {
19
+ const flagHost = flagStr(p, "host");
20
+ const host = flagHost ? normalizeHost(flagHost) : ctx.env.HIVEFLOOR_URL ? hostFromEnv(ctx.env) : defaultHost() ?? hostFromEnv(ctx.env);
21
+ const toolFlag = flagStr(p, "tool");
22
+ const t = await loadToken(host, toolFlag && isTool(toolFlag) ? toolFlag : void 0);
23
+ if (t) return { host, token: t.token, tool: t.tool };
24
+ if (ctx.env.HIVEFLOOR_TOKEN) return { host, token: ctx.env.HIVEFLOOR_TOKEN, tool: toolFlag ?? "other" };
25
+ return void 0;
26
+ }
27
+
28
+ export {
29
+ resolveSession
30
+ };
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ home,
4
+ isRecord,
5
+ readText
6
+ } from "./chunk-WNDUGQDX.js";
7
+ import {
8
+ PROFILE_KEY_RE
9
+ } from "./chunk-ZYRPQPGL.js";
10
+
11
+ // src/profiles.ts
12
+ import fs from "fs";
13
+ import path from "path";
14
+ function profileKeyOf(tool, dir, homeDir = home()) {
15
+ const std = tool === "claude" ? path.join(homeDir, ".claude") : path.join(homeDir, ".codex");
16
+ if (path.resolve(dir) === path.resolve(std)) return "default";
17
+ let base = path.basename(path.resolve(dir)).toLowerCase().replace(/^\.+/, "");
18
+ base = base.replace(new RegExp(`^${tool}[-_.]?`), "").replace(new RegExp(`[-_.]?${tool}$`), "");
19
+ base = base.replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "").slice(0, 40);
20
+ return PROFILE_KEY_RE.test(base) ? base : "profile";
21
+ }
22
+ function profileKeys(tool, dirs, homeDir = home()) {
23
+ const out = /* @__PURE__ */ new Map();
24
+ const used = /* @__PURE__ */ new Set();
25
+ for (const d of dirs) {
26
+ out.set(d, uniqueProfileKey(profileKeyOf(tool, d, homeDir), used));
27
+ }
28
+ return out;
29
+ }
30
+ function uniqueProfileKey(base, used) {
31
+ const stem = base.replace(/-\d+$/, "");
32
+ let k = base;
33
+ for (let n = 2; used.has(k); n++) {
34
+ const suf = `-${n}`;
35
+ k = `${stem.slice(0, Math.max(0, 40 - suf.length))}${suf}`;
36
+ }
37
+ used.add(k);
38
+ return k;
39
+ }
40
+ var RC_FILES = [".zshrc", ".zprofile", ".zshenv", ".bashrc", ".bash_profile", ".profile", ".config/fish/config.fish"];
41
+ function claudeDirsFromRc(homeDir = home()) {
42
+ const found = [];
43
+ for (const rel of RC_FILES) {
44
+ const text = readText0(path.join(homeDir, rel));
45
+ if (!text) continue;
46
+ for (const line of text.split("\n")) {
47
+ if (/^\s*#/.test(line)) continue;
48
+ for (const m of line.matchAll(/CLAUDE_CONFIG_DIR(?:=|\s+)(?:"([^"\n]+)"|'([^'\n]+)'|([^\s"'`;&|)]+))/g)) {
49
+ const raw = (m[1] ?? m[2] ?? m[3] ?? "").trim();
50
+ const dir = expand(raw, homeDir);
51
+ if (dir && isDir(dir)) found.push(dir);
52
+ }
53
+ }
54
+ }
55
+ return found;
56
+ }
57
+ function isDir(p) {
58
+ try {
59
+ return fs.statSync(p).isDirectory();
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function readText0(file) {
65
+ try {
66
+ return readText(file);
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+ function expand(raw, homeDir) {
72
+ let s = raw.replace(/^~(?=\/|$)/, homeDir).replace(/\$\{?HOME\}?/g, homeDir);
73
+ if (/[$`]/.test(s) || !path.isAbsolute(s)) return void 0;
74
+ s = path.resolve(s);
75
+ return s;
76
+ }
77
+ function claudeSiblingDirs(homeDir = home()) {
78
+ const out = [];
79
+ let names = [];
80
+ try {
81
+ names = fs.readdirSync(homeDir);
82
+ } catch {
83
+ return out;
84
+ }
85
+ for (const n of names) {
86
+ if (!n.startsWith(".claude") || n === ".claude.json" || n.endsWith(".bak")) continue;
87
+ const dir = path.join(homeDir, n);
88
+ try {
89
+ if (!fs.statSync(dir).isDirectory()) continue;
90
+ if (fs.existsSync(path.join(dir, "settings.json")) || fs.existsSync(path.join(dir, ".claude.json"))) out.push(dir);
91
+ } catch {
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function claudeConfigDirs(fromProcesses, homeDir = home()) {
97
+ const all = [path.join(homeDir, ".claude"), ...fromProcesses, ...claudeDirsFromRc(homeDir), ...claudeSiblingDirs(homeDir)];
98
+ const seen = /* @__PURE__ */ new Set();
99
+ const out = [];
100
+ for (const d of all) {
101
+ const r = path.resolve(d);
102
+ if (seen.has(r)) continue;
103
+ seen.add(r);
104
+ out.push(r);
105
+ }
106
+ return out;
107
+ }
108
+ function codexConfigDir(env = process.env) {
109
+ return env.CODEX_HOME ? path.resolve(env.CODEX_HOME) : path.join(home(), ".codex");
110
+ }
111
+ var PLAN = { max: "Max", pro: "Pro", team: "Team", enterprise: "Enterprise", free: "Free" };
112
+ function claudeLabel(dir, key) {
113
+ let plan;
114
+ try {
115
+ const j = JSON.parse(fs.readFileSync(path.join(dir, ".credentials.json"), "utf8"));
116
+ const oauth = isRecord(j) ? j.claudeAiOauth : void 0;
117
+ const t = isRecord(oauth) ? oauth.subscriptionType : void 0;
118
+ if (typeof t === "string") plan = PLAN[t.toLowerCase()];
119
+ } catch {
120
+ }
121
+ return `Claude${plan ? ` ${plan}` : ""} (${key})`;
122
+ }
123
+ function codexLabel(dir) {
124
+ try {
125
+ const j = JSON.parse(fs.readFileSync(path.join(dir, "auth.json"), "utf8"));
126
+ const mode = isRecord(j) && typeof j.auth_mode === "string" ? j.auth_mode.toLowerCase() : "";
127
+ if (mode === "chatgpt") return "Codex (ChatGPT)";
128
+ if (mode === "apikey" || mode === "api_key") return "Codex (API key)";
129
+ } catch {
130
+ }
131
+ return "Codex";
132
+ }
133
+
134
+ export {
135
+ profileKeyOf,
136
+ profileKeys,
137
+ uniqueProfileKey,
138
+ claudeDirsFromRc,
139
+ claudeConfigDirs,
140
+ codexConfigDir,
141
+ claudeLabel,
142
+ codexLabel
143
+ };