linkshell-cli 0.8.2 → 0.10.0

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 (173) hide show
  1. package/README.md +11 -6
  2. package/dist/cli/src/commands/gateway.d.ts +2 -0
  3. package/dist/cli/src/commands/gateway.js +29 -0
  4. package/dist/cli/src/commands/gateway.js.map +1 -0
  5. package/dist/cli/src/commands/host.d.ts +4 -0
  6. package/dist/cli/src/commands/host.js +12 -2
  7. package/dist/cli/src/commands/host.js.map +1 -1
  8. package/dist/cli/src/commands/screen.d.ts +10 -0
  9. package/dist/cli/src/commands/screen.js +82 -74
  10. package/dist/cli/src/commands/screen.js.map +1 -1
  11. package/dist/cli/src/commands/setup.d.ts +11 -1
  12. package/dist/cli/src/commands/setup.js +102 -38
  13. package/dist/cli/src/commands/setup.js.map +1 -1
  14. package/dist/cli/src/index.js +28 -338
  15. package/dist/cli/src/index.js.map +1 -1
  16. package/dist/cli/src/utils/daemon.d.ts +0 -7
  17. package/dist/cli/src/utils/daemon.js +1 -18
  18. package/dist/cli/src/utils/daemon.js.map +1 -1
  19. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  20. package/dist/gateway/src/accounts.d.ts +6 -0
  21. package/dist/gateway/src/accounts.js +32 -0
  22. package/dist/gateway/src/accounts.js.map +1 -0
  23. package/dist/gateway/src/index.d.ts +4 -0
  24. package/dist/gateway/src/index.js +5 -0
  25. package/dist/gateway/src/index.js.map +1 -0
  26. package/dist/gateway/src/main.d.ts +1 -0
  27. package/dist/gateway/src/main.js +57 -0
  28. package/dist/gateway/src/main.js.map +1 -0
  29. package/dist/gateway/src/rate-limit.d.ts +22 -0
  30. package/dist/gateway/src/rate-limit.js +66 -0
  31. package/dist/gateway/src/rate-limit.js.map +1 -0
  32. package/dist/gateway/src/relay.d.ts +84 -0
  33. package/dist/gateway/src/relay.js +434 -0
  34. package/dist/gateway/src/relay.js.map +1 -0
  35. package/dist/gateway/src/serve.d.ts +34 -0
  36. package/dist/gateway/src/serve.js +102 -0
  37. package/dist/gateway/src/serve.js.map +1 -0
  38. package/dist/gateway/src/store.d.ts +35 -0
  39. package/dist/gateway/src/store.js +109 -0
  40. package/dist/gateway/src/store.js.map +1 -0
  41. package/dist/gateway/src/subscription.d.ts +18 -0
  42. package/dist/gateway/src/subscription.js +46 -0
  43. package/dist/gateway/src/subscription.js.map +1 -0
  44. package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
  45. package/dist/host/src/fs.d.ts +3 -3
  46. package/dist/host/src/input.d.ts +112 -27
  47. package/dist/host/src/input.js +214 -144
  48. package/dist/host/src/input.js.map +1 -1
  49. package/dist/host/src/rpc/server.js +1 -1
  50. package/dist/host/src/rpc/server.js.map +1 -1
  51. package/dist/host/src/screen-pacer.d.ts +25 -11
  52. package/dist/host/src/screen-pacer.js +28 -6
  53. package/dist/host/src/screen-pacer.js.map +1 -1
  54. package/dist/host/src/screen-viewer.d.ts +5 -0
  55. package/dist/host/src/screen-viewer.js +1231 -55
  56. package/dist/host/src/screen-viewer.js.map +1 -1
  57. package/dist/host/src/screen.d.ts +19 -10
  58. package/dist/host/src/screen.js +314 -108
  59. package/dist/host/src/screen.js.map +1 -1
  60. package/dist/host/src/terminals.d.ts +1 -1
  61. package/dist/host/src/terminals.js +1 -0
  62. package/dist/host/src/terminals.js.map +1 -1
  63. package/dist/wire/src/model.d.ts +6 -6
  64. package/dist/wire/src/relay.d.ts +8 -8
  65. package/dist/wire/src/rpc.d.ts +818 -818
  66. package/dist/wire/src/updates.d.ts +18 -18
  67. package/package.json +6 -15
  68. package/src/commands/gateway.ts +31 -0
  69. package/src/commands/host.ts +15 -2
  70. package/src/commands/screen.ts +78 -72
  71. package/src/commands/setup.ts +102 -60
  72. package/src/index.ts +29 -433
  73. package/src/utils/daemon.ts +2 -27
  74. package/dist/cli/src/config.d.ts +0 -14
  75. package/dist/cli/src/config.js +0 -23
  76. package/dist/cli/src/config.js.map +0 -1
  77. package/dist/cli/src/machine-id.d.ts +0 -6
  78. package/dist/cli/src/machine-id.js +0 -50
  79. package/dist/cli/src/machine-id.js.map +0 -1
  80. package/dist/cli/src/providers.d.ts +0 -13
  81. package/dist/cli/src/providers.js +0 -77
  82. package/dist/cli/src/providers.js.map +0 -1
  83. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
  84. package/dist/cli/src/runtime/acp/acp-client.js +0 -430
  85. package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
  86. package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
  87. package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
  88. package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
  89. package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
  90. package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
  91. package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
  92. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
  93. package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
  94. package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
  95. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
  96. package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
  97. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
  98. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
  99. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
  100. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
  101. package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
  102. package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
  103. package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
  104. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
  105. package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
  106. package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
  107. package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
  108. package/dist/cli/src/runtime/bridge-session.js +0 -1364
  109. package/dist/cli/src/runtime/bridge-session.js.map +0 -1
  110. package/dist/cli/src/runtime/hook-command.d.ts +0 -9
  111. package/dist/cli/src/runtime/hook-command.js +0 -80
  112. package/dist/cli/src/runtime/hook-command.js.map +0 -1
  113. package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
  114. package/dist/cli/src/runtime/screen-fallback.js +0 -202
  115. package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
  116. package/dist/cli/src/runtime/screen-share.d.ts +0 -34
  117. package/dist/cli/src/runtime/screen-share.js +0 -376
  118. package/dist/cli/src/runtime/screen-share.js.map +0 -1
  119. package/dist/cli/src/runtime/scrollback.d.ts +0 -11
  120. package/dist/cli/src/runtime/scrollback.js +0 -33
  121. package/dist/cli/src/runtime/scrollback.js.map +0 -1
  122. package/dist/cli/src/runtime/usage-report.d.ts +0 -34
  123. package/dist/cli/src/runtime/usage-report.js +0 -332
  124. package/dist/cli/src/runtime/usage-report.js.map +0 -1
  125. package/dist/cli/src/utils/keep-awake.d.ts +0 -6
  126. package/dist/cli/src/utils/keep-awake.js +0 -48
  127. package/dist/cli/src/utils/keep-awake.js.map +0 -1
  128. package/dist/cli/src/utils/lan-ip.d.ts +0 -9
  129. package/dist/cli/src/utils/lan-ip.js +0 -61
  130. package/dist/cli/src/utils/lan-ip.js.map +0 -1
  131. package/dist/host/src/input-helper.d.ts +0 -1
  132. package/dist/host/src/input-helper.js +0 -564
  133. package/dist/host/src/input-helper.js.map +0 -1
  134. package/dist/shared-protocol/src/index.d.ts +0 -17029
  135. package/dist/shared-protocol/src/index.js +0 -1064
  136. package/dist/shared-protocol/src/index.js.map +0 -1
  137. package/scripts/copy-web.mjs +0 -34
  138. package/src/config.ts +0 -36
  139. package/src/machine-id.ts +0 -61
  140. package/src/providers.ts +0 -97
  141. package/src/runtime/acp/acp-client.ts +0 -521
  142. package/src/runtime/acp/agent-process-discovery.ts +0 -308
  143. package/src/runtime/acp/agent-session.ts +0 -1186
  144. package/src/runtime/acp/agent-workspace.ts +0 -6590
  145. package/src/runtime/acp/claude-sdk-client.ts +0 -1221
  146. package/src/runtime/acp/claude-stream-json-client.ts +0 -633
  147. package/src/runtime/acp/json-rpc.ts +0 -184
  148. package/src/runtime/acp/provider-resolver.ts +0 -154
  149. package/src/runtime/bridge-session.ts +0 -1549
  150. package/src/runtime/hook-command.ts +0 -73
  151. package/src/runtime/screen-fallback.ts +0 -235
  152. package/src/runtime/screen-share.ts +0 -426
  153. package/src/runtime/scrollback.ts +0 -43
  154. package/src/runtime/usage-report.ts +0 -342
  155. package/src/utils/keep-awake.ts +0 -61
  156. package/src/utils/lan-ip.ts +0 -61
  157. package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
  158. package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
  159. package/web/assets/TerminalPanel-CTSklkct.js +0 -1
  160. package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
  161. package/web/assets/index-BsnDaeCt.css +0 -33
  162. package/web/assets/index-q85psYZ5.js +0 -36
  163. package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
  164. package/web/assets/vendor-react-M7xG3qiw.js +0 -49
  165. package/web/assets/vendor-xterm-D15THcI4.js +0 -9
  166. package/web/index.html +0 -37
  167. package/web/logos/claude.png +0 -0
  168. package/web/logos/codex.png +0 -0
  169. package/web/logos/copilot.png +0 -0
  170. package/web/logos/gemini.png +0 -0
  171. package/web/logos/linkshell-1024.png +0 -0
  172. package/web/logos/linkshell-splash.png +0 -0
  173. package/web/logos/linkshell.png +0 -0
