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 +25 -3
- package/dist/cli.js +205 -64
- package/dist/cli.js.map +1 -1
- package/dist/daemon-entry.js +1087 -375
- package/dist/daemon-entry.js.map +1 -1
- package/dist/hook-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 扫描二维码;
|
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
385
|
-
visible +=
|
|
386
|
-
dimAt.push(...Array.from(
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
}
|