lark-coding-assistant 0.2.2 → 0.2.4

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
@@ -57,7 +57,7 @@ claude --version
57
57
  npm install -g lark-coding-assistant@latest && lark-coding-assistant init
58
58
  ```
59
59
 
60
- 初始化向导会让你选择飞书或 Lark,并显示 PersonalAgent 注册二维码:
60
+ 初始化向导会让你选择飞书或 Lark、可选配置常用 workspace 目录,并显示 PersonalAgent 注册二维码:
61
61
 
62
62
  1. 保持 `init` 命令运行;
63
63
  2. 使用飞书/Lark 扫描二维码;
@@ -108,13 +108,15 @@ lca start --name docs --agent claude --cwd ~/workspace/docs
108
108
 
109
109
  名称只允许字母、数字、下划线和短横线,最长 40 个字符。不指定名称时使用 `default`。
110
110
 
111
+ Session 创建和恢复采用 30 秒启动事务。本地与飞书都会等到 agent 可用后再报告成功;超过 30 秒会取消底层启动、清理临时 tmux 和状态,并返回包含失败阶段与最近终端输出的提示,不会继续在后台运行。同名 session 正在启动时会直接提示“正在启动”,不同名称可以并行启动。Resume Picker 等待用户选择不计入 30 秒,提交选择后重新开始一次 30 秒恢复事务。该限制只作用于启动链路,不影响消息、审批、Question、`status`、`tail`、`attach`、`stop` 或 daemon 管理。
112
+
111
113
  一个 PersonalAgent 私聊同一时刻只连接一个 active session。在飞书发送:
112
114
 
113
115
  ```text
114
116
  /sessions
115
117
  ```
116
118
 
117
- 机器人会发送一张交互卡,按 codex、traex、claude 分组展示仍存活的 session,并显示每个项目的绝对路径。`● 当前` 表示 active session,点击其他 session 的“连接”按钮即可切换,点击“关闭”可停止对应 agent 和 tmux session;也可点击“新建 Session”,填写名称、Agent、绝对工作目录和恢复方式。创建成功后会自动连接;原 session 列表仍可查看、连接和关闭,只会禁用已经使用过的“新建 Session”按钮。也可使用:
119
+ 机器人会发送一张交互卡,按 codex、traex、claude 分组展示仍存活的 session,并显示每个项目的绝对路径。`● 当前` 表示 active session,点击其他 session 的“连接”按钮即可切换,点击“关闭”可停止对应 agent 和 tmux session;也可点击“新建 Session”,填写名称、Agent、项目目录和恢复方式。项目目录可直接从已有 Session、最近成功目录和常用 workspace 中选择,必要时可手动填写 `/绝对路径` 或 `~/路径`。创建成功后会自动连接;原 session 列表仍可查看、连接和关闭,只会禁用已经使用过的“新建 Session”按钮。也可使用:
118
120
 
119
121
  ```text
120
122
  /use web
@@ -122,6 +124,26 @@ lca start --name docs --agent claude --cwd ~/workspace/docs
122
124
 
123
125
  切换只改变飞书消息和通知的路由,不会停止其他 session。普通消息、`/tail`、`/status`、审批、`/stop` 和停止输出通知都只作用于 active session。
124
126
 
127
+ ## Workspace 项目目录
128
+
129
+ 可以配置多个常用 workspace root:
130
+
131
+ ```bash
132
+ lca workspace add .
133
+ lca workspace add ~/workspace
134
+ lca workspace add ~/code/company
135
+ lca workspace list
136
+ lca workspace remove ~/code/company
137
+ ```
138
+
139
+ `add` 和 `remove` 支持相对路径,并按执行命令时的当前目录解析。例如 `lca workspace add .` 会把当前目录保存为绝对路径。
140
+
141
+ 配置修改会在下一次打开飞书“新建 Session”表单时生效,不需要重启 daemon。
142
+
143
+ 飞书新建 Session 时,LCA 按以下顺序合并项目目录:仍存活 Session 的目录、最近成功启动的目录、已配置 workspace 下的项目。每个 workspace 只扫描下一级目录,Git 仓库优先于普通目录;同一路径只展示一次。项目目录与其他启动信息在同一张卡片内一次提交;需要使用列表外目录时,选择“手动填写其他路径…”并填写其他路径。最近目录最多保留 30 条。
144
+
145
+ 候选扫描有 2 秒和 500 个目录的上限。达到上限或部分目录不可访问时,卡片仍会展示已经找到的项目,并保留“手动填写其他路径”兜底。手动输入支持 `~/...`,启动前会展开并校验;启动结果、状态和 Session 列表始终展示完整绝对路径。
146
+
125
147
  ## 恢复 agent 历史会话