@@ -312,7 +312,7 @@ export declare const sessionConfigOptionSchema: z.ZodObject<{
312
312
  }[];
313
313
  id: string;
314
314
  name: string;
315
- category: "other" | "model" | "mode" | "effort";
315
+ category: "mode" | "other" | "model" | "effort";
316
316
  current: string;
317
317
  }, {
318
318
  values: {
@@ -323,7 +323,7 @@ export declare const sessionConfigOptionSchema: z.ZodObject<{
323
323
  id: string;
324
324
  name: string;
325
325
  current: string;
326
- category?: "other" | "model" | "mode" | "effort" | undefined;
326
+ category?: "mode" | "other" | "model" | "effort" | undefined;
327
327
  }>;
328
328
  export type SessionConfigOption = z.infer<typeof sessionConfigOptionSchema>;
329
329
  export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate", [z.ZodObject<{
@@ -795,7 +795,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
795
795
  }>]>>;
796
796
  }, "strip", z.ZodTypeAny, {
797
797
  status: "failed" | "pending" | "in_progress" | "completed";
798
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
798
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
799
799
  title: string;
800
800
  toolCallId: string;
801
801
  sessionUpdate: "tool_call";
@@ -868,7 +868,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
868
868
  }[] | undefined;
869
869
  }, {
870
870
  status: "failed" | "pending" | "in_progress" | "completed";
871
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
871
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
872
872
  title: string;
873
873
  toolCallId: string;
874
874
  sessionUpdate: "tool_call";
@@ -1430,7 +1430,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
1430
1430
  }[];
