lark-coding-assistant 0.1.1
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/LICENSE +21 -0
- package/README.md +329 -0
- package/bin/lark-coding-assistant-hook.mjs +2 -0
- package/bin/lark-coding-assistant.mjs +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +963 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon-entry.d.ts +2 -0
- package/dist/daemon-entry.js +2794 -0
- package/dist/daemon-entry.js.map +1 -0
- package/dist/hook-entry.d.ts +2 -0
- package/dist/hook-entry.js +95 -0
- package/dist/hook-entry.js.map +1 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3
|
+
import { access, readFile as readFile3 } from "fs/promises";
|
|
4
|
+
import { spawn as spawn3 } from "child_process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import { registerApp } from "@larksuite/channel";
|
|
10
|
+
import qrcode from "qrcode-terminal";
|
|
11
|
+
|
|
12
|
+
// src/core/paths.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
|
|
16
|
+
const base = root || join(homedir(), ".lark-coding-assistant");
|
|
17
|
+
return {
|
|
18
|
+
root: base,
|
|
19
|
+
config: join(base, "config.json"),
|
|
20
|
+
secrets: join(base, "secrets.json"),
|
|
21
|
+
state: join(base, "state.json"),
|
|
22
|
+
logsDir: join(base, "logs"),
|
|
23
|
+
logFile: join(base, "logs", "assistant.log"),
|
|
24
|
+
runtimeDir: join(base, "runtime"),
|
|
25
|
+
socket: join(base, "runtime", "daemon.sock"),
|
|
26
|
+
pid: join(base, "runtime", "daemon.pid")
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/core/store.ts
|
|
31
|
+
import { randomBytes, scryptSync, timingSafeEqual } from "crypto";
|
|
32
|
+
import { chmod as chmod2, mkdir as mkdir2 } from "fs/promises";
|
|
33
|
+
|
|
34
|
+
// src/core/model.ts
|
|
35
|
+
function emptyState(now = Date.now()) {
|
|
36
|
+
return { schemaVersion: 2, sessions: {}, updatedAt: now };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/core/atomic-json.ts
|
|
40
|
+
import { chmod, mkdir, open, readFile, rename } from "fs/promises";
|
|
41
|
+
import { dirname } from "path";
|
|
42
|
+
async function readJson(path) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "ENOENT") return void 0;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function writeJsonAtomic(path, value, mode = 384) {
|
|
51
|
+
const parent = dirname(path);
|
|
52
|
+
await mkdir(parent, { recursive: true, mode: 448 });
|
|
53
|
+
await chmod(parent, 448).catch(() => void 0);
|
|
54
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
55
|
+
const file = await open(temporary, "w", mode);
|
|
56
|
+
try {
|
|
57
|
+
await file.writeFile(`${JSON.stringify(value, null, 2)}
|
|
58
|
+
`, "utf8");
|
|
59
|
+
await file.sync();
|
|
60
|
+
} finally {
|
|
61
|
+
await file.close();
|
|
62
|
+
}
|
|
63
|
+
await rename(temporary, path);
|
|
64
|
+
await chmod(path, mode);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/core/store.ts
|
|
68
|
+
var AppStore = class {
|
|
69
|
+
constructor(paths2) {
|
|
70
|
+
this.paths = paths2;
|
|
71
|
+
}
|
|
72
|
+
paths;
|
|
73
|
+
async ensure() {
|
|
74
|
+
await mkdir2(this.paths.root, { recursive: true, mode: 448 });
|
|
75
|
+
await mkdir2(this.paths.runtimeDir, { recursive: true, mode: 448 });
|
|
76
|
+
await mkdir2(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
77
|
+
await Promise.all([
|
|
78
|
+
chmod2(this.paths.root, 448),
|
|
79
|
+
chmod2(this.paths.runtimeDir, 448),
|
|
80
|
+
chmod2(this.paths.logsDir, 448)
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
loadConfig() {
|
|
84
|
+
return readJson(this.paths.config);
|
|
85
|
+
}
|
|
86
|
+
saveConfig(config) {
|
|
87
|
+
return writeJsonAtomic(this.paths.config, config);
|
|
88
|
+
}
|
|
89
|
+
loadSecrets() {
|
|
90
|
+
return readJson(this.paths.secrets);
|
|
91
|
+
}
|
|
92
|
+
saveSecrets(secrets) {
|
|
93
|
+
return writeJsonAtomic(this.paths.secrets, secrets);
|
|
94
|
+
}
|
|
95
|
+
async loadState() {
|
|
96
|
+
return await readJson(this.paths.state) ?? emptyState();
|
|
97
|
+
}
|
|
98
|
+
saveState(state) {
|
|
99
|
+
return writeJsonAtomic(this.paths.state, state);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/platform/process.ts
|
|
104
|
+
import { execFile, spawn } from "child_process";
|
|
105
|
+
function runFile(file, args, options = {}) {
|
|
106
|
+
return new Promise((resolve2, reject) => {
|
|
107
|
+
execFile(
|
|
108
|
+
file,
|
|
109
|
+
[...args],
|
|
110
|
+
{
|
|
111
|
+
cwd: options.cwd,
|
|
112
|
+
timeout: options.timeoutMs ?? 1e4,
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
maxBuffer: 4 * 1024 * 1024
|
|
115
|
+
},
|
|
116
|
+
(error, stdout, stderr) => {
|
|
117
|
+
if (error) {
|
|
118
|
+
reject(Object.assign(error, { stdout, stderr }));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
resolve2({ stdout, stderr });
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/daemon/client.ts
|
|
128
|
+
import { randomUUID } from "crypto";
|
|
129
|
+
import { createConnection } from "net";
|
|
130
|
+
function requestDaemon(socketPath, request, timeoutMs = 5e3) {
|
|
131
|
+
return new Promise((resolve2, reject) => {
|
|
132
|
+
const id = randomUUID();
|
|
133
|
+
const socket = createConnection(socketPath);
|
|
134
|
+
let buffer = "";
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
socket.destroy();
|
|
137
|
+
reject(new Error("daemon request timed out"));
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
socket.setEncoding("utf8");
|
|
140
|
+
socket.once("connect", () => socket.write(`${JSON.stringify({ ...request, id })}
|
|
141
|
+
`));
|
|
142
|
+
socket.on("data", (chunk) => {
|
|
143
|
+
buffer += chunk;
|
|
144
|
+
const newline = buffer.indexOf("\n");
|
|
145
|
+
if (newline === -1) return;
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
socket.destroy();
|
|
148
|
+
try {
|
|
149
|
+
resolve2(JSON.parse(buffer.slice(0, newline)));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
reject(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
socket.once("error", (error) => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
reject(error);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/agents/resume.ts
|
|
162
|
+
function resolveResumeOption(options) {
|
|
163
|
+
const modes = [options.resume !== void 0, options.resumeLast, options.resumeAll].filter(Boolean).length;
|
|
164
|
+
if (modes > 1) throw new Error("--resume, --resume-last, and --resume-all cannot be used together");
|
|
165
|
+
if (options.resumeLast) return { mode: "last" };
|
|
166
|
+
if (options.resumeAll) return { mode: "picker", all: true };
|
|
167
|
+
if (typeof options.resume === "string") return { mode: "session", sessionId: options.resume };
|
|
168
|
+
if (options.resume) return { mode: "picker" };
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
171
|
+
function resumeArgs(resume) {
|
|
172
|
+
if (!resume) return [];
|
|
173
|
+
if (resume.mode === "last") return ["resume", "--last"];
|
|
174
|
+
if (resume.mode === "session") return ["resume", resume.sessionId];
|
|
175
|
+
return resume.all ? ["resume", "--all"] : ["resume"];
|
|
176
|
+
}
|
|
177
|
+
function claudeResumeArgs(resume) {
|
|
178
|
+
if (!resume) return [];
|
|
179
|
+
if (resume.mode === "last") return ["--continue"];
|
|
180
|
+
if (resume.mode === "session") return ["--resume", resume.sessionId];
|
|
181
|
+
return ["--resume"];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/screen/detector.ts
|
|
185
|
+
import { createHash } from "crypto";
|
|
186
|
+
|
|
187
|
+
// src/screen/normalize.ts
|
|
188
|
+
var ANSI = /\u001b(?:\][^\u0007]*(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
|
|
189
|
+
function stripAnsi(raw) {
|
|
190
|
+
return raw.replace(ANSI, "");
|
|
191
|
+
}
|
|
192
|
+
function normalizeScreen(raw) {
|
|
193
|
+
return stripAnsi(raw).replace(/\r/g, "").split("\n").map((line) => line.replace(/\u00a0|\u3000/g, " ").replace(/\t/g, " ").replace(/ +$/g, "")).filter((line, index, all) => line !== "" || all[index - 1] !== "").join("\n").trim();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/screen/dialects.ts
|
|
197
|
+
var commonHeaders = [
|
|
198
|
+
/^\s*(?:Would you like to|Do you want to|Approval required|Allow command|.*requires approval)/i
|
|
199
|
+
];
|
|
200
|
+
var commonFooters = [
|
|
201
|
+
/(?:press\s+)?enter\s+to\s+(?:confirm|submit(?:\s+answer)?|select|choose|continue)/i,
|
|
202
|
+
/esc\s+to\s+cancel.*(?:tab\s+to\s+amend|ctrl\+e\s+to\s+explain)/i
|
|
203
|
+
];
|
|
204
|
+
var CODEX_DIALECT = {
|
|
205
|
+
id: "codex",
|
|
206
|
+
headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
|
|
207
|
+
footerPatterns: commonFooters,
|
|
208
|
+
submitControls: [/^(?:submit|confirm)$/i],
|
|
209
|
+
customInputControls: [/^(?:type something|none of the above|add notes)$/i],
|
|
210
|
+
chatControls: [/^chat about this$/i]
|
|
211
|
+
};
|
|
212
|
+
var TRAE_DIALECT = {
|
|
213
|
+
...CODEX_DIALECT,
|
|
214
|
+
id: "trae-cli",
|
|
215
|
+
headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
|
|
216
|
+
customInputControls: [/^(?:other|none of the above|add notes)$/i]
|
|
217
|
+
};
|
|
218
|
+
var CLAUDE_DIALECT = {
|
|
219
|
+
id: "claude-code",
|
|
220
|
+
headerPatterns: [
|
|
221
|
+
...commonHeaders,
|
|
222
|
+
/^\s*(?:←\s*)?[☐☑☒]\s+.+?(?:\s+✔\s+Submit\s*→)?\s*$/i,
|
|
223
|
+
/^\s*[☐☑☒]\s+\S/
|
|
224
|
+
],
|
|
225
|
+
footerPatterns: commonFooters,
|
|
226
|
+
footerlessChoiceHeaders: [/^ready to submit your answers\?$/i],
|
|
227
|
+
submitControls: [/^(?:submit|confirm)$/i],
|
|
228
|
+
customInputControls: [/^(?:type(?:\s+.+)?|add notes)\.?$/i, /^notes:\s*press\s+n\s+to\s+add\b/i],
|
|
229
|
+
directInputControls: [/^type(?:\s+.+)?\.?$/i],
|
|
230
|
+
customInputValuePattern: /^type\s+(.+?)\.?$/i,
|
|
231
|
+
chatControls: [/^chat about this$/i]
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/screen/detector.ts
|
|
235
|
+
function detectCodexScreen(raw, paneAlive = true, cursor) {
|
|
236
|
+
return detectAgentScreen(raw, paneAlive, {
|
|
237
|
+
brandPattern: /OpenAI Codex|codex/i,
|
|
238
|
+
brandEvidence: "codex",
|
|
239
|
+
failurePattern: /fatal error|panicked at|segmentation fault|codex exited/i,
|
|
240
|
+
dialect: CODEX_DIALECT
|
|
241
|
+
}, cursor);
|
|
242
|
+
}
|
|
243
|
+
function detectTraeScreen(raw, paneAlive = true, cursor) {
|
|
244
|
+
return detectAgentScreen(raw, paneAlive, {
|
|
245
|
+
brandPattern: /TraeCode CLI|traecli/i,
|
|
246
|
+
brandEvidence: "trae-cli",
|
|
247
|
+
failurePattern: /fatal error|panicked at|segmentation fault|traecli exited/i,
|
|
248
|
+
dialect: TRAE_DIALECT
|
|
249
|
+
}, cursor);
|
|
250
|
+
}
|
|
251
|
+
function detectClaudeScreen(raw, paneAlive = true, cursor) {
|
|
252
|
+
return detectAgentScreen(raw, paneAlive, {
|
|
253
|
+
brandPattern: /Claude Code/i,
|
|
254
|
+
brandEvidence: "claude-code",
|
|
255
|
+
failurePattern: /fatal error|segmentation fault|Claude Code exited/i,
|
|
256
|
+
dialect: CLAUDE_DIALECT
|
|
257
|
+
}, cursor);
|
|
258
|
+
}
|
|
259
|
+
function detectAgentScreen(raw, paneAlive, options, cursor) {
|
|
260
|
+
const normalized = normalizeScreen(raw);
|
|
261
|
+
const lines = normalized.split("\n").slice(-80);
|
|
262
|
+
const tail = lines.join("\n");
|
|
263
|
+
const bottom = lines.slice(-16);
|
|
264
|
+
const bottomTail = bottom.join("\n");
|
|
265
|
+
const fingerprint = createHash("sha256").update(tail).digest("hex");
|
|
266
|
+
if (!paneAlive) return result("exited", 1, normalized, fingerprint, ["pane exited"]);
|
|
267
|
+
if (options.failurePattern.test(bottomTail)) {
|
|
268
|
+
return result("failed", 0.95, normalized, fingerprint, matching(bottom, /fatal|panic|exited/i));
|
|
269
|
+
}
|
|
270
|
+
const choice = parseChoiceInteraction(lines.slice(-60), options.dialect);
|
|
271
|
+
if (choice) {
|
|
272
|
+
const choiceFingerprint = choice.interaction?.interactionId ?? createHash("sha256").update(choice.evidence.join("\n")).digest("hex");
|
|
273
|
+
return { ...choice, normalized, fingerprint: choiceFingerprint, hasDraftInput: false };
|
|
274
|
+
}
|
|
275
|
+
const inputEvidence = matching(bottom, /(?:select an option|choose one|enter your answer|provide .*input|request_user_input|do you trust|trust the contents)/i);
|
|
276
|
+
if (inputEvidence.length > 0) return result("input", 0.72, normalized, fingerprint, inputEvidence);
|
|
277
|
+
const activeRunningPattern = /(?:model:\s+loading|esc to interrupt|running\s+.+?hooks?|^\s*[•◦◆◇◈✦✻▍]?\s*(?:working|thinking|executing)(?:…|\s*\(|$))/i;
|
|
278
|
+
const activeRunningEvidence = matching(bottom, activeRunningPattern);
|
|
279
|
+
if (activeRunningEvidence.length > 0) return result("running", 0.85, normalized, fingerprint, activeRunningEvidence);
|
|
280
|
+
const runningPattern = /(?:waiting for)/i;
|
|
281
|
+
const runningEvidence = matching(bottom, runningPattern);
|
|
282
|
+
const lastRunningIndex = lastMatchingIndex(bottom, runningPattern);
|
|
283
|
+
const lastPromptIndex = lastMatchingIndex(bottom, /^\s*[›>❯]\s?/);
|
|
284
|
+
if (lastRunningIndex > lastPromptIndex) return result("running", 0.75, normalized, fingerprint, runningEvidence);
|
|
285
|
+
const promptLines = bottom.slice(-4).filter((line) => /^\s*[›>❯]\s?/.test(line));
|
|
286
|
+
if (promptLines.length > 0) {
|
|
287
|
+
const last = promptLines.at(-1) ?? "";
|
|
288
|
+
const content = last.replace(/^\s*[›>❯]\s?/, "").trim();
|
|
289
|
+
const rawPrompt = raw.split("\n").filter((line) => /^\s*[›>❯]\s?/.test(stripAnsi(line))).at(-1) ?? "";
|
|
290
|
+
const draft = content.length > 0 && !promptContentIsDim(rawPrompt) && !cursorIsAtPromptStart(rawPrompt, cursor?.x);
|
|
291
|
+
return { ...result("idle", 0.8, normalized, fingerprint, [last]), hasDraftInput: draft };
|
|
292
|
+
}
|
|
293
|
+
if (runningEvidence.length > 0) return result("running", 0.75, normalized, fingerprint, runningEvidence);
|
|
294
|
+
if (options.brandPattern.test(tail)) {
|
|
295
|
+
return result("starting", 0.55, normalized, fingerprint, [options.brandEvidence]);
|
|
296
|
+
}
|
|
297
|
+
return result("unknown", 0.2, normalized, fingerprint, []);
|
|
298
|
+
}
|
|
299
|
+
function cursorIsAtPromptStart(rawLine, cursorX) {
|
|
300
|
+
if (cursorX === void 0) return false;
|
|
301
|
+
const visible = stripAnsi(rawLine);
|
|
302
|
+
const marker = visible.search(/[›>❯]/);
|
|
303
|
+
if (marker === -1) return false;
|
|
304
|
+
let contentStart = marker + 1;
|
|
305
|
+
while (/\s/.test(visible[contentStart] ?? "")) contentStart += 1;
|
|
306
|
+
return cursorX === contentStart;
|
|
307
|
+
}
|
|
308
|
+
function promptContentIsDim(rawLine) {
|
|
309
|
+
let dim = false;
|
|
310
|
+
let visible = "";
|
|
311
|
+
const dimAt = [];
|
|
312
|
+
const sgr = /\u001b\[([0-9;]*)m/g;
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
for (const match of rawLine.matchAll(sgr)) {
|
|
315
|
+
const index = match.index ?? cursor;
|
|
316
|
+
const text = rawLine.slice(cursor, index);
|
|
317
|
+
visible += text;
|
|
318
|
+
dimAt.push(...Array.from(text, () => dim));
|
|
319
|
+
const params = (match[1] || "0").split(";").map(Number);
|
|
320
|
+
if (params.includes(0)) dim = false;
|
|
321
|
+
if (params.includes(2)) dim = true;
|
|
322
|
+
if (params.includes(22)) dim = false;
|
|
323
|
+
cursor = index + match[0].length;
|
|
324
|
+
}
|
|
325
|
+
const rest = rawLine.slice(cursor);
|
|
326
|
+
visible += rest;
|
|
327
|
+
dimAt.push(...Array.from(rest, () => dim));
|
|
328
|
+
const marker = visible.search(/[›>❯]/);
|
|
329
|
+
if (marker === -1) return false;
|
|
330
|
+
let contentStart = marker + 1;
|
|
331
|
+
while (/\s/.test(visible[contentStart] ?? "")) contentStart += 1;
|
|
332
|
+
const contentEnd = visible.trimEnd().length;
|
|
333
|
+
return contentEnd > contentStart && dimAt.slice(contentStart, contentEnd).every(Boolean);
|
|
334
|
+
}
|
|
335
|
+
function parseChoiceInteraction(lines, dialect) {
|
|
336
|
+
const explicitFooterIndex = lastMatchingIndex(lines, new RegExp(dialect.footerPatterns.map(({ source }) => `(?:${source})`).join("|"), "i"));
|
|
337
|
+
const footerlessHeaderIndex = dialect.footerlessChoiceHeaders ? lastMatchingIndex(lines, new RegExp(dialect.footerlessChoiceHeaders.map(({ source }) => `(?:${source})`).join("|"), "i")) : -1;
|
|
338
|
+
if (explicitFooterIndex === -1 && footerlessHeaderIndex === -1) return void 0;
|
|
339
|
+
const footerIndex = explicitFooterIndex >= 0 ? explicitFooterIndex : lines.length;
|
|
340
|
+
if (explicitFooterIndex >= 0 && lines.slice(footerIndex + 1).some((line) => /^\s*[›>❯]\s?/.test(line))) return void 0;
|
|
341
|
+
const allStarts = lines.slice(0, footerIndex).map((line, index) => choiceOptionStart(line) ? index : -1).filter((index) => index >= 0);
|
|
342
|
+
if (allStarts.length < 2) return void 0;
|
|
343
|
+
const optionStarts = latestOptionGroup(allStarts);
|
|
344
|
+
if (optionStarts.length < 2) return void 0;
|
|
345
|
+
if (explicitFooterIndex === -1 && (optionStarts[0] ?? -1) <= footerlessHeaderIndex) return void 0;
|
|
346
|
+
const firstOption = optionStarts[0] ?? 0;
|
|
347
|
+
const beforeOptions = lines.slice(0, firstOption);
|
|
348
|
+
const knownHeader = lastMatchingIndex(beforeOptions, new RegExp(dialect.headerPatterns.map(({ source }) => `(?:${source})`).join("|"), "i"));
|
|
349
|
+
const hardBoundary = lastMatchingIndex(beforeOptions, /^(?:\s*[-─━═]{8,}\s*|\s*[›>❯]\s+.+|\s*[✻◆◦•]\s+.+)$/);
|
|
350
|
+
const contextStart = knownHeader > hardBoundary ? knownHeader : hardBoundary >= 0 ? hardBoundary + 1 : Math.max(0, firstOption - 12);
|
|
351
|
+
const context = lines.slice(contextStart, firstOption).filter((line) => Boolean(line) && !/^\s*[-─━═]{8,}\s*$/.test(line));
|
|
352
|
+
const footer = lines[footerIndex] ?? "";
|
|
353
|
+
const indexedActions = optionStarts.flatMap((start, optionIndex) => {
|
|
354
|
+
const firstLine = lines[start] ?? "";
|
|
355
|
+
const parsed = parseChoiceOptionLine(firstLine);
|
|
356
|
+
if (!parsed) return [];
|
|
357
|
+
const end = optionStarts[optionIndex + 1] ?? footerIndex;
|
|
358
|
+
const rawLabel = parsed.label;
|
|
359
|
+
const marker = selectionMarker(parsed.marker);
|
|
360
|
+
const segments = [
|
|
361
|
+
rawLabel,
|
|
362
|
+
...lines.slice(start + 1, end).map((line) => line.trim()).filter((line) => Boolean(line) && !/^[-─━═]{3,}$/.test(line) && !matchesAny(withoutFocusMarker(line), dialect.submitControls) && !isSidePanelLine(line) && !matchesAny(withoutFocusMarker(line), dialect.customInputControls) && !matchesAny(withoutFocusMarker(line), dialect.chatControls) && !/^notes$/i.test(line) && !/^\[[^\]]+\]$/.test(line))
|
|
363
|
+
];
|
|
364
|
+
const shortcutMatch = segments.join(" ").match(/\(([^()\s]{1,16})\)\s*$/);
|
|
365
|
+
if (shortcutMatch) {
|
|
366
|
+
const last = segments.length - 1;
|
|
367
|
+
segments[last] = (segments[last] ?? "").replace(/\s*\([^()\s]{1,16}\)\s*$/, "").trim();
|
|
368
|
+
}
|
|
369
|
+
const parts = (segments.shift() ?? "").split(/\s{2,}/).map((part) => part.trim()).filter(Boolean);
|
|
370
|
+
const label = parts.shift();
|
|
371
|
+
if (!label) return [];
|
|
372
|
+
const continuation = segments.join(" ").replace(/\bnotes:\s*press\s+n\s+to\s+add\s+notes\b/gi, "").trim();
|
|
373
|
+
let description = parts.filter((part) => !isSidePanelLine(part)).join(" ").trim();
|
|
374
|
+
let fullLabel = label;
|
|
375
|
+
if (continuation && marker) {
|
|
376
|
+
description = [description, continuation].filter(Boolean).join(" ");
|
|
377
|
+
} else if (continuation) {
|
|
378
|
+
if (description) description = `${description} ${continuation}`;
|
|
379
|
+
else fullLabel = `${fullLabel} ${continuation}`;
|
|
380
|
+
}
|
|
381
|
+
const role = controlRole(fullLabel, dialect);
|
|
382
|
+
const customValue = role === "custom-input" ? dialect.customInputValuePattern?.exec(fullLabel)?.[1]?.trim() : void 0;
|
|
383
|
+
const inputValue = customValue && !/^something\.?$/i.test(customValue) ? customValue : void 0;
|
|
384
|
+
if (role === "custom-input" && /^type\b/i.test(fullLabel)) fullLabel = "Type something";
|
|
385
|
+
const risk = choiceRisk(`${fullLabel} ${description}`);
|
|
386
|
+
const editor = role === "custom-input" ? /(?:notes?\s*\(tab\)|tab\s+to\s+(?:add|edit)\s+notes?)/i.test(`${description} ${footer}`) ? { openKey: "Tab", submitKey: "Enter", commitsInteraction: true } : dialect.directInputControls && matchesAny(fullLabel, dialect.directInputControls) ? { submitKey: "Enter", commitsInteraction: true } : void 0 : void 0;
|
|
387
|
+
return [{ index: start, action: {
|
|
388
|
+
id: `option-${parsed.key}`,
|
|
389
|
+
label: fullLabel,
|
|
390
|
+
key: parsed.key,
|
|
391
|
+
description: description || void 0,
|
|
392
|
+
inputValue,
|
|
393
|
+
shortcut: shortcutMatch?.[1]?.toLowerCase(),
|
|
394
|
+
editor,
|
|
395
|
+
focused: parsed.focused,
|
|
396
|
+
marker,
|
|
397
|
+
role,
|
|
398
|
+
risk,
|
|
399
|
+
danger: risk === "persistent" || risk === "privileged"
|
|
400
|
+
} }];
|
|
401
|
+
});
|
|
402
|
+
const standaloneControls = lines.slice(firstOption, footerIndex).flatMap((line, relativeIndex) => {
|
|
403
|
+
const visible = line.replace(/^\s*[›>❯]\s*/, "").trim();
|
|
404
|
+
const role = matchesAny(visible, dialect.submitControls) ? "submit" : matchesAny(visible, dialect.customInputControls) ? "custom-input" : matchesAny(visible, dialect.chatControls) ? "chat" : void 0;
|
|
405
|
+
if (!role) return [];
|
|
406
|
+
const id = role === "submit" ? "submit" : role === "chat" ? "chat" : "custom-input";
|
|
407
|
+
const label = /^notes:/i.test(visible) ? "Add notes" : visible;
|
|
408
|
+
return [{ index: firstOption + relativeIndex, action: {
|
|
409
|
+
id,
|
|
410
|
+
key: id,
|
|
411
|
+
label,
|
|
412
|
+
role,
|
|
413
|
+
shortcut: /^notes:/i.test(visible) ? "n" : void 0,
|
|
414
|
+
focused: /^\s*[›>❯]/.test(line),
|
|
415
|
+
risk: "normal",
|
|
416
|
+
danger: false
|
|
417
|
+
} }];
|
|
418
|
+
});
|
|
419
|
+
const actions = [...indexedActions, ...standaloneControls].sort((left, right) => left.index - right.index).map(({ action }) => action);
|
|
420
|
+
const submitIndex = actions.findIndex(({ role }) => role === "submit");
|
|
421
|
+
const inlineCustomIndex = submitIndex - 1;
|
|
422
|
+
const inlineCustom = actions[inlineCustomIndex];
|
|
423
|
+
if (inlineCustom?.role === "answer" && (inlineCustom.marker === "checked" || inlineCustom.marker === "unchecked") && !inlineCustom.description && actions.slice(0, inlineCustomIndex).some(({ description, marker }) => Boolean(description && marker))) {
|
|
424
|
+
const value = inlineCustom.label.trim();
|
|
425
|
+
inlineCustom.role = "custom-input";
|
|
426
|
+
inlineCustom.inputValue = /^(?:type something|其他|其它|other)$/i.test(value) ? void 0 : value;
|
|
427
|
+
inlineCustom.label = "Type something";
|
|
428
|
+
}
|
|
429
|
+
if (actions.length < 2 || actions.filter(({ focused }) => focused).length !== 1) return void 0;
|
|
430
|
+
const kind = classifyChoice(context, actions, footer);
|
|
431
|
+
const semantics = selectionSemantics(actions, footer);
|
|
432
|
+
const canonical = actions.map(({ key, label, description, role, editor }) => ({ key, label, description, role, editor }));
|
|
433
|
+
const identityContext = context.map((line) => line.replace(/([←\s]*)[☐☑☒]/, "$1\u2610"));
|
|
434
|
+
const interactionId = createHash("sha256").update(JSON.stringify([kind, identityContext, canonical])).digest("hex");
|
|
435
|
+
const revision = createHash("sha256").update(JSON.stringify([
|
|
436
|
+
interactionId,
|
|
437
|
+
actions.map(({ key, marker, inputValue }) => ({ key, marker, inputValue }))
|
|
438
|
+
])).digest("hex");
|
|
439
|
+
const questionContext = context.filter((line) => !matchesAny(line, dialect.headerPatterns));
|
|
440
|
+
const questionTitle = [...questionContext].reverse().find((line) => /[??]/.test(line)) ?? questionContext.at(-1);
|
|
441
|
+
return {
|
|
442
|
+
state: kind === "approval" ? "approval" : "input",
|
|
443
|
+
confidence: kind === "question" ? 0.92 : kind === "approval" ? 0.9 : 0.85,
|
|
444
|
+
evidence: lines.slice(contextStart, footerIndex + 1).filter(Boolean),
|
|
445
|
+
actions,
|
|
446
|
+
interaction: {
|
|
447
|
+
kind,
|
|
448
|
+
title: questionTitle?.trim() || context[0]?.trim() || "\u8BF7\u9009\u62E9\u4E00\u4E2A\u9009\u9879",
|
|
449
|
+
context,
|
|
450
|
+
interactionId,
|
|
451
|
+
revision,
|
|
452
|
+
semantics,
|
|
453
|
+
contentConfidence: knownHeader >= 0 || hardBoundary >= 0 ? 0.95 : 0.65,
|
|
454
|
+
actionConfidence: semantics ? semantics.confidence : 0.55
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function selectionMarker(value) {
|
|
459
|
+
if (!value) return void 0;
|
|
460
|
+
if (value === "[ ]" || value === "\u2610") return "unchecked";
|
|
461
|
+
if (/^\[[xX✓✔]\]$/.test(value) || value === "\u2611") return "checked";
|
|
462
|
+
if (value === "\u25CB") return "unselected";
|
|
463
|
+
if (value === "\u25CF") return "selected";
|
|
464
|
+
return void 0;
|
|
465
|
+
}
|
|
466
|
+
function matchesAny(value, patterns) {
|
|
467
|
+
return patterns.some((pattern) => pattern.test(value));
|
|
468
|
+
}
|
|
469
|
+
function withoutFocusMarker(value) {
|
|
470
|
+
return value.replace(/^\s*[›>❯]\s*/, "").trim();
|
|
471
|
+
}
|
|
472
|
+
function isSidePanelLine(value) {
|
|
473
|
+
return /[│┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬║═]/.test(value);
|
|
474
|
+
}
|
|
475
|
+
function controlRole(label, dialect) {
|
|
476
|
+
if (matchesAny(label, dialect.customInputControls)) return "custom-input";
|
|
477
|
+
if (matchesAny(label, dialect.chatControls)) return "chat";
|
|
478
|
+
return "answer";
|
|
479
|
+
}
|
|
480
|
+
function selectionSemantics(actions, footer) {
|
|
481
|
+
const answers = actions.filter(({ role }) => role === "answer");
|
|
482
|
+
const submit = actions.find(({ role }) => role === "submit");
|
|
483
|
+
const hasCheckbox = answers.length > 0 && answers.every(({ marker }) => marker === "checked" || marker === "unchecked");
|
|
484
|
+
const hasRadio = answers.length > 0 && answers.every(({ marker }) => marker === "selected" || marker === "unselected");
|
|
485
|
+
if (hasCheckbox || hasRadio) {
|
|
486
|
+
const commit = submit ? { mode: "explicit", controlId: submit.id } : /ctrl\+enter\s+to\s+(?:submit|confirm)/i.test(footer) ? { mode: "key", key: "C-Enter" } : /enter\s+to\s+(?:submit|confirm)(?:\s+answer)?/i.test(footer) ? { mode: "key", key: "Enter" } : void 0;
|
|
487
|
+
if (!commit) return void 0;
|
|
488
|
+
const toggleKey = /space\s+to\s+toggle/i.test(footer) ? "Space" : "Enter";
|
|
489
|
+
return {
|
|
490
|
+
cardinality: hasCheckbox ? "many" : "one",
|
|
491
|
+
activation: "toggle",
|
|
492
|
+
toggleKey,
|
|
493
|
+
commit,
|
|
494
|
+
confidence: 0.97,
|
|
495
|
+
evidence: [hasCheckbox ? "answer controls use checkbox markers" : "answer controls use radio markers", "an explicit commit mechanism exists"]
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (answers.some(({ marker }) => marker)) return void 0;
|
|
499
|
+
return {
|
|
500
|
+
cardinality: "one",
|
|
501
|
+
activation: "submit",
|
|
502
|
+
commit: { mode: "immediate" },
|
|
503
|
+
confidence: 0.9,
|
|
504
|
+
evidence: ["numbered controls use a single focus marker and no persistent selection markers"]
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function latestOptionGroup(starts) {
|
|
508
|
+
const group = [starts.at(-1)];
|
|
509
|
+
for (let index = starts.length - 2; index >= 0; index -= 1) {
|
|
510
|
+
const start = starts[index];
|
|
511
|
+
if (group[0] - start > 8) break;
|
|
512
|
+
group.unshift(start);
|
|
513
|
+
}
|
|
514
|
+
return group;
|
|
515
|
+
}
|
|
516
|
+
function classifyChoice(context, actions, footer) {
|
|
517
|
+
const contextText = context.join(" ");
|
|
518
|
+
if (/(?:^|\s)[☐☑☒]\s+\S|Question\s+\d+\/\d+/i.test(contextText) || /submit answer|add notes/i.test(footer)) return "question";
|
|
519
|
+
const optionText = actions.map(({ label, description }) => `${label} ${description ?? ""}`).join(" ");
|
|
520
|
+
if (/(?:Would you like to|Do you want to).*(?:run|edit|grant|access)|approval required|allow command|requires approval/i.test(contextText) || /full access|don't ask again|do not ask again|always allow|auto mode|permissions?\b/i.test(optionText)) return "approval";
|
|
521
|
+
return "choice";
|
|
522
|
+
}
|
|
523
|
+
function choiceOptionStart(line) {
|
|
524
|
+
return Boolean(parseChoiceOptionLine(line));
|
|
525
|
+
}
|
|
526
|
+
function parseChoiceOptionLine(line) {
|
|
527
|
+
const match = line.match(/^\s*([›>❯])?\s*(?:(\[[ xX✓✔]\]|[☐☑○●])\s*)?(\d+)[.)]\s+(?:(\[[ xX✓✔]\]|[☐☑○●])\s*)?(.+?)\s*$/);
|
|
528
|
+
if (!match?.[3] || !match[5]) return void 0;
|
|
529
|
+
return { focused: Boolean(match[1]), marker: match[2] ?? match[4], key: match[3], label: match[5] };
|
|
530
|
+
}
|
|
531
|
+
function choiceRisk(text) {
|
|
532
|
+
if (/full access|bypass|without sandbox|dangerously/i.test(text)) return "privileged";
|
|
533
|
+
if (/always|don't ask again|do not ask again|auto mode|persist/i.test(text)) return "persistent";
|
|
534
|
+
if (/^\s*no\b|reject|deny|cancel|do differently/i.test(text)) return "reject";
|
|
535
|
+
return "normal";
|
|
536
|
+
}
|
|
537
|
+
function result(state, confidence, normalized, fingerprint, evidence) {
|
|
538
|
+
return { state, confidence, normalized, fingerprint, evidence, actions: [], hasDraftInput: false };
|
|
539
|
+
}
|
|
540
|
+
function matching(lines, pattern) {
|
|
541
|
+
return lines.filter((line) => pattern.test(line)).slice(-4);
|
|
542
|
+
}
|
|
543
|
+
function lastMatchingIndex(lines, pattern) {
|
|
544
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
545
|
+
if (pattern.test(lines[index] ?? "")) return index;
|
|
546
|
+
}
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/agents/stop-hook.ts
|
|
551
|
+
function codexStyleStopHookArgs(command) {
|
|
552
|
+
const hook = `{hooks=[{type="command",command=${JSON.stringify(command)},timeout=5}]}`;
|
|
553
|
+
return ["--dangerously-bypass-hook-trust", "-c", `hooks.Stop=[${hook}]`];
|
|
554
|
+
}
|
|
555
|
+
function claudeStopHookArgs(command) {
|
|
556
|
+
return ["--settings", JSON.stringify({
|
|
557
|
+
hooks: {
|
|
558
|
+
Stop: [{ hooks: [{ type: "command", command, timeout: 5 }] }]
|
|
559
|
+
}
|
|
560
|
+
})];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/agents/codex.ts
|
|
564
|
+
var codexAdapter = {
|
|
565
|
+
id: "codex",
|
|
566
|
+
displayName: "Codex",
|
|
567
|
+
groupOrder: 10,
|
|
568
|
+
binary: (config) => config.agentBinaries.codex,
|
|
569
|
+
versionArgs: ["--version"],
|
|
570
|
+
buildLaunchArgs: ({ resume, stopHookCommand }) => [
|
|
571
|
+
...codexStyleStopHookArgs(stopHookCommand),
|
|
572
|
+
...resumeArgs(resume)
|
|
573
|
+
],
|
|
574
|
+
detectScreen: detectCodexScreen
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/agents/trae-cli.ts
|
|
578
|
+
var traeCliAdapter = {
|
|
579
|
+
id: "trae-cli",
|
|
580
|
+
displayName: "Trae CLI",
|
|
581
|
+
groupOrder: 20,
|
|
582
|
+
binary: (config) => config.agentBinaries["trae-cli"],
|
|
583
|
+
versionArgs: ["--version"],
|
|
584
|
+
buildLaunchArgs: ({ resume, stopHookCommand }) => [
|
|
585
|
+
...codexStyleStopHookArgs(stopHookCommand),
|
|
586
|
+
...resumeArgs(resume)
|
|
587
|
+
],
|
|
588
|
+
detectScreen: detectTraeScreen
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/agents/claude-code.ts
|
|
592
|
+
var claudeCodeAdapter = {
|
|
593
|
+
id: "claude-code",
|
|
594
|
+
displayName: "Claude Code",
|
|
595
|
+
groupOrder: 30,
|
|
596
|
+
binary: (config) => config.agentBinaries["claude-code"],
|
|
597
|
+
versionArgs: ["--version"],
|
|
598
|
+
buildLaunchArgs: ({ resume, stopHookCommand }) => [
|
|
599
|
+
...claudeStopHookArgs(stopHookCommand),
|
|
600
|
+
...claudeResumeArgs(resume)
|
|
601
|
+
],
|
|
602
|
+
detectScreen: detectClaudeScreen
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// src/agents/types.ts
|
|
606
|
+
var AGENT_IDS = ["codex", "trae-cli", "claude-code"];
|
|
607
|
+
|
|
608
|
+
// src/agents/registry.ts
|
|
609
|
+
var adapters = /* @__PURE__ */ new Map([
|
|
610
|
+
[codexAdapter.id, codexAdapter],
|
|
611
|
+
[traeCliAdapter.id, traeCliAdapter],
|
|
612
|
+
[claudeCodeAdapter.id, claudeCodeAdapter]
|
|
613
|
+
]);
|
|
614
|
+
function getAgentAdapter(id) {
|
|
615
|
+
const adapter = adapters.get(id);
|
|
616
|
+
if (!adapter) throw new Error(`unsupported coding agent: ${id}`);
|
|
617
|
+
return adapter;
|
|
618
|
+
}
|
|
619
|
+
function isAgentId(value) {
|
|
620
|
+
return AGENT_IDS.includes(value);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/daemon/lifecycle.ts
|
|
624
|
+
import { mkdir as mkdir3, open as open2, readFile as readFile2 } from "fs/promises";
|
|
625
|
+
import { spawn as spawn2 } from "child_process";
|
|
626
|
+
async function daemonInfo(paths2, timeoutMs = 500) {
|
|
627
|
+
try {
|
|
628
|
+
const response = await requestDaemon(paths2.socket, { method: "ping" }, timeoutMs);
|
|
629
|
+
if (!response.ok) return void 0;
|
|
630
|
+
return isDaemonInfo(response.value) ? response.value : void 0;
|
|
631
|
+
} catch {
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async function startDaemonProcess(paths2, daemonEntry, readyTimeoutMs = 5e3) {
|
|
636
|
+
const existing = await daemonInfo(paths2);
|
|
637
|
+
if (existing) return existing;
|
|
638
|
+
await Promise.all([
|
|
639
|
+
mkdir3(paths2.runtimeDir, { recursive: true, mode: 448 }),
|
|
640
|
+
mkdir3(paths2.logsDir, { recursive: true, mode: 448 })
|
|
641
|
+
]);
|
|
642
|
+
const log2 = await open2(paths2.logFile, "a", 384);
|
|
643
|
+
const child = spawn2(process.execPath, [daemonEntry], {
|
|
644
|
+
detached: true,
|
|
645
|
+
stdio: ["ignore", log2.fd, log2.fd],
|
|
646
|
+
env: process.env
|
|
647
|
+
});
|
|
648
|
+
let spawnError;
|
|
649
|
+
child.once("error", (error) => {
|
|
650
|
+
spawnError = error;
|
|
651
|
+
});
|
|
652
|
+
await log2.close();
|
|
653
|
+
child.unref();
|
|
654
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
655
|
+
while (Date.now() < deadline) {
|
|
656
|
+
if (spawnError) throw spawnError;
|
|
657
|
+
await delay(100);
|
|
658
|
+
const info = await daemonInfo(paths2, 300);
|
|
659
|
+
if (info) return info;
|
|
660
|
+
}
|
|
661
|
+
throw new Error("daemon did not become ready");
|
|
662
|
+
}
|
|
663
|
+
async function stopDaemonProcess(paths2) {
|
|
664
|
+
const info = await daemonInfo(paths2);
|
|
665
|
+
if (!info) return stopDaemonByPid(paths2);
|
|
666
|
+
try {
|
|
667
|
+
const response = await requestDaemon(paths2.socket, { method: "shutdown" }, 1e3);
|
|
668
|
+
if (!response.ok) throw new Error(response.error);
|
|
669
|
+
} catch {
|
|
670
|
+
signalDaemon(info.pid);
|
|
671
|
+
}
|
|
672
|
+
await waitUntilStopped(paths2, info.pid);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
async function stopDaemonByPid(paths2) {
|
|
676
|
+
const pid = Number.parseInt(await readFile2(paths2.pid, "utf8").catch(() => ""), 10);
|
|
677
|
+
if (!validPid(pid) || !processIsAlive(pid)) return false;
|
|
678
|
+
if (!await isCurrentDaemonProcess(pid)) {
|
|
679
|
+
throw new Error(`PID ${pid} is alive but is not a lark-coding-assistant daemon; refusing to signal it`);
|
|
680
|
+
}
|
|
681
|
+
signalDaemon(pid);
|
|
682
|
+
await waitUntilStopped(paths2, pid);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
async function isCurrentDaemonProcess(pid) {
|
|
686
|
+
try {
|
|
687
|
+
const command = (await runFile("ps", ["-p", String(pid), "-o", "command="])).stdout.trim();
|
|
688
|
+
return command.includes("lark-coding-assistant") && /(?:^|\/)daemon-entry\.js(?:\s|$)/.test(command);
|
|
689
|
+
} catch {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function waitUntilStopped(paths2, pid) {
|
|
694
|
+
const deadline = Date.now() + 5e3;
|
|
695
|
+
while (Date.now() < deadline) {
|
|
696
|
+
if (!processIsAlive(pid) && !await daemonInfo(paths2, 200)) return;
|
|
697
|
+
await delay(100);
|
|
698
|
+
}
|
|
699
|
+
throw new Error(`daemon PID ${pid} did not stop within 5 seconds`);
|
|
700
|
+
}
|
|
701
|
+
function signalDaemon(pid) {
|
|
702
|
+
if (!validPid(pid) || pid === process.pid) throw new Error("refusing to signal invalid daemon PID");
|
|
703
|
+
process.kill(pid, "SIGTERM");
|
|
704
|
+
}
|
|
705
|
+
function processIsAlive(pid) {
|
|
706
|
+
try {
|
|
707
|
+
process.kill(pid, 0);
|
|
708
|
+
return true;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function validPid(pid) {
|
|
714
|
+
return Number.isInteger(pid) && pid > 1;
|
|
715
|
+
}
|
|
716
|
+
function isDaemonInfo(value) {
|
|
717
|
+
if (!value || typeof value !== "object") return false;
|
|
718
|
+
const info = value;
|
|
719
|
+
return typeof info.version === "string" && typeof info.pid === "number";
|
|
720
|
+
}
|
|
721
|
+
function delay(ms) {
|
|
722
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/lark/registration.ts
|
|
726
|
+
var registrationDomains = {
|
|
727
|
+
domain: "accounts.feishu.cn",
|
|
728
|
+
larkDomain: "accounts.larksuite.com"
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/cli.ts
|
|
732
|
+
var program = new Command();
|
|
733
|
+
var paths = resolveAppPaths();
|
|
734
|
+
var store = new AppStore(paths);
|
|
735
|
+
var packageInfo = JSON.parse(
|
|
736
|
+
await readFile3(new URL("../package.json", import.meta.url), "utf8")
|
|
737
|
+
);
|
|
738
|
+
program.name("lark-coding-assistant").version(packageInfo.version);
|
|
739
|
+
program.command("init").description("Configure Feishu/Lark PersonalAgent").action(runInit);
|
|
740
|
+
program.command("start").option("-n, --name <name>", "Session name", "default").option("--agent <agent>", "Coding agent (codex, trae-cli, or claude-code)", parseAgentId, "codex").option("--cwd <path>", "Coding agent working directory").option("--resume [session-id]", "Resume agent session; omit the ID to open the picker").option("--resume-last", "Resume the most recent agent session in this working directory").option("--resume-all", "Show all agent sessions in the resume picker").action(runStart);
|
|
741
|
+
program.command("attach").argument("[name]", "Session name", "default").description("Attach local terminal to coding-agent tmux session").action(runAttach);
|
|
742
|
+
program.command("bind-code").description("Generate a new one-time Lark binding code").action(runBindCode);
|
|
743
|
+
program.command("status").argument("[name]", "Session name").description("Show daemon and session status").action(runStatus);
|
|
744
|
+
program.command("stop").argument("[name]", "Session name").description("Stop managed coding-agent/tmux session").action(runStop);
|
|
745
|
+
program.command("logs").option("-n, --lines <count>", "Number of lines", "100").action(runLogs);
|
|
746
|
+
program.command("reset-owner").description("Clear persistent Lark owner").action(runResetOwner);
|
|
747
|
+
var daemonCommand = program.command("daemon").description("Manage the Lark bridge daemon");
|
|
748
|
+
daemonCommand.command("start").description("Start the bridge daemon").action(runDaemonStart);
|
|
749
|
+
daemonCommand.command("stop").description("Stop the bridge daemon without stopping coding-agent sessions").action(runDaemonStop);
|
|
750
|
+
daemonCommand.command("restart").description("Restart the bridge daemon without stopping coding-agent sessions").action(runDaemonRestart);
|
|
751
|
+
daemonCommand.command("status").description("Show bridge daemon status and version").action(runDaemonStatus);
|
|
752
|
+
await program.parseAsync();
|
|
753
|
+
async function runInit() {
|
|
754
|
+
await store.ensure();
|
|
755
|
+
const tenantAnswer = await p.select({
|
|
756
|
+
message: "\u9009\u62E9\u5E73\u53F0",
|
|
757
|
+
options: [
|
|
758
|
+
{ value: "feishu", label: "\u98DE\u4E66" },
|
|
759
|
+
{ value: "lark", label: "Lark" }
|
|
760
|
+
]
|
|
761
|
+
});
|
|
762
|
+
if (p.isCancel(tenantAnswer)) return p.cancel("\u5DF2\u53D6\u6D88");
|
|
763
|
+
const tenant = tenantAnswer;
|
|
764
|
+
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");
|
|
765
|
+
const registration = await registerApp({
|
|
766
|
+
...registrationDomains,
|
|
767
|
+
source: "lark-coding-assistant",
|
|
768
|
+
appPreset: {
|
|
769
|
+
name: "Coding Assistant",
|
|
770
|
+
desc: "Bridge local coding-agent tmux sessions to private chat"
|
|
771
|
+
},
|
|
772
|
+
addons: {
|
|
773
|
+
scopes: { tenant: ["im:message", "im:message:send_as_bot"] },
|
|
774
|
+
events: { items: { tenant: ["im.message.receive_v1"] } },
|
|
775
|
+
callbacks: { items: ["card.action.trigger"] }
|
|
776
|
+
},
|
|
777
|
+
onQRCodeReady: ({ url, expireIn }) => {
|
|
778
|
+
qrcode.generate(url, { small: true }, (output) => console.log(output));
|
|
779
|
+
console.log(`\u4E8C\u7EF4\u7801 ${Math.ceil(expireIn / 60)} \u5206\u949F\u5185\u6709\u6548\u3002\u82E5\u7EC8\u7AEF\u65E0\u6CD5\u626B\u7801\uFF0C\u8BF7\u6253\u5F00\uFF1A
|
|
780
|
+
${url}
|
|
781
|
+
`);
|
|
782
|
+
},
|
|
783
|
+
onStatusChange: ({ status }) => {
|
|
784
|
+
if (status === "domain_switched") p.log.info("\u5DF2\u8BC6\u522B\u8D26\u53F7\u57DF\uFF0C\u7B49\u5F85\u5B8C\u6210\u5E94\u7528\u786E\u8BA4\u2026");
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
const registeredTenant = registration.user_info?.tenant_brand ?? tenant;
|
|
788
|
+
const config = {
|
|
789
|
+
tenant: registeredTenant,
|
|
790
|
+
appId: registration.client_id,
|
|
791
|
+
tmuxBinary: "tmux",
|
|
792
|
+
agentBinaries: { codex: "codex", "trae-cli": "trae-cli", "claude-code": "claude" },
|
|
793
|
+
pollIntervalMs: 650
|
|
794
|
+
};
|
|
795
|
+
const previousState = await store.loadState();
|
|
796
|
+
const registeredOwnerOpenId = registration.user_info?.open_id;
|
|
797
|
+
const ownerChanged = Boolean(
|
|
798
|
+
registeredOwnerOpenId && previousState.ownerOpenId && registeredOwnerOpenId !== previousState.ownerOpenId
|
|
799
|
+
);
|
|
800
|
+
await Promise.all([
|
|
801
|
+
store.saveConfig(config),
|
|
802
|
+
store.saveSecrets({
|
|
803
|
+
appSecret: registration.client_secret,
|
|
804
|
+
callbackSecret: randomBytes2(32).toString("base64url")
|
|
805
|
+
}),
|
|
806
|
+
store.saveState({
|
|
807
|
+
...previousState,
|
|
808
|
+
ownerOpenId: registeredOwnerOpenId ?? previousState.ownerOpenId,
|
|
809
|
+
boundChatId: ownerChanged ? void 0 : previousState.boundChatId,
|
|
810
|
+
autoBindDisabled: false,
|
|
811
|
+
updatedAt: Date.now()
|
|
812
|
+
})
|
|
813
|
+
]);
|
|
814
|
+
p.outro("PersonalAgent \u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002");
|
|
815
|
+
}
|
|
816
|
+
async function runStart(options) {
|
|
817
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
818
|
+
const resume = resolveResumeOption(options);
|
|
819
|
+
await access(cwd);
|
|
820
|
+
await ensureInitialized();
|
|
821
|
+
await preflight(options.agent);
|
|
822
|
+
await ensureDaemon();
|
|
823
|
+
const response = await requestDaemon(paths.socket, {
|
|
824
|
+
method: "start",
|
|
825
|
+
cwd,
|
|
826
|
+
sessionId: options.name,
|
|
827
|
+
agent: options.agent,
|
|
828
|
+
resume
|
|
829
|
+
});
|
|
830
|
+
if (!response.ok) throw new Error(response.error);
|
|
831
|
+
const value = response.value;
|
|
832
|
+
if (value.binding.mode === "reused") {
|
|
833
|
+
console.log("\n\u5DF2\u81EA\u52A8\u6CBF\u7528\u539F\u6709\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u3002\n");
|
|
834
|
+
} else if (value.binding.mode === "awaiting-owner-message") {
|
|
835
|
+
console.log("\n\u6253\u5F00\u98DE\u4E66/Lark \u79C1\u804A\u5E76\u76F4\u63A5\u53D1\u9001\u6D88\u606F\u5373\u53EF\u81EA\u52A8\u8FDE\u63A5\uFF0C\u65E0\u9700\u7ED1\u5B9A\u7801\u3002\n");
|
|
836
|
+
} else {
|
|
837
|
+
console.log(`
|
|
838
|
+
\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u547D\u4EE4\uFF08${value.binding.expiresInSeconds / 60} \u5206\u949F\u6709\u6548\uFF09\uFF1A`);
|
|
839
|
+
console.log(` /attach ${value.binding.bindCode}
|
|
840
|
+
`);
|
|
841
|
+
}
|
|
842
|
+
if (!value.active) {
|
|
843
|
+
console.log(`\u98DE\u4E66\u5F53\u524D\u4ECD\u8FDE\u63A5\u5176\u5B83 session\uFF1B\u53D1\u9001 /use ${options.name} \u540E\u5207\u6362\u5230\u672C session\u3002
|
|
844
|
+
`);
|
|
845
|
+
}
|
|
846
|
+
await attachLocal(options.name);
|
|
847
|
+
}
|
|
848
|
+
async function runAttach(name) {
|
|
849
|
+
await attachLocal(name);
|
|
850
|
+
}
|
|
851
|
+
async function runBindCode() {
|
|
852
|
+
await ensureInitialized();
|
|
853
|
+
await ensureDaemon();
|
|
854
|
+
const response = await requestDaemon(paths.socket, { method: "bindCode" });
|
|
855
|
+
if (!response.ok) throw new Error(response.error);
|
|
856
|
+
const value = response.value;
|
|
857
|
+
console.log(`\u98DE\u4E66/Lark \u79C1\u804A\u7ED1\u5B9A\u547D\u4EE4\uFF08${value.expiresInSeconds / 60} \u5206\u949F\u6709\u6548\uFF09\uFF1A`);
|
|
858
|
+
console.log(`/attach ${value.bindCode}`);
|
|
859
|
+
}
|
|
860
|
+
async function attachLocal(name) {
|
|
861
|
+
const state = await store.loadState();
|
|
862
|
+
const config = await store.loadConfig();
|
|
863
|
+
const session = state.sessions?.[name];
|
|
864
|
+
if (!session) throw new Error(`no managed session: ${name}`);
|
|
865
|
+
if (!config) throw new Error("not initialized");
|
|
866
|
+
const child = spawn3(config.tmuxBinary, ["attach-session", "-t", `=${session.sessionName}`], { stdio: "inherit" });
|
|
867
|
+
const code = await new Promise((resolveExit, reject) => {
|
|
868
|
+
child.once("error", reject);
|
|
869
|
+
child.once("exit", resolveExit);
|
|
870
|
+
});
|
|
871
|
+
if (code && code !== 0) process.exitCode = code;
|
|
872
|
+
}
|
|
873
|
+
async function runStatus(name) {
|
|
874
|
+
try {
|
|
875
|
+
const response = await requestDaemon(paths.socket, { method: "status", sessionId: name });
|
|
876
|
+
if (!response.ok) throw new Error(response.error);
|
|
877
|
+
console.log(JSON.stringify(response.value, null, 2));
|
|
878
|
+
} catch {
|
|
879
|
+
const state = await store.loadState();
|
|
880
|
+
console.log(JSON.stringify({ daemon: "stopped", state }, null, 2));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function runStop(name) {
|
|
884
|
+
const response = await requestDaemon(paths.socket, { method: "stop", sessionId: name });
|
|
885
|
+
if (!response.ok) throw new Error(response.error);
|
|
886
|
+
console.log(`Coding-agent tmux session stopped${name ? `: ${name}` : "."}`);
|
|
887
|
+
}
|
|
888
|
+
async function runResetOwner() {
|
|
889
|
+
const response = await requestDaemon(paths.socket, { method: "resetOwner" });
|
|
890
|
+
if (!response.ok) throw new Error(response.error);
|
|
891
|
+
console.log("Owner cleared.");
|
|
892
|
+
}
|
|
893
|
+
async function runLogs(options) {
|
|
894
|
+
const count = Math.max(1, Math.min(1e3, Number.parseInt(options.lines, 10) || 100));
|
|
895
|
+
const content = await readFile3(paths.logFile, "utf8").catch(() => "");
|
|
896
|
+
console.log(content.split("\n").slice(-count).join("\n"));
|
|
897
|
+
}
|
|
898
|
+
async function runDaemonStart() {
|
|
899
|
+
await ensureInitialized();
|
|
900
|
+
const before = await daemonInfo(paths);
|
|
901
|
+
if (before?.version === packageInfo.version) {
|
|
902
|
+
console.log(`Bridge daemon is already running (PID ${before.pid}, version ${before.version}).`);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (before) await stopDaemonProcess(paths);
|
|
906
|
+
const info = await startDaemonProcess(paths, daemonEntryPath());
|
|
907
|
+
console.log(`Bridge daemon started (PID ${info.pid}, version ${info.version}).`);
|
|
908
|
+
}
|
|
909
|
+
async function runDaemonStop() {
|
|
910
|
+
const stopped = await stopDaemonProcess(paths);
|
|
911
|
+
console.log(stopped ? "Bridge daemon stopped. Coding-agent/tmux sessions were preserved." : "Bridge daemon is not running.");
|
|
912
|
+
}
|
|
913
|
+
async function runDaemonRestart() {
|
|
914
|
+
await ensureInitialized();
|
|
915
|
+
await stopDaemonProcess(paths);
|
|
916
|
+
const info = await startDaemonProcess(paths, daemonEntryPath());
|
|
917
|
+
console.log(`Bridge daemon restarted (PID ${info.pid}, version ${info.version}). Coding-agent/tmux sessions were preserved.`);
|
|
918
|
+
}
|
|
919
|
+
async function runDaemonStatus() {
|
|
920
|
+
const info = await daemonInfo(paths);
|
|
921
|
+
if (!info) {
|
|
922
|
+
console.log(`Bridge daemon: stopped
|
|
923
|
+
CLI version: ${packageInfo.version}`);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
console.log([
|
|
927
|
+
"Bridge daemon: running",
|
|
928
|
+
`PID: ${info.pid}`,
|
|
929
|
+
`Daemon version: ${info.version}`,
|
|
930
|
+
`CLI version: ${packageInfo.version}`,
|
|
931
|
+
`Up to date: ${info.version === packageInfo.version ? "yes" : "no"}`
|
|
932
|
+
].join("\n"));
|
|
933
|
+
}
|
|
934
|
+
async function ensureInitialized() {
|
|
935
|
+
if (!await store.loadConfig() || !await store.loadSecrets()) {
|
|
936
|
+
throw new Error("not initialized; run lark-coding-assistant init first");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function preflight(agentId) {
|
|
940
|
+
const config = await store.loadConfig();
|
|
941
|
+
const adapter = getAgentAdapter(agentId);
|
|
942
|
+
await Promise.all([
|
|
943
|
+
runFile(config.tmuxBinary, ["-V"]),
|
|
944
|
+
runFile(adapter.binary(config), [...adapter.versionArgs])
|
|
945
|
+
]);
|
|
946
|
+
}
|
|
947
|
+
function parseAgentId(value) {
|
|
948
|
+
if (!isAgentId(value)) throw new Error(`unsupported coding agent: ${value}`);
|
|
949
|
+
return value;
|
|
950
|
+
}
|
|
951
|
+
async function ensureDaemon() {
|
|
952
|
+
const info = await daemonInfo(paths);
|
|
953
|
+
if (info?.version === packageInfo.version) return;
|
|
954
|
+
if (info) {
|
|
955
|
+
console.log(`\u68C0\u6D4B\u5230 bridge daemon \u7248\u672C ${info.version}\uFF0C\u6B63\u5728\u66F4\u65B0\u5230 ${packageInfo.version}\u2026`);
|
|
956
|
+
await stopDaemonProcess(paths);
|
|
957
|
+
}
|
|
958
|
+
await startDaemonProcess(paths, daemonEntryPath());
|
|
959
|
+
}
|
|
960
|
+
function daemonEntryPath() {
|
|
961
|
+
return fileURLToPath(new URL("./daemon-entry.js", import.meta.url));
|
|
962
|
+
}
|
|
963
|
+
//# sourceMappingURL=cli.js.map
|