linkshell-cli 0.3.12 → 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 +156 -26
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/runtime/acp/agent-workspace.ts +174 -27
|
@@ -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";
|
|
@@ -616,6 +617,23 @@ function providerLabel(provider) {
|
|
|
616
617
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
617
618
|
const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
618
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
|
+
];
|
|
619
637
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
620
638
|
"add-dir",
|
|
621
639
|
"agents",
|
|
@@ -877,17 +895,30 @@ function customClaudeCommands(cwd) {
|
|
|
877
895
|
}
|
|
878
896
|
function defaultProviderCommands(provider, cwd, enabled) {
|
|
879
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
|
+
}));
|
|
880
911
|
if (provider === "codex") {
|
|
881
|
-
return
|
|
912
|
+
return linkshellCommands;
|
|
882
913
|
}
|
|
883
914
|
if (provider === "claude") {
|
|
884
915
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
885
916
|
...command,
|
|
886
917
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
887
918
|
}));
|
|
888
|
-
return custom;
|
|
919
|
+
return [...linkshellCommands, ...custom];
|
|
889
920
|
}
|
|
890
|
-
return
|
|
921
|
+
return linkshellCommands;
|
|
891
922
|
}
|
|
892
923
|
function mergeCommands(...groups) {
|
|
893
924
|
const map = new Map();
|
|
@@ -904,6 +935,76 @@ function mergeCommands(...groups) {
|
|
|
904
935
|
}
|
|
905
936
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
906
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
|
+
}
|
|
907
1008
|
function runtimeCommands(provider, value) {
|
|
908
1009
|
const raw = asRecord(value);
|
|
909
1010
|
const commandsValue = Array.isArray(value) ? value :
|
|
@@ -1640,14 +1741,8 @@ export class AgentWorkspaceProxy {
|
|
|
1640
1741
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1641
1742
|
return;
|
|
1642
1743
|
}
|
|
1643
|
-
if (
|
|
1644
|
-
this.
|
|
1645
|
-
id: id("error"),
|
|
1646
|
-
conversationId: conversation.id,
|
|
1647
|
-
type: "error",
|
|
1648
|
-
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1649
|
-
createdAt: now,
|
|
1650
|
-
});
|
|
1744
|
+
if (isGitNativeCommand(command.name)) {
|
|
1745
|
+
await this.executeGitNativeCommand(conversation, command.name, args);
|
|
1651
1746
|
return;
|
|
1652
1747
|
}
|
|
1653
1748
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1662,6 +1757,21 @@ export class AgentWorkspaceProxy {
|
|
|
1662
1757
|
: "已退出 Plan mode。");
|
|
1663
1758
|
return;
|
|
1664
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
|
+
}
|
|
1665
1775
|
if (command.name === "compact") {
|
|
1666
1776
|
if (!(client instanceof AcpClient))
|
|
1667
1777
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -1700,21 +1810,6 @@ export class AgentWorkspaceProxy {
|
|
|
1700
1810
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
1701
1811
|
return;
|
|
1702
1812
|
}
|
|
1703
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1704
|
-
const prompt = command.name === "review"
|
|
1705
|
-
? args || "Review the current local changes."
|
|
1706
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1707
|
-
await this.sendPrompt({
|
|
1708
|
-
conversationId: conversation.id,
|
|
1709
|
-
clientMessageId: id(command.name),
|
|
1710
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
1711
|
-
model: conversation.model,
|
|
1712
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
1713
|
-
permissionMode: conversation.permissionMode,
|
|
1714
|
-
collaborationMode: conversation.collaborationMode,
|
|
1715
|
-
});
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
1813
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1719
1814
|
}
|
|
1720
1815
|
catch (error) {
|
|
@@ -1729,6 +1824,41 @@ export class AgentWorkspaceProxy {
|
|
|
1729
1824
|
});
|
|
1730
1825
|
}
|
|
1731
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
|
+
}
|
|
1732
1862
|
handleRequest(method, params) {
|
|
1733
1863
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1734
1864
|
return this.handleStructuredInput(params, true);
|