1431
1431
  id: string;
1432
1432
  name: string;
1433
- category: "other" | "model" | "mode" | "effort";
1433
+ category: "mode" | "other" | "model" | "effort";
1434
1434
  current: string;
1435
1435
  }, {
1436
1436
  values: {
@@ -1441,7 +1441,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
1441
1441
  id: string;
1442
1442
  name: string;
1443
1443
  current: string;
1444
- category?: "other" | "model" | "mode" | "effort" | undefined;
1444
+ category?: "mode" | "other" | "model" | "effort" | undefined;
1445
1445
  }>, "many">;
1446
1446
  }, "strip", z.ZodTypeAny, {
1447
1447
  options: {
@@ -1452,7 +1452,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
1452
1452
  }[];
1453
1453
  id: string;
1454
1454
  name: string;
1455
- category: "other" | "model" | "mode" | "effort";
1455
+ category: "mode" | "other" | "model" | "effort";
1456
1456
  current: string;
1457
1457
  }[];
1458
1458
  sessionUpdate: "ls_config";
@@ -1466,7 +1466,7 @@ export declare const sessionUpdateSchema: z.ZodDiscriminatedUnion<"sessionUpdate
1466
1466
  id: string;
1467
1467
  name: string;
1468
1468
  current: string;
1469
- category?: "other" | "model" | "mode" | "effort" | undefined;
1469
+ category?: "mode" | "other" | "model" | "effort" | undefined;
1470
1470
  }[];
