linkshell-cli 0.3.24 → 0.3.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkshell-cli",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Remote terminal bridge for Claude Code / Codex / Gemini / Copilot — control from your phone",
5
5
  "type": "module",
6
6
  "main": "dist/cli/src/index.js",
@@ -29,14 +29,21 @@
29
29
  "type": "git",
30
30
  "url": "git+https://github.com/LiuTianjie/LinkShell.git"
31
31
  },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "postinstall": "node scripts/postinstall.mjs",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "dev": "tsx src/index.ts",
37
+ "test": "vitest run"
38
+ },
32
39
  "dependencies": {
40
+ "@linkshell/gateway": "0.3.9",
41
+ "@linkshell/protocol": "0.3.11",
33
42
  "commander": "^13.1.0",
34
43
  "node-pty": "^1.0.0",
35
44
  "qrcode-terminal": "^0.12.0",
36
45
  "ws": "^8.18.1",
37
- "zod": "^4.4.3",
38
- "@linkshell/gateway": "0.3.9",
39
- "@linkshell/protocol": "0.3.11"
46
+ "zod": "^4.4.3"
40
47
  },
41
48
  "optionalDependencies": {
42
49
  "@anthropic-ai/claude-agent-sdk": "^0.2.128",
@@ -46,12 +53,5 @@
46
53
  "@types/ws": "^8.5.14",
47
54
  "tsx": "^4.19.3",
48
55
  "vitest": "^3.2.1"
49
- },
50
- "scripts": {
51
- "build": "tsc -p tsconfig.json",
52
- "postinstall": "node scripts/postinstall.mjs",
53
- "typecheck": "tsc -p tsconfig.json --noEmit",
54
- "dev": "tsx src/index.ts",
55
- "test": "vitest run"
56
56
  }
57
- }
57
+ }
@@ -4,6 +4,9 @@ import { createRequire } from "node:module";
4
4
  const require = createRequire(import.meta.url);
5
5
  const pkg = require("../../../../package.json") as { version: string };
6
6
 
