linkshell-cli 0.6.2 → 0.6.4

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 (93) hide show
  1. package/README.md +47 -132
  2. package/dist/cli/src/auth.js +1 -1
  3. package/dist/cli/src/auth.js.map +1 -1
  4. package/dist/cli/src/commands/doctor.d.ts +1 -1
  5. package/dist/cli/src/commands/doctor.js +76 -36
  6. package/dist/cli/src/commands/doctor.js.map +1 -1
  7. package/dist/cli/src/commands/host.d.ts +14 -0
  8. package/dist/cli/src/commands/host.js +94 -15
  9. package/dist/cli/src/commands/host.js.map +1 -1
  10. package/dist/cli/src/commands/logout.d.ts +1 -1
  11. package/dist/cli/src/commands/logout.js +10 -2
  12. package/dist/cli/src/commands/logout.js.map +1 -1
  13. package/dist/cli/src/commands/upgrade.js +11 -0
  14. package/dist/cli/src/commands/upgrade.js.map +1 -1
  15. package/dist/cli/src/config.js +1 -1
  16. package/dist/cli/src/config.js.map +1 -1
  17. package/dist/cli/src/index.js +94 -114
  18. package/dist/cli/src/index.js.map +1 -1
  19. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  20. package/dist/host/src/carry.d.ts +15 -0
  21. package/dist/host/src/carry.js +83 -0
  22. package/dist/host/src/carry.js.map +1 -0
  23. package/dist/host/src/drivers/acp/driver.d.ts +13 -3
  24. package/dist/host/src/drivers/acp/driver.js +43 -8
  25. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  26. package/dist/host/src/drivers/acp/mapper.d.ts +5 -1
  27. package/dist/host/src/drivers/acp/mapper.js +53 -5
  28. package/dist/host/src/drivers/acp/mapper.js.map +1 -1
  29. package/dist/host/src/drivers/claude/driver.d.ts +53 -8
  30. package/dist/host/src/drivers/claude/driver.js +357 -40
  31. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  32. package/dist/host/src/drivers/claude/holders.d.ts +9 -0
  33. package/dist/host/src/drivers/claude/holders.js +61 -0
  34. package/dist/host/src/drivers/claude/holders.js.map +1 -0
  35. package/dist/host/src/drivers/claude/transcript.d.ts +100 -62
  36. package/dist/host/src/drivers/claude/transcript.js +246 -26
  37. package/dist/host/src/drivers/claude/transcript.js.map +1 -1
  38. package/dist/host/src/drivers/codex/commands.d.ts +27 -0
  39. package/dist/host/src/drivers/codex/commands.js +23 -0
  40. package/dist/host/src/drivers/codex/commands.js.map +1 -0
  41. package/dist/host/src/drivers/codex/driver.d.ts +15 -1
  42. package/dist/host/src/drivers/codex/driver.js +110 -1
  43. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  44. package/dist/host/src/drivers/codex/mapper.d.ts +4 -0
  45. package/dist/host/src/drivers/codex/mapper.js +29 -5
  46. package/dist/host/src/drivers/codex/mapper.js.map +1 -1
  47. package/dist/host/src/drivers/types.d.ts +34 -3
  48. package/dist/host/src/fs.d.ts +26 -4
  49. package/dist/host/src/fs.js +71 -17
  50. package/dist/host/src/fs.js.map +1 -1
  51. package/dist/host/src/gateway.d.ts +3 -0
  52. package/dist/host/src/gateway.js +9 -0
  53. package/dist/host/src/gateway.js.map +1 -1
  54. package/dist/host/src/host.d.ts +7 -3
  55. package/dist/host/src/host.js +42 -18
  56. package/dist/host/src/host.js.map +1 -1
  57. package/dist/host/src/hub.d.ts +91 -15
  58. package/dist/host/src/hub.js +556 -33
  59. package/dist/host/src/hub.js.map +1 -1
  60. package/dist/host/src/rpc/server.d.ts +4 -2
  61. package/dist/host/src/rpc/server.js +51 -6
  62. package/dist/host/src/rpc/server.js.map +1 -1
  63. package/dist/host/src/screen.d.ts +11 -0
  64. package/dist/host/src/screen.js +57 -3
  65. package/dist/host/src/screen.js.map +1 -1
  66. package/dist/host/src/slim.d.ts +17 -0
  67. package/dist/host/src/slim.js +66 -0
  68. package/dist/host/src/slim.js.map +1 -0
  69. package/dist/host/src/store.d.ts +75 -3
  70. package/dist/host/src/store.js +216 -16
  71. package/dist/host/src/store.js.map +1 -1
  72. package/dist/host/src/terminals.d.ts +4 -4
  73. package/dist/host/src/worktrees.d.ts +36 -0
  74. package/dist/host/src/worktrees.js +96 -0
  75. package/dist/host/src/worktrees.js.map +1 -0
  76. package/dist/wire/src/model.d.ts +301 -153
  77. package/dist/wire/src/model.js +55 -0
  78. package/dist/wire/src/model.js.map +1 -1
  79. package/dist/wire/src/relay-client.d.ts +2 -0
  80. package/dist/wire/src/relay-client.js +12 -0
  81. package/dist/wire/src/relay-client.js.map +1 -1
  82. package/dist/wire/src/rpc.d.ts +6740 -1022
  83. package/dist/wire/src/rpc.js +130 -7
  84. package/dist/wire/src/rpc.js.map +1 -1
  85. package/dist/wire/src/updates.d.ts +498 -498
  86. package/package.json +5 -5
  87. package/src/auth.ts +1 -1
  88. package/src/commands/doctor.ts +76 -34
  89. package/src/commands/host.ts +98 -14
  90. package/src/commands/logout.ts +10 -2
  91. package/src/commands/upgrade.ts +11 -0
  92. package/src/config.ts +1 -1
  93. package/src/index.ts +93 -131
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "linkshell-cli",
3
- "version": "0.6.2",
4
- "description": "Remote terminal bridge for Claude Code / Codex / Gemini / Copilot — control from your phone",
3
+ "version": "0.6.4",
4
+ "description": "Follow, steer and approve the coding agents on your computer (Claude Code, Codex, Gemini, Copilot and more) from your phone",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -36,10 +36,10 @@
36
36
  "qrcode-terminal": "^0.12.0",
