linkshell-cli 0.2.100 → 0.2.102
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/README.md +28 -7
- package/dist/cli/src/index.js +1 -0
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.js +49 -3
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +8 -3
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +44 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +12 -0
- 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/index.ts +1 -0
- package/src/providers.ts +50 -3
- package/src/runtime/acp/agent-workspace.ts +6 -3
- package/src/runtime/acp/claude-sdk-client.ts +46 -1
- package/src/runtime/acp/claude-stream-json-client.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkshell-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.102",
|
|
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.33",
|
|
41
|
+
"@linkshell/protocol": "workspace:^0.2.28",
|
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ program
|
|
|
81
81
|
.option("--verbose", "Enable verbose logging")
|
|
82
82
|
.option("--_foreground-bridge", undefined) // internal
|
|
83
83
|
.allowUnknownOption(true)
|
|
84
|
+
.allowExcessArguments(true)
|
|
84
85
|
.action(async (options, command) => {
|
|
85
86
|
const daemon = await import("./utils/daemon.js");
|
|
86
87
|
const keepAwake = shouldKeepAwake(options.keepAwake);
|
package/src/providers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
2
3
|
|
|
3
4
|
export type ProviderName = "claude" | "codex" | "gemini" | "copilot" | "custom";
|
|
4
5
|
|
|
@@ -9,12 +10,58 @@ export interface ProviderConfig {
|
|
|
9
10
|
env: NodeJS.ProcessEnv;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
+
function stripOuterQuotes(value: string): string {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (
|
|
16
|
+
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
17
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
18
|
+
) {
|
|
19
|
+
return trimmed.slice(1, -1);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function canExecute(path: string): boolean {
|
|
13
25
|
try {
|
|
14
|
-
|
|
26
|
+
accessSync(path, constants.X_OK);
|
|
27
|
+
return true;
|
|
15
28
|
} catch {
|
|
29
|
+
return process.platform === "win32" && existsSync(path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function executableExtensions(bin: string): string[] {
|
|
34
|
+
if (process.platform !== "win32") return [""];
|
|
35
|
+
if (/\.[^\\/]+$/.test(bin)) return [""];
|
|
36
|
+
const pathext = process.env.PATHEXT?.split(";").filter(Boolean) ?? [];
|
|
37
|
+
return ["", ...pathext.map((ext) => ext.toLowerCase()), ...pathext.map((ext) => ext.toUpperCase())];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function commandHasPathSeparator(command: string): boolean {
|
|
41
|
+
return command.includes("/") || command.includes("\\");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function which(bin: string): string | undefined {
|
|
45
|
+
const command = stripOuterQuotes(bin);
|
|
46
|
+
if (!command) return undefined;
|
|
47
|
+
const extensions = executableExtensions(command);
|
|
48
|
+
if (commandHasPathSeparator(command)) {
|
|
49
|
+
for (const extension of extensions) {
|
|
50
|
+
const candidate = `${command}${extension}`;
|
|
51
|
+
if (canExecute(candidate)) return candidate;
|
|
52
|
+
}
|
|
16
53
|
return undefined;
|
|
17
54
|
}
|
|
55
|
+
const pathEntries = (process.env.PATH ?? "")
|
|
56
|
+
.split(delimiter)
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
for (const dir of pathEntries) {
|
|
59
|
+
for (const extension of extensions) {
|
|
60
|
+
const candidate = join(dir, `${command}${extension}`);
|
|
61
|
+
if (canExecute(candidate)) return candidate;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
18
65
|
}
|
|
19
66
|
|
|
20
67
|
function resolveClaudeProvider(input: {
|
|
@@ -997,9 +997,11 @@ export class AgentWorkspaceProxy {
|
|
|
997
997
|
client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
|
|
998
998
|
protocol: AgentProtocol,
|
|
999
999
|
): Promise<void> {
|
|
1000
|
-
if (
|
|
1000
|
+
if (client instanceof AcpClient && protocol !== "codex-app-server") return;
|
|
1001
|
+
const listModels = (client as { listModels?: () => Promise<unknown> }).listModels;
|
|
1002
|
+
if (typeof listModels !== "function") return;
|
|
1001
1003
|
try {
|
|
1002
|
-
const result = await
|
|
1004
|
+
const result = await listModels.call(client);
|
|
1003
1005
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1004
1006
|
if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1005
1007
|
} catch (error) {
|
|
@@ -1781,10 +1783,11 @@ export class AgentWorkspaceProxy {
|
|
|
1781
1783
|
if (normalized === "reasoning" || normalized === "thinking") {
|
|
1782
1784
|
const text = firstString(item, ["text", "content", "summary", "message"]) ??
|
|
1783
1785
|
stringifyDefined(item.contentItems ?? item.summary);
|
|
1786
|
+
if (!text?.trim()) return true;
|
|
1784
1787
|
this.upsertItem(conversationId, {
|
|
1785
1788
|
...base,
|
|
1786
1789
|
kind: "thinking",
|
|
1787
|
-
text
|
|
1790
|
+
text,
|
|
1788
1791
|
});
|
|
1789
1792
|
return true;
|
|
1790
1793
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { readdirSync, existsSync } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
|
|
5
5
|
|
|
6
6
|
type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
@@ -68,6 +68,29 @@ function isRealClaudeSessionId(value: string | undefined): value is string {
|
|
|
68
68
|
return Boolean(value && !value.startsWith("agent-session-"));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
72
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
73
|
+
? value as Record<string, unknown>
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stringField(value: unknown, keys: string[]): string | undefined {
|
|
78
|
+
const record = asRecord(value);
|
|
79
|
+
if (!record) return undefined;
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
const candidate = record[key];
|
|
82
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isInsideCwd(cwd: string, candidate: string): boolean {
|
|
88
|
+
const root = resolve(cwd);
|
|
89
|
+
const target = resolve(root, candidate);
|
|
90
|
+
const rel = relative(root, target);
|
|
91
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
function outcomeFromPermissionResponse(value: unknown): "allow" | "deny" {
|
|
72
95
|
const raw = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
73
96
|
if (raw.behavior === "allow") return "allow";
|
|
@@ -158,9 +181,18 @@ export class ClaudeSdkClient {
|
|
|
158
181
|
abortController,
|
|
159
182
|
canUseTool: async (toolName: string, toolInput: unknown) => {
|
|
160
183
|
if (input.permissionMode === "full_access") return { behavior: "allow" };
|
|
184
|
+
if (["Read", "Glob", "Grep", "LS", "NotebookRead", "TodoRead"].includes(toolName)) {
|
|
185
|
+
return { behavior: "allow" };
|
|
186
|
+
}
|
|
161
187
|
if (input.permissionMode === "read_only" && ["Write", "Edit", "MultiEdit", "Bash"].includes(toolName)) {
|
|
162
188
|
return { behavior: "deny", message: "Read-only mode is active." };
|
|
163
189
|
}
|
|
190
|
+
if (input.permissionMode === "workspace_write" && ["Write", "Edit", "MultiEdit"].includes(toolName)) {
|
|
191
|
+
const filePath = stringField(toolInput, ["file_path", "path", "notebook_path"]);
|
|
192
|
+
if (filePath && isInsideCwd(input.cwd ?? this.input.cwd, filePath)) {
|
|
193
|
+
return { behavior: "allow" };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
164
196
|
const requestId = id("claude-perm");
|
|
165
197
|
const response = await this.input.onRequest("claude/requestApproval", {
|
|
166
198
|
requestId,
|
|
@@ -248,6 +280,19 @@ export class ClaudeSdkClient {
|
|
|
248
280
|
return { sessions };
|
|
249
281
|
}
|
|
250
282
|
|
|
283
|
+
async listModels(): Promise<unknown> {
|
|
284
|
+
return {
|
|
285
|
+
defaultModel: "default",
|
|
286
|
+
models: [
|
|
287
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
288
|
+
{ id: "opus", label: "Opus" },
|
|
289
|
+
{ id: "haiku", label: "Haiku" },
|
|
290
|
+
{ id: "sonnet[1m]", label: "Sonnet 1M" },
|
|
291
|
+
{ id: "opusplan", label: "Opus Plan" },
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
251
296
|
stop(): void {
|
|
252
297
|
this.cancel({});
|
|
253
298
|
}
|
|
@@ -443,6 +443,19 @@ export class ClaudeStreamJsonClient {
|
|
|
443
443
|
return { sessions };
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
async listModels(): Promise<unknown> {
|
|
447
|
+
return {
|
|
448
|
+
defaultModel: "default",
|
|
449
|
+
models: [
|
|
450
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
451
|
+
{ id: "opus", label: "Opus" },
|
|
452
|
+
{ id: "haiku", label: "Haiku" },
|
|
453
|
+
{ id: "sonnet[1m]", label: "Sonnet 1M" },
|
|
454
|
+
{ id: "opusplan", label: "Opus Plan" },
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
446
459
|
stop(): void {
|
|
447
460
|
if (this.child && !this.child.killed) {
|
|
448
461
|
this.child.kill("SIGTERM");
|