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/dist/cli/src/runtime/acp/agent-session.js +6 -1
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +7 -2
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +43 -2
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +11 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/src/runtime/acp/agent-session.ts +7 -1
- package/src/runtime/acp/agent-workspace.ts +8 -2
- package/src/runtime/acp/claude-sdk-client.ts +54 -2
- package/src/runtime/acp/claude-stream-json-client.ts +19 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkshell-cli",
|
|
3
|
-
"version": "0.2.
|
|
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": "
|
|
34
|
-
"@linkshell/protocol": "
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
164
|
+
const contentBlocks = (input.content as AgentInputContentBlock[]).map(
|
|
149
165
|
(block) => {
|
|
150
166
|
if (block.type === "image" && block.data) {
|
|
151
|
-
|
|
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
|
},
|