126
148
 
127
149
  恢复参数同时适用于三个 agent,bridge 会转换为各 CLI 的原生参数:
@@ -166,7 +188,7 @@ lca attach web
166
188
 
167
189
  - `/sessions`:按 agent 分组展示所有仍存活的 session 及其绝对路径,可直接连接、关闭或打开新建表单。
168
190
  - `/start`:直接打开新建 Session 表单。
169
- - `/start <name> --agent <codex|traex|claude> --cwd <绝对路径> [--resume]`:用文本命令启动并自动连接新 session;包含空格的路径请加引号。使用 `--resume` 会打开原生 Resume Picker。飞书端不支持“恢复上次”或直接输入历史 Session ID。
191
+ - `/start <name> --agent <codex|traex|claude> --cwd <绝对路径或 ~/路径> [--resume]`:用文本命令启动并自动连接新 session;包含空格的路径请加引号。使用 `--resume` 会打开原生 Resume Picker。飞书端不支持“恢复上次”或直接输入历史 Session ID。
170
192
  - `/use <name>`:用文本命令切换 session。
171
193
  - `/tail [20-300]`:返回当前 session 最近的终端输出,默认 80 行。
172
194
  - `/manual`:打开当前 active session 的手动终端遥控卡。
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { randomBytes as randomBytes2 } from "crypto";
3
3
  import { readFile as readFile3 } from "fs/promises";
4
4
  import { spawn as spawn3 } from "child_process";
5
5
  import { fileURLToPath } from "url";
6
- import { resolve } from "path";
6
+ import { resolve as resolve3 } from "path";
7
7
  import { Command } from "commander";
8
8
  import * as p from "@clack/prompts";
9
9
  import { registerApp } from "@larksuite/channel";
@@ -72,6 +72,77 @@ function normalizeAgentId(value) {
72
72
  return AGENT_IDS.includes(value) ? value : void 0;
73
73
  }
74
74
 
75
+ // src/workspace/path.ts
76
+ import { constants } from "fs";
77
+ import { access, stat } from "fs/promises";
78
+ import { homedir as homedir2 } from "os";
79
+ import { isAbsolute, normalize, resolve } from "path";
80
+
81
+ // src/core/errors.ts
82
+ var AppError = class extends Error {
83
+ code;
84
+ context;
85
+ constructor(code, message, context = {}, options = {}) {
86
+ super(message, options);
87
+ this.name = "AppError";
88
+ this.code = code;
89
+ this.context = context;
90
+ }
91
+ };
92
+ function isAppError(error) {
93
+ return error instanceof AppError;
94
+ }
95
+ function asAppError(error, code = "UNKNOWN", context = {}) {
96
+ if (isAppError(error)) {
97
+ if (Object.keys(context).length === 0) return error;
98
+ return new AppError(error.code, error.message, { ...context, ...error.context }, { cause: error });
99
+ }
100
+ return new AppError(code, errorMessage(error), context, { cause: error });
101
+ }
102
+ function errorMessage(error) {
103
+ return error instanceof Error ? error.message : String(error);
104
+ }
105
+ function systemErrorCode(error) {
106
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
107
+ }
108
+
109
+ // src/workspace/path.ts
110
+ function normalizeWorkspacePath(input, home = homedir2()) {
111
+ const value = input.trim();
112
+ if (!value) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
113
+ let expanded = value;
114
+ if (value === "~") expanded = home;
115
+ else if (value.startsWith("~/")) expanded = resolve(home, value.slice(2));
116
+ else if (value.startsWith("~")) throw invalidCwd(input, "\u4E0D\u652F\u6301 ~other-user\uFF0C\u8BF7\u4F7F\u7528 ~ \u6216 ~/\u76EE\u5F55");
117
+ if (!isAbsolute(expanded)) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u6216\u4F7F\u7528 ~/\u76EE\u5F55");
118
+ return normalize(expanded);
119
+ }
120
+ async function validateWorkspaceDirectory(input, home = homedir2()) {
121
+ const cwd = normalizeWorkspacePath(input, home);
122
+ let info;
123
+ try {
124
+ info = await stat(cwd);
125
+ } catch (error) {
126
+ const reason = systemErrorCode(error) === "EACCES" ? `\u65E0\u6743\u8BBF\u95EE\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}` : `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u7528\uFF1A${cwd}`;
127
+ throw invalidCwd(cwd, reason, error);
128
+ }
129
+ if (!info.isDirectory()) throw invalidCwd(cwd, `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\u5939\uFF1A${cwd}`);
130
+ try {
131
+ await access(cwd, constants.R_OK | constants.X_OK);
132
+ } catch (error) {
133
+ throw invalidCwd(cwd, `\u65E0\u6743\u8FDB\u5165\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}`, error);
134
+ }
135
+ return cwd;
136
+ }
137
+ function normalizeWorkspaceRoots(values, home = homedir2()) {
138
+ const unique = /* @__PURE__ */ new Set();
139
+ for (const value of values) unique.add(normalizeWorkspacePath(value, home));
140
+ return [...unique];
141
+ }
142
+ function invalidCwd(cwd, reason, cause) {
143
+ return new AppError("INVALID_CWD", reason, { cwd, reason }, cause === void 0 ? {} : { cause });
144
+ }
145
+
75
146
  // src/core/store.ts