1471
1471
  sessionUpdate: "ls_config";
1472
1472
  }>, z.ZodObject<{
@@ -2174,7 +2174,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2174
2174
  }>]>>;
2175
2175
  }, "strip", z.ZodTypeAny, {
2176
2176
  status: "failed" | "pending" | "in_progress" | "completed";
2177
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
2177
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
2178
2178
  title: string;
2179
2179
  toolCallId: string;
2180
2180
  sessionUpdate: "tool_call";
@@ -2247,7 +2247,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2247
2247
  }[] | undefined;
2248
2248
  }, {
2249
2249
  status: "failed" | "pending" | "in_progress" | "completed";
2250
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
2250
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
2251
2251
  title: string;
2252
2252
  toolCallId: string;
2253
2253
  sessionUpdate: "tool_call";
@@ -2809,7 +2809,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2809
2809
  }[];
2810
2810
  id: string;
2811
2811
  name: string;
2812
- category: "other" | "model" | "mode" | "effort";
2812
+ category: "mode" | "other" | "model" | "effort";
2813
2813
  current: string;
2814
2814
  }, {
2815
2815
  values: {
@@ -2820,7 +2820,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2820
2820
  id: string;
2821
2821
  name: string;
2822
2822
  current: string;
2823
- category?: "other" | "model" | "mode" | "effort" | undefined;
2823
+ category?: "mode" | "other" | "model" | "effort" | undefined;
2824
2824
  }>, "many">;
2825
2825
  }, "strip", z.ZodTypeAny, {
2826
2826
  options: {
@@ -2831,7 +2831,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2831
2831
  }[];
2832
2832
  id: string;
2833
2833
  name: string;
2834
- category: "other" | "model" | "mode" | "effort";
2834
+ category: "mode" | "other" | "model" | "effort";
2835
2835
  current: string;
2836
2836
  }[];
2837
2837
  sessionUpdate: "ls_config";
@@ -2845,7 +2845,7 @@ export declare const sessionEventSchema: z.ZodObject<{
2845
2845
  id: string;
2846
2846
  name: string;
2847
2847
  current: string;
2848
- category?: "other" | "model" | "mode" | "effort" | undefined;
2848
+ category?: "mode" | "other" | "model" | "effort" | undefined;
2849
2849
  }[];
2850
2850
  sessionUpdate: "ls_config";
2851
2851
  }>, z.ZodObject<{
@@ -3133,7 +3133,7 @@ export declare const sessionEventSchema: z.ZodObject<{
3133
3133
  parentToolCallId?: string | undefined;
3134
3134
  } | {
3135
3135
  status: "failed" | "pending" | "in_progress" | "completed";
3136
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
3136
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
3137
3137
  title: string;
3138
3138
  toolCallId: string;
3139
3139
  sessionUpdate: "tool_call";
@@ -3312,7 +3312,7 @@ export declare const sessionEventSchema: z.ZodObject<{
3312
3312
  }[];
3313
3313
  id: string;
3314
3314
  name: string;
3315
- category: "other" | "model" | "mode" | "effort";
3315
+ category: "mode" | "other" | "model" | "effort";
3316
3316
  current: string;
3317
3317
  }[];
3318
3318
  sessionUpdate: "ls_config";
@@ -3433,7 +3433,7 @@ export declare const sessionEventSchema: z.ZodObject<{
3433
3433
  parentToolCallId?: string | undefined;
3434
3434
  } | {
3435
3435
  status: "failed" | "pending" | "in_progress" | "completed";
3436
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "other";
3436
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "other";
3437
3437
  title: string;
3438
3438
  toolCallId: string;
3439
3439
  sessionUpdate: "tool_call";
@@ -3613,7 +3613,7 @@ export declare const sessionEventSchema: z.ZodObject<{
3613
3613
  id: string;
3614
3614
  name: string;
3615
3615
  current: string;
3616
- category?: "other" | "model" | "mode" | "effort" | undefined;
3616
+ category?: "mode" | "other" | "model" | "effort" | undefined;
3617
3617
  }[];
3618
3618
  sessionUpdate: "ls_config";
3619
3619
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkshell-cli",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
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",
@@ -11,7 +11,6 @@
11
11
  "dist",
12
12
  "src",
13
13
  "scripts",
14
- "web",
15
14
  "README.md",
16
15
  "LICENSE"
17
16
  ],
@@ -32,21 +31,13 @@
32
31
  },
