lark-coding-assistant 0.2.2 → 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 +23 -3
- package/dist/cli.js +178 -61
- package/dist/cli.js.map +1 -1
- package/dist/daemon-entry.js +517 -132
- package/dist/daemon-entry.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
385
|
-
visible +=
|
|
386
|
-
dimAt.push(...Array.from(
|
|
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(
|
|
600
|
-
if (/full access|bypass|without sandbox|dangerously/i.test(
|
|
601
|
-
if (/always|don't ask again|do not ask again|auto mode|persist/i.test(
|
|
602
|
-
if (/^\s*no\b|reject|deny|cancel|do differently/i.test(
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
}
|