37
37
  "ws": "^8.18.1",
38
38
  "zod": "^3.24.3",
39
- "@linkshell/gateway": "0.5.1",
40
- "@linkshell/host": "0.1.1",
39
+ "@linkshell/gateway": "0.5.3",
41
40
  "@linkshell/protocol": "0.4.10",
42
- "@linkshell/wire": "0.1.1"
41
+ "@linkshell/wire": "0.1.3",
42
+ "@linkshell/host": "0.1.3"
43
43
  },
44
44
  "optionalDependencies": {
45
45
  "@anthropic-ai/claude-agent-sdk": "^0.2.128",
package/src/auth.ts CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
 
5
- const AUTH_DIR = join(homedir(), ".linkshell");
5
+ const AUTH_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
6
6
  const AUTH_FILE = join(AUTH_DIR, "auth.json");
7
7
 
8
8
  export interface AuthTokens {
@@ -1,6 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
- import { loadConfig, getConfigPath } from "../config.js";
3
+ import { loadAuth } from "../auth.js";
4
+ import { hostRuntimeOk, resolveGateway, withRunningHost } from "./host.js";
4
5
 
5
6
  const requireFromCli = createRequire(import.meta.url);
6
7
 
@@ -26,32 +27,32 @@ function which(bin: string): string | undefined {
26
27
  }
27
28
  }
28
29
 
29
- async function checkGateway(url: string): Promise<CheckResult> {
30
+ async function checkGateway(name: string, url: string): Promise<CheckResult> {
30
31
  try {
31
- const httpUrl = url.replace(/\/ws\/?$/, "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
32
+ const httpUrl = url.replace(/\/(ws|v2\/connect)\/?$/, "").replace(/\/$/, "").replace(/^wss:/, "https:").replace(/^ws:/, "http:");
32
33
  const start = Date.now();
33
34
  const res = await fetch(`${httpUrl}/healthz`, { signal: AbortSignal.timeout(5000) });
34
35
  const latency = Date.now() - start;
35
- if (!res.ok) return { name: "Gateway", ok: false, detail: `HTTP ${res.status}` };
36
- return { name: "Gateway", ok: true, detail: `reachable (${latency}ms)` };
36
+ if (!res.ok) return { name, ok: false, detail: `${url}: HTTP ${res.status}` };
37
+ return { name, ok: true, detail: `${url} answers (${latency}ms)` };
37
38
  } catch (e) {
38
- return { name: "Gateway", ok: false, detail: e instanceof Error ? e.message : "unreachable" };
39
+ return { name, ok: false, detail: `${url}: ${e instanceof Error ? e.message : "unreachable"}` };
39
40
  }
40
41
  }
41
42
 
42
- export async function runDoctor(gatewayUrl?: string): Promise<void> {
43
- const config = loadConfig();
44
- const gateway = gatewayUrl ?? config.gateway;
43
+ const AGENT_INSTALL: Record<string, string> = {
44
+ claude: "npm i -g @anthropic-ai/claude-code",
45
+ codex: "npm i -g @openai/codex",
46
+ };
45
47
 
48
+ export async function runDoctor(version: string, gatewayUrl?: string): Promise<void> {
46
49
  process.stdout.write("\n LinkShell Doctor\n\n");
47
50
 
48
51
  const results: CheckResult[] = [];
49
52
 
50
53
  results.push(check("Node.js", () => {
51
- const ver = process.versions.node;
52
- const major = Number(ver.split(".")[0]);
53
- if (major < 18) throw new Error(`v${ver} (need >= 18)`);
54
- return `v${ver}`;
54
+ if (!hostRuntimeOk()) throw new Error(`v${process.versions.node} (the host needs 22.13 or newer)`);
55
+ return `v${process.versions.node}`;
55
56
  }));
56
57
 
57
58
  results.push(check("node-pty", () => {
@@ -69,32 +70,73 @@ export async function runDoctor(gatewayUrl?: string): Promise<void> {
69
70
  }
70
71
  }));
71
72
 
72
- // Optional CLI hints — not required, since LinkShell now bridges a plain shell.
73
- for (const [label, bin, hint] of [
74
- ["Claude CLI (optional)", "claude", "npm i -g @anthropic-ai/claude-code"],
75
- ["Codex CLI (optional)", "codex", "npm i -g @openai/codex"],
76
- ] as const) {
77
- const path = which(bin);
78
- if (path) {
79
- results.push({ name: label, ok: true, detail: `found (${path})` });
80
- } else {
81
- results.push({ name: label, ok: true, detail: `not installed — ${hint}` });
73
+ const host = await withRunningHost(async (client) => ({
74
+ info: await client.call("machine.info", {}),
75
+ gateway: await client.call("gateway.status", {}),
76
+ })).catch(() => undefined);
77
+
78
+ if (!host) {
79
+ results.push({ name: "Host", ok: false, detail: "not running — start it with: linkshell host --daemon" });
80
+ // Without the host, at least say whether the two main agents are on the PATH.
81
+ for (const [bin, hint] of Object.entries(AGENT_INSTALL)) {
82
+ const path = which(bin);
83
+ results.push({ name: `${bin} (optional)`, ok: true, detail: path ? `found (${path})` : `not installed — ${hint}` });
84
+ }
85
+ } else {
86
+ results.push(
87
+ host.info.hostVersion === version
88
+ ? { name: "Host", ok: true, detail: `running, ${version}` }
89
+ : {
90
+ name: "Host",
91
+ ok: false,
92
+ detail: `running ${host.info.hostVersion}, but this CLI is ${version} — update it with: linkshell host stop && linkshell host --daemon`,
93
+ },
94
+ );
95
+ // Agents as the host sees them: what the phone can actually start.
96
+ for (const agent of host.info.agents) {
97
+ if (!agent.installed) {
98
+ const hint = AGENT_INSTALL[agent.id];
99
+ if (hint) results.push({ name: `${agent.label} (optional)`, ok: true, detail: `not installed — ${hint}` });
100
+ continue;
101
+ }
102
+ const signedOut = agent.auth?.state === "missing";
103
+ const problem = agent.problem ?? (signedOut ? (agent.auth?.hint ?? "not logged in") : undefined);
104
+ results.push({ name: agent.label, ok: !problem, detail: `v${agent.version ?? "?"}${problem ? ` — ${problem}` : ""}` });
82
105
  }
83
106
  }
84
107
 
85
- results.push(check("Config", () => {
86
- const path = getConfigPath();
87
- const cfg = loadConfig();
88
- const keys = Object.keys(cfg).filter((k) => cfg[k as keyof typeof cfg] !== undefined);
89
- if (keys.length === 0) return `${path} (empty — run: linkshell setup)`;
90
- return `${path} (${keys.join(", ")})`;
91
- }));
108
+ const auth = loadAuth();
109
+ results.push({
110
+ name: "Account",
111
+ ok: true,
112
+ detail: auth?.accessToken ? `logged in as ${auth.email || auth.userId}` : "not logged in (only the official gateway needs it: linkshell login)",
113
+ });
92
114
 
93
- if (gateway) {
94
- results.push(await checkGateway(gateway));
95
- } else {
96
- results.push({ name: "Gateway", ok: false, detail: "no gateway configured — run: linkshell setup" });
115
+ if (host) {
116
+ const gateway = host.gateway;
117
+ if (gateway.status === "online") {
118
+ const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
119
+ const paired = gateway.devices.length ? `, ${gateway.devices.length} paired device(s)` : "";
120
+ results.push({ name: "Gateway", ok: true, detail: `online ${gateway.url}${account}${paired}` });
121
+ } else if (gateway.status === "off") {
122
+ results.push({
123
+ name: "Gateway",
124
+ ok: false,
125
+ detail: "off, so a phone can't reach this computer — linkshell login (official gateway), or linkshell host --gateway <url>",
126
+ });
127
+ } else {
128
+ results.push({ name: "Gateway", ok: false, detail: `${gateway.status} ${gateway.url}${gateway.error ? ` — ${gateway.error.message}` : ""}` });
129
+ }
130
+ } else if (!gatewayUrl && hostRuntimeOk()) {
131
+ const { defaultHome } = await import("@linkshell/host");
132
+ const chosen = resolveGateway(defaultHome());
133
+ results.push(
134
+ chosen
135
+ ? await checkGateway("Gateway", chosen)
136
+ : { name: "Gateway", ok: false, detail: "none chosen — linkshell login (official gateway), or linkshell host --gateway <url>" },
137
+ );
97
138
  }
139
+ if (gatewayUrl) results.push(await checkGateway(host ? "Gateway (--gateway)" : "Gateway", gatewayUrl));
98
140
 
99
141
  for (const r of results) {
100
142
  const icon = r.ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import type { GatewayStatus } from "@linkshell/wire";
4
5
  import { getValidToken, isLoggedIn } from "../auth.js";
5
6
  import * as daemon from "../utils/daemon.js";
6
7
 
@@ -30,7 +31,8 @@ export const OFFICIAL_GATEWAY = "wss://gateway.itool.tech";
30
31
  export function resolveGateway(home: string): string | undefined {
31
32
  const chosen = process.env.LINKSHELL_GATEWAY || readHostConfig(home).gateway;
32
33
  if (chosen === "off") return undefined;
33
- if (chosen) return chosen;
34
+ // config.json is shared with the v1 bridge, whose gateway URLs end in /ws.
35
+ if (chosen) return chosen.replace(/\/ws\/?$/, "");
34
36
  return isLoggedIn() ? OFFICIAL_GATEWAY : undefined;
35
37
  }
36
38
 
@@ -52,9 +54,13 @@ export function writeHostConfig(home: string, config: HostConfig): void {
52
54
  }
53
55
 
54
56
  /** The host keeps its state in node:sqlite, unflagged from Node 22.13. */
55
- export function assertHostRuntime(): void {
57
+ export function hostRuntimeOk(): boolean {
56
58
  const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
57
- if (major > 22 || (major === 22 && minor >= 13)) return;
59
+ return major > 22 || (major === 22 && minor >= 13);
60
+ }
61
+
62
+ export function assertHostRuntime(): void {
63
+ if (hostRuntimeOk()) return;
58
64
  process.stderr.write(
59
65
  `\n LinkShell's host needs Node.js 22.13 or newer (this is ${process.version}).\n` +
60
66
  " Upgrade Node (for example: nvm install 22, or brew upgrade node), then try again.\n\n",
@@ -83,12 +89,13 @@ export async function runHostForeground(version: string): Promise<void> {
83
89
  const devPort = process.env.LINKSHELL_DEV_PORT ? Number(process.env.LINKSHELL_DEV_PORT) : undefined;
84
90
  // Where devices reach this machine from outside the LAN. The account from
85
91
  // `linkshell login`, if any, lets that account's devices in without pairing.
86
- const gatewayUrl = resolveGateway(host.defaultHome());
92
+ // Asked again on `gateway.refresh`, so login, logout and `--gateway` apply to a running host.
93
+ const home = host.defaultHome();
87
94
  const running = await host.startHost({
88
95
  version,
89
96
  env,
90
97
  log,
91
- gateway: gatewayUrl ? { url: gatewayUrl, token: async () => (await getValidToken()) ?? undefined } : undefined,
98
+ gateway: { url: () => resolveGateway(home), token: async () => (await getValidToken()) ?? undefined },
92
99
  tcpPort: devPort,
93
100
  claudeCommand: process.env.LINKSHELL_CLAUDE_COMMAND || undefined,
94
101
  claudeAdapter: adapter ? { command: adapter, args: [] } : undefined,
@@ -127,6 +134,89 @@ export async function ensureHostRunning(): Promise<string> {
127
134
  throw new Error(`LinkShell host did not start; see ${daemon.getLogFile("host")}`);
128
135
  }
129
136
 
137
+ type HostClient = Awaited<ReturnType<Awaited<ReturnType<typeof loadHost>>["connectHost"]>>;
138
+
139
+ /** Runs `use` against the running host; undefined when there is none. */
140
+ export async function withRunningHost<T>(use: (client: HostClient) => Promise<T>): Promise<T | undefined> {
141
+ if (!hostRuntimeOk()) return undefined;
142
+ const host = await loadHost();
143
+ const { hostSocket } = host.hostPaths();
144
+ if (!(await host.isHostRunning(hostSocket))) return undefined;
145
+ const client = await host.connectHost(hostSocket);
146
+ try {
147
+ return await use(client);
148
+ } finally {
149
+ client.close();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Has the running host apply the current gateway choice and account, then
155
+ * waits (up to `waitMs`) for it to come online or be refused.
156
+ */
157
+ export async function refreshGateway(waitMs: number): Promise<GatewayStatus | undefined> {
158
+ return withRunningHost(async (client) => {
159
+ let status: GatewayStatus;
160
+ try {
161
+ status = await client.call("gateway.refresh", {});
162
+ } catch {
163
+ // A host from before 0.6.3 (still running after an upgrade) only reads its gateway at start.
164
+ process.stderr.write(" The running host is older than this CLI. Restart it to apply this (stops running agent turns):\n");
165
+ process.stderr.write(" linkshell host stop && linkshell host --daemon\n");
166
+ return client.call("gateway.status", {});
167
+ }
168
+ const deadline = Date.now() + waitMs;
169
+ while (status.status === "connecting" && Date.now() < deadline) {
170
+ await new Promise((resolve) => setTimeout(resolve, 200));
171
+ status = await client.call("gateway.status", {});
172
+ }
173
+ return status;
174
+ });
175
+ }
176
+
177
+ export function describeGateway(gateway: GatewayStatus): string {
178
+ if (gateway.status === "off") return "off (linkshell login for the official one, or linkshell host --gateway <url>)";
179
+ const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
180
+ const problem = gateway.error ? ` ⚠ ${gateway.error.message}` : "";
181
+ return `${gateway.status} ${gateway.url}${account}${problem}`;
182
+ }
183
+
184
+ /** After `linkshell login`: this computer joins the gateway its account is for. */
185
+ export async function joinGatewayAfterLogin(plan: "pro" | "free"): Promise<void> {
186
+ const out = (line: string) => process.stderr.write(` ${line}\n`);
187
+ if (!hostRuntimeOk()) {
188
+ out(`The LinkShell host needs Node.js 22.13 or newer (this is ${process.version}); upgrade Node, then: linkshell host --daemon\n`);
189
+ return;
190
+ }
191
+ const host = await loadHost();
192
+ const chosen = process.env.LINKSHELL_GATEWAY || readHostConfig(host.defaultHome()).gateway;
193
+ if (chosen === "off") {
194
+ out("The gateway is turned off on this computer. To use the official one: linkshell host --gateway default\n");
195
+ return;
196
+ }
197
+ if (!chosen && plan !== "pro") {
198
+ // A running host now gets the gateway's own answer, instead of an old login's.
199
+ await refreshGateway(0);
200
+ out("The official gateway needs a Pro subscription: https://itool.tech");
201
+ out("Or use your own gateway: linkshell host --gateway <url>, then linkshell pair\n");
202
+ return;
203
+ }
204
+ await ensureHostRunning();
205
+ const gateway = await refreshGateway(15_000);
206
+ const where = chosen ? (gateway?.url ?? chosen) : "the official gateway";
207
+ if (gateway?.status === "online") {
208
+ out(`\x1b[32m✓\x1b[0m This computer is online on ${where}.`);
209
+ out(
210
+ gateway.account
211
+ ? " Open LinkShell on your phone and sign in with the same account: it shows up there, no pairing needed.\n"
212
+ : " Pair your phone with: linkshell pair\n",
213
+ );
214
+ } else {
215
+ out(`\x1b[31m✗\x1b[0m Not connected to ${where}${gateway?.error ? `: ${gateway.error.message}` : " yet"}`);
216
+ out(" Check again with: linkshell host status\n");
217
+ }
218
+ }
219
+
130
220
  export async function printHostStatus(): Promise<void> {
131
221
  const host = await loadHost();
132
222
  const { hostSocket } = host.hostPaths();
@@ -153,15 +243,9 @@ export async function printHostStatus(): Promise<void> {
153
243
  const active = sessions.filter((s) => s.state === "running" || s.state === "waiting");
154
244
  process.stdout.write(` Sessions: ${sessions.length} known, ${active.length} active\n`);
155
245
  const gateway = await client.call("gateway.status", {});
156
- if (gateway.status === "off") {
157
- process.stdout.write(" Gateway: off (linkshell login for the official one, or linkshell host --gateway <url>)\n");
158
- } else {
159
- const account = gateway.account ? `, account ${gateway.account.email ?? gateway.account.userId}` : "";
160
- const problem = gateway.error ? ` ⚠ ${gateway.error.message}` : "";
161
- process.stdout.write(` Gateway: ${gateway.status} ${gateway.url}${account}${problem}\n`);
162
- for (const device of gateway.devices) {
163
- process.stdout.write(` ${device.online ? "●" : "○"} ${device.name} paired ${new Date(device.pairedAt).toLocaleDateString()}\n`);
164
- }
246
+ process.stdout.write(` Gateway: ${describeGateway(gateway)}\n`);
247
+ for (const device of gateway.devices) {
248
+ process.stdout.write(` ${device.online ? "●" : "○"} ${device.name} paired ${new Date(device.pairedAt).toLocaleDateString()}\n`);
165
249
  }
166
250
  } finally {
167
251
  client.close();
@@ -1,12 +1,20 @@
1
1
  import { clearAuth, loadAuth } from "../auth.js";
2
+ import { refreshGateway, withRunningHost } from "./host.js";
2
3
 
3
- export function runLogout(): void {
4
+ export async function runLogout(): Promise<void> {
4
5
  const auth = loadAuth();
5
6
  if (!auth || !auth.accessToken) {
6
7
  process.stderr.write("\n Not currently logged in.\n\n");
7
8
  return;
8
9
  }
9
10
 
11
+ const before = await withRunningHost((client) => client.call("gateway.status", {}));
10
12
  clearAuth();
11
- process.stderr.write(`\n \x1b[32m✓\x1b[0m Logged out${auth.email ? ` (${auth.email})` : ""}.\n\n`);
13
+ process.stderr.write(`\n \x1b[32m✓\x1b[0m Logged out${auth.email ? ` (${auth.email})` : ""}.\n`);
14
+ // A running host leaves the account's gateway now, not at its next reconnect.
15
+ const after = await refreshGateway(0);
16
+ if (before && before.status !== "off" && after?.status === "off") {
17
+ process.stderr.write(" This computer is off the official gateway; the host and its sessions keep running.\n");
18
+ }
19
+ process.stderr.write("\n");
12
20
  }
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
+ import { withRunningHost } from "./host.js";
3
4
 
4
5
  const require = createRequire(import.meta.url);
5
6
  const pkg = require("../../../../package.json") as { version: string };
@@ -31,6 +32,14 @@ async function fetchLatestVersion(): Promise<string | null> {
31
32
  }
32
33
  }
33
34
 
35
+ /** The background host keeps running the version it was started with. */
36
+ async function noteRunningHost(latest: string): Promise<void> {
37
+ const running = await withRunningHost((client) => client.call("machine.info", {})).catch(() => undefined);
38
+ if (!running || running.hostVersion === latest) return;
39
+ process.stderr.write(` The running host is still ${running.hostVersion}. Restart it to use ${latest} (stops running agent turns):\n`);
40
+ process.stderr.write(" linkshell host stop && linkshell host --daemon\n\n");
41
+ }
42
+
34
43
  export async function runUpgrade(): Promise<void> {
35
44
  const current = pkg.version;
36
45
  process.stderr.write(`\n Current version: ${current}\n`);
@@ -55,6 +64,7 @@ export async function runUpgrade(): Promise<void> {
55
64
  try {
56
65
  execSync("brew upgrade linkshell", { stdio: "inherit", timeout: 120_000 });
57
66
  process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
67
+ await noteRunningHost(latest);
58
68
  } catch {
59
69
  process.stderr.write("\n \x1b[31m✗\x1b[0m Homebrew upgrade failed. Try manually:\n");
60
70
  process.stderr.write(" brew upgrade linkshell\n\n");
@@ -65,6 +75,7 @@ export async function runUpgrade(): Promise<void> {
65
75
  try {
66
76
  execSync("npm install -g linkshell-cli@latest", { stdio: "inherit", timeout: 120_000 });
67
77
  process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
78
+ await noteRunningHost(latest);
68
79
  } catch {
69
80
  process.stderr.write("\n \x1b[31m✗\x1b[0m npm upgrade failed. Try manually:\n");
70
81
  process.stderr.write(" npm install -g linkshell-cli@latest\n\n");
package/src/config.ts CHANGED
@@ -14,7 +14,7 @@ export interface LinkShellConfig {
14
14
  rows?: number;
15
15
  }
16
16
 
17
- const CONFIG_DIR = join(homedir(), ".linkshell");
17
+ const CONFIG_DIR = process.env.LINKSHELL_HOME || join(homedir(), ".linkshell");
18
18
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
19
19
 
20
20
  export function loadConfig(): LinkShellConfig {