@hivefloor/cli 0.1.4 → 0.1.8

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.
Files changed (55) hide show
  1. package/LICENSE +6 -20
  2. package/README.md +31 -9
  3. package/dist/chunk-2ERPDCOT.js +38 -0
  4. package/dist/chunk-4G362W6G.js +19 -0
  5. package/dist/chunk-5OJUJCE3.js +817 -0
  6. package/dist/chunk-5XXYQQG6.js +274 -0
  7. package/dist/chunk-AGIYVFDQ.js +10984 -0
  8. package/dist/{chunk-2BSA7LFD.js → chunk-BLLPDPTL.js} +7 -3
  9. package/dist/{chunk-R5KDI4WI.js → chunk-FPJNGKT5.js} +9 -3
  10. package/dist/chunk-HW3KRNCO.js +201 -0
  11. package/dist/{chunk-ZC2DAKB5.js → chunk-HWBUESB4.js} +1500 -316
  12. package/dist/{chunk-DNG5CRT3.js → chunk-K2IRTZH5.js} +8 -7
  13. package/dist/chunk-KQMPCV57.js +122 -0
  14. package/dist/{chunk-UJ4BQFWV.js → chunk-L6EFW2SJ.js} +53 -31
  15. package/dist/{chunk-T7TG6WA4.js → chunk-LX5A25O6.js} +4 -2
  16. package/dist/{chunk-W7DROKC7.js → chunk-MYJCU5JL.js} +89 -13
  17. package/dist/chunk-NMUH5NXN.js +52 -0
  18. package/dist/chunk-NRJONTFT.js +67 -0
  19. package/dist/chunk-QBBKGORP.js +327 -0
  20. package/dist/chunk-QGULSOQ2.js +142 -0
  21. package/dist/{chunk-PDXYOREI.js → chunk-QLMLB7RB.js} +110 -31
  22. package/dist/{chunk-TKMIV7BG.js → chunk-QPAFN47B.js} +3 -1
  23. package/dist/chunk-SBOEAOHD.js +48 -0
  24. package/dist/{chunk-SD65CIIY.js → chunk-TIR4CV3P.js} +117 -33
  25. package/dist/{chunk-QHYZBHXO.js → chunk-X2KH6AR4.js} +195 -251
  26. package/dist/chunk-XAXKZ572.js +106 -0
  27. package/dist/{chunk-VNCQFFQO.js → chunk-XPGL7VYE.js} +114 -1
  28. package/dist/cli-BFOJWJNG.js +279 -0
  29. package/dist/connect-ZJ4JXBAJ.js +27 -0
  30. package/dist/control-6GHTZQJV.js +25 -0
  31. package/dist/{disconnect-MKRHK4CX.js → disconnect-3T25NTKX.js} +55 -34
  32. package/dist/hook-TPSHBCWU.js +575 -0
  33. package/dist/index.js +9 -6
  34. package/dist/{link-HXBMMSG3.js → link-EAS5F5PC.js} +34 -18
  35. package/dist/{machine-connect-BYHZRRRB.js → machine-connect-KEI4BRWF.js} +30 -24
  36. package/dist/mcp-LVIYR3EN.js +1882 -0
  37. package/dist/orch-KVRL2YA2.js +137 -0
  38. package/dist/{run-336BSU72.js → run-JL2OZWK3.js} +1933 -29
  39. package/dist/{status-KBBSAADD.js → status-QXOOJGGN.js} +29 -14
  40. package/dist/token-BFWWQIDA.js +44 -0
  41. package/dist/voice-QXHFXRWG.js +264 -0
  42. package/dist/{where-KR7VWSEC.js → where-CMUORQIO.js} +20 -8
  43. package/package.json +6 -7
  44. package/dist/chunk-2IMJECYD.js +0 -71
  45. package/dist/chunk-7KF7IQJN.js +0 -30
  46. package/dist/chunk-GT3HFFT4.js +0 -132
  47. package/dist/chunk-JCLWCMEM.js +0 -8
  48. package/dist/chunk-K3NQKI34.js +0 -10
  49. package/dist/chunk-O2TC3WSS.js +0 -303
  50. package/dist/chunk-PY74LT2W.js +0 -41
  51. package/dist/cli-P3DYE5IR.js +0 -103
  52. package/dist/connect-ACNM5DQG.js +0 -23
  53. package/dist/hook-KR2HGVAR.js +0 -301
  54. package/dist/mcp-N2TMD4GR.js +0 -121
  55. package/dist/token-JDRLEV5D.js +0 -32
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __hfCreateRequire } from 'node:module';
3
+ const require = __hfCreateRequire(import.meta.url);
4
+ import {
5
+ userBinDirs
6
+ } from "./chunk-XAXKZ572.js";
2
7
  import {
3
8
  atomicWrite,
4
9
  atomicWriteUtf16,
@@ -8,11 +13,15 @@ import {
8
13
  isRecord,
9
14
  readText,
10
15
  removeFile
11
- } from "./chunk-VNCQFFQO.js";
16
+ } from "./chunk-XPGL7VYE.js";
17
+ import {
18
+ normalizeHost,
19
+ runCmd,
20
+ sameOrigin
21
+ } from "./chunk-QGULSOQ2.js";
12
22
  import {
13
- cliNpxCommand,
14
- runCmd
15
- } from "./chunk-ZC2DAKB5.js";
23
+ cliNpxCommand
24
+ } from "./chunk-HWBUESB4.js";
16
25
 
