linkshell-cli 0.2.104 → 0.2.106

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.2.104",
3
+ "version": "0.2.106",
4
4
  "description": "Remote terminal bridge for Claude Code / Codex / Gemini / Copilot — control from your phone",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,16 @@
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": {
33
- "@linkshell/gateway": "^0.2.34",
34
- "@linkshell/protocol": "^0.2.29",
40
+ "@linkshell/gateway": "workspace:^0.2.35",
41
+ "@linkshell/protocol": "workspace:^0.2.29",
35
42
  "commander": "^13.1.0",
36
43
  "node-pty": "^1.0.0",
37
44
  "qrcode-terminal": "^0.12.0",
@@ -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
+ }
@@ -11,6 +11,12 @@ import { resolveAgentCommand } from "./provider-resolver.js";
11
11
 
12
12
  type AgentStatus = "unavailable" | "idle" | "running" | "waiting_permission" | "error";
13
13
 
14
+ function protocolSupportsImages(protocol: AgentProtocol | undefined): boolean {
15
+ return protocol === "codex-app-server" ||
16
+ protocol === "claude-agent-sdk" ||
17
+ protocol === "claude-stream-json";
18
+ }
19
+
14
20
  interface AgentMessage {
15
21
  id: string;
16
22
  role: "user" | "assistant" | "system";
@@ -1032,7 +1038,7 @@ export class AgentSessionProxy {
1032
1038
  error: enabled ? undefined : this.error,
1033
1039
  supportsSessionList: enabled,
1034
1040
  supportsSessionLoad: enabled,
1035
- supportsImages: false,
1041
+ supportsImages: enabled && protocolSupportsImages(this.activeProtocol),
1036
1042
  supportsAudio: false,
1037
1043
  supportsPermission,
1038
1044
  supportsPlan: enabled,
@@ -502,6 +502,12 @@ function textFromBlocks(blocks: AgentContentBlock[]): string {
502
502
  .join("\n");
503
503
  }
504
504
 
505
+ function protocolSupportsImages(protocol: AgentProtocol | undefined): boolean {
506
+ return protocol === "codex-app-server" ||
507
+ protocol === "claude-agent-sdk" ||
508
+ protocol === "claude-stream-json";
509
+ }
510
+
505
511
  function isSubagentItemType(itemType: string | undefined): boolean {
506
512
  const normalized = normalizedIdentifier(itemType);
507
513
  return (
@@ -1056,7 +1062,7 @@ export class AgentWorkspaceProxy {
1056
1062
  const protocol = this.agentProtocols.get(provider);
1057
1063
  const runtimeCapabilities = this.providerCapabilities.get(provider);
1058
1064
  const enabled = Boolean(client);
1059
- const supportsImages = enabled && protocol === "codex-app-server";
1065
+ const supportsImages = enabled && protocolSupportsImages(protocol);
1060
1066
  const isClaudeFallback = protocol === "claude-stream-json";
1061
1067
  const supportsPermission = enabled && !isClaudeFallback;
1062
1068
  const supportsReasoningEffort = enabled && !isClaudeFallback;
@@ -1260,7 +1266,7 @@ export class AgentWorkspaceProxy {
1260
1266
  if (!client) return;
1261
1267
 
1262
1268
  const protocol = this.protocolForProvider(conversation.provider);
1263
- if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
1269
+ if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1264
1270
  conversation.status = "idle";
1265
1271
  conversation.lastActivityAt = Date.now();
1266
1272
  this.emitConversation(conversation);
@@ -32,6 +32,13 @@ interface ClaudeSdkMessage {
32
32
 
33
33
  type ClaudeQuery = (input: Record<string, unknown>) => AsyncIterable<ClaudeSdkMessage>;
34
34
 
35
+ type AgentInputContentBlock = {
36
+ type?: string;
37
+ text?: string;
38
+ data?: string;
39
+ mimeType?: string;
40
+ };
41
+
35
42
  function id(prefix: string): string {
36
43
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
37
44
  }
@@ -64,6 +71,48 @@ function extractToolResultText(content: unknown): string {
64
71
  return String(content ?? "");
65
72
  }
66
73
 
74
+ function splitImageDataUrl(value: string, fallbackMimeType = "image/png"): { data: string; mimeType: string } {
75
+ const match = value.match(/^data:([^;,]+)?;base64,(.*)$/is);
76
+ if (!match) return { data: value, mimeType: fallbackMimeType };
77
+ return {
78
+ data: match[2] ?? "",
79
+ mimeType: match[1] || fallbackMimeType,
80
+ };
81
+ }
82
+
83
+ function toClaudeMessageContent(blocks: AgentInputContentBlock[]): Record<string, unknown>[] {
84
+ return blocks
85
+ .map((block) => {
86
+ if (block.type === "image" && block.data) {
87
+ const image = splitImageDataUrl(block.data, block.mimeType);
88
+ return {
89
+ type: "image",
90
+ source: {
91
+ type: "base64",
92
+ media_type: image.mimeType,
93
+ data: image.data,
94
+ },
95
+ };
96
+ }
97
+ return { type: "text", text: block.text ?? "" };
98
+ })
99
+ .filter((block) =>
100
+ block.type === "image" ||
101
+ (typeof block.text === "string" && block.text.trim().length > 0),
102
+ );
103
+ }
104
+
105
+ async function* singleUserMessage(content: Record<string, unknown>[]): AsyncIterable<Record<string, unknown>> {
106
+ yield {
107
+ type: "user",
108
+ message: {
109
+ role: "user",
110
+ content,
111
+ },
112
+ parent_tool_use_id: null,
113
+ };
114
+ }
115
+
67
116
  function isRealClaudeSessionId(value: string | undefined): value is string {
68
117
  return Boolean(value && !value.startsWith("agent-session-"));
69
118
  }
@@ -168,7 +217,9 @@ export class ClaudeSdkClient {
168
217
  const abortController = new AbortController();
169
218
  this.abortController = abortController;
170
219
 
171
- const prompt = (input.content as Array<{ type?: string; text?: string; data?: string; mimeType?: string }>)
220
+ const inputBlocks = input.content as AgentInputContentBlock[];
221
+ const hasImages = inputBlocks.some((block) => block.type === "image" && block.data);
222
+ const prompt = inputBlocks
172
223
  .map((block) => {
173
224
  if (block.type === "image") return `[${block.mimeType ?? "image"} attachment]`;
174
225
  return block.text ?? "";
@@ -221,7 +272,8 @@ export class ClaudeSdkClient {
221
272
  let currentMessageId: string | undefined;
222
273
 
223
274
  try {
224
- for await (const message of this.query({ prompt, options: sdkOptions })) {
275
+ const queryPrompt = hasImages ? singleUserMessage(toClaudeMessageContent(inputBlocks)) : prompt;
276
+ for await (const message of this.query({ prompt: queryPrompt, options: sdkOptions })) {
225
277
  if (abortController.signal.aborted) break;
226
278
  this.handleSdkMessage(message, {
227
279
  cwd: input.cwd ?? this.input.cwd,
@@ -7,6 +7,13 @@ import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
7
7
 
8
8
  type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
9
9
 
10
+ type AgentInputContentBlock = {
11
+ type?: string;
12
+ text?: string;
13
+ data?: string;
14
+ mimeType?: string;
15
+ };
16
+
10
17
  interface ClaudeStreamEvent {
11
18
  type: string;
12
19
  subtype?: string;
@@ -65,6 +72,15 @@ function extractToolResultText(content: unknown): string {
65
72
  return String(content ?? "");
66
73
  }
67
74
 
75
+ function splitImageDataUrl(value: string, fallbackMimeType = "image/png"): { data: string; mimeType: string } {
76
+ const match = value.match(/^data:([^;,]+)?;base64,(.*)$/is);
77
+ if (!match) return { data: value, mimeType: fallbackMimeType };
78
+ return {
79
+ data: match[2] ?? "",
80
+ mimeType: match[1] || fallbackMimeType,
81
+ };
82
+ }
83
+
68
84
  export class ClaudeStreamJsonClient {
69
85
  private child: ChildProcessWithoutNullStreams | undefined;
70
86
  private claudeSessionId: string | undefined;
@@ -145,10 +161,11 @@ export class ClaudeStreamJsonClient {
145
161
  }
146
162
 
147
163
  // Build the user message
148
- const contentBlocks = (input.content as Array<{ type: string; text?: string; data?: string; mimeType?: string }>).map(
164
+ const contentBlocks = (input.content as AgentInputContentBlock[]).map(
149
165
  (block) => {
150
166
  if (block.type === "image" && block.data) {
151
- return { type: "image", source: { type: "base64", media_type: block.mimeType ?? "image/png", data: block.data } };
167
+ const image = splitImageDataUrl(block.data, block.mimeType);
168
+ return { type: "image", source: { type: "base64", media_type: image.mimeType, data: image.data } };
152
169
  }
153
170
  return { type: "text", text: block.text ?? "" };
154
171
  },