linkshell-cli 0.3.11 → 0.3.13
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-workspace.d.ts +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +220 -32
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-sessions.js +87 -4
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/agent-workspace.ts +247 -33
- package/src/runtime/acp/codex-sessions.ts +88 -5
|
@@ -46,6 +46,7 @@ export declare class AgentWorkspaceProxy {
|
|
|
46
46
|
private commandForConversation;
|
|
47
47
|
private executeCommand;
|
|
48
48
|
private executeNativeCommand;
|
|
49
|
+
private executeGitNativeCommand;
|
|
49
50
|
private handleRequest;
|
|
50
51
|
private handleNotification;
|
|
51
52
|
private handleAgentMessageDelta;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, join, relative } from "node:path";
|
|
4
5
|
import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
|
|
@@ -10,6 +11,8 @@ import { listCodexStoredSessions, loadCodexStoredTimeline } from "./codex-sessio
|
|
|
10
11
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
11
12
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
12
13
|
const MAX_TIMELINE_ITEMS = 200;
|
|
14
|
+
const MAX_SNAPSHOT_ITEMS = 80;
|
|
15
|
+
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
13
16
|
function id(prefix) {
|
|
14
17
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
15
18
|
}
|
|
@@ -23,6 +26,62 @@ function stringify(value) {
|
|
|
23
26
|
return String(value);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
29
|
+
function truncateUtf8(value, maxBytes = MAX_SNAPSHOT_TEXT_BYTES) {
|
|
30
|
+
if (!value)
|
|
31
|
+
return value;
|
|
32
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
33
|
+
return value;
|
|
34
|
+
let end = Math.min(value.length, maxBytes);
|
|
35
|
+
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
|
|
36
|
+
end = Math.floor(end * 0.9);
|
|
37
|
+
}
|
|
38
|
+
return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
39
|
+
}
|
|
40
|
+
function snapshotContentBlocks(blocks, options = {}) {
|
|
41
|
+
if (!blocks)
|
|
42
|
+
return undefined;
|
|
43
|
+
return blocks.map((block) => block.type === "image" && options.stripImages !== false
|
|
44
|
+
? { ...block, data: undefined, text: block.text || "图片附件" }
|
|
45
|
+
: { ...block, text: truncateUtf8(block.text) });
|
|
46
|
+
}
|
|
47
|
+
function snapshotTimelineItem(item, options = {}) {
|
|
48
|
+
return {
|
|
49
|
+
...item,
|
|
50
|
+
content: snapshotContentBlocks(item.content, options),
|
|
51
|
+
text: truncateUtf8(item.text),
|
|
52
|
+
toolCall: item.toolCall
|
|
53
|
+
? {
|
|
54
|
+
...item.toolCall,
|
|
55
|
+
input: truncateUtf8(item.toolCall.input),
|
|
56
|
+
output: truncateUtf8(item.toolCall.output),
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
commandExecution: item.commandExecution
|
|
60
|
+
? {
|
|
61
|
+
...item.commandExecution,
|
|
62
|
+
command: truncateUtf8(item.commandExecution.command, 16 * 1024),
|
|
63
|
+
output: truncateUtf8(item.commandExecution.output),
|
|
64
|
+
}
|
|
65
|
+
: undefined,
|
|
66
|
+
fileChange: item.fileChange
|
|
67
|
+
? {
|
|
68
|
+
...item.fileChange,
|
|
69
|
+
diff: truncateUtf8(item.fileChange.diff),
|
|
70
|
+
summary: truncateUtf8(item.fileChange.summary),
|
|
71
|
+
}
|
|
72
|
+
: undefined,
|
|
73
|
+
permission: item.permission
|
|
74
|
+
? {
|
|
75
|
+
...item.permission,
|
|
76
|
+
toolInput: truncateUtf8(item.permission.toolInput),
|
|
77
|
+
context: truncateUtf8(item.permission.context),
|
|
78
|
+
}
|
|
79
|
+
: undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function snapshotTimelineItems(items) {
|
|
83
|
+
return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
|
|
84
|
+
}
|
|
26
85
|
function asRecord(value) {
|
|
27
86
|
return typeof value === "object" && value ? value : undefined;
|
|
28
87
|
}
|
|
@@ -558,6 +617,23 @@ function providerLabel(provider) {
|
|
|
558
617
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
559
618
|
const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
560
619
|
const AGENT_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
620
|
+
const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
|
|
621
|
+
const LINKSHELL_NATIVE_COMMANDS = [
|
|
622
|
+
{ name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
|
|
623
|
+
{ name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
|
|
624
|
+
{ name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
|
|
625
|
+
{ name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
|
|
626
|
+
{ name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
|
|
627
|
+
{ name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
|
|
628
|
+
{ name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
|
|
629
|
+
{ name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
|
|
630
|
+
{ name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
|
|
631
|
+
{ name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
|
|
632
|
+
{ name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
|
|
633
|
+
{ name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
|
|
634
|
+
{ name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
|
|
635
|
+
{ name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
|
|
636
|
+
];
|
|
561
637
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
562
638
|
"add-dir",
|
|
563
639
|
"agents",
|
|
@@ -819,17 +895,30 @@ function customClaudeCommands(cwd) {
|
|
|
819
895
|
}
|
|
820
896
|
function defaultProviderCommands(provider, cwd, enabled) {
|
|
821
897
|
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
898
|
+
const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
|
|
899
|
+
.filter((command) => !command.providers || command.providers.includes(provider))
|
|
900
|
+
.map((command) => makeCommand({
|
|
901
|
+
provider,
|
|
902
|
+
name: command.name,
|
|
903
|
+
description: command.description,
|
|
904
|
+
source: "linkshell",
|
|
905
|
+
category: command.category,
|
|
906
|
+
argsMode: command.argsMode ?? "optional",
|
|
907
|
+
destructive: command.destructive,
|
|
908
|
+
disabledReason,
|
|
909
|
+
executionKind: "native",
|
|
910
|
+
}));
|
|
822
911
|
if (provider === "codex") {
|
|
823
|
-
return
|
|
912
|
+
return linkshellCommands;
|
|
824
913
|
}
|
|
825
914
|
if (provider === "claude") {
|
|
826
915
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
827
916
|
...command,
|
|
828
917
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
829
918
|
}));
|
|
830
|
-
return custom;
|
|
919
|
+
return [...linkshellCommands, ...custom];
|
|
831
920
|
}
|
|
832
|
-
return
|
|
921
|
+
return linkshellCommands;
|
|
833
922
|
}
|
|
834
923
|
function mergeCommands(...groups) {
|
|
835
924
|
const map = new Map();
|
|
@@ -846,6 +935,76 @@ function mergeCommands(...groups) {
|
|
|
846
935
|
}
|
|
847
936
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
848
937
|
}
|
|
938
|
+
function isGitNativeCommand(name) {
|
|
939
|
+
return name === "git-status" ||
|
|
940
|
+
name === "git-diff" ||
|
|
941
|
+
name === "git-commit" ||
|
|
942
|
+
name === "git-pull" ||
|
|
943
|
+
name === "git-push" ||
|
|
944
|
+
name === "git-stash" ||
|
|
945
|
+
name === "git-stash-pop";
|
|
946
|
+
}
|
|
947
|
+
function gitCommandArgs(name, args) {
|
|
948
|
+
const message = args?.trim();
|
|
949
|
+
switch (name) {
|
|
950
|
+
case "git-status":
|
|
951
|
+
return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
|
|
952
|
+
case "git-diff":
|
|
953
|
+
return { display: "git diff --stat", argv: ["diff", "--stat"] };
|
|
954
|
+
case "git-commit":
|
|
955
|
+
if (!message)
|
|
956
|
+
throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
|
|
957
|
+
return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
|
|
958
|
+
case "git-pull":
|
|
959
|
+
return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
|
|
960
|
+
case "git-push":
|
|
961
|
+
return { display: "git push", argv: ["push"] };
|
|
962
|
+
case "git-stash":
|
|
963
|
+
return {
|
|
964
|
+
display: "git stash push -u",
|
|
965
|
+
argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
|
|
966
|
+
};
|
|
967
|
+
case "git-stash-pop":
|
|
968
|
+
return { display: "git stash pop", argv: ["stash", "pop"] };
|
|
969
|
+
default:
|
|
970
|
+
throw new Error(`未知 Git 命令:/${name}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function runProcess(command, args, options) {
|
|
974
|
+
return new Promise((resolve, reject) => {
|
|
975
|
+
const child = spawn(command, args, {
|
|
976
|
+
cwd: options.cwd,
|
|
977
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
978
|
+
windowsHide: true,
|
|
979
|
+
});
|
|
980
|
+
const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
|
|
981
|
+
let output = "";
|
|
982
|
+
let bytes = 0;
|
|
983
|
+
let truncated = false;
|
|
984
|
+
const append = (chunk) => {
|
|
985
|
+
if (bytes >= maxBytes) {
|
|
986
|
+
truncated = true;
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const remaining = maxBytes - bytes;
|
|
990
|
+
const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
991
|
+
output += slice.toString("utf8");
|
|
992
|
+
bytes += slice.byteLength;
|
|
993
|
+
if (slice.byteLength < chunk.byteLength)
|
|
994
|
+
truncated = true;
|
|
995
|
+
};
|
|
996
|
+
child.stdout.on("data", append);
|
|
997
|
+
child.stderr.on("data", append);
|
|
998
|
+
child.once("error", reject);
|
|
999
|
+
child.once("close", (exitCode, signal) => {
|
|
1000
|
+
resolve({
|
|
1001
|
+
output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
|
|
1002
|
+
exitCode,
|
|
1003
|
+
signal,
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
849
1008
|
function runtimeCommands(provider, value) {
|
|
850
1009
|
const raw = asRecord(value);
|
|
851
1010
|
const commandsValue = Array.isArray(value) ? value :
|
|
@@ -1308,7 +1467,7 @@ export class AgentWorkspaceProxy {
|
|
|
1308
1467
|
hostDeviceId: this.input.hostDeviceId,
|
|
1309
1468
|
payload: {
|
|
1310
1469
|
conversation: existingConversation,
|
|
1311
|
-
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1470
|
+
snapshot: snapshotTimelineItems(this.timelines.get(existingConversation.id) ?? []),
|
|
1312
1471
|
},
|
|
1313
1472
|
}));
|
|
1314
1473
|
return existingConversation;
|
|
@@ -1355,7 +1514,7 @@ export class AgentWorkspaceProxy {
|
|
|
1355
1514
|
this.input.send(createEnvelope({
|
|
1356
1515
|
type: "agent.v2.conversation.opened",
|
|
1357
1516
|
hostDeviceId: this.input.hostDeviceId,
|
|
1358
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1517
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1359
1518
|
}));
|
|
1360
1519
|
return conversation;
|
|
1361
1520
|
}
|
|
@@ -1394,7 +1553,7 @@ export class AgentWorkspaceProxy {
|
|
|
1394
1553
|
this.input.send(createEnvelope({
|
|
1395
1554
|
type: "agent.v2.conversation.opened",
|
|
1396
1555
|
hostDeviceId: this.input.hostDeviceId,
|
|
1397
|
-
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1556
|
+
payload: { conversation, snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []) },
|
|
1398
1557
|
}));
|
|
1399
1558
|
return conversation;
|
|
1400
1559
|
}
|
|
@@ -1582,14 +1741,8 @@ export class AgentWorkspaceProxy {
|
|
|
1582
1741
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1583
1742
|
return;
|
|
1584
1743
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
this.
|
|
1587
|
-
id: id("error"),
|
|
1588
|
-
conversationId: conversation.id,
|
|
1589
|
-
type: "error",
|
|
1590
|
-
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1591
|
-
createdAt: now,
|
|
1592
|
-
});
|
|
1744
|
+
if (isGitNativeCommand(command.name)) {
|
|
1745
|
+
await this.executeGitNativeCommand(conversation, command.name, args);
|
|
1593
1746
|
return;
|
|
1594
1747
|
}
|
|
1595
1748
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1604,6 +1757,21 @@ export class AgentWorkspaceProxy {
|
|
|
1604
1757
|
: "已退出 Plan mode。");
|
|
1605
1758
|
return;
|
|
1606
1759
|
}
|
|
1760
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1761
|
+
const prompt = command.name === "review"
|
|
1762
|
+
? args || "Review the current local changes."
|
|
1763
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1764
|
+
await this.sendPrompt({
|
|
1765
|
+
conversationId: conversation.id,
|
|
1766
|
+
clientMessageId: id(command.name),
|
|
1767
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1768
|
+
model: conversation.model,
|
|
1769
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1770
|
+
permissionMode: conversation.permissionMode,
|
|
1771
|
+
collaborationMode: conversation.collaborationMode,
|
|
1772
|
+
});
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1607
1775
|
if (command.name === "compact") {
|
|
1608
1776
|
if (!(client instanceof AcpClient))
|
|
1609
1777
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -1642,21 +1810,6 @@ export class AgentWorkspaceProxy {
|
|
|
1642
1810
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
1643
1811
|
return;
|
|
1644
1812
|
}
|
|
1645
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1646
|
-
const prompt = command.name === "review"
|
|
1647
|
-
? args || "Review the current local changes."
|
|
1648
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1649
|
-
await this.sendPrompt({
|
|
1650
|
-
conversationId: conversation.id,
|
|
1651
|
-
clientMessageId: id(command.name),
|
|
1652
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
1653
|
-
model: conversation.model,
|
|
1654
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
1655
|
-
permissionMode: conversation.permissionMode,
|
|
1656
|
-
collaborationMode: conversation.collaborationMode,
|
|
1657
|
-
});
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
1813
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1661
1814
|
}
|
|
1662
1815
|
catch (error) {
|
|
@@ -1671,6 +1824,41 @@ export class AgentWorkspaceProxy {
|
|
|
1671
1824
|
});
|
|
1672
1825
|
}
|
|
1673
1826
|
}
|
|
1827
|
+
async executeGitNativeCommand(conversation, commandName, args) {
|
|
1828
|
+
const git = gitCommandArgs(commandName, args);
|
|
1829
|
+
const toolId = id(commandName);
|
|
1830
|
+
const now = Date.now();
|
|
1831
|
+
conversation.status = "running";
|
|
1832
|
+
conversation.lastMessagePreview = git.display;
|
|
1833
|
+
conversation.lastActivityAt = now;
|
|
1834
|
+
this.emitConversation(conversation);
|
|
1835
|
+
this.upsertTool(conversation.id, {
|
|
1836
|
+
id: toolId,
|
|
1837
|
+
name: "命令",
|
|
1838
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
1839
|
+
createdAt: now,
|
|
1840
|
+
status: "running",
|
|
1841
|
+
});
|
|
1842
|
+
const result = await runProcess("git", git.argv, {
|
|
1843
|
+
cwd: conversation.cwd,
|
|
1844
|
+
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
1845
|
+
});
|
|
1846
|
+
const ok = result.exitCode === 0;
|
|
1847
|
+
this.upsertTool(conversation.id, {
|
|
1848
|
+
id: toolId,
|
|
1849
|
+
name: "命令",
|
|
1850
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
1851
|
+
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
1852
|
+
createdAt: now,
|
|
1853
|
+
status: ok ? "completed" : "failed",
|
|
1854
|
+
});
|
|
1855
|
+
conversation.status = ok ? "idle" : "error";
|
|
1856
|
+
conversation.lastMessagePreview = ok
|
|
1857
|
+
? `${git.display} 完成`
|
|
1858
|
+
: `${git.display} 失败`;
|
|
1859
|
+
conversation.lastActivityAt = Date.now();
|
|
1860
|
+
this.emitConversation(conversation);
|
|
1861
|
+
}
|
|
1674
1862
|
handleRequest(method, params) {
|
|
1675
1863
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1676
1864
|
return this.handleStructuredInput(params, true);
|
|
@@ -2550,7 +2738,7 @@ export class AgentWorkspaceProxy {
|
|
|
2550
2738
|
this.input.send(createEnvelope({
|
|
2551
2739
|
type: "agent.v2.event",
|
|
2552
2740
|
hostDeviceId: this.input.hostDeviceId,
|
|
2553
|
-
payload: { conversationId, conversation, item },
|
|
2741
|
+
payload: { conversationId, conversation, item: snapshotTimelineItem(item, { stripImages: false }) },
|
|
2554
2742
|
}));
|
|
2555
2743
|
}
|
|
2556
2744
|
emitConversation(conversation) {
|
|
@@ -2603,8 +2791,8 @@ export class AgentWorkspaceProxy {
|
|
|
2603
2791
|
}
|
|
2604
2792
|
const conversations = [...this.conversations.values()];
|
|
2605
2793
|
const items = conversationId
|
|
2606
|
-
? this.timelines.get(conversationId) ?? []
|
|
2607
|
-
: [
|
|
2794
|
+
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
2795
|
+
: [];
|
|
2608
2796
|
this.input.send(createEnvelope({
|
|
2609
2797
|
type: "agent.v2.snapshot",
|
|
2610
2798
|
hostDeviceId: this.input.hostDeviceId,
|