lark-coding-assistant 0.1.1 → 0.1.2

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 CHANGED
@@ -271,6 +271,25 @@ npm uninstall -g lark-coding-assistant
271
271
 
272
272
  daemon 的常规日志、stdout、stderr 和启动崩溃都会写入 `logs/assistant.log`。
273
273
 
274
+ ## 错误与调试
275
+
276
+ CLI 默认只显示简洁的中文错误和可执行的解决建议,不会把 Node.js 堆栈、源码路径或内部异常直接输出到终端。例如 session 已经运行时,可以按提示连接现有 session 或换一个名称。
277
+
278
+ 查看 daemon 状态和日志:
279
+
280
+ ```bash
281
+ lark-coding-assistant daemon status
282
+ lark-coding-assistant logs
283
+ ```
284
+
285
+ 需要排查未知异常时,可以只为当前命令开启调试输出:
286
+
287
+ ```bash
288
+ LARK_CODING_ASSISTANT_DEBUG=1 lark-coding-assistant <command>
289
+ ```
290
+
291
+ 调试模式会在友好提示后追加原始错误堆栈,请勿把可能包含本机路径或环境信息的完整输出直接发布到公开渠道。
292
+
274
293
  ## 常见问题
275
294
 
276
295
  ### 飞书消息进入了错误的 session
