lark-coding-assistant 0.2.1 → 0.2.3

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  - codex、traex 与 claude 可混合运行,默认使用 codex;
10
10
  - 支持多个命名 session,一个飞书账号可在 `/sessions` 卡片中直接切换或新建;
11
- - 普通飞书消息默认发送到当前 active session
11
+ - 普通飞书消息默认发送到当前 active session;消息被 bridge 接收后会在原消息上显示处理中表情,下一条 bridge 通知发送前或等待满 10 分钟时自动移除;
12
12
  - 支持 `/tail`、`/status`、`/stop`、远程处理审批与 Question,以及无法识别画面的手动遥控兜底;
13
13
  - 本地终端保持 attach 时,飞书仍可操作同一个 TUI;
14
14
  - 首次扫码后保存唯一 owner,后续通常无需绑定码;
@@ -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 扫描二维码;
@@ -114,7 +114,7 @@ lca start --name docs --agent claude --cwd ~/workspace/docs
114
114
  /sessions
115
115
  ```
116
116
 
117
- 机器人会发送一张交互卡,按 codex、traex、claude 分组展示仍存活的 session,并显示每个项目的绝对路径。`● 当前` 表示 active session,点击其他 session 的“连接”按钮即可切换,点击“关闭”可停止对应 agent 和 tmux session;也可点击“新建 Session”,填写名称、Agent、绝对工作目录和恢复方式。创建成功后会自动连接;原 session 列表仍可查看、连接和关闭,只会禁用已经使用过的“新建 Session”按钮。也可使用:
117
+ 机器人会发送一张交互卡,按 codex、traex、claude 分组展示仍存活的 session,并显示每个项目的绝对路径。`● 当前` 表示 active session,点击其他 session 的“连接”按钮即可切换,点击“关闭”可停止对应 agent 和 tmux session;也可点击“新建 Session”,填写名称、Agent、项目目录和恢复方式。项目目录可直接从已有 Session、最近成功目录和常用 workspace 中选择,必要时可手动填写 `/绝对路径` 或 `~/路径`。创建成功后会自动连接;原 session 列表仍可查看、连接和关闭,只会禁用已经使用过的“新建 Session”按钮。也可使用:
118
118
 
119
119
  ```text
120
120
  /use web
@@ -122,6 +122,26 @@ lca start --name docs --agent claude --cwd ~/workspace/docs
122
122
 
123
123
  切换只改变飞书消息和通知的路由,不会停止其他 session。普通消息、`/tail`、`/status`、审批、`/stop` 和停止输出通知都只作用于 active session。
124
124
 
125
+ ## Workspace 项目目录
126
+
127
+ 可以配置多个常用 workspace root:
128
+
129
+ ```bash
130
+ lca workspace add .
131
+ lca workspace add ~/workspace
132
+ lca workspace add ~/code/company
133
+ lca workspace list
134
+ lca workspace remove ~/code/company
135
+ ```
136
+
137
+ `add` 和 `remove` 支持相对路径,并按执行命令时的当前目录解析。例如 `lca workspace add .` 会把当前目录保存为绝对路径。
138
+
139
+ 配置修改会在下一次打开飞书“新建 Session”表单时生效,不需要重启 daemon。
140
+
141
+ 飞书新建 Session 时,LCA 按以下顺序合并项目目录:仍存活 Session 的目录、最近成功启动的目录、已配置 workspace 下的项目。每个 workspace 只扫描下一级目录,Git 仓库优先于普通目录;同一路径只展示一次。项目目录与其他启动信息在同一张卡片内一次提交;需要使用列表外目录时,选择“手动填写其他路径…”并填写其他路径。最近目录最多保留 30 条。
142
+
143
+ 候选扫描有 2 秒和 500 个目录的上限。达到上限或部分目录不可访问时,卡片仍会展示已经找到的项目,并保留“手动填写其他路径”兜底。手动输入支持 `~/...`,启动前会展开并校验;启动结果、状态和 Session 列表始终展示完整绝对路径。
144
+
125
145
  ## 恢复 agent 历史会话
126
146
 
127
147
  恢复参数同时适用于三个 agent,bridge 会转换为各 CLI 的原生参数:
@@ -166,7 +186,7 @@ lca attach web
166
186
 
