lark-coding-assistant 0.1.3 → 0.2.0

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/cli.ts
2
2
  import { randomBytes as randomBytes2 } from "crypto";
3
- import { access, readFile as readFile3 } from "fs/promises";
3
+ import { readFile as readFile3 } from "fs/promises";
4
4
  import { spawn as spawn3 } from "child_process";
5
5
  import { fileURLToPath } from "url";
6
6
  import { resolve } from "path";
@@ -64,6 +64,14 @@ async function writeJsonAtomic(path, value, mode = 384) {
64
64
  await chmod(path, mode);
65
65
  }
66
66
 
67
+ // src/agents/types.ts
68
+ var AGENT_IDS = ["codex", "traex", "claude"];
69
+ function normalizeAgentId(value) {
70
+ if (value === "trae-cli") return "traex";
71
+ if (value === "claude-code") return "claude";
72
+ return AGENT_IDS.includes(value) ? value : void 0;
73
+ }
74
+
67
75
  // src/core/store.ts
68
76
  var AppStore = class {
69
77
  constructor(paths2) {
@@ -80,8 +88,21 @@ var AppStore = class {
80
88
  chmod2(this.paths.logsDir, 448)
81
89
  ]);
82
90
  }
83
- loadConfig() {
84
- return readJson(this.paths.config);
91
+ async loadConfig() {
92
+ const config = await readJson(this.paths.config);
93
+ if (!config) return void 0;
94
+ const binaries = config.agentBinaries ?? {};
95
+ return {
96
+ tenant: config.tenant,
97
+ appId: config.appId,
98
+ tmuxBinary: config.tmuxBinary,
99
+ agentBinaries: {
100
+ codex: binaries.codex ?? "codex",
101
+ traex: binaries.traex ?? binaries["trae-cli"] ?? "trae-cli",
102
+ claude: binaries.claude ?? binaries["claude-code"] ?? "claude"
103
+ },
104
+ pollIntervalMs: config.pollIntervalMs
105
+ };
85
106
  }
86
107
  saveConfig(config) {
87
108
  return writeJsonAtomic(this.paths.config, config);
@@ -93,7 +114,13 @@ var AppStore = class {
93
114
  return writeJsonAtomic(this.paths.secrets, secrets);
94
115
  }
95
116
  async loadState() {
96
- return await readJson(this.paths.state) ?? emptyState();
117
+ const state = await readJson(this.paths.state);
118
+ if (!state) return emptyState();
119
+ const sessions = Object.fromEntries(Object.entries(state.sessions ?? {}).flatMap(([id, session]) => {
120
+ const agent = normalizeAgentId(session.agent);
121
+ return agent ? [[id, { ...session, agent }]] : [];
122
+ }));
123
+ return { ...state, sessions };
97
124
  }
98
125
  saveState(state) {
99
126
  return writeJsonAtomic(this.paths.state, state);
@@ -198,7 +225,7 @@ function resolveResumeOption(options) {
198
225
  const modes = [options.resume !== void 0, options.resumeLast, options.resumeAll].filter(Boolean).length;
199
226
  if (modes > 1) {
200
227
  throw new AppError(
201
- "INVALID_OPTIONS",
228
+ "INVALID_RESUME",
202
229
  "--resume, --resume-last, and --resume-all cannot be used together",
203
230
  { reason: "--resume\u3001--resume-last \u548C --resume-all \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528" }
204
231
  );
@@ -252,12 +279,12 @@ var CODEX_DIALECT = {
252
279
  };
253
280
  var TRAE_DIALECT = {
254
281
  ...CODEX_DIALECT,
255
- id: "trae-cli",
282
+ id: "traex",
256
283
  headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
257
284
  customInputControls: [/^(?:other|none of the above|add notes)$/i]
258
285
  };
259
286
  var CLAUDE_DIALECT = {
260
- id: "claude-code",
287
+ id: "claude",
261
288
  headerPatterns: [
262
289
  ...commonHeaders,
263
290
  /^\s*(?:←\s*)?[☐☑☒]\s+.+?(?:\s+✔\s+Submit\s*→)?\s*$/i,
@@ -591,11 +618,18 @@ function lastMatchingIndex(lines, pattern) {
591
618
  // src/agents/stop-hook.ts
592
619
  function codexStyleStopHookArgs(command) {
593
620
  const hook = `{hooks=[{type="command",command=${JSON.stringify(command)},timeout=5}]}`;
594
- return ["--dangerously-bypass-hook-trust", "-c", `hooks.Stop=[${hook}]`];
621
+ return [
622
+ "--dangerously-bypass-hook-trust",
623
+ "-c",
624
+ `hooks.SessionStart=[${hook}]`,
625
+ "-c",
626
+ `hooks.Stop=[${hook}]`
627
+ ];
595
628
  }
596
629
  function claudeStopHookArgs(command) {
597
630
  return ["--settings", JSON.stringify({
598
631
  hooks: {
632
+ SessionStart: [{ hooks: [{ type: "command", command, timeout: 5 }] }],
599
633
  Stop: [{ hooks: [{ type: "command", command, timeout: 5 }] }]
600
634
  }
601
635
  })];
@@ -604,7 +638,7 @@ function claudeStopHookArgs(command) {
604
638
  // src/agents/codex.ts
605
639
  var codexAdapter = {
606
640
  id: "codex",
607
- displayName: "Codex",
641
+ displayName: "codex",
608
642
  groupOrder: 10,
609
643
  binary: (config) => config.agentBinaries.codex,
610
644
  versionArgs: ["--version"],
@@ -617,10 +651,10 @@ var codexAdapter = {
617
651
 
618
652
  // src/agents/trae-cli.ts
619
653
  var traeCliAdapter = {
620
- id: "trae-cli",
621
- displayName: "Trae CLI",
654
+ id: "traex",
655
+ displayName: "traex",
622
656
  groupOrder: 20,
623
- binary: (config) => config.agentBinaries["trae-cli"],
657
+ binary: (config) => config.agentBinaries.traex,
624
658
  versionArgs: ["--version"],
625
659
  buildLaunchArgs: ({ resume, stopHookCommand }) => [
626
660
  ...codexStyleStopHookArgs(stopHookCommand),
@@ -631,10 +665,10 @@ var traeCliAdapter = {
631
665
 
632
666
  // src/agents/claude-code.ts
633
667
  var claudeCodeAdapter = {
634
- id: "claude-code",
635
- displayName: "Claude Code",
668
+ id: "claude",
669
+ displayName: "claude",
636
670
  groupOrder: 30,
637
- binary: (config) => config.agentBinaries["claude-code"],
671
+ binary: (config) => config.agentBinaries.claude,
638
672
  versionArgs: ["--version"],
639
673
  buildLaunchArgs: ({ resume, stopHookCommand }) => [
640
674
  ...claudeStopHookArgs(stopHookCommand),
@@ -643,9 +677,6 @@ var claudeCodeAdapter = {
643
677
  detectScreen: detectClaudeScreen
644
678
  };
645
679
 
646
- // src/agents/types.ts
647
- var AGENT_IDS = ["codex", "trae-cli", "claude-code"];
648
-
649
680
  // src/agents/registry.ts
650
681
  var adapters = /* @__PURE__ */ new Map([
651
682
  [codexAdapter.id, codexAdapter],
@@ -657,9 +688,6 @@ function getAgentAdapter(id) {
657
688
  if (!adapter) throw new Error(`unsupported coding agent: ${id}`);
658
689
  return adapter;
659
690
  }
660
- function isAgentId(value) {
661
- return AGENT_IDS.includes(value);
662
- }
663
691
 
664
692
  // src/daemon/lifecycle.ts
665
693
  import { mkdir as mkdir3, open as open2, readFile as readFile2 } from "fs/promises";
@@ -676,6 +704,13 @@ async function daemonInfo(paths2, timeoutMs = 500) {
676
704
  async function startDaemonProcess(paths2, daemonEntry, readyTimeoutMs = 5e3) {
677
705
  const existing = await daemonInfo(paths2);
678
706
  if (existing) return existing;
707
+ const health = await daemonHealth(paths2);
708
+ if (health.status === "running") return health.info;
709
+ if (health.status === "unresponsive") {
710
+ throw new AppError("DAEMON_UNRESPONSIVE", "daemon process is alive but its control socket is unresponsive", {
711
+ pid: health.pid
712
+ });
713
+ }
679
714
  await Promise.all([
680
715
  mkdir3(paths2.runtimeDir, { recursive: true, mode: 448 }),
681
716
  mkdir3(paths2.logsDir, { recursive: true, mode: 448 })
@@ -701,6 +736,25 @@ async function startDaemonProcess(paths2, daemonEntry, readyTimeoutMs = 5e3) {
701
736
  }
702
737
  throw new Error("daemon did not become ready");
703
738
  }
739
+ async function daemonHealth(paths2, probe = defaultHealthProbe) {
740
+ for (let attempt = 0; attempt < 3; attempt += 1) {
741
+ const info = await probe.ping(paths2, 400);
742
+ if (info) return { status: "running", info };
743
+ if (attempt < 2) await probe.wait(80);
744
+ }
745
+ const pid = await probe.readPid(paths2);
746
+ if (validPid(pid) && probe.processIsAlive(pid) && await probe.isCurrentDaemonProcess(pid)) {
747
+ return { status: "unresponsive", pid };
748
+ }
749
+ return { status: "stopped" };
750
+ }
751
+ var defaultHealthProbe = {
752
+ ping: daemonInfo,
753
+ readPid: async (paths2) => Number.parseInt(await readFile2(paths2.pid, "utf8").catch(() => ""), 10),
754
+ processIsAlive,
755
+ isCurrentDaemonProcess,
756
+ wait: delay
757
+ };
704
758
  async function stopDaemonProcess(paths2) {
705
759
  const info = await daemonInfo(paths2);
706
760
  if (!info) return stopDaemonByPid(paths2);
@@ -794,6 +848,37 @@ function formatKnownError(error, context) {
794
848
  ` lark-coding-assistant attach ${sessionId}`,
795
849
  " lark-coding-assistant start --name <\u65B0\u540D\u79F0>"
796
850
  ];
851
+ case "AGENT_SESSION_IN_USE": {
852
+ const ownerSessionId = safeValue(context.ownerSessionId, "\u73B0\u6709 session");
853
+ return [
854
+ `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u5BF9\u5E94\u7684 Agent \u539F\u751F session \u5DF2\u7531\u300C${ownerSessionId}\u300D\u8FDE\u63A5\u3002`,
855
+ "",
856
+ "\u8BF7\u76F4\u63A5\u8FDE\u63A5\u73B0\u6709 session\uFF1A",
857
+ ` lark-coding-assistant attach ${ownerSessionId}`
858
+ ];
859
+ }
860
+ case "AGENT_EXITED_DURING_STARTUP": {
861
+ const exitStatus = typeof context.exitStatus === "number" ? `\uFF08\u9000\u51FA\u7801 ${context.exitStatus}\uFF09` : "";
862
+ return [
863
+ `Agent \u542F\u52A8\u540E\u7ACB\u5373\u9000\u51FA${exitStatus}\uFF0Csession\u300C${sessionId}\u300D\u672A\u521B\u5EFA\u3002`,
864
+ "",
865
+ "\u539F\u59CB\u9519\u8BEF\uFF1A",
866
+ safeValue(context.terminalExcerpt, "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002"),
867
+ "",
868
+ "\u53EF\u67E5\u770B\u65E5\u5FD7\u6216\u6539\u4E3A\u542F\u52A8\u65B0\u4F1A\u8BDD\uFF1A",
869
+ " lark-coding-assistant logs",
870
+ ` lark-coding-assistant start --name ${sessionId} --agent ${safeValue(context.agent, "codex")}`
871
+ ];
872
+ }
873
+ case "AGENT_IDENTITY_TIMEOUT":
874
+ return [
875
+ `\u65E0\u6CD5\u786E\u8BA4\u6062\u590D\u76EE\u6807\uFF0Csession\u300C${sessionId}\u300D\u672A\u521B\u5EFA\u3002`,
876
+ "Agent \u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46 LCA \u672A\u80FD\u8BC6\u522B\u539F\u751F session ID\u3002",
877
+ "",
878
+ "\u4E34\u65F6 tmux \u5DF2\u6E05\u7406\uFF0C\u8BF7\u67E5\u770B\u65E5\u5FD7\u540E\u91CD\u8BD5\u6216\u542F\u52A8\u65B0\u4F1A\u8BDD\uFF1A",
879
+ " lark-coding-assistant logs",
880
+ ` lark-coding-assistant start --name ${sessionId} --agent ${safeValue(context.agent, "codex")}`
881
+ ];
797
882
  case "SESSION_NOT_FOUND":
798
883
  return [
799
884
  `\u627E\u4E0D\u5230 session\u300C${sessionId}\u300D\u3002`,
@@ -827,6 +912,19 @@ function formatKnownError(error, context) {
827
912
  "",
828
913
  "\u8BF7\u8FD0\u884C lark-coding-assistant --help \u67E5\u770B\u53EF\u7528\u53C2\u6570\u3002"
829
914
  ];
915
+ case "INVALID_RESUME":
916
+ return [
917
+ `\u6062\u590D\u53C2\u6570\u65E0\u6548\uFF1A${safeValue(context.reason, error.message)}`,
918
+ "",
919
+ "\u8BF7\u53EA\u9009\u62E9\u4E00\u79CD\u6062\u590D\u65B9\u5F0F\uFF0C\u5E76\u68C0\u67E5\u5386\u53F2 session ID\u3002"
920
+ ];
921
+ case "START_FAILED":
922
+ return [
923
+ `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\u3002`,
924
+ "",
925
+ "\u8BF7\u68C0\u67E5\u5DE5\u4F5C\u76EE\u5F55\u3001Agent \u5B89\u88C5\u548C daemon \u65E5\u5FD7\uFF1A",
926
+ " lark-coding-assistant logs"
927
+ ];
830
928
  case "DAEMON_UNAVAILABLE":
831
929
  return [
832
930
  "\u65E0\u6CD5\u8FDE\u63A5 bridge daemon\u3002",
@@ -835,6 +933,13 @@ function formatKnownError(error, context) {
835
933
  " lark-coding-assistant daemon restart",
836
934
  " lark-coding-assistant logs"
837
935
  ];
936
+ case "DAEMON_UNRESPONSIVE":
937
+ return [
938
+ "bridge daemon \u8FDB\u7A0B\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u63A7\u5236\u901A\u9053\u65E0\u54CD\u5E94\u3002",
939
+ "",
940
+ "\u8BF7\u91CD\u542F daemon\uFF1B\u73B0\u6709 coding-agent/tmux sessions \u4F1A\u4FDD\u7559\uFF1A",
941
+ " lark-coding-assistant daemon restart"
942
+ ];
838
943
  case "REQUEST_TIMEOUT":
839
944
  return [
840
945
  "bridge daemon \u672A\u53CA\u65F6\u54CD\u5E94\u3002",
@@ -888,6 +993,37 @@ function debugStack(error) {
888
993
  return values.join("\nCaused by:\n");
889
994
  }
890
995
 
996
+ // src/session/start-request.ts
997
+ import { stat } from "fs/promises";
998
+ import { isAbsolute } from "path";
999
+ async function validateStartSessionRequest(request) {
1000
+ if (!validSessionId(request.sessionId)) {
1001
+ throw new AppError(
1002
+ "INVALID_SESSION_NAME",
1003
+ "session name must use letters, digits, underscore, or dash",
1004
+ { sessionId: request.sessionId }
1005
+ );
1006
+ }
1007
+ if (!isAbsolute(request.cwd)) {
1008
+ throw new AppError("INVALID_CWD", "working directory must be absolute", { cwd: request.cwd });
1009
+ }
1010
+ const info = await stat(request.cwd).catch((error) => {
1011
+ throw new AppError("INVALID_CWD", `working directory is unavailable: ${request.cwd}`, { cwd: request.cwd }, { cause: error });
1012
+ });
1013
+ if (!info.isDirectory()) {
1014
+ throw new AppError("INVALID_CWD", `working directory is not a directory: ${request.cwd}`, { cwd: request.cwd });
1015
+ }
1016
+ if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
1017
+ throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
1018
+ reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
1019
+ });
1020
+ }
1021
+ return request;
1022
+ }
1023
+ function validSessionId(value) {
1024
+ return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
1025
+ }
1026
+
891
1027
  // src/cli.ts
892
1028
  var program = new Command();
893
1029
  var paths = resolveAppPaths();
@@ -897,14 +1033,14 @@ var packageInfo = JSON.parse(
897
1033
  );
898
1034
  program.name("lark-coding-assistant").version(packageInfo.version);
899
1035
  program.command("init").description("Configure Feishu/Lark PersonalAgent").action(runInit);
900
- program.command("start").option("-n, --name <name>", "Session name", "default").option("--agent <agent>", "Coding agent (codex, trae-cli, or claude-code)", parseAgentId, "codex").option("--cwd <path>", "Coding agent working directory").option("--resume [session-id]", "Resume agent session; omit the ID to open the picker").option("--resume-last", "Resume the most recent agent session in this working directory").option("--resume-all", "Show all agent sessions in the resume picker").action(runStart);
1036
+ program.command("start").option("-n, --name <name>", "Session name", "default").option("--agent <agent>", "Coding agent (codex, traex, or claude)", parseAgentId, "codex").option("--cwd <path>", "Coding agent working directory").option("--resume [session-id]", "Resume agent session; omit the ID to open the picker").option("--resume-last", "Resume the most recent agent session in this working directory").option("--resume-all", "Show all agent sessions in the resume picker").action(runStart);
901
1037
  program.command("attach").argument("[name]", "Session name", "default").description("Attach local terminal to coding-agent tmux session").action(runAttach);
902
1038
  program.command("bind-code").description("Generate a new one-time Lark binding code").action(runBindCode);
903
1039
  program.command("status").argument("[name]", "Session name").description("Show daemon and session status").action(runStatus);
904
1040
  program.command("stop").argument("[name]", "Session name").description("Stop managed coding-agent/tmux session").action(runStop);
905
1041
  program.command("logs").option("-n, --lines <count>", "Number of lines", "100").action(runLogs);
906
1042
  program.command("reset-owner").description("Clear persistent Lark owner").action(runResetOwner);
907
- var daemonCommand = program.command("daemon").description("Manage the Lark bridge daemon");
1043
+ var daemonCommand = program.command("daemon").description("Start or manage the Lark bridge daemon").action(runDaemonStart);
908
1044
  daemonCommand.command("start").description("Start the bridge daemon").action(runDaemonStart);
909
1045
  daemonCommand.command("stop").description("Stop the bridge daemon without stopping coding-agent sessions").action(runDaemonStop);
910
1046
  daemonCommand.command("restart").description("Restart the bridge daemon without stopping coding-agent sessions").action(runDaemonRestart);
@@ -956,7 +1092,7 @@ ${url}
956
1092
  tenant: registeredTenant,
957
1093
  appId: registration.client_id,
958
1094
  tmuxBinary: "tmux",
959
- agentBinaries: { codex: "codex", "trae-cli": "trae-cli", "claude-code": "claude" },
1095
+ agentBinaries: { codex: "codex", traex: "trae-cli", claude: "claude" },
960
1096
  pollIntervalMs: 650
961
1097
  };
962
1098
  const previousState = await store.loadState();
@@ -983,18 +1119,18 @@ ${url}
983
1119
  async function runStart(options) {
984
1120
  const cwd = resolve(options.cwd ?? process.cwd());
985
1121
  const resume = resolveResumeOption(options);
986
- await access(cwd).catch((error) => {
987
- throw new AppError("INVALID_CWD", `working directory is unavailable: ${cwd}`, { cwd }, { cause: error });
1122
+ const request = await validateStartSessionRequest({
1123
+ sessionId: options.name,
1124
+ agent: options.agent,
1125
+ cwd,
1126
+ resume
988
1127
  });
989
1128
  await ensureInitialized();
990
1129
  await preflight(options.agent);
991
1130
  await ensureDaemon();
992
1131
  const value = await daemonValue({
993
1132
  method: "start",
994
- cwd,
995
- sessionId: options.name,
996
- agent: options.agent,
997
- resume
1133
+ ...request
998
1134
  });
999
1135
  if (value.binding.mode === "reused") {
1000
1136
  console.log("\n\u5DF2\u81EA\u52A8\u6CBF\u7528\u539F\u6709\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u3002\n");
@@ -1062,7 +1198,12 @@ async function runStatus(name) {
1062
1198
  console.log(JSON.stringify(response.value, null, 2));
1063
1199
  } catch {
1064
1200
  const state = await store.loadState();
1065
- console.log(JSON.stringify({ daemon: "stopped", state }, null, 2));
1201
+ const health = await daemonHealth(paths);
1202
+ console.log(JSON.stringify({
1203
+ daemon: health.status,
1204
+ ...health.status === "unresponsive" ? { daemonPid: health.pid } : {},
1205
+ state
1206
+ }, null, 2));
1066
1207
  }
1067
1208
  }
1068
1209
  async function runStop(name) {
@@ -1100,12 +1241,22 @@ async function runDaemonRestart() {
1100
1241
  console.log(`Bridge daemon restarted (PID ${info.pid}, version ${info.version}). Coding-agent/tmux sessions were preserved.`);
1101
1242
  }
1102
1243
  async function runDaemonStatus() {
1103
- const info = await daemonInfo(paths);
1104
- if (!info) {
1244
+ const health = await daemonHealth(paths);
1245
+ if (health.status === "stopped") {
1105
1246
  console.log(`Bridge daemon: stopped
1106
1247
  CLI version: ${packageInfo.version}`);
1107
1248
  return;
1108
1249
  }
1250
+ if (health.status === "unresponsive") {
1251
+ console.log([
1252
+ "Bridge daemon: unresponsive",
1253
+ `PID: ${health.pid}`,
1254
+ `CLI version: ${packageInfo.version}`,
1255
+ "\u5EFA\u8BAE\u8FD0\u884C\uFF1Alark-coding-assistant daemon restart"
1256
+ ].join("\n"));
1257
+ return;
1258
+ }
1259
+ const info = health.info;
1109
1260
  console.log([
1110
1261
  "Bridge daemon: running",
1111
1262
  `PID: ${info.pid}`,
@@ -1128,12 +1279,13 @@ async function preflight(agentId) {
1128
1279
  ]);
1129
1280
  }
1130
1281
  function parseAgentId(value) {
1131
- if (!isAgentId(value)) {
1282
+ const agent = normalizeAgentId(value);
1283
+ if (!agent) {
1132
1284
  throw new AppError("INVALID_OPTIONS", `unsupported coding agent: ${value}`, {
1133
1285
  reason: `\u4E0D\u652F\u6301 coding agent\u300C${value}\u300D`
1134
1286
  });
1135
1287
  }
1136
- return value;
1288
+ return agent;
1137
1289
  }
1138
1290
  async function ensureDaemon() {
1139
1291
  const info = await daemonInfo(paths);