76
147
  var AppStore = class {
77
148
  constructor(paths2) {
@@ -101,11 +172,15 @@ var AppStore = class {
101
172
  traex: binaries.traex ?? binaries["trae-cli"] ?? "trae-cli",
102
173
  claude: binaries.claude ?? binaries["claude-code"] ?? "claude"
103
174
  },
104
- pollIntervalMs: config.pollIntervalMs
175
+ pollIntervalMs: config.pollIntervalMs,
176
+ workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots ?? [])
105
177
  };
106
178
  }
107
179
  saveConfig(config) {
108
- return writeJsonAtomic(this.paths.config, config);
180
+ return writeJsonAtomic(this.paths.config, {
181
+ ...config,
182
+ workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots)
183
+ });
109
184
  }
110
185
  loadSecrets() {
111
186
  return readJson(this.paths.secrets);
@@ -120,23 +195,45 @@ var AppStore = class {
120
195
  const agent = normalizeAgentId(session.agent);
121
196
  return agent ? [[id, { ...session, agent }]] : [];
122
197
  }));
123
- return { ...state, sessions };
198
+ const recentWorkspaces = await normalizeRecentWorkspaces(state.recentWorkspaces ?? []);
199
+ return { ...state, sessions, recentWorkspaces };
124
200
  }
125
201
  saveState(state) {
126
202
  return writeJsonAtomic(this.paths.state, state);
127
203
  }
128
204
  };
205
+ async function normalizeRecentWorkspaces(values) {
206
+ const unique = /* @__PURE__ */ new Set();
207
+ const result2 = [];
208
+ const ordered = [...values].sort((left, right) => right.lastUsedAt - left.lastUsedAt);
209
+ for (const value of ordered) {
210
+ if (result2.length >= 30) break;
211
+ if (!Number.isFinite(value.lastUsedAt)) continue;
212
+ let cwd;
213
+ try {
214
+ cwd = normalizeWorkspacePath(value.cwd);
215
+ await validateWorkspaceDirectory(cwd);
216
+ } catch {
217
+ continue;
218
+ }
219
+ if (unique.has(cwd)) continue;
220
+ unique.add(cwd);
221
+ result2.push({ cwd, lastUsedAt: value.lastUsedAt });
222
+ }
223
+ return result2;
224
+ }
129
225
 
130
226
  // src/platform/process.ts
131
227
  import { execFile, spawn } from "child_process";