33
32
  "dependencies": {
34
33
  "commander": "^13.1.0",
35
- "node-pty": "^1.0.0",
34
+ "node-pty": "^1.1.0",
36
35
  "qrcode-terminal": "^0.12.0",
37
- "ws": "^8.18.1",
38
- "zod": "^3.24.3",
39
- "@linkshell/gateway": "0.5.3",
40
- "@linkshell/protocol": "0.4.10",
41
- "@linkshell/wire": "0.2.1",
42
- "@linkshell/host": "0.3.2"
43
- },
44
- "optionalDependencies": {
45
- "@anthropic-ai/claude-agent-sdk": "^0.2.128",
46
- "werift": "^0.22.9"
36
+ "@linkshell/gateway": "0.6.0",
37
+ "@linkshell/host": "0.4.0",
38
+ "@linkshell/wire": "0.2.1"
47
39
  },
48
40
  "devDependencies": {
49
- "@types/ws": "^8.5.14",
50
41
  "tsx": "^4.19.3",
51
42
  "vitest": "^3.2.1"
52
43
  },
@@ -54,7 +45,7 @@
54
45
  "node": ">=22.13"
55
46
  },
56
47
  "scripts": {
57
- "build": "tsc -p tsconfig.json && node scripts/copy-web.mjs",
48
+ "build": "rm -rf dist *.tsbuildinfo && tsc -p tsconfig.json",
58
49
  "postinstall": "node scripts/postinstall.mjs",
59
50
  "typecheck": "tsc -p tsconfig.json --noEmit",
60
51
  "dev": "tsx src/index.ts",
@@ -0,0 +1,31 @@
1
+ import { join } from "node:path";
2
+ import * as daemon from "../utils/daemon.js";
3
+ import { assertHostRuntime, silenceSqliteWarning } from "./host.js";
4
+
5
+ /** Runs a gateway in this process until SIGINT/SIGTERM. */
6
+ export async function runGatewayForeground(version: string, port: number): Promise<void> {
7
+ // The relay keeps its pairings in node:sqlite, like the host.
8
+ assertHostRuntime();
9
+ silenceSqliteWarning();
10
+ const { startGateway } = await import("@linkshell/gateway");
11
+ // Pairings live here; keeping this file keeps phones paired.
12
+ const databasePath = join(daemon.linkshellDir(), "relay.db");
13
+ const gateway = await startGateway({ port, databasePath });
14
+ daemon.savePid("gateway", process.pid);
15
+
16
+ process.stderr.write(`\n LinkShell Gateway ${version}\n`);
17
+ process.stderr.write(` Listening on http://0.0.0.0:${gateway.port}\n`);
18
+ process.stderr.write(` PID: ${process.pid}\n`);
19
+ process.stderr.write(` Pairings: ${databasePath}\n\n`);
20
+ process.stderr.write(` Computers: linkshell host --gateway ws://this-server:${gateway.port}\n`);
21
+ process.stderr.write(` Health check: curl http://this-server:${gateway.port}/healthz\n\n`);
22
+
23
+ const shutdown = async () => {
24
+ process.stderr.write("[gateway] shutting down...\n");
25
+ daemon.removePid("gateway");
26
+ await gateway.close();
27
+ process.exit(0);
28
+ };
29
+ process.on("SIGINT", shutdown);
30
+ process.on("SIGTERM", shutdown);
31
+ }
@@ -19,11 +19,19 @@ export function silenceSqliteWarning(): void {
19
19
  interface HostConfig {
20
20
  /** v2 gateway base URL (ws:// or wss://), or "off". Unset: the official gateway once logged in. */
21
21
  gateway?: string;
22
+ /** Set once `linkshell setup` has run here (by itself on the first start, or by hand). */
23
+ setup?: "done";
22
24
  }
23
25
 
24
26
  /** LinkShell's own gateway: a Pro account's computers are reachable there. */
25
27
  export const OFFICIAL_GATEWAY = "wss://gateway.itool.tech";
26
28
 
29
+ /** Whether two gateway addresses name the same one, however they were typed (scheme, trailing slash). */
30
+ export function sameGateway(a: string | undefined, b: string): boolean {
31
+ const key = (url: string) => url.trim().replace(/^http/i, "ws").replace(/\/+$/, "").toLowerCase();
32
+ return a !== undefined && key(a) === key(b);
33
+ }
34
+
27
35
  /**
28
36
  * Where devices reach this computer from outside the LAN: LINKSHELL_GATEWAY,
29
37
  * else the saved choice, else the official gateway when logged in.
@@ -31,7 +39,7 @@ export const OFFICIAL_GATEWAY = "wss://gateway.itool.tech";
31
39
  export function resolveGateway(home: string): string | undefined {
32
40
  const chosen = process.env.LINKSHELL_GATEWAY || readHostConfig(home).gateway;
33
41
  if (chosen === "off") return undefined;
34
- // config.json is shared with the v1 bridge, whose gateway URLs end in /ws.
42
+ // A config.json written by 1.x holds a gateway URL ending in /ws.
35
43
  if (chosen) return chosen.replace(/\/ws\/?$/, "");
36
44
  return isLoggedIn() ? OFFICIAL_GATEWAY : undefined;
37
45
  }
@@ -95,7 +103,12 @@ export async function runHostForeground(version: string): Promise<void> {
95
103
  version,
96
104
  env,
97
105
  log,
98
- gateway: { url: () => resolveGateway(home), token: async () => (await getValidToken()) ?? undefined },
106
+ gateway: {
107
+ url: () => resolveGateway(home),
108
+ // The account is LinkShell's, so its token goes to LinkShell's gateway and to no other: a
109
+ // gateway of one's own has no accounts, and would refuse a computer that offered one.
110
+ token: async () => (sameGateway(resolveGateway(home), OFFICIAL_GATEWAY) ? ((await getValidToken()) ?? undefined) : undefined),
111
+ },
99
112
  tcpPort: devPort,
100
113
  claudeCommand: process.env.LINKSHELL_CLAUDE_COMMAND || undefined,
101
114
  claudeAdapter: adapter ? { command: adapter, args: [] } : undefined,
@@ -7,6 +7,7 @@ import { ensureHostRunning, hostRuntimeOk, withRunningHost } from "./host.js";
7
7
 
8
8
  export interface ScreenAccess {
9
9
  supported: boolean;
10
+ /** Whether what captures the screen is there: LinkShell.app on a Mac (installed with the CLI), ffmpeg on Linux. */
10
11
  ffmpeg: boolean;
11
12
  recording: boolean | null;
12
13
  control: boolean | null;
@@ -39,8 +40,8 @@ export function screenReady(access: ScreenAccess): boolean {
39
40
  export function describeScreen(access: ScreenAccess | "old" | undefined): string | undefined {
40
41
  if (!access) return undefined;
41
42
  if (access === "old") return "restart the host to check (linkshell host stop && linkshell host --daemon)";
42
- if (!access.supported) return "not available on this system";
43
- if (!access.ffmpeg) return `needs ffmpeg (${process.platform === "darwin" ? "brew install ffmpeg" : "install it with your package manager"})`;
43
+ if (!access.supported) return process.platform === "darwin" ? "needs a Mac with Apple silicon" : "not available on this system";
44
+ if (!access.ffmpeg) return process.platform === "darwin" ? MISSING_APP : "needs ffmpeg (install it with your package manager)";
44
45
  if (process.platform !== "darwin") return "can be watched (controlling it needs macOS)";
45
46
  if (access.recording === null || access.control === null) return `can't tell${access.problem ? `: ${access.problem}` : ""}`;
46
47
  if (access.recording && access.control) return "can be watched and controlled from the phone";
@@ -48,96 +49,101 @@ export function describeScreen(access: ScreenAccess | "old" | undefined): string
48
49
  return `${missing} isn't allowed yet — set it up with: linkshell screen`;
49
50
  }
50
51
 
52
+ /** A Mac without LinkShell.app: the optional package that carries it wasn't installed (npm run with --omit=optional, or a failed download). */
53
+ const MISSING_APP = "LinkShell.app is missing — reinstall with: npm install -g linkshell-cli";
54
+
51
55
  const PERMISSIONS = {
52
- recording: {
53
- label: "Watching the screen",
54
- where: "Screen & System Audio Recording (录屏与系统录音)",
55
- },
56
- control: {
57
- label: "Controlling it",
58
- where: "Accessibility (辅助功能)",
59
- },
56
+ recording: "Watching the screen",
57
+ control: "Controlling it",
60
58
  } as const;
61
59
 
62
- export async function runScreenSetup(options: { check?: boolean }): Promise<void> {
63
- const out = (line = "") => process.stdout.write(`${line}\n`);
64
- const row = (ok: boolean, label: string, detail: string) => out(` ${ok ? green("✓") : red("✗")} ${label.padEnd(22)} ${detail}`);
65
- out("\n LinkShell screen\n");
66
- if (!hostRuntimeOk()) {
67
- row(false, "Host", `needs Node.js 22.13 or newer (this is ${process.version})`);
68
- process.exitCode = 1;
69
- return;
70
- }
71
- if (!options.check) await ensureHostRunning();
60
+ type Print = (line?: string) => void;
61
+ const row = (out: Print, ok: boolean, label: string, detail: string) => out(` ${ok ? green("✓") : red("✗")} ${label.padEnd(22)} ${detail}`);
62
+
63
+ /**
64
+ * Gets this computer's screen ready for the phone, as far as it can be, saying
65
+ * what it finds. On a Mac the two permissions are LinkShell.app's: its own
66
+ * window comes up on this computer, takes the user to each switch and ticks
67
+ * them off, and this waits beside it. True when everything is in place.
68
+ */
69
+ export async function setUpScreen(out: Print, interactive: boolean): Promise<boolean> {
72
70
  let access = await screenAccess();
73
71
  if (!access) {
74
- row(false, "Host", "not running — start it with: linkshell host --daemon");
75
- process.exitCode = 1;
76
- return;
72
+ row(out, false, "Host", "not running — start it with: linkshell host --daemon");
73
+ return false;
77
74
  }
78
75
  if (access === "old") {
79
- row(false, "Host", "is older than this CLI — restart it (stops running agent turns):");
80
- out(" linkshell host stop && linkshell host --daemon\n");
81
- process.exitCode = 1;
82
- return;
76
+ row(out, false, "Host", "is older than this CLI — restart it (stops running agent turns):");
77
+ out(" linkshell host stop && linkshell host --daemon");
78
+ return false;
83
79
  }
84
80
  if (!access.supported) {
85
- row(false, "Screen", "not available on this system");
86
- process.exitCode = 1;
87
- return;
81
+ row(out, false, "Screen", process.platform === "darwin" ? "needs a Mac with Apple silicon" : "not available on this system");
82
+ return false;
88
83
  }
89
- row(access.ffmpeg, "ffmpeg", access.ffmpeg ? "found" : `not installed — ${process.platform === "darwin" ? "brew install ffmpeg" : "install it with your package manager"}, then run this again`);
90
84
  if (process.platform !== "darwin") {
91
- out(`\n ${access.ffmpeg ? "The screen can be watched from the phone." : ""} Controlling it needs macOS.\n`);
92
- if (!access.ffmpeg) process.exitCode = 1;
93
- return;
85
+ row(out, access.ffmpeg, "ffmpeg", access.ffmpeg ? "found" : "not installed — install it with your package manager, then run this again");
86
+ if (access.ffmpeg) out(dim(" The screen can be watched from the phone; controlling it needs macOS."));
87
+ return access.ffmpeg;
88
+ }
89
+ if (!access.ffmpeg) {
90
+ row(out, false, "LinkShell.app", "missing from this installation — reinstall with: npm install -g linkshell-cli");
91
+ return false;
94
92
  }
95
93
  if (access.recording === null || access.control === null) {
96
- row(false, "Permissions", access.problem ?? "could not be checked");
97
- process.exitCode = 1;
98
- return;
94
+ row(out, false, "Permissions", access.problem ?? "could not be checked");
95
+ return false;
99
96
  }
100
97
 
101
- const app = access.app || "LinkShell";
102
- const interactive = !options.check && process.stdin.isTTY === true;
103
- for (const kind of ["recording", "control"] as const) {
104
- const permission = PERMISSIONS[kind];
105
- if (access[kind]) {
106
- row(true, permission.label, "allowed");
107
- continue;
108
- }
109
- if (!interactive) {
110
- row(false, permission.label, "not allowed yet");
111
- continue;
112
- }
113
- // The system's own question comes up on this computer, and its settings open at the switch.
114
- await screenAccess(true);
115
- out(` ${red("✗")} ${permission.label.padEnd(22)} not allowed yet`);
116
- out(` System Settings has opened at Privacy & Security › ${permission.where}.`);
117
- out(` Turn on the switch for "${app}" there.${kind === "recording" ? " If macOS offers to quit and reopen it, later is fine." : ""}`);
118
- out(dim(" Waiting for the switch… (Ctrl+C to stop; run this again any time)"));
119
- const deadline = Date.now() + 10 * 60_000;
120
- let allowed = false;
121
- while (!allowed && Date.now() < deadline) {
122
- await new Promise((resolve) => setTimeout(resolve, 1500));
123
- const now = await screenAccess().catch(() => undefined);
124
- if (now && now !== "old") {
125
- access = now;
126
- allowed = now[kind] === true;
98
+ const kinds = ["recording", "control"] as const;
99
+ const said = new Set<string>();
100
+ const tell = (now: ScreenAccess) => {
101
+ for (const kind of kinds) {
102
+ if (now[kind] && !said.has(kind)) {
103
+ said.add(kind);
104
+ row(out, true, PERMISSIONS[kind], "allowed");
127
105
  }
128
106
  }
129
- if (!allowed) {
130
- out(`\n Still not allowed. Run ${"`linkshell screen`"} again when you are at the switch.\n`);
131
- process.exitCode = 1;
132
- return;
133
- }
134
- row(true, permission.label, "allowed");
107
+ };
108
+ tell(access);
109
+ if (screenReady(access)) return true;
110
+ if (!interactive) {
111
+ for (const kind of kinds) if (!access[kind]) row(out, false, PERMISSIONS[kind], "not allowed yet");
112
+ return false;
113
+ }
114
+
115
+ // The app's own window: it has the system ask, opens the settings at each switch, and shows what is done.
116
+ await screenAccess(true);
117
+ const missing = kinds.filter((kind) => !(access as ScreenAccess)[kind]).length;
118
+ out(` A LinkShell window has opened on this Mac: turn on the ${missing === 2 ? "two switches" : "switch"} it shows.`);
119
+ out(dim(" Waiting… (Ctrl+C to stop; `linkshell screen` picks up where this left off)"));
120
+ const deadline = Date.now() + 10 * 60_000;
121
+ while (Date.now() < deadline) {
122
+ await new Promise((resolve) => setTimeout(resolve, 1500));
123
+ const now = await screenAccess().catch(() => undefined);
124
+ if (!now || now === "old") continue;
125
+ access = now;
126
+ tell(now);
127
+ if (screenReady(now)) return true;
135
128
  }
129
+ for (const kind of kinds) if (!access[kind]) row(out, false, PERMISSIONS[kind], "still not allowed");
130
+ return false;
131
+ }
136
132
 
137
- if (screenReady(access)) {
138
- out(`\n ${green("Ready.")} In the app: 电脑 › 屏幕. To take the pointer and keyboard, pick 触控板 or 点按 in the viewer's toolbar.\n`);
133
+ export async function runScreenSetup(options: { check?: boolean }): Promise<void> {
134
+ const out: Print = (line = "") => process.stdout.write(`${line}\n`);
135
+ out("\n LinkShell screen\n");
136
+ if (!hostRuntimeOk()) {
137
+ row(out, false, "Host", `needs Node.js 22.13 or newer (this is ${process.version})`);
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ if (!options.check) await ensureHostRunning();
142
+ const interactive = !options.check && process.stdin.isTTY === true;
143
+ if (await setUpScreen(out, interactive)) {
144
+ out(process.platform === "darwin" ? `\n ${green("Ready.")} In the app: 电脑 › 屏幕. To take the pointer and keyboard, pick 触控板 or 点按 in the viewer's toolbar.\n` : "");
139
145
  } else {
140
- out(`\n Not ready yet${access.ffmpeg ? "" : ": install ffmpeg"}${interactive ? "" : "; run `linkshell screen` in a terminal to set it up"}.\n`);
146
+ out(`\n Not ready yet${interactive ? "" : "; run `linkshell screen` in a terminal to set it up"}.\n`);
141
147
  process.exitCode = 1;
142
148
  }
143
149
  }