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.
@@ -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 (conversation.provider !== "codex") {
1644
- this.addItem(conversation.id, {
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);