7
+ const NPM_UPGRADE_ATTEMPTS = 4;
8
+ const NPM_UPGRADE_RETRY_DELAY_MS = 8_000;
9
+
7
10
  function isBrewInstall(): boolean {
8
11
  try {
9
12
  const out = execSync("brew list linkshell 2>/dev/null", {
@@ -31,6 +34,30 @@ async function fetchLatestVersion(): Promise<string | null> {
31
34
  }
32
35
  }
33
36
 
37
+ function sleep(ms: number): Promise<void> {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ async function upgradeViaNpm(): Promise<void> {
42
+ for (let attempt = 1; attempt <= NPM_UPGRADE_ATTEMPTS; attempt++) {
43
+ try {
44
+ execSync("npm install -g linkshell-cli@latest", { stdio: "inherit", timeout: 120_000 });
45
+ process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
46
+ return;
47
+ } catch {
48
+ if (attempt >= NPM_UPGRADE_ATTEMPTS) break;
49
+ process.stderr.write(
50
+ `\n npm registry may still be syncing the new release. Retrying in ${NPM_UPGRADE_RETRY_DELAY_MS / 1000}s... (${attempt}/${NPM_UPGRADE_ATTEMPTS})\n`,
51
+ );
52
+ await sleep(NPM_UPGRADE_RETRY_DELAY_MS);
53
+ }
54
+ }
55
+
56
+ process.stderr.write("\n \x1b[31m✗\x1b[0m npm upgrade failed. Try manually:\n");
57
+ process.stderr.write(" npm install -g linkshell-cli@latest\n\n");
58
+ process.exitCode = 1;
59
+ }
60
+
34
61
  export async function runUpgrade(): Promise<void> {
35
62
  const current = pkg.version;
36
63
  process.stderr.write(`\n Current version: ${current}\n`);
@@ -62,13 +89,6 @@ export async function runUpgrade(): Promise<void> {
62
89
  }
63
90
  } else {
64
91
  process.stderr.write(" Upgrading via npm...\n");
65
- try {
66
- execSync("npm install -g linkshell-cli@latest", { stdio: "inherit", timeout: 120_000 });
67
- process.stderr.write("\n \x1b[32m✓\x1b[0m Upgraded successfully.\n\n");
68
- } catch {
69
- process.stderr.write("\n \x1b[31m✗\x1b[0m npm upgrade failed. Try manually:\n");
70
- process.stderr.write(" npm install -g linkshell-cli@latest\n\n");
71
- process.exitCode = 1;
72
- }
92
+ await upgradeViaNpm();
73
93
  }
74
94
  }
@@ -2497,6 +2497,7 @@ export class AgentWorkspaceProxy {
2497
2497
  if (
2498
2498
  method.startsWith("account/") ||
2499
2499
  method.startsWith("mcpServer/startupStatus/") ||
2500
+ method.startsWith("remoteControl/") ||
2500
2501
  method === "thread/status/changed" ||
2501
2502
  method === "thread/tokenUsage/updated" ||
2502
2503
  method === "serverRequest/resolved" ||
@@ -5,6 +5,8 @@ import { resolveCodexAppServerCommands, type AgentCommandConfig } from "./provid
5
5
 
6
6
  const CODEX_PROXY_STARTUP_GRACE_MS = 250;
7
7
 
8
+ type JsonRpcId = string | number;
9
+
8
10
  export class CodexRpcBridge {
9
11
  private transport: JsonRpcStdioTransport | undefined;
10
12
  private transportReady = false;
@@ -13,6 +15,7 @@ export class CodexRpcBridge {
13
15
  private candidateIndex = 0;
14
16
  private startupTimer: ReturnType<typeof setTimeout> | undefined;
15
17
  private queuedMessages: JsonRpcMessage[] = [];
18
+ private inFlightRequests = new Map<JsonRpcId, JsonRpcMessage>();
16
19
 
17
20
  constructor(
18
21
  private readonly input: {
@@ -32,6 +35,7 @@ export class CodexRpcBridge {
32
35
  this.queuedMessages.push(jsonMessage);
33
36
  return;
34
37
  }
38
+ this.rememberRequest(jsonMessage);
35
39
  this.transport?.send(jsonMessage);
36
40
  }
37
41
 
@@ -46,6 +50,7 @@ export class CodexRpcBridge {
46
50
  this.transportReady = false;
47
51
  this.starting = false;
48
52
  this.queuedMessages = [];
53
+ this.inFlightRequests.clear();
49
54
  this.stopping = false;
50
55
  }
51
56
 
@@ -80,6 +85,7 @@ export class CodexRpcBridge {
80
85
  () => ({}),
81
86
  (message) => this.handleTransportExit(message),
82
87
  (message) => {
88
+ this.forgetRequest(message);
83
89
  this.input.send(createEnvelope({
84
90
  type: "agent.codex.rpc",
85
91
  hostDeviceId: this.input.hostDeviceId,
@@ -87,7 +93,11 @@ export class CodexRpcBridge {
87
93
  }));
88
94
  return true;
89
95
  },
90
- { includeJsonrpc: false, verbose: this.input.verbose },
96
+ {
97
+ includeJsonrpc: false,
98
+ verbose: this.input.verbose,
99
+ onStderrData: (message) => this.handleTransportStderr(message),
100
+ },
91
101
  );
92
102
  this.transport.start(this.input.cwd);
93
103
  if (this.input.verbose) {
@@ -120,12 +130,14 @@ export class CodexRpcBridge {
120
130
  return;
121
131
  }
122
132
  this.failQueued(message);
133
+ this.failInFlight(message);
123
134
  this.emitError(message);
124
135
  }
125
136
 
126
137
  private flushQueued(): void {
127
138
  const messages = this.queuedMessages.splice(0);
128
139
  for (const message of messages) {
140
+ this.rememberRequest(message);
129
141
  this.transport?.send(message);
130
142
  }
131
143
  }
@@ -160,4 +172,50 @@ export class CodexRpcBridge {
160
172
  },
161
173
  }));
162
174
  }
175
+
176
+ private handleTransportStderr(message: string): void {
177
+ if (this.input.verbose) {
178
+ process.stderr.write(`[agent:codex-rpc] stderr ignored while app-server is running: ${message}\n`);
179
+ }
180
+ }
181
+
182
+ private rememberRequest(message: JsonRpcMessage): void {
183
+ if (!isJsonRpcRequest(message)) return;
184
+ this.inFlightRequests.set(message.id, message);
185
+ }
186
+
187
+ private forgetRequest(message: JsonRpcMessage): void {
188
+ if (!isJsonRpcResponse(message)) return;
189
+ this.inFlightRequests.delete(message.id);
190
+ }
191
+
192
+ private failInFlight(message: string): void {
193
+ const requests = [...this.inFlightRequests.values()];
194
+ this.inFlightRequests.clear();
195
+ for (const item of requests) {
196
+ if (!isJsonRpcRequest(item)) continue;
197
+ this.input.send(createEnvelope({
198
+ type: "agent.codex.rpc",
199
+ hostDeviceId: this.input.hostDeviceId,
200
+ payload: {
201
+ jsonrpc: "2.0",
202
+ id: item.id,
203
+ error: { code: -32000, message },
204
+ },
205
+ }));
206
+ }
207
+ }
208
+ }
209
+
210
+ function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcMessage & { id: JsonRpcId; method: string } {
211
+ return "id" in message &&
212
+ "method" in message &&
213
+ (typeof message.id === "string" || typeof message.id === "number") &&
214
+ typeof message.method === "string";
215
+ }
216
+
217
+ function isJsonRpcResponse(message: JsonRpcMessage): message is JsonRpcMessage & { id: JsonRpcId } {
218
+ return "id" in message &&
219
+ (typeof message.id === "string" || typeof message.id === "number") &&
220
+ ("result" in message || "error" in message);
163
221
  }
@@ -26,6 +26,7 @@ export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotificat
26
26
  interface JsonRpcTransportOptions {
27
27
  includeJsonrpc?: boolean;
28
28
  verbose?: boolean;
29
+ onStderrData?: (message: string) => void;
29
30
  }
30
31
 
31
32
  export class JsonRpcStdioTransport {
@@ -64,7 +65,10 @@ export class JsonRpcStdioTransport {
64
65
  this.child.stdout.on("data", (chunk: string) => this.read(chunk));
65
66
  this.child.stderr.on("data", (chunk: string) => {
66
67
  const trimmed = chunk.trim();
67
- if (trimmed) process.stderr.write(`[agent:stderr] ${trimmed}\n`);
68
+ if (trimmed) {
69
+ process.stderr.write(`[agent:stderr] ${trimmed}\n`);
70
+ this.options.onStderrData?.(trimmed);
71
+ }
68
72
  });
69
73
  this.child.on("error", (error) => this.failAll(error.message));
70
74
  this.child.on("exit", (code, signal) => {