132
228
  function runFile(file, args, options = {}) {
133
- return new Promise((resolve2, reject) => {
229
+ return new Promise((resolve4, reject) => {
134
230
  execFile(
135
231
  file,
136
232
  [...args],
137
233
  {
138
234
  cwd: options.cwd,
139
235
  timeout: options.timeoutMs ?? 1e4,
236
+ signal: options.signal,
140
237
  encoding: "utf8",
141
238
  maxBuffer: 4 * 1024 * 1024
142
239
  },
@@ -145,7 +242,7 @@ function runFile(file, args, options = {}) {
145
242
  reject(Object.assign(error, { stdout, stderr }));
146
243
  return;
147
244
  }
148
- resolve2({ stdout, stderr });
245
+ resolve4({ stdout, stderr });
149
246
  }
150
247
  );
151
248
  });
@@ -154,38 +251,8 @@ function runFile(file, args, options = {}) {
154
251
  // src/daemon/client.ts
155
252
  import { randomUUID } from "crypto";
156
253
  import { createConnection } from "net";
157
-
158
- // src/core/errors.ts
159
- var AppError = class extends Error {
160
- code;
161
- context;
162
- constructor(code, message, context = {}, options = {}) {
163
- super(message, options);
164
- this.name = "AppError";
165
- this.code = code;
166
- this.context = context;
167
- }
168
- };
169
- function isAppError(error) {
170
- return error instanceof AppError;
171
- }
172
- function asAppError(error, code = "UNKNOWN", context = {}) {
173
- if (isAppError(error)) {
174
- if (Object.keys(context).length === 0) return error;
175
- return new AppError(error.code, error.message, { ...context, ...error.context }, { cause: error });
176
- }
177
- return new AppError(code, errorMessage(error), context, { cause: error });
178
- }
179
- function errorMessage(error) {
180
- return error instanceof Error ? error.message : String(error);
181
- }
182
- function systemErrorCode(error) {
183
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
184
- }
185
-
186
- // src/daemon/client.ts
187
254
  function requestDaemon(socketPath, request, timeoutMs = 5e3) {
188
- return new Promise((resolve2, reject) => {
255
+ return new Promise((resolve4, reject) => {
189
256
  const id = randomUUID();
190
257
  const socket = createConnection(socketPath);
191
258
  let buffer = "";
@@ -203,7 +270,7 @@ function requestDaemon(socketPath, request, timeoutMs = 5e3) {
203
270
  clearTimeout(timer);
204
271
  socket.destroy();
205
272
  try {
206
- resolve2(JSON.parse(buffer.slice(0, newline)));
273
+ resolve4(JSON.parse(buffer.slice(0, newline)));
207
274
  } catch (error) {
208
275
  reject(error);
209
276
  }
@@ -381,9 +448,9 @@ function promptContentIsDim(rawLine) {
381
448
  let cursor = 0;
382
449
  for (const match of rawLine.matchAll(sgr)) {
383
450
  const index = match.index ?? cursor;
384
- const text = rawLine.slice(cursor, index);
385
- visible += text;
386
- dimAt.push(...Array.from(text, () => dim));
451
+ const text2 = rawLine.slice(cursor, index);
452
+ visible += text2;
453
+ dimAt.push(...Array.from(text2, () => dim));
387
454
  const params = (match[1] || "0").split(";").map(Number);
388
455
  if (params.includes(0)) dim = false;
389
456
  if (params.includes(2)) dim = true;
@@ -596,10 +663,10 @@ function parseChoiceOptionLine(line) {
596
663
  if (!match?.[3] || !match[5]) return void 0;
597
664
  return { focused: Boolean(match[1]), marker: match[2] ?? match[4], key: match[3], label: match[5] };
598
665
  }
599
- function choiceRisk(text) {
600
- if (/full access|bypass|without sandbox|dangerously/i.test(text)) return "privileged";
601
- if (/always|don't ask again|do not ask again|auto mode|persist/i.test(text)) return "persistent";
602
- if (/^\s*no\b|reject|deny|cancel|do differently/i.test(text)) return "reject";
666
+ function choiceRisk(text2) {
667
+ if (/full access|bypass|without sandbox|dangerously/i.test(text2)) return "privileged";
668
+ if (/always|don't ask again|do not ask again|auto mode|persist/i.test(text2)) return "persistent";
669
+ if (/^\s*no\b|reject|deny|cancel|do differently/i.test(text2)) return "reject";
603
670
  return "normal";
604
671
  }
605
672
  function result(state, confidence, normalized, fingerprint, evidence) {
@@ -814,7 +881,7 @@ function isDaemonInfo(value) {
814
881
  return typeof info.version === "string" && typeof info.pid === "number";
815
882
  }
816
883
  function delay(ms) {
817
- return new Promise((resolve2) => setTimeout(resolve2, ms));
884
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
818
885
  }
819
886
 
820
887
  // src/lark/registration.ts
@@ -848,6 +915,28 @@ function formatKnownError(error, context) {
848
915
  ` lark-coding-assistant attach ${sessionId}`,
849
916
  " lark-coding-assistant start --name <\u65B0\u540D\u79F0>"
850
917
  ];
918
+ case "SESSION_STARTING":
919
+ return [
920
+ `session\u300C${sessionId}\u300D\u6B63\u5728\u542F\u52A8\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u5B8C\u6210\u3002`,
921
+ "",
922
+ "\u7A0D\u540E\u53EF\u67E5\u770B\u72B6\u6001\uFF1A",
923
+ ` lark-coding-assistant status ${sessionId}`
924
+ ];
925
+ case "SESSION_START_TIMEOUT": {
926
+ const excerpt = safeValue(context.terminalExcerpt, "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002");
927
+ return [
928
+ `session\u300C${sessionId}\u300D\u542F\u52A8\u8D85\u8FC7 30 \u79D2\uFF0C\u5DF2\u53D6\u6D88\u5E76\u6E05\u7406\u3002`,
929
+ "",
930
+ `Agent\uFF1A${safeValue(context.agent, "\u672A\u77E5")}`,
931
+ `\u5DE5\u4F5C\u76EE\u5F55\uFF1A${safeValue(context.cwd, "\u672A\u77E5")}`,
932
+ `\u8D85\u65F6\u9636\u6BB5\uFF1A${safeValue(context.stage, "unknown")}`,
933
+ "",
934
+ "\u6700\u8FD1\u7EC8\u7AEF\u8F93\u51FA\uFF1A",
935
+ excerpt,
936
+ "",
937
+ "\u53EF\u4EE5\u76F4\u63A5\u4F7F\u7528\u540C\u540D session \u91CD\u8BD5\u3002"
938
+ ];
939
+ }
851
940
  case "AGENT_SESSION_IN_USE": {
852
941
  const ownerSessionId = safeValue(context.ownerSessionId, "\u73B0\u6709 session");
853
942
  return [
@@ -972,7 +1061,8 @@ function operationLabel(value) {
972
1061
  stop: "\u505C\u6B62 session",
973
1062
  logs: "\u8BFB\u53D6\u65E5\u5FD7",
974
1063
  "reset-owner": "\u91CD\u7F6E owner",
975
- daemon: "\u7BA1\u7406 bridge daemon"
1064
+ daemon: "\u7BA1\u7406 bridge daemon",
1065
+ workspace: "\u7BA1\u7406 workspace"
976
1066
  };
977
1067
  return typeof value === "string" ? labels[value] ?? "\u6267\u884C\u547D\u4EE4" : "\u6267\u884C\u547D\u4EE4";
978
1068
  }
@@ -994,8 +1084,6 @@ function debugStack(error) {
994
1084
  }
995
1085
 
996
1086
  // src/session/start-request.ts
997
- import { stat } from "fs/promises";
998
- import { isAbsolute } from "path";
999
1087
  async function validateStartSessionRequest(request) {
1000
1088
  if (!validSessionId(request.sessionId)) {
1001
1089
  throw new AppError(
@@ -1004,21 +1092,13 @@ async function validateStartSessionRequest(request) {
1004
1092
  { sessionId: request.sessionId }
1005
1093
  );
1006
1094
  }
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
- }
1095
+ const cwd = await validateWorkspaceDirectory(request.cwd);
1016
1096
  if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
1017
1097
  throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
1018
1098
  reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
1019
1099
  });
1020
1100
  }
1021
- return request;
1101
+ return { ...request, cwd };
1022
1102
  }
1023
1103
  function validSessionId(value) {
1024
1104
  return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
@@ -1170,6 +1250,31 @@ function sanitize(value) {
1170
1250
  return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
1171
1251
  }
1172
1252
 
1253
+ // src/workspace/config.ts
1254
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1255
+ async function addWorkspaceRoot(config, input, cwd = process.cwd()) {
1256
+ const path = await validateWorkspaceDirectory(resolveWorkspaceRootInput(input, cwd));
1257
+ const existing = new Set(config.workspaceRoots);
1258
+ if (existing.has(path)) return { config, path, added: false };
1259
+ return { config: { ...config, workspaceRoots: [...config.workspaceRoots, path] }, path, added: true };
1260
+ }
1261
+ function removeWorkspaceRoot(config, input, cwd = process.cwd()) {
1262
+ const path = normalizeWorkspacePath(resolveWorkspaceRootInput(input, cwd));
1263
+ const workspaceRoots = config.workspaceRoots.filter((candidate) => candidate !== path);
1264
+ return {
1265
+ config: workspaceRoots.length === config.workspaceRoots.length ? config : { ...config, workspaceRoots },
1266
+ path,
1267
+ removed: workspaceRoots.length !== config.workspaceRoots.length
1268
+ };
1269
+ }
1270
+ function listWorkspaceRoots(config) {
1271
+ return [...config.workspaceRoots];
1272
+ }
1273
+ function resolveWorkspaceRootInput(input, cwd) {
1274
+ const value = input.trim();
1275
+ return value && !value.startsWith("~") && !isAbsolute2(value) ? resolve2(cwd, value) : value;
1276
+ }
1277
+
1173
1278
  // src/cli.ts
1174
1279
  var program = new Command();
1175
1280
  var paths = resolveAppPaths();
@@ -1191,6 +1296,10 @@ daemonCommand.command("start").description("Start the bridge daemon").action(run
1191
1296
  daemonCommand.command("stop").description("Stop the bridge daemon without stopping coding-agent sessions").action(runDaemonStop);
1192
1297
  daemonCommand.command("restart").description("Restart the bridge daemon without stopping coding-agent sessions").action(runDaemonRestart);
1193
1298
  daemonCommand.command("status").description("Show bridge daemon status and version").action(runDaemonStatus);
1299
+ var workspaceCommand = program.command("workspace").description("Manage project workspace roots");
1300
+ workspaceCommand.command("add").argument("<path>", "Workspace root path").description("Add a workspace root").action(runWorkspaceAdd);
1301
+ workspaceCommand.command("list").description("List workspace roots").action(runWorkspaceList);
1302
+ workspaceCommand.command("remove").argument("<path>", "Workspace root path").description("Remove a workspace root").action(runWorkspaceRemove);
1194
1303
  try {
1195
1304
  await program.parseAsync();
1196
1305
  } catch (error) {
@@ -1201,6 +1310,7 @@ try {
1201
1310
  }
1202
1311
  async function runInit() {
1203
1312
  await store.ensure();
1313
+ const previousConfig = await store.loadConfig();
1204
1314
  const tenantAnswer = await p.select({
1205
1315
  message: "\u9009\u62E9\u5E73\u53F0",
1206
1316
  options: [
@@ -1210,6 +1320,15 @@ async function runInit() {
1210
1320
  });
1211
1321
  if (p.isCancel(tenantAnswer)) return p.cancel("\u5DF2\u53D6\u6D88");
1212
1322
  const tenant = tenantAnswer;
1323
+ const workspaceAnswer = await p.text({
1324
+ message: "\u5E38\u7528 workspace \u76EE\u5F55\uFF08\u53EF\u9009\uFF0C\u591A\u4E2A\u76EE\u5F55\u7528\u9017\u53F7\u5206\u9694\uFF09",
1325
+ placeholder: "~/workspace, ~/code",
1326
+ initialValue: previousConfig?.workspaceRoots.join(", ") ?? ""
1327
+ });
1328
+ if (p.isCancel(workspaceAnswer)) return p.cancel("\u5DF2\u53D6\u6D88");
1329
+ const workspaceRoots = normalizeWorkspaceRoots(
1330
+ String(workspaceAnswer).split(",").map((value) => value.trim()).filter(Boolean)
1331
+ );
1213
1332
  p.log.info("\u8BF7\u4F7F\u7528\u98DE\u4E66/Lark \u626B\u63CF\u4E0B\u9762\u7684\u4E8C\u7EF4\u7801\uFF0C\u521B\u5EFA\u6216\u9009\u62E9 PersonalAgent\u3002");
1214
1333
  const registration = await registerApp({
1215
1334
  ...registrationDomains,
@@ -1239,7 +1358,8 @@ ${url}
1239
1358
  appId: registration.client_id,
1240
1359
  tmuxBinary: "tmux",
1241
1360
  agentBinaries: { codex: "codex", traex: "trae-cli", claude: "claude" },
1242
- pollIntervalMs: 650
1361
+ pollIntervalMs: 650,
1362
+ workspaceRoots
1243
1363
  };
1244
1364
  const previousState = await store.loadState();
1245
1365
  const registeredOwnerOpenId = registration.user_info?.open_id;
@@ -1263,7 +1383,8 @@ ${url}
1263
1383
  p.outro("PersonalAgent \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002");
1264
1384
  }
1265
1385
  async function runStart(options) {
1266
- const cwd = resolve(options.cwd ?? process.cwd());
1386
+ const requestedCwd = options.cwd ?? process.cwd();
1387
+ const cwd = requestedCwd.startsWith("~") ? requestedCwd : resolve3(requestedCwd);
1267
1388
  const resume = resolveResumeOption(options);
1268
1389
  const request = await validateStartSessionRequest({
1269
1390
  sessionId: options.name,
@@ -1274,10 +1395,11 @@ async function runStart(options) {
1274
1395
  await ensureInitialized();
1275
1396
  await preflight(options.agent);
1276
1397
  await ensureDaemon();
1398
+ console.log(`\u6B63\u5728\u542F\u52A8 ${options.agent} session\u300C${options.name}\u300D\u2026`);
1277
1399
  const value = await daemonValue({
1278
1400
  method: "start",
1279
1401
  ...request
1280
- });
1402
+ }, 35e3);
1281
1403
  if (value.binding.mode === "reused") {
1282
1404
  console.log("\n\u5DF2\u81EA\u52A8\u6CBF\u7528\u539F\u6709\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u3002\n");
1283
1405
  } else if (value.binding.mode === "awaiting-owner-message") {
@@ -1294,6 +1416,25 @@ async function runStart(options) {
1294
1416
  }
1295
1417
  await attachLocal(options.name);
1296
1418
  }
1419
+ async function runWorkspaceAdd(path) {
1420
+ await ensureInitialized();
1421
+ const config = await store.loadConfig();
1422
+ const result2 = await addWorkspaceRoot(config, path);
1423
+ if (result2.added) await store.saveConfig(result2.config);
1424
+ console.log(result2.added ? `Workspace \u5DF2\u6DFB\u52A0\uFF1A${result2.path}` : `Workspace \u5DF2\u5B58\u5728\uFF1A${result2.path}`);
1425
+ }
1426
+ async function runWorkspaceList() {
1427
+ await ensureInitialized();
1428
+ const roots = listWorkspaceRoots(await store.loadConfig());
1429
+ console.log(roots.length > 0 ? roots.join("\n") : "\u5C1A\u672A\u914D\u7F6E workspace\u3002\u53EF\u8FD0\u884C\uFF1Alca workspace add ~/workspace");
1430
+ }
1431
+ async function runWorkspaceRemove(path) {
1432
+ await ensureInitialized();
1433
+ const config = await store.loadConfig();
1434
+ const result2 = removeWorkspaceRoot(config, path);
1435
+ if (result2.removed) await store.saveConfig(result2.config);
1436
+ console.log(result2.removed ? `Workspace \u5DF2\u79FB\u9664\uFF1A${result2.path}` : `Workspace \u672A\u914D\u7F6E\uFF1A${result2.path}`);
1437
+ }
1297
1438
  async function runAttach(name) {
1298
1439
  await attachLocal(name);
1299
1440
  }
@@ -1458,8 +1599,8 @@ async function ensureDaemon() {
1458
1599
  function daemonEntryPath() {
1459
1600
  return fileURLToPath(new URL("./daemon-entry.js", import.meta.url));
1460
1601
  }
1461
- async function daemonValue(request) {
1462
- const response = await requestDaemon(paths.socket, request);
1602
+ async function daemonValue(request, timeoutMs) {
1603
+ const response = await requestDaemon(paths.socket, request, timeoutMs);
1463
1604
  if (!response.ok) throw daemonResultError(response);
1464
1605
  return response.value;
1465
1606
  }