package/dist/cli.js CHANGED
@@ -127,6 +127,36 @@ function runFile(file, args, options = {}) {
127
127
  // src/daemon/client.ts
128
128
  import { randomUUID } from "crypto";
129
129
  import { createConnection } from "net";
130
+
131
+ // src/core/errors.ts
132
+ var AppError = class extends Error {
133
+ code;
134
+ context;
135
+ constructor(code, message, context = {}, options = {}) {
136
+ super(message, options);
137
+ this.name = "AppError";
138
+ this.code = code;
139
+ this.context = context;
140
+ }
141
+ };
142
+ function isAppError(error) {
143
+ return error instanceof AppError;
144
+ }
145
+ function asAppError(error, code = "UNKNOWN", context = {}) {
146
+ if (isAppError(error)) {
147
+ if (Object.keys(context).length === 0) return error;
148
+ return new AppError(error.code, error.message, { ...context, ...error.context }, { cause: error });
149
+ }
150
+ return new AppError(code, errorMessage(error), context, { cause: error });
151
+ }
152
+ function errorMessage(error) {
153
+ return error instanceof Error ? error.message : String(error);
154
+ }
155
+ function systemErrorCode(error) {
156
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
157
+ }
158
+
159
+ // src/daemon/client.ts
130
160
  function requestDaemon(socketPath, request, timeoutMs = 5e3) {
131
161
  return new Promise((resolve2, reject) => {
132
162
  const id = randomUUID();
@@ -134,7 +164,7 @@ function requestDaemon(socketPath, request, timeoutMs = 5e3) {
134
164
  let buffer = "";
135
165
  const timer = setTimeout(() => {
136
166
  socket.destroy();
137
- reject(new Error("daemon request timed out"));
167
+ reject(new AppError("REQUEST_TIMEOUT", "daemon request timed out"));
138
168
  }, timeoutMs);
139
169
  socket.setEncoding("utf8");
140
170
  socket.once("connect", () => socket.write(`${JSON.stringify({ ...request, id })}
@@ -153,6 +183,11 @@ function requestDaemon(socketPath, request, timeoutMs = 5e3) {
153
183
  });
154
184
  socket.once("error", (error) => {
155
185
  clearTimeout(timer);
186
+ const code = systemErrorCode(error);
187
+ if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE") {
188
+ reject(new AppError("DAEMON_UNAVAILABLE", "unable to connect to daemon", {}, { cause: error }));
189
+ return;
190
+ }
156
191
  reject(error);
157
192
  });
158
193
  });
@@ -161,7 +196,13 @@ function requestDaemon(socketPath, request, timeoutMs = 5e3) {
161
196
  // src/agents/resume.ts
162
197
  function resolveResumeOption(options) {
163
198
  const modes = [options.resume !== void 0, options.resumeLast, options.resumeAll].filter(Boolean).length;
164
- if (modes > 1) throw new Error("--resume, --resume-last, and --resume-all cannot be used together");
199
+ if (modes > 1) {
200
+ throw new AppError(
201
+ "INVALID_OPTIONS",
202
+ "--resume, --resume-last, and --resume-all cannot be used together",
203
+ { reason: "--resume\u3001--resume-last \u548C --resume-all \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528" }
204
+ );
205
+ }
165
206
  if (options.resumeLast) return { mode: "last" };
166
207
  if (options.resumeAll) return { mode: "picker", all: true };
167
208
  if (typeof options.resume === "string") return { mode: "session", sessionId: options.resume };
@@ -728,6 +769,125 @@ var registrationDomains = {
728
769
  larkDomain: "accounts.larksuite.com"
729
770
  };
730
771
 
772
+ // src/cli-errors.ts
773
+ function withCliOperation(error, operation) {
774
+ return asAppError(error, "UNKNOWN", operation ? { operation } : {});
775
+ }
776
+ function formatCliError(error, debug = false) {
777
+ const appError = asAppError(error);
778
+ const context = appError.context;
779
+ const lines = formatKnownError(appError, context);
780
+ if (!debug) return lines.join("\n");
781
+ return [...lines, "", "\u8C03\u8BD5\u4FE1\u606F\uFF1A", debugStack(appError)].join("\n");
782
+ }
783
+ function cliDebugEnabled(env = process.env) {
784
+ return env.LARK_CODING_ASSISTANT_DEBUG === "1";
785
+ }
786
+ function formatKnownError(error, context) {
787
+ const sessionId = safeValue(context.sessionId, "default");
788
+ switch (error.code) {
789
+ case "SESSION_EXISTS":
790
+ return [
791
+ `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 session \u5DF2\u5728\u8FD0\u884C\u3002`,
792
+ "",
793
+ "\u4F60\u53EF\u4EE5\uFF1A",
794
+ ` lark-coding-assistant attach ${sessionId}`,
795
+ " lark-coding-assistant start --name <\u65B0\u540D\u79F0>"
796
+ ];
797
+ case "SESSION_NOT_FOUND":
798
+ return [
799
+ `\u627E\u4E0D\u5230 session\u300C${sessionId}\u300D\u3002`,
800
+ "",
801
+ "\u8BF7\u5148\u67E5\u770B\u6216\u521B\u5EFA session\uFF1A",
802
+ " lark-coding-assistant status",
803
+ ` lark-coding-assistant start --name ${sessionId}`
804
+ ];
805
+ case "INVALID_SESSION_NAME":
806
+ return [
807
+ `session \u540D\u79F0\u300C${safeValue(context.sessionId, "")}\u300D\u65E0\u6548\u3002`,
808
+ "\u540D\u79F0\u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF\uFF0C\u957F\u5EA6\u4E3A 1\u201340 \u4E2A\u5B57\u7B26\u3002"
809
+ ];
810
+ case "NOT_INITIALIZED":
811
+ return ["\u5C1A\u672A\u5B8C\u6210\u521D\u59CB\u5316\u3002", "", "\u8BF7\u8FD0\u884C\uFF1A", " lark-coding-assistant init"];
812
+ case "INVALID_CWD":
813
+ return [
814
+ `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u7528\uFF1A${safeValue(context.cwd, "\u672A\u77E5\u76EE\u5F55")}`,
815
+ "",
816
+ "\u8BF7\u68C0\u67E5\u76EE\u5F55\u662F\u5426\u5B58\u5728\u4E14\u6709\u8BBF\u95EE\u6743\u9650\uFF0C\u6216\u91CD\u65B0\u6307\u5B9A --cwd\u3002"
817
+ ];
818
+ case "BINARY_NOT_FOUND":
819
+ return [
820
+ `\u627E\u4E0D\u5230\u6240\u9700\u547D\u4EE4\uFF1A${safeValue(context.binary, "\u672A\u77E5\u547D\u4EE4")}`,
821
+ "",
822
+ "\u8BF7\u5148\u5B89\u88C5\u8BE5\u547D\u4EE4\uFF0C\u6216\u68C0\u67E5 PATH \u548C\u521D\u59CB\u5316\u914D\u7F6E\u3002"
823
+ ];
824
+ case "INVALID_OPTIONS":
825
+ return [
826
+ `\u547D\u4EE4\u53C2\u6570\u65E0\u6548\uFF1A${safeValue(context.reason, error.message)}`,
827
+ "",
828
+ "\u8BF7\u8FD0\u884C lark-coding-assistant --help \u67E5\u770B\u53EF\u7528\u53C2\u6570\u3002"
829
+ ];
830
+ case "DAEMON_UNAVAILABLE":
831
+ return [
832
+ "\u65E0\u6CD5\u8FDE\u63A5 bridge daemon\u3002",
833
+ "",
834
+ "\u8BF7\u5C1D\u8BD5\uFF1A",
835
+ " lark-coding-assistant daemon restart",
836
+ " lark-coding-assistant logs"
837
+ ];
838
+ case "REQUEST_TIMEOUT":
839
+ return [
840
+ "bridge daemon \u672A\u53CA\u65F6\u54CD\u5E94\u3002",
841
+ "",
842
+ "\u8BF7\u5C1D\u8BD5\uFF1A",
843
+ " lark-coding-assistant daemon status",
844
+ " lark-coding-assistant daemon restart",
845
+ " lark-coding-assistant logs"
846
+ ];
847
+ case "UNKNOWN":
848
+ return [
849
+ `\u64CD\u4F5C\u5931\u8D25\uFF1A${operationLabel(context.operation)} \u65F6\u53D1\u751F\u5F02\u5E38\u3002`,
850
+ "",
851
+ "\u8BF7\u5C1D\u8BD5\uFF1A",
852
+ " lark-coding-assistant status",
853
+ " lark-coding-assistant logs",
854
+ "",
855
+ "\u9700\u8981\u67E5\u770B\u5B8C\u6574\u9519\u8BEF\uFF1A",
856
+ " LARK_CODING_ASSISTANT_DEBUG=1 lark-coding-assistant ..."
857
+ ];
858
+ }
859
+ }
860
+ function operationLabel(value) {
861
+ const labels = {
862
+ init: "\u521D\u59CB\u5316",
863
+ start: "\u542F\u52A8 session",
864
+ attach: "\u8FDE\u63A5 session",
865
+ "bind-code": "\u751F\u6210\u7ED1\u5B9A\u7801",
866
+ status: "\u8BFB\u53D6\u72B6\u6001",
867
+ stop: "\u505C\u6B62 session",
868
+ logs: "\u8BFB\u53D6\u65E5\u5FD7",
869
+ "reset-owner": "\u91CD\u7F6E owner",
870
+ daemon: "\u7BA1\u7406 bridge daemon"
871
+ };
872
+ return typeof value === "string" ? labels[value] ?? "\u6267\u884C\u547D\u4EE4" : "\u6267\u884C\u547D\u4EE4";
873
+ }
874
+ function safeValue(value, fallback) {
875
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return fallback;
876
+ const normalized = String(value).replace(/[\u0000-\u001f\u007f\u009b]|\u001b\[[0-?]*[ -/]*[@-~]/g, " ").trim();
877
+ return normalized.slice(0, 160) || fallback;
878
+ }
879
+ function debugStack(error) {
880
+ const values = [];
881
+ let current = error;
882
+ const seen = /* @__PURE__ */ new Set();
883
+ while (current instanceof Error && !seen.has(current)) {
884
+ seen.add(current);
885
+ values.push(current.stack ?? `${current.name}: ${current.message}`);
886
+ current = current.cause;
887
+ }
888
+ return values.join("\nCaused by:\n");
889
+ }
890
+
731
891
  // src/cli.ts
732
892
  var program = new Command();
733
893
  var paths = resolveAppPaths();
@@ -749,7 +909,14 @@ daemonCommand.command("start").description("Start the bridge daemon").action(run
749
909
  daemonCommand.command("stop").description("Stop the bridge daemon without stopping coding-agent sessions").action(runDaemonStop);
750
910
  daemonCommand.command("restart").description("Restart the bridge daemon without stopping coding-agent sessions").action(runDaemonRestart);
751
911
  daemonCommand.command("status").description("Show bridge daemon status and version").action(runDaemonStatus);
752
- await program.parseAsync();
912
+ try {
913
+ await program.parseAsync();
914
+ } catch (error) {
915
+ const cliError = withCliOperation(error, commandOperation(process.argv));
916
+ process.stderr.write(`${formatCliError(cliError, cliDebugEnabled())}
917
+ `);
918
+ process.exitCode = 1;
919
+ }
753
920
  async function runInit() {
754
921
  await store.ensure();
755
922
  const tenantAnswer = await p.select({
@@ -816,19 +983,19 @@ ${url}
816
983
  async function runStart(options) {
817
984
  const cwd = resolve(options.cwd ?? process.cwd());
818
985
  const resume = resolveResumeOption(options);
819
- await access(cwd);
986
+ await access(cwd).catch((error) => {
987
+ throw new AppError("INVALID_CWD", `working directory is unavailable: ${cwd}`, { cwd }, { cause: error });
988
+ });
820
989
  await ensureInitialized();
821
990
  await preflight(options.agent);
822
991
  await ensureDaemon();
823
- const response = await requestDaemon(paths.socket, {
992
+ const value = await daemonValue({
824
993
  method: "start",
825
994
  cwd,
826
995
  sessionId: options.name,
827
996
  agent: options.agent,
828
997
  resume
829
998
  });
830
- if (!response.ok) throw new Error(response.error);
831
- const value = response.value;
832
999
  if (value.binding.mode === "reused") {
833
1000
  console.log("\n\u5DF2\u81EA\u52A8\u6CBF\u7528\u539F\u6709\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u3002\n");
834
1001
  } else if (value.binding.mode === "awaiting-owner-message") {
@@ -851,9 +1018,7 @@ async function runAttach(name) {
851
1018
  async function runBindCode() {
852
1019
  await ensureInitialized();
853
1020
  await ensureDaemon();
854
- const response = await requestDaemon(paths.socket, { method: "bindCode" });
855
- if (!response.ok) throw new Error(response.error);
856
- const value = response.value;
1021
+ const value = await daemonValue({ method: "bindCode" });
857
1022
  console.log(`\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u547D\u4EE4\uFF08${value.expiresInSeconds / 60} \u5206\u949F\u6709\u6548\uFF09\uFF1A`);
858
1023
  console.log(`/attach ${value.bindCode}`);
859
1024
  }
@@ -861,11 +1026,31 @@ async function attachLocal(name) {
861
1026
  const state = await store.loadState();
862
1027
  const config = await store.loadConfig();
863
1028
  const session = state.sessions?.[name];
864
- if (!session) throw new Error(`no managed session: ${name}`);
865
- if (!config) throw new Error("not initialized");
1029
+ if (!session) throw new AppError("SESSION_NOT_FOUND", `no managed session: ${name}`, { sessionId: name });
1030
+ if (!config) throw new AppError("NOT_INITIALIZED", "not initialized");
1031
+ try {
1032
+ await runFile(config.tmuxBinary, ["has-session", "-t", `=${session.sessionName}`]);
1033
+ } catch (error) {
1034
+ if (systemErrorCode(error) === "ENOENT") {
1035
+ throw new AppError("BINARY_NOT_FOUND", `command not found: ${config.tmuxBinary}`, {
1036
+ binary: config.tmuxBinary
1037
+ }, { cause: error });
1038
+ }
1039
+ throw new AppError("SESSION_NOT_FOUND", `managed session is no longer running: ${name}`, {
1040
+ sessionId: name
1041
+ }, { cause: error });
1042
+ }
866
1043
  const child = spawn3(config.tmuxBinary, ["attach-session", "-t", `=${session.sessionName}`], { stdio: "inherit" });
867
1044
  const code = await new Promise((resolveExit, reject) => {
868
- child.once("error", reject);
1045
+ child.once("error", (error) => {
1046
+ if (systemErrorCode(error) === "ENOENT") {
1047
+ reject(new AppError("BINARY_NOT_FOUND", `command not found: ${config.tmuxBinary}`, {
1048
+ binary: config.tmuxBinary
1049
+ }, { cause: error }));
1050
+ return;
1051
+ }
1052
+ reject(error);
1053
+ });
869
1054
  child.once("exit", resolveExit);
870
1055
  });
871
1056
  if (code && code !== 0) process.exitCode = code;
@@ -881,13 +1066,11 @@ async function runStatus(name) {
881
1066
  }
882
1067
  }
883
1068
  async function runStop(name) {
884
- const response = await requestDaemon(paths.socket, { method: "stop", sessionId: name });
885
- if (!response.ok) throw new Error(response.error);
1069
+ await daemonValue({ method: "stop", sessionId: name });
886
1070
  console.log(`Coding-agent tmux session stopped${name ? `: ${name}` : "."}`);
887
1071
  }
888
1072
  async function runResetOwner() {
889
- const response = await requestDaemon(paths.socket, { method: "resetOwner" });
890
- if (!response.ok) throw new Error(response.error);
1073
+ await daemonValue({ method: "resetOwner" });
891
1074
  console.log("Owner cleared.");
892
1075
  }
893
1076
  async function runLogs(options) {
@@ -933,19 +1116,23 @@ CLI version: ${packageInfo.version}`);
933
1116
  }
934
1117
  async function ensureInitialized() {
935
1118
  if (!await store.loadConfig() || !await store.loadSecrets()) {
936
- throw new Error("not initialized; run lark-coding-assistant init first");
1119
+ throw new AppError("NOT_INITIALIZED", "not initialized; run lark-coding-assistant init first");
937
1120
  }
938
1121
  }
939
1122
  async function preflight(agentId) {
940
1123
  const config = await store.loadConfig();
941
1124
  const adapter = getAgentAdapter(agentId);
942
1125
  await Promise.all([
943
- runFile(config.tmuxBinary, ["-V"]),
944
- runFile(adapter.binary(config), [...adapter.versionArgs])
1126
+ preflightBinary(config.tmuxBinary, ["-V"]),
1127
+ preflightBinary(adapter.binary(config), [...adapter.versionArgs])
945
1128
  ]);
946
1129
  }
947
1130
  function parseAgentId(value) {
948
- if (!isAgentId(value)) throw new Error(`unsupported coding agent: ${value}`);
1131
+ if (!isAgentId(value)) {
1132
+ throw new AppError("INVALID_OPTIONS", `unsupported coding agent: ${value}`, {
1133
+ reason: `\u4E0D\u652F\u6301 coding agent\u300C${value}\u300D`
1134
+ });
1135
+ }
949
1136
  return value;
950
1137
  }
951
1138
  async function ensureDaemon() {
@@ -960,4 +1147,29 @@ async function ensureDaemon() {
960
1147
  function daemonEntryPath() {
961
1148
  return fileURLToPath(new URL("./daemon-entry.js", import.meta.url));
962
1149
  }
1150
+ async function daemonValue(request) {
1151
+ const response = await requestDaemon(paths.socket, request);
1152
+ if (!response.ok) throw daemonResultError(response);
1153
+ return response.value;
1154
+ }
1155
+ function daemonResultError(response) {
1156
+ return new AppError(
1157
+ response.errorCode ?? "UNKNOWN",
1158
+ response.error,
1159
+ response.errorContext ?? {}
1160
+ );
1161
+ }
1162
+ async function preflightBinary(binary, args) {
1163
+ try {
1164
+ await runFile(binary, args);
1165
+ } catch (error) {
1166
+ if (systemErrorCode(error) === "ENOENT") {
1167
+ throw new AppError("BINARY_NOT_FOUND", `command not found: ${binary}`, { binary }, { cause: error });
1168
+ }
1169
+ throw error;
1170
+ }
1171
+ }
1172
+ function commandOperation(argv) {
1173
+ return argv.slice(2).find((argument) => !argument.startsWith("-"));
1174
+ }
963
1175
  //# sourceMappingURL=cli.js.map