167
187
  - `/sessions`:按 agent 分组展示所有仍存活的 session 及其绝对路径,可直接连接、关闭或打开新建表单。
168
188
  - `/start`:直接打开新建 Session 表单。
169
- - `/start <name> --agent <codex|traex|claude> --cwd <绝对路径> [--resume]`:用文本命令启动并自动连接新 session;包含空格的路径请加引号。使用 `--resume` 会打开原生 Resume Picker。飞书端不支持“恢复上次”或直接输入历史 Session ID。
189
+ - `/start <name> --agent <codex|traex|claude> --cwd <绝对路径或 ~/路径> [--resume]`:用文本命令启动并自动连接新 session;包含空格的路径请加引号。使用 `--resume` 会打开原生 Resume Picker。飞书端不支持“恢复上次”或直接输入历史 Session ID。
170
190
  - `/use <name>`:用文本命令切换 session。
171
191
  - `/tail [20-300]`:返回当前 session 最近的终端输出,默认 80 行。
172
192
  - `/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,17 +195,38 @@ 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],
@@ -145,7 +241,7 @@ function runFile(file, args, options = {}) {
145
241
  reject(Object.assign(error, { stdout, stderr }));
146
242
  return;
147
243
  }
148
- resolve2({ stdout, stderr });
244
+ resolve4({ stdout, stderr });
149
245
  }
150
246
  );
151
247
  });
@@ -154,38 +250,8 @@ function runFile(file, args, options = {}) {
154
250
  // src/daemon/client.ts
155
251
  import { randomUUID } from "crypto";
156
252
  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
253
  function requestDaemon(socketPath, request, timeoutMs = 5e3) {
188
- return new Promise((resolve2, reject) => {
254
+ return new Promise((resolve4, reject) => {
189
255
  const id = randomUUID();
190
256
  const socket = createConnection(socketPath);
191
257
  let buffer = "";
@@ -203,7 +269,7 @@ function requestDaemon(socketPath, request, timeoutMs = 5e3) {
203
269
  clearTimeout(timer);
204
270
  socket.destroy();
205
271
  try {
206
- resolve2(JSON.parse(buffer.slice(0, newline)));
272
+ resolve4(JSON.parse(buffer.slice(0, newline)));
207
273
  } catch (error) {
208
274
  reject(error);
209
275
  }
@@ -381,9 +447,9 @@ function promptContentIsDim(rawLine) {
381
447
  let cursor = 0;
382
448
  for (const match of rawLine.matchAll(sgr)) {
383
449
  const index = match.index ?? cursor;
384
- const text = rawLine.slice(cursor, index);
385
- visible += text;
386
- dimAt.push(...Array.from(text, () => dim));
450
+ const text2 = rawLine.slice(cursor, index);
451
+ visible += text2;
452
+ dimAt.push(...Array.from(text2, () => dim));
387
453
  const params = (match[1] || "0").split(";").map(Number);
388
454
  if (params.includes(0)) dim = false;
389
455
  if (params.includes(2)) dim = true;
@@ -596,10 +662,10 @@ function parseChoiceOptionLine(line) {
596
662
  if (!match?.[3] || !match[5]) return void 0;
597
663
  return { focused: Boolean(match[1]), marker: match[2] ?? match[4], key: match[3], label: match[5] };
598
664
  }
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";
665
+ function choiceRisk(text2) {
666
+ if (/full access|bypass|without sandbox|dangerously/i.test(text2)) return "privileged";
667
+ if (/always|don't ask again|do not ask again|auto mode|persist/i.test(text2)) return "persistent";
668
+ if (/^\s*no\b|reject|deny|cancel|do differently/i.test(text2)) return "reject";
603
669
  return "normal";
604
670
  }
605
671
  function result(state, confidence, normalized, fingerprint, evidence) {
@@ -814,7 +880,7 @@ function isDaemonInfo(value) {
814
880
  return typeof info.version === "string" && typeof info.pid === "number";
815
881
  }
816
882
  function delay(ms) {
817
- return new Promise((resolve2) => setTimeout(resolve2, ms));
883
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
818
884
  }
819
885
 
820
886
  // src/lark/registration.ts
@@ -972,7 +1038,8 @@ function operationLabel(value) {
972
1038
  stop: "\u505C\u6B62 session",
973
1039
  logs: "\u8BFB\u53D6\u65E5\u5FD7",
974
1040
  "reset-owner": "\u91CD\u7F6E owner",
975
- daemon: "\u7BA1\u7406 bridge daemon"
1041
+ daemon: "\u7BA1\u7406 bridge daemon",
1042
+ workspace: "\u7BA1\u7406 workspace"
976
1043
  };
977
1044
  return typeof value === "string" ? labels[value] ?? "\u6267\u884C\u547D\u4EE4" : "\u6267\u884C\u547D\u4EE4";
978
1045
  }
@@ -994,8 +1061,6 @@ function debugStack(error) {
994
1061
  }
995
1062
 
996
1063
  // src/session/start-request.ts
997
- import { stat } from "fs/promises";
998
- import { isAbsolute } from "path";
999
1064
  async function validateStartSessionRequest(request) {
1000
1065
  if (!validSessionId(request.sessionId)) {
1001
1066
  throw new AppError(
@@ -1004,21 +1069,13 @@ async function validateStartSessionRequest(request) {
1004
1069
  { sessionId: request.sessionId }
1005
1070
  );
1006
1071
  }
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
- }
1072
+ const cwd = await validateWorkspaceDirectory(request.cwd);
1016
1073
  if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
1017
1074
  throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
1018
1075
  reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
1019
1076
  });
1020
1077
  }
1021
- return request;
1078
+ return { ...request, cwd };
1022
1079
  }
1023
1080
  function validSessionId(value) {
1024
1081
  return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
@@ -1170,6 +1227,31 @@ function sanitize(value) {
1170
1227
  return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
1171
1228
  }
1172
1229
 
1230
+ // src/workspace/config.ts
1231
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
1232
+ async function addWorkspaceRoot(config, input, cwd = process.cwd()) {
1233
+ const path = await validateWorkspaceDirectory(resolveWorkspaceRootInput(input, cwd));
1234
+ const existing = new Set(config.workspaceRoots);
1235
+ if (existing.has(path)) return { config, path, added: false };
1236
+ return { config: { ...config, workspaceRoots: [...config.workspaceRoots, path] }, path, added: true };
1237
+ }
1238
+ function removeWorkspaceRoot(config, input, cwd = process.cwd()) {
1239
+ const path = normalizeWorkspacePath(resolveWorkspaceRootInput(input, cwd));
1240
+ const workspaceRoots = config.workspaceRoots.filter((candidate) => candidate !== path);
1241
+ return {
1242
+ config: workspaceRoots.length === config.workspaceRoots.length ? config : { ...config, workspaceRoots },
1243
+ path,
1244
+ removed: workspaceRoots.length !== config.workspaceRoots.length
1245
+ };
1246
+ }
1247
+ function listWorkspaceRoots(config) {
1248
+ return [...config.workspaceRoots];
1249
+ }
1250
+ function resolveWorkspaceRootInput(input, cwd) {
1251
+ const value = input.trim();
1252
+ return value && !value.startsWith("~") && !isAbsolute2(value) ? resolve2(cwd, value) : value;
1253
+ }
1254
+
1173
1255
  // src/cli.ts
1174
1256
  var program = new Command();
1175
1257
  var paths = resolveAppPaths();
@@ -1191,6 +1273,10 @@ daemonCommand.command("start").description("Start the bridge daemon").action(run
1191
1273
  daemonCommand.command("stop").description("Stop the bridge daemon without stopping coding-agent sessions").action(runDaemonStop);
1192
1274
  daemonCommand.command("restart").description("Restart the bridge daemon without stopping coding-agent sessions").action(runDaemonRestart);
1193
1275
  daemonCommand.command("status").description("Show bridge daemon status and version").action(runDaemonStatus);
1276
+ var workspaceCommand = program.command("workspace").description("Manage project workspace roots");
1277
+ workspaceCommand.command("add").argument("<path>", "Workspace root path").description("Add a workspace root").action(runWorkspaceAdd);
1278
+ workspaceCommand.command("list").description("List workspace roots").action(runWorkspaceList);
1279
+ workspaceCommand.command("remove").argument("<path>", "Workspace root path").description("Remove a workspace root").action(runWorkspaceRemove);
1194
1280
  try {
1195
1281
  await program.parseAsync();
1196
1282
  } catch (error) {
@@ -1201,6 +1287,7 @@ try {
1201
1287
  }
1202
1288
  async function runInit() {
1203
1289
  await store.ensure();
1290
+ const previousConfig = await store.loadConfig();
1204
1291
  const tenantAnswer = await p.select({
1205
1292
  message: "\u9009\u62E9\u5E73\u53F0",
1206
1293
  options: [
@@ -1210,6 +1297,15 @@ async function runInit() {
1210
1297
  });
1211
1298
  if (p.isCancel(tenantAnswer)) return p.cancel("\u5DF2\u53D6\u6D88");
1212
1299
  const tenant = tenantAnswer;
1300
+ const workspaceAnswer = await p.text({
1301
+ message: "\u5E38\u7528 workspace \u76EE\u5F55\uFF08\u53EF\u9009\uFF0C\u591A\u4E2A\u76EE\u5F55\u7528\u9017\u53F7\u5206\u9694\uFF09",
1302
+ placeholder: "~/workspace, ~/code",
1303
+ initialValue: previousConfig?.workspaceRoots.join(", ") ?? ""
1304
+ });
1305
+ if (p.isCancel(workspaceAnswer)) return p.cancel("\u5DF2\u53D6\u6D88");
1306
+ const workspaceRoots = normalizeWorkspaceRoots(
1307
+ String(workspaceAnswer).split(",").map((value) => value.trim()).filter(Boolean)
1308
+ );
1213
1309
  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
1310
  const registration = await registerApp({
1215
1311
  ...registrationDomains,
@@ -1239,7 +1335,8 @@ ${url}
1239
1335
  appId: registration.client_id,
1240
1336
  tmuxBinary: "tmux",
1241
1337
  agentBinaries: { codex: "codex", traex: "trae-cli", claude: "claude" },
1242
- pollIntervalMs: 650
1338
+ pollIntervalMs: 650,
1339
+ workspaceRoots
1243
1340
  };
1244
1341
  const previousState = await store.loadState();
1245
1342
  const registeredOwnerOpenId = registration.user_info?.open_id;
@@ -1263,7 +1360,8 @@ ${url}
1263
1360
  p.outro("PersonalAgent \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002");
1264
1361
  }
1265
1362
  async function runStart(options) {
1266
- const cwd = resolve(options.cwd ?? process.cwd());
1363
+ const requestedCwd = options.cwd ?? process.cwd();
1364
+ const cwd = requestedCwd.startsWith("~") ? requestedCwd : resolve3(requestedCwd);
1267
1365
  const resume = resolveResumeOption(options);
1268
1366
  const request = await validateStartSessionRequest({
1269
1367
  sessionId: options.name,
@@ -1294,6 +1392,25 @@ async function runStart(options) {
1294
1392
  }
1295
1393
  await attachLocal(options.name);
1296
1394
  }
1395
+ async function runWorkspaceAdd(path) {
1396
+ await ensureInitialized();
1397
+ const config = await store.loadConfig();
1398
+ const result2 = await addWorkspaceRoot(config, path);
1399
+ if (result2.added) await store.saveConfig(result2.config);
1400
+ console.log(result2.added ? `Workspace \u5DF2\u6DFB\u52A0\uFF1A${result2.path}` : `Workspace \u5DF2\u5B58\u5728\uFF1A${result2.path}`);
1401
+ }
1402
+ async function runWorkspaceList() {
1403
+ await ensureInitialized();
1404
+ const roots = listWorkspaceRoots(await store.loadConfig());
1405
+ console.log(roots.length > 0 ? roots.join("\n") : "\u5C1A\u672A\u914D\u7F6E workspace\u3002\u53EF\u8FD0\u884C\uFF1Alca workspace add ~/workspace");
1406
+ }
1407
+ async function runWorkspaceRemove(path) {
1408
+ await ensureInitialized();
1409
+ const config = await store.loadConfig();
1410
+ const result2 = removeWorkspaceRoot(config, path);
1411
+ if (result2.removed) await store.saveConfig(result2.config);
1412
+ console.log(result2.removed ? `Workspace \u5DF2\u79FB\u9664\uFF1A${result2.path}` : `Workspace \u672A\u914D\u7F6E\uFF1A${result2.path}`);
1413
+ }
1297
1414
  async function runAttach(name) {
1298
1415
  await attachLocal(name);
1299
1416
  }