17
26
  // src/service/units.ts
18
27
  import os from "os";
@@ -23,8 +32,11 @@ var SCHTASKS_TASK = "Hivefloor Machine";
23
32
  var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24
33
  var sdEscape = (s) => s.replace(/(["\\$])/g, "\\$1").replace(/%/g, "%%");
25
34
  var sdQuote = (s) => /^[\w@+=:,./-]+$/.test(s) ? s : `"${sdEscape(s)}"`;
35
+ function unitPath(plan, system, homeDir = home()) {
36
+ if (/[\r\n\0]/.test(homeDir)) throw new Error("refusing to write a service unit: HOME contains a control character");
37
+ return [path.dirname(plan.node), ...userBinDirs(homeDir), ...system].filter((d, i, a) => a.indexOf(d) === i).join(":");
38
+ }
26
39
  function launchdPlist(plan) {
27
- const bin = path.dirname(plan.node);
28
40
  return `<?xml version="1.0" encoding="UTF-8"?>
29
41
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
30
42
  <plist version="1.0">
@@ -50,7 +62,7 @@ function launchdPlist(plan) {
50
62
  <key>EnvironmentVariables</key>
51
63
  <dict>
52
64
  <key>PATH</key>
53
- <string>${xml(`${bin}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`)}</string>
65
+ <string>${xml(unitPath(plan, ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]))}</string>
54
66
  </dict>
55
67
  <key>StandardOutPath</key>
56
68
  <string>/dev/null</string>
@@ -62,14 +74,14 @@ function launchdPlist(plan) {
62
74
  }
63
75
  function systemdUnit(plan) {
64
76
  return `[Unit]
65
- Description=Hivefloor machine service
77
+ Description=HiveFloor machine service
66
78
  After=network-online.target
67
79
 
68
80
  [Service]
69
81
  ExecStart=${sdQuote(plan.node)} ${sdQuote(plan.script)} service run
70
82
  Restart=on-failure
71
83
  RestartSec=30
72
- Environment="PATH=${sdEscape(`${path.dirname(plan.node)}:/usr/local/bin:/usr/bin:/bin`)}"
84
+ Environment="PATH=${sdEscape(unitPath(plan, ["/usr/bin", "/bin"]))}"
73
85
 
74
86
  [Install]
75
87
  WantedBy=default.target
@@ -91,7 +103,7 @@ function windowsTaskXml(plan, user) {
91
103
  return `<?xml version="1.0" encoding="UTF-16"?>
92
104
  <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
93
105
  <RegistrationInfo>
94
- <Description>Hivefloor machine service</Description>
106
+ <Description>HiveFloor machine service</Description>
95
107
  </RegistrationInfo>
96
108
  <Triggers>
97
109
  <LogonTrigger>
@@ -215,8 +227,13 @@ async function uninstallService(svc, run, uid) {
215
227
  }
216
228
 
217
229
  // src/credentials.ts
230
+ import { createHash } from "crypto";
218
231
  import fs from "fs";
219
- var SERVICE = "hivefloor";
232
+ var KEYCHAIN_SERVICE = "hivefloor";
233
+ var TOKEN_FP_HEX = 16;
234
+ function tokenFingerprint(token) {
235
+ return createHash("sha256").update(token, "utf8").digest("hex").slice(0, TOKEN_FP_HEX);
236
+ }
220
237
  var realKeyring = async () => {
221
238
  if (process.env.HIVEFLOOR_KEYRING === "0") return void 0;
222
239
  try {
@@ -227,6 +244,19 @@ var realKeyring = async () => {
227
244
  };
228
245
  var loadKeyring = realKeyring;
229
246
  var account = (host, tool) => `${host}#${tool}`;
247
+ function canonicalHost(host) {
248
+ try {
249
+ return normalizeHost(host);
250
+ } catch {
251
+ return void 0;
252
+ }
253
+ }
254
+ function hostKey(creds, host) {
255
+ const want = canonicalHost(host);
256
+ if (want === void 0) return creds.hosts[host] ? host : void 0;
257
+ if (creds.hosts[want]) return want;
258
+ return Object.keys(creds.hosts).find((k) => k === want || sameOrigin(k, want));
259
+ }
230
260
  function readCredentials() {
231
261
  const text = readText(credentialsFile());
232
262
  const empty = { version: 1, hosts: {} };
@@ -256,33 +286,53 @@ function writeCredentials(c) {
256
286
  }
257
287
  atomicWrite(file, JSON.stringify(c, null, 2) + "\n", 384);
258
288
  }
289
+ var windowsAclError = () => new Error("could not restrict the HiveFloor config to your Windows user with icacls; the token was not kept");
259
290
  async function secureWindowsCredentials(run, username) {
260
291
  if (!username) throw new Error("could not determine the Windows user; the token was not kept");
261
292
  const dir = prepareConfigDir();
262
293
  const d = await run("icacls", [dir, "/inheritance:r", "/grant:r", `${username}:(OI)(CI)F`]);
263
- if (d.code !== 0) throw new Error("could not restrict the Hivefloor config to your Windows user with icacls; the token was not kept");
294
+ if (d.code !== 0) throw windowsAclError();
295
+ }
296
+ async function secureWindowsCredentialsFile(run, username) {
297
+ const f = await run("icacls", [credentialsFile(), "/inheritance:r", "/grant:r", `${username}:F`]);
298
+ if (f.code !== 0) throw windowsAclError();
264
299
  }
265
300
  async function saveToken(host, tool, token, meta = {}, opts = {}) {
266
301
  const creds = readCredentials();
267
- const h = creds.hosts[host] ??= { tools: {} };
302
+ const key = hostKey(creds, host) ?? canonicalHost(host) ?? host;
303
+ const h = creds.hosts[key] ??= { tools: {} };
268
304
  let storage = "file";
269
305
  const kr = await loadKeyring();
270
306
  if (kr) {
271
307
  try {
272
- const entry = new kr.Entry(SERVICE, account(host, tool));
308
+ const entry = new kr.Entry(KEYCHAIN_SERVICE, account(key, tool));
273
309
  entry.setPassword(token);
274
310
  if (entry.getPassword() === token) storage = "keychain";
275
311
  } catch {
276
312
  storage = "file";
277
313
  }
278
314
  }
279
- if (storage === "file" && (opts.platform ?? process.platform) === "win32") {
280
- await secureWindowsCredentials(opts.run ?? runCmd, opts.username ?? windowsUser(process.env));
315
+ const winFile = storage === "file" && (opts.platform ?? process.platform) === "win32";
316
+ const run = opts.run ?? runCmd;
317
+ const username = opts.username ?? windowsUser(process.env);
318
+ if (winFile) {
319
+ await secureWindowsCredentials(run, username);
281
320
  }
321
+ const previous = winFile ? readText(credentialsFile()) : void 0;
282
322
  h.tools[tool] = { ...storage === "keychain" ? { keychain: true } : { token }, ...meta, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
283
323
  h.last_tool = tool;
284
- creds.default_host = host;
324
+ h.token_fp = tokenFingerprint(token);
325
+ creds.default_host = key;
285
326
  writeCredentials(creds);
327
+ if (winFile) {
328
+ try {
329
+ await secureWindowsCredentialsFile(run, username);
330
+ } catch (e) {
331
+ if (previous === void 0) fs.rmSync(credentialsFile(), { force: true });
332
+ else atomicWrite(credentialsFile(), previous, 384);
333
+ throw e;
334
+ }
335
+ }
286
336
  return storage;
287
337
  }
288
338
  function pickTool(h, tool) {
@@ -291,8 +341,10 @@ function pickTool(h, tool) {
291
341
  return Object.keys(h.tools)[0];
292
342
  }
293
343
  async function loadToken(host, tool) {
294
- const h = readCredentials().hosts[host];
295
- if (!h) return void 0;
344
+ const creds = readCredentials();
345
+ const key = hostKey(creds, host);
346
+ const h = key ? creds.hosts[key] : void 0;
347
+ if (!key || !h) return void 0;
296
348
  const t = pickTool(h, tool);
297
349
  const c = t ? h.tools[t] : void 0;
298
350
  if (!t || !c) return void 0;
@@ -301,7 +353,7 @@ async function loadToken(host, tool) {
301
353
  const kr = await loadKeyring();
302
354
  if (!kr) return void 0;
303
355
  try {
304
- const v = new kr.Entry(SERVICE, account(host, t)).getPassword();
356
+ const v = new kr.Entry(KEYCHAIN_SERVICE, account(key, t)).getPassword();
305
357
  if (v) return { token: v, tool: t };
306
358
  } catch {
307
359
  }
@@ -312,34 +364,57 @@ function defaultHost() {
312
364
  const c = readCredentials();
313
365
  return c.default_host && c.hosts[c.default_host] ? c.default_host : Object.keys(c.hosts)[0];
314
366
  }
367
+ function deletedKeychainEntry(kr, host, tool) {
368
+ try {
369
+ const e = new kr.Entry(KEYCHAIN_SERVICE, account(host, tool));
370
+ let dropped;
371
+ if (e.deletePassword) dropped = e.deletePassword();
372
+ else if (e.deleteCredential) dropped = e.deleteCredential();
373
+ else return false;
374
+ if (dropped !== false) return true;
375
+ try {
376
+ const still = e.getPassword();
377
+ return still === null || still === void 0;
378
+ } catch {
379
+ return false;
380
+ }
381
+ } catch {
382
+ return false;
383
+ }
384
+ }
315
385
  async function deleteCredentials(host, tool) {
316
386
  const creds = readCredentials();
317
387
  const kr = await loadKeyring();
318
- let n = 0;
388
+ let removed = 0;
389
+ const leftover = [];
319
390
  for (const [h, hc] of Object.entries(creds.hosts)) {
320
- if (host && h !== host) continue;
391
+ if (host && h !== host && !sameOrigin(h, host)) continue;
392
+ let removedHere = 0;
321
393
  for (const [t, c] of Object.entries(hc.tools)) {
322
394
  if (tool && t !== tool) continue;
323
- if (c.keychain && kr) {
324
- try {
325
- const e = new kr.Entry(SERVICE, account(h, t));
326
- if (e.deletePassword) e.deletePassword();
327
- else e.deleteCredential?.();
328
- } catch {
329
- }
395
+ if (c.keychain && !(kr && deletedKeychainEntry(kr, h, t))) {
396
+ leftover.push(account(h, t));
397
+ continue;
330
398
  }
331
399
  delete hc.tools[t];
332
- n += 1;
400
+ removed += 1;
401
+ removedHere += 1;
333
402
  }
403
+ if (removedHere === 0) continue;
334
404
  if (hc.last_tool && !hc.tools[hc.last_tool]) hc.last_tool = Object.keys(hc.tools)[0];
335
405
  if (Object.keys(hc.tools).length === 0) delete creds.hosts[h];
406
+ else {
407
+ const last = hc.last_tool ? hc.tools[hc.last_tool] : void 0;
408
+ if (last?.token) hc.token_fp = tokenFingerprint(last.token);
409
+ else delete hc.token_fp;
410
+ }
336
411
  }
337
412
  if (creds.default_host && !creds.hosts[creds.default_host]) creds.default_host = Object.keys(creds.hosts)[0];
338
- if (n > 0) {
413
+ if (removed > 0) {
339
414
  if (Object.keys(creds.hosts).length === 0) fs.rmSync(credentialsFile(), { force: true });
340
415
  else writeCredentials(creds);
341
416
  }
342
- return n;
417
+ return { removed, leftover };
343
418
  }
344
419
 
345
420
  export {
@@ -348,6 +423,10 @@ export {
348
423
  installService,
349
424
  windowsServiceStatus,
350
425
  uninstallService,
426
+ KEYCHAIN_SERVICE,
427
+ TOKEN_FP_HEX,
428
+ tokenFingerprint,
429
+ hostKey,
351
430
  readCredentials,
352
431
  saveToken,
353
432
  loadToken,
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __hfCreateRequire } from 'node:module';
3
+ const require = __hfCreateRequire(import.meta.url);
2
4
 
3
5
  // src/args.ts
4
- var BOOLEAN = /* @__PURE__ */ new Set(["no-open", "yes", "y", "reveal", "help", "h", "json", "no-cli", "agent", "no-service"]);
6
+ var BOOLEAN = /* @__PURE__ */ new Set(["no-open", "yes", "y", "reveal", "help", "h", "json", "no-cli", "agent", "no-service", "permission-prompt", "full", "no-full"]);
5
7
  function parseArgs(argv) {
6
8
  const positionals = [];
7
9
  const flags = {};
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __hfCreateRequire } from 'node:module';
3
+ const require = __hfCreateRequire(import.meta.url);
4
+
5
+ // src/service/control/private.ts
6
+ import fs from "fs";
7
+ var uid = () => typeof process.getuid === "function" ? process.getuid() : void 0;
8
+ function ensurePrivateDir(dir) {
9
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
10
+ if (process.platform !== "win32") fs.chmodSync(dir, 448);
11
+ }
12
+ function privateDirTrouble(dir) {
13
+ let st;
14
+ try {
15
+ st = fs.lstatSync(dir);
16
+ } catch {
17
+ return `${dir} does not exist`;
18
+ }
19
+ if (st.isSymbolicLink() || !st.isDirectory()) return `${dir} is not a plain folder`;
20
+ if (process.platform === "win32") return void 0;
21
+ if (uid() !== void 0 && st.uid !== uid()) return `${dir} is owned by another user`;
22
+ if ((st.mode & 63) !== 0) return `${dir} is open to group or others`;
23
+ return void 0;
24
+ }
25
+ function privateFileTrouble(file) {
26
+ let st;
27
+ try {
28
+ st = fs.lstatSync(file);
29
+ } catch {
30
+ return `${file} does not exist`;
31
+ }
32
+ if (st.isSymbolicLink() || !st.isFile()) return `${file} is not a regular file`;
33
+ if (process.platform === "win32") return void 0;
34
+ if (uid() !== void 0 && st.uid !== uid()) return `${file} is owned by another user`;
35
+ if ((st.mode & 63) !== 0) return `${file} is open to group or others`;
36
+ return void 0;
37
+ }
38
+ function safeName(id) {
39
+ const s = id.replace(/[^A-Za-z0-9_.-]/g, "_").replace(/^\.+/, "_").slice(0, 120);
40
+ return s === "" ? "_" : s;
41
+ }
42
+
43
+ export {
44
+ ensurePrivateDir,
45
+ privateDirTrouble,
46
+ privateFileTrouble,
47
+ safeName
48
+ };
@@ -1,57 +1,69 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __hfCreateRequire } from 'node:module';
3
+ const require = __hfCreateRequire(import.meta.url);
2
4
  import {
3
5
  deviceLogin
4
- } from "./chunk-R5KDI4WI.js";
6
+ } from "./chunk-FPJNGKT5.js";
5
7
  import {
6
8
  RC_LINE,
7
9
  addRcLine,
8
10
  hasRcLine,
9
- installClaude,
10
- installCodex,
11
11
  installCursor,
12
12
  installGemini,
13
13
  installGrok,
14
- rcFileFor,
14
+ rcFileFor
15
+ } from "./chunk-QBBKGORP.js";
16
+ import {
17
+ emptyReport,
18
+ envTokenSharedWith,
19
+ installClaude,
20
+ installCodex,
21
+ readManifest,
22
+ toolReadsEnvToken,
15
23
  updateManifest
16
- } from "./chunk-QHYZBHXO.js";
24
+ } from "./chunk-X2KH6AR4.js";
17
25
  import {
18
26
  detectRepo
19
- } from "./chunk-2BSA7LFD.js";
27
+ } from "./chunk-BLLPDPTL.js";
20
28
  import {
21
- REAL_TOOLS,
22
- TOOL_LABEL,
23
- detectTools,
24
- findBin,
25
- isTool,
26
- toolFromEnv
27
- } from "./chunk-2IMJECYD.js";
29
+ defaultHost,
30
+ loadToken,
31
+ saveToken
32
+ } from "./chunk-QLMLB7RB.js";
28
33
  import {
29
34
  checkUnknownFlags,
30
35
  flagStr
31
- } from "./chunk-TKMIV7BG.js";
32
- import {
33
- saveToken
34
- } from "./chunk-PDXYOREI.js";
36
+ } from "./chunk-QPAFN47B.js";
35
37
  import {
36
38
  VERSION
37
- } from "./chunk-DNG5CRT3.js";
39
+ } from "./chunk-K2IRTZH5.js";
38
40
  import {
39
41
  readMachine
40
- } from "./chunk-T7TG6WA4.js";
42
+ } from "./chunk-LX5A25O6.js";
41
43
  import {
42
44
  ApiError
43
- } from "./chunk-PY74LT2W.js";
45
+ } from "./chunk-NRJONTFT.js";
46
+ import {
47
+ ConfigError,
48
+ REAL_TOOLS,
49
+ TOOL_LABEL,
50
+ detectTools,
51
+ findBin,
52
+ isTool,
53
+ toolFromEnv
54
+ } from "./chunk-XPGL7VYE.js";
44
55
  import {
45
- ConfigError
46
- } from "./chunk-VNCQFFQO.js";
56
+ hostFromEnv,
57
+ hostFromFlag,
58
+ machineName,
59
+ normalizeHost,
60
+ sameOrigin
61
+ } from "./chunk-QGULSOQ2.js";
47
62
  import {
48
63
  CLI_BIN_NAME,
49
64
  CLI_INSTALL_COMMAND,
50
- cliNpxCommand,
51
- hostFromEnv,
52
- machineName,
53
- normalizeHost
54
- } from "./chunk-ZC2DAKB5.js";
65
+ cliNpxCommand
66
+ } from "./chunk-HWBUESB4.js";
55
67
 
56
68
  // src/connect.ts
57
69
  var INSTALLERS = {
@@ -61,9 +73,59 @@ var INSTALLERS = {
61
73
  gemini: installGemini,
62
74
  grok: installGrok
63
75
  };
76
+ async function freezeForeignEnvTools(ctx, host, tool, noCli) {
77
+ const report = emptyReport();
78
+ const manifest = readManifest();
79
+ for (const [name, entry] of Object.entries(manifest.tools)) {
80
+ const real = REAL_TOOLS.find((t) => t === name);
81
+ if (!real || real === tool || !entry || !toolReadsEnvToken(name, entry, true)) continue;
82
+ const owner = entry.host;
83
+ if (typeof owner !== "string" || owner.trim() === "") {
84
+ throw new ApiError(
85
+ 0,
86
+ "manifest",
87
+ `${TOOL_LABEL[real]} reads HIVEFLOOR_TOKEN, but its manifest entry has no host. Reconnect it with ${cliNpxCommand(`connect --tool ${real}`)} so the shell token is not sent to the wrong server.`
88
+ );
89
+ }
90
+ if (sameOrigin(owner, host)) continue;
91
+ const stored = await loadToken(owner, real);
92
+ if (!stored) {
93
+ throw new ApiError(
94
+ 0,
95
+ "token",
96
+ `${TOOL_LABEL[real]} on ${owner} reads HIVEFLOOR_TOKEN, which would now be the token for ${host}, and no stored token is left for it. Run ${cliNpxCommand(`connect --host ${owner} --tool ${real}`)} again.`
97
+ );
98
+ }
99
+ const r = await INSTALLERS[real]({
100
+ ctx,
101
+ mcpUrl: `${normalizeHost(owner)}/mcp`,
102
+ token: stored.token,
103
+ useEnvToken: false,
104
+ requireStoredToken: true,
105
+ noCli,
106
+ mcpOnly: true,
107
+ onMcpRegistered: (mcp) => {
108
+ updateManifest((m) => {
109
+ m.tools[real] = { mcp, host: owner, envToken: false };
110
+ });
111
+ }
112
+ });
113
+ updateManifest((m) => {
114
+ m.tools[real] = { mcp: r.mcp, host: owner, envToken: false };
115
+ });
116
+ report.lines.push(...r.report.lines);
117
+ report.warnings.push(...r.report.warnings);
118
+ report.warnings.push(`${TOOL_LABEL[real]} on ${owner} now stores its own token, so HIVEFLOOR_TOKEN is not sent there.`);
119
+ }
120
+ return report;
121
+ }
64
122
  function resolveHost(p, ctx) {
65
123
  const h = flagStr(p, "host");
66
- return h ? normalizeHost(h) : hostFromEnv(ctx.env);
124
+ return h ? hostFromFlag(ctx, h) : hostFromEnv(ctx.env);
125
+ }
126
+ function shellExportFollowsHost(env, host) {
127
+ if (!env.HIVEFLOOR_URL) return true;
128
+ return sameOrigin(env.HIVEFLOOR_URL, host);
67
129
  }
68
130
  async function chooseTool(ctx, p) {
69
131
  const detected = detectTools(ctx.env).map((d) => d.tool);
@@ -95,7 +157,6 @@ async function connectAgent(ctx, p) {
95
157
  };
96
158
  const canOpen = !p.flags["no-open"] && (process.platform !== "linux" || Boolean(ctx.env.DISPLAY || ctx.env.WAYLAND_DISPLAY));
97
159
  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
160
  const rc = rcFileFor(ctx.env);
100
161
  let rcInstalled = rc !== void 0 && hasRcLine(rc);
101
162
  let rcAdded;
@@ -118,34 +179,57 @@ Add it? [y/N] `)).trim());
118
179
  const lines = [];
119
180
  const warnings = [];
120
181
  const mcpUrl = `${host}/mcp`;
182
+ const exportFollowsHost = shellExportFollowsHost(ctx.env, host);
183
+ const previousHost = defaultHost();
184
+ const shared = envTokenSharedWith(host, tool, rcInstalled);
185
+ const allowEnv = rcInstalled && exportFollowsHost && (previousHost === void 0 || sameOrigin(previousHost, host)) && shared;
186
+ if (rcInstalled && !shared) {
187
+ try {
188
+ const frozen = await freezeForeignEnvTools(ctx, host, tool, Boolean(p.flags["no-cli"]));
189
+ lines.push(...frozen.lines);
190
+ warnings.push(...frozen.warnings);
191
+ } catch (e) {
192
+ if (e instanceof ConfigError) {
193
+ throw new ApiError(0, "config", `${e.message} Left the other host's tool config in place and did not finish connecting ${host}.`);
194
+ }
195
+ throw e;
196
+ }
197
+ }
121
198
  if (tool === "other") {
122
199
  lines.push(`MCP server: ${mcpUrl} (streamable HTTP), header "Authorization: Bearer <token>" (run: ${cliNpxCommand("token --reveal")})`);
123
200
  lines.push("The agent shows up the first time it calls a tool.");
124
201
  } else {
202
+ let mcpRegistered = false;
125
203
  try {
126
204
  const r = await INSTALLERS[tool]({
127
205
  ctx,
128
206
  mcpUrl,
129
207
  token: result.token,
130
- useEnvToken: rcInstalled,
208
+ useEnvToken: allowEnv,
209
+ requireStoredToken: rcInstalled && !allowEnv,
131
210
  noCli: Boolean(p.flags["no-cli"]),
132
211
  skipHooks: ownsHooks,
133
212
  onMcpRegistered: (mcp) => {
213
+ mcpRegistered = true;
134
214
  updateManifest((m) => {
135
- m.tools[tool] = { mcp, host };
215
+ m.tools[tool] = { mcp, host, envToken: allowEnv };
136
216
  });
137
217
  }
138
218
  });
139
219
  lines.push(...r.report.lines);
140
220
  warnings.push(...r.report.warnings);
141
221
  updateManifest((m) => {
142
- m.tools[tool] = { mcp: r.mcp, host };
222
+ m.tools[tool] = { mcp: r.mcp, host, envToken: allowEnv };
143
223
  });
144
224
  } catch (e) {
145
225
  if (!(e instanceof ConfigError)) throw e;
226
+ if (!mcpRegistered) {
227
+ throw new ApiError(0, "config", `${e.message} Did not finish connecting ${host}; the default host is unchanged.`);
228
+ }
146
229
  warnings.push(e.message);
147
230
  }
148
231
  }
232
+ const storage = await saveToken(host, tool, result.token, { agent: result.agent, project: result.project });
149
233
  if (rcAdded) lines.push(`Shell: added the token export to ${rcAdded}`);
150
234
  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
235
  ctx.out(`
@@ -172,7 +256,7 @@ async function connect(ctx, p) {
172
256
  return 2;
173
257
  }
174
258
  if (p.flags.agent || p.flags.tool !== void 0) return connectAgent(ctx, p);
175
- return (await import("./machine-connect-BYHZRRRB.js")).connectMachine(ctx, p);
259
+ return (await import("./machine-connect-KEI4BRWF.js")).connectMachine(ctx, p);
176
260
  }
177
261
 
178
262
  export {