@mobvibe/cli 0.1.6 → 0.1.8
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 +41 -19
- package/bin/mobvibe.mjs +1 -1
- package/dist/acp/__tests__/acp-connection.test.d.ts +1 -0
- package/dist/acp/__tests__/session-manager.test.d.ts +1 -0
- package/dist/acp/acp-connection.d.ts +98 -0
- package/dist/acp/session-manager.d.ts +178 -0
- package/dist/auth/credentials.d.ts +42 -0
- package/dist/auth/login.d.ts +18 -0
- package/dist/config-loader.d.ts +7 -0
- package/dist/config.d.ts +40 -0
- package/dist/daemon/daemon.d.ts +27 -0
- package/dist/daemon/socket-client.d.ts +36 -0
- package/dist/e2ee/__tests__/crypto-service.test.d.ts +1 -0
- package/dist/e2ee/crypto-service.d.ts +33 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +2329 -892
- package/dist/index.js.map +25 -1
- package/dist/lib/__tests__/git-utils.test.d.ts +1 -0
- package/dist/lib/git-utils.d.ts +32 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/wal/__tests__/wal-store.test.d.ts +1 -0
- package/dist/wal/compactor.d.ts +59 -0
- package/dist/wal/index.d.ts +6 -0
- package/dist/wal/migrations.d.ts +2 -0
- package/dist/wal/seq-generator.d.ts +29 -0
- package/dist/wal/wal-store.d.ts +150 -0
- package/package.json +14 -12
package/dist/index.js
CHANGED
|
@@ -1,47 +1,10 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
1
4
|
// src/index.ts
|
|
5
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
2
6
|
import { Command } from "commander";
|
|
3
7
|
|
|
4
|
-
// src/auth/login.ts
|
|
5
|
-
import * as readline from "readline/promises";
|
|
6
|
-
|
|
7
|
-
// src/lib/logger.ts
|
|
8
|
-
import pino from "pino";
|
|
9
|
-
var LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
|
|
10
|
-
var isPretty = process.env.NODE_ENV !== "production";
|
|
11
|
-
var redact = {
|
|
12
|
-
paths: [
|
|
13
|
-
"req.headers.authorization",
|
|
14
|
-
"req.headers.cookie",
|
|
15
|
-
"req.headers['x-api-key']",
|
|
16
|
-
"headers.authorization",
|
|
17
|
-
"headers.cookie",
|
|
18
|
-
"headers['x-api-key']",
|
|
19
|
-
"apiKey",
|
|
20
|
-
"token"
|
|
21
|
-
],
|
|
22
|
-
censor: "[redacted]"
|
|
23
|
-
};
|
|
24
|
-
var transport = isPretty ? {
|
|
25
|
-
target: "pino-pretty",
|
|
26
|
-
options: {
|
|
27
|
-
colorize: true,
|
|
28
|
-
translateTime: "SYS:standard",
|
|
29
|
-
ignore: "pid,hostname"
|
|
30
|
-
}
|
|
31
|
-
} : void 0;
|
|
32
|
-
var logger = pino(
|
|
33
|
-
{
|
|
34
|
-
level: LOG_LEVEL,
|
|
35
|
-
redact,
|
|
36
|
-
base: { service: "mobvibe-cli" },
|
|
37
|
-
serializers: {
|
|
38
|
-
err: pino.stdSerializers.err,
|
|
39
|
-
error: pino.stdSerializers.err
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
transport ? pino.transport(transport) : void 0
|
|
43
|
-
);
|
|
44
|
-
|
|
45
8
|
// src/auth/credentials.ts
|
|
46
9
|
import fs from "fs/promises";
|
|
47
10
|
import os from "os";
|
|
@@ -55,7 +18,7 @@ async function loadCredentials() {
|
|
|
55
18
|
try {
|
|
56
19
|
const data = await fs.readFile(CREDENTIALS_FILE, "utf8");
|
|
57
20
|
const credentials = JSON.parse(data);
|
|
58
|
-
if (!credentials.
|
|
21
|
+
if (!credentials.masterSecret) {
|
|
59
22
|
return null;
|
|
60
23
|
}
|
|
61
24
|
return credentials;
|
|
@@ -65,25 +28,19 @@ async function loadCredentials() {
|
|
|
65
28
|
}
|
|
66
29
|
async function saveCredentials(credentials) {
|
|
67
30
|
await ensureMobvibeDir();
|
|
68
|
-
await fs.writeFile(
|
|
69
|
-
CREDENTIALS_FILE,
|
|
70
|
-
JSON.stringify(credentials, null, 2),
|
|
71
|
-
{ mode: 384 }
|
|
72
|
-
// Read/write only for owner
|
|
73
|
-
);
|
|
31
|
+
await fs.writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
74
32
|
}
|
|
75
33
|
async function deleteCredentials() {
|
|
76
34
|
try {
|
|
77
35
|
await fs.unlink(CREDENTIALS_FILE);
|
|
78
|
-
} catch {
|
|
79
|
-
}
|
|
36
|
+
} catch {}
|
|
80
37
|
}
|
|
81
|
-
async function
|
|
82
|
-
if (process.env.
|
|
83
|
-
return process.env.
|
|
38
|
+
async function getMasterSecret() {
|
|
39
|
+
if (process.env.MOBVIBE_MASTER_SECRET) {
|
|
40
|
+
return process.env.MOBVIBE_MASTER_SECRET;
|
|
84
41
|
}
|
|
85
42
|
const credentials = await loadCredentials();
|
|
86
|
-
return credentials?.
|
|
43
|
+
return credentials?.masterSecret;
|
|
87
44
|
}
|
|
88
45
|
var DEFAULT_GATEWAY_URL = "https://mobvibe.zeabur.app";
|
|
89
46
|
async function getGatewayUrl() {
|
|
@@ -98,37 +55,195 @@ async function getGatewayUrl() {
|
|
|
98
55
|
}
|
|
99
56
|
|
|
100
57
|
// src/auth/login.ts
|
|
58
|
+
import os2 from "os";
|
|
59
|
+
import * as readline from "readline/promises";
|
|
60
|
+
import { Writable } from "stream";
|
|
61
|
+
import {
|
|
62
|
+
deriveAuthKeyPair,
|
|
63
|
+
generateMasterSecret,
|
|
64
|
+
getSodium,
|
|
65
|
+
initCrypto
|
|
66
|
+
} from "@mobvibe/shared";
|
|
67
|
+
|
|
68
|
+
// src/lib/logger.ts
|
|
69
|
+
import pino from "pino";
|
|
70
|
+
var LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
|
|
71
|
+
var isPretty = true;
|
|
72
|
+
var redact = {
|
|
73
|
+
paths: [
|
|
74
|
+
"req.headers.authorization",
|
|
75
|
+
"req.headers.cookie",
|
|
76
|
+
"req.headers['x-api-key']",
|
|
77
|
+
"headers.authorization",
|
|
78
|
+
"headers.cookie",
|
|
79
|
+
"headers['x-api-key']",
|
|
80
|
+
"apiKey",
|
|
81
|
+
"token"
|
|
82
|
+
],
|
|
83
|
+
censor: "[redacted]"
|
|
84
|
+
};
|
|
85
|
+
var transport = isPretty ? {
|
|
86
|
+
target: "pino-pretty",
|
|
87
|
+
options: {
|
|
88
|
+
colorize: true,
|
|
89
|
+
translateTime: "SYS:standard",
|
|
90
|
+
ignore: "pid,hostname"
|
|
91
|
+
}
|
|
92
|
+
} : undefined;
|
|
93
|
+
var logger = pino({
|
|
94
|
+
level: LOG_LEVEL,
|
|
95
|
+
redact,
|
|
96
|
+
base: { service: "mobvibe-cli" },
|
|
97
|
+
serializers: {
|
|
98
|
+
err: pino.stdSerializers.err,
|
|
99
|
+
error: pino.stdSerializers.err
|
|
100
|
+
}
|
|
101
|
+
}, transport ? pino.transport(transport) : undefined);
|
|
102
|
+
|
|
103
|
+
// src/auth/login.ts
|
|
104
|
+
function readPassword(prompt) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
process.stdout.write(prompt);
|
|
107
|
+
const chars = [];
|
|
108
|
+
if (!process.stdin.isTTY) {
|
|
109
|
+
const rl = readline.createInterface({
|
|
110
|
+
input: process.stdin,
|
|
111
|
+
output: new Writable({ write: (_c, _e, cb) => cb() })
|
|
112
|
+
});
|
|
113
|
+
rl.question("").then((answer) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
process.stdout.write(`
|
|
116
|
+
`);
|
|
117
|
+
resolve(answer);
|
|
118
|
+
}, reject);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
process.stdin.setRawMode(true);
|
|
122
|
+
process.stdin.resume();
|
|
123
|
+
process.stdin.setEncoding("utf8");
|
|
124
|
+
const onData = (key) => {
|
|
125
|
+
for (const ch of key) {
|
|
126
|
+
const code = ch.charCodeAt(0);
|
|
127
|
+
if (ch === "\r" || ch === `
|
|
128
|
+
`) {
|
|
129
|
+
process.stdin.setRawMode(false);
|
|
130
|
+
process.stdin.pause();
|
|
131
|
+
process.stdin.removeListener("data", onData);
|
|
132
|
+
process.stdout.write(`
|
|
133
|
+
`);
|
|
134
|
+
resolve(chars.join(""));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (code === 3) {
|
|
138
|
+
process.stdin.setRawMode(false);
|
|
139
|
+
process.stdin.pause();
|
|
140
|
+
process.stdin.removeListener("data", onData);
|
|
141
|
+
process.stdout.write(`
|
|
142
|
+
`);
|
|
143
|
+
reject(new Error("User cancelled"));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (code === 127 || code === 8) {
|
|
147
|
+
if (chars.length > 0) {
|
|
148
|
+
chars.pop();
|
|
149
|
+
process.stdout.write("\b \b");
|
|
150
|
+
}
|
|
151
|
+
} else if (code >= 32) {
|
|
152
|
+
chars.push(ch);
|
|
153
|
+
process.stdout.write("*");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
process.stdin.on("data", onData);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
101
160
|
async function login() {
|
|
161
|
+
await initCrypto();
|
|
162
|
+
const sodium = getSodium();
|
|
102
163
|
logger.info("login_prompt_start");
|
|
103
|
-
console.log(
|
|
104
|
-
|
|
105
|
-
console.log(" 2. Go to Settings (gear icon) -> API Keys");
|
|
106
|
-
console.log(" 3. Click 'Create API Key' and copy it");
|
|
107
|
-
console.log(" 4. Paste the API key below\n");
|
|
164
|
+
console.log(`Mobvibe E2EE Login
|
|
165
|
+
`);
|
|
108
166
|
const rl = readline.createInterface({
|
|
109
167
|
input: process.stdin,
|
|
110
168
|
output: process.stdout
|
|
111
169
|
});
|
|
112
170
|
try {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
|
|
116
|
-
|
|
171
|
+
const email = await rl.question("Email: ");
|
|
172
|
+
if (!email.trim()) {
|
|
173
|
+
return { success: false, error: "No email provided" };
|
|
174
|
+
}
|
|
175
|
+
rl.close();
|
|
176
|
+
const password = await readPassword("Password: ");
|
|
177
|
+
if (!password.trim()) {
|
|
178
|
+
return { success: false, error: "No password provided" };
|
|
179
|
+
}
|
|
180
|
+
const gatewayUrl = await getGatewayUrl();
|
|
181
|
+
console.log(`
|
|
182
|
+
Signing in...`);
|
|
183
|
+
const signInResponse = await fetch(`${gatewayUrl}/api/auth/sign-in/email`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
email: email.trim(),
|
|
188
|
+
password: password.trim()
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
if (!signInResponse.ok) {
|
|
192
|
+
const body = await signInResponse.text();
|
|
193
|
+
logger.warn({ status: signInResponse.status, body }, "login_sign_in_failed");
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: `Sign-in failed (${signInResponse.status}): ${body}`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const setCookieHeaders = signInResponse.headers.getSetCookie?.() ?? [];
|
|
200
|
+
const cookieHeader = setCookieHeaders.map((c) => c.split(";")[0]).join("; ");
|
|
201
|
+
if (!cookieHeader) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: "No session cookie received from sign-in"
|
|
205
|
+
};
|
|
117
206
|
}
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
const masterSecret = generateMasterSecret();
|
|
208
|
+
const authKeyPair = deriveAuthKeyPair(masterSecret);
|
|
209
|
+
const publicKeyBase64 = sodium.to_base64(authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
|
|
210
|
+
console.log("Registering device...");
|
|
211
|
+
const registerResponse = await fetch(`${gatewayUrl}/auth/device/register`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
Cookie: cookieHeader
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
publicKey: publicKeyBase64,
|
|
219
|
+
deviceName: os2.hostname()
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
if (!registerResponse.ok) {
|
|
223
|
+
const body = await registerResponse.text();
|
|
224
|
+
logger.warn({ status: registerResponse.status, body }, "login_device_register_failed");
|
|
120
225
|
return {
|
|
121
226
|
success: false,
|
|
122
|
-
error:
|
|
227
|
+
error: `Device registration failed (${registerResponse.status}): ${body}`
|
|
123
228
|
};
|
|
124
229
|
}
|
|
230
|
+
const masterSecretBase64 = sodium.to_base64(masterSecret, sodium.base64_variants.ORIGINAL);
|
|
125
231
|
const credentials = {
|
|
126
|
-
|
|
232
|
+
masterSecret: masterSecretBase64,
|
|
127
233
|
createdAt: Date.now()
|
|
128
234
|
};
|
|
129
235
|
await saveCredentials(credentials);
|
|
130
236
|
logger.info("login_credentials_saved");
|
|
131
|
-
console.log(
|
|
237
|
+
console.log(`
|
|
238
|
+
Login successful!`);
|
|
239
|
+
console.log(`
|
|
240
|
+
WARNING: The master secret below will appear in your terminal history.`);
|
|
241
|
+
console.log(" Clear your terminal after copying it, or use 'mobvibe e2ee show' later.");
|
|
242
|
+
console.log(`
|
|
243
|
+
Your master secret (for pairing WebUI/Tauri devices):`);
|
|
244
|
+
console.log(` ${masterSecretBase64}`);
|
|
245
|
+
console.log(`
|
|
246
|
+
Keep this secret safe. You can view it again with 'mobvibe e2ee show'.`);
|
|
132
247
|
console.log("Run 'mobvibe start' to connect to the gateway.");
|
|
133
248
|
return { success: true };
|
|
134
249
|
} finally {
|
|
@@ -143,9 +258,14 @@ async function logout() {
|
|
|
143
258
|
async function loginStatus() {
|
|
144
259
|
const credentials = await loadCredentials();
|
|
145
260
|
if (credentials) {
|
|
261
|
+
await initCrypto();
|
|
262
|
+
const sodium = getSodium();
|
|
263
|
+
const masterSecret = sodium.from_base64(credentials.masterSecret, sodium.base64_variants.ORIGINAL);
|
|
264
|
+
const authKeyPair = deriveAuthKeyPair(masterSecret);
|
|
265
|
+
const pubKeyBase64 = sodium.to_base64(authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
|
|
146
266
|
logger.info("login_status_logged_in");
|
|
147
|
-
console.log("Status: Logged in");
|
|
148
|
-
console.log(`
|
|
267
|
+
console.log("Status: Logged in (E2EE)");
|
|
268
|
+
console.log(`Auth public key: ${pubKeyBase64.slice(0, 16)}...`);
|
|
149
269
|
console.log(`Saved: ${new Date(credentials.createdAt).toLocaleString()}`);
|
|
150
270
|
} else {
|
|
151
271
|
logger.info("login_status_logged_out");
|
|
@@ -155,7 +275,7 @@ async function loginStatus() {
|
|
|
155
275
|
}
|
|
156
276
|
|
|
157
277
|
// src/config.ts
|
|
158
|
-
import
|
|
278
|
+
import os3 from "os";
|
|
159
279
|
import path3 from "path";
|
|
160
280
|
|
|
161
281
|
// src/config-loader.ts
|
|
@@ -182,14 +302,14 @@ var validateAgentConfig = (agent, index) => {
|
|
|
182
302
|
id: record.id.trim(),
|
|
183
303
|
command: record.command.trim()
|
|
184
304
|
};
|
|
185
|
-
if (record.label !==
|
|
305
|
+
if (record.label !== undefined) {
|
|
186
306
|
if (typeof record.label !== "string") {
|
|
187
307
|
errors.push(`${prefix}.label: must be a string`);
|
|
188
308
|
} else if (record.label.trim().length > 0) {
|
|
189
309
|
validated.label = record.label.trim();
|
|
190
310
|
}
|
|
191
311
|
}
|
|
192
|
-
if (record.args !==
|
|
312
|
+
if (record.args !== undefined) {
|
|
193
313
|
if (!Array.isArray(record.args)) {
|
|
194
314
|
errors.push(`${prefix}.args: must be an array of strings`);
|
|
195
315
|
} else {
|
|
@@ -205,7 +325,7 @@ var validateAgentConfig = (agent, index) => {
|
|
|
205
325
|
}
|
|
206
326
|
}
|
|
207
327
|
}
|
|
208
|
-
if (record.env !==
|
|
328
|
+
if (record.env !== undefined) {
|
|
209
329
|
if (typeof record.env !== "object" || record.env === null) {
|
|
210
330
|
errors.push(`${prefix}.env: must be an object`);
|
|
211
331
|
} else {
|
|
@@ -238,13 +358,13 @@ var validateUserConfig = (data) => {
|
|
|
238
358
|
}
|
|
239
359
|
const record = data;
|
|
240
360
|
const config = {};
|
|
241
|
-
if (record.agents !==
|
|
361
|
+
if (record.agents !== undefined) {
|
|
242
362
|
if (!Array.isArray(record.agents)) {
|
|
243
363
|
errors.push("agents: must be an array");
|
|
244
364
|
} else {
|
|
245
365
|
const validAgents = [];
|
|
246
|
-
const seenIds =
|
|
247
|
-
for (let i = 0;
|
|
366
|
+
const seenIds = new Set;
|
|
367
|
+
for (let i = 0;i < record.agents.length; i++) {
|
|
248
368
|
const result = validateAgentConfig(record.agents[i], i);
|
|
249
369
|
errors.push(...result.errors);
|
|
250
370
|
if (result.valid) {
|
|
@@ -261,7 +381,7 @@ var validateUserConfig = (data) => {
|
|
|
261
381
|
}
|
|
262
382
|
}
|
|
263
383
|
}
|
|
264
|
-
if (record.defaultAgentId !==
|
|
384
|
+
if (record.defaultAgentId !== undefined) {
|
|
265
385
|
if (typeof record.defaultAgentId !== "string") {
|
|
266
386
|
errors.push("defaultAgentId: must be a string");
|
|
267
387
|
} else if (record.defaultAgentId.trim().length > 0) {
|
|
@@ -309,6 +429,14 @@ var loadUserConfig = async (homePath) => {
|
|
|
309
429
|
};
|
|
310
430
|
|
|
311
431
|
// src/config.ts
|
|
432
|
+
var DEFAULT_COMPACTION_CONFIG = {
|
|
433
|
+
enabled: false,
|
|
434
|
+
ackedEventRetentionDays: 7,
|
|
435
|
+
keepLatestRevisionsCount: 2,
|
|
436
|
+
runOnStartup: false,
|
|
437
|
+
runIntervalHours: 24,
|
|
438
|
+
minEventsToKeep: 1000
|
|
439
|
+
};
|
|
312
440
|
var DEFAULT_OPENCODE_BACKEND = {
|
|
313
441
|
id: "opencode",
|
|
314
442
|
label: "opencode",
|
|
@@ -316,10 +444,10 @@ var DEFAULT_OPENCODE_BACKEND = {
|
|
|
316
444
|
args: ["acp"]
|
|
317
445
|
};
|
|
318
446
|
var generateMachineId = () => {
|
|
319
|
-
const hostname =
|
|
320
|
-
const platform =
|
|
321
|
-
const arch =
|
|
322
|
-
const username =
|
|
447
|
+
const hostname = os3.hostname();
|
|
448
|
+
const platform = os3.platform();
|
|
449
|
+
const arch = os3.arch();
|
|
450
|
+
const username = os3.userInfo().username;
|
|
323
451
|
return `${hostname}-${platform}-${arch}-${username}`;
|
|
324
452
|
};
|
|
325
453
|
var userAgentToBackendConfig = (agent) => ({
|
|
@@ -331,92 +459,786 @@ var userAgentToBackendConfig = (agent) => ({
|
|
|
331
459
|
});
|
|
332
460
|
var mergeBackends = (defaultBackend, userAgents) => {
|
|
333
461
|
if (!userAgents || userAgents.length === 0) {
|
|
334
|
-
return
|
|
462
|
+
return [defaultBackend];
|
|
335
463
|
}
|
|
336
464
|
const userOpencode = userAgents.find((a) => a.id === "opencode");
|
|
337
465
|
if (userOpencode) {
|
|
338
|
-
return
|
|
339
|
-
backends: userAgents.map(userAgentToBackendConfig),
|
|
340
|
-
defaultId: userAgents[0].id
|
|
341
|
-
};
|
|
466
|
+
return userAgents.map(userAgentToBackendConfig);
|
|
342
467
|
}
|
|
343
|
-
return
|
|
344
|
-
backends: [defaultBackend, ...userAgents.map(userAgentToBackendConfig)],
|
|
345
|
-
defaultId: defaultBackend.id
|
|
346
|
-
};
|
|
468
|
+
return [defaultBackend, ...userAgents.map(userAgentToBackendConfig)];
|
|
347
469
|
};
|
|
348
470
|
var getCliConfig = async () => {
|
|
349
471
|
const env = process.env;
|
|
350
|
-
const homePath = env.MOBVIBE_HOME ?? path3.join(
|
|
472
|
+
const homePath = env.MOBVIBE_HOME ?? path3.join(os3.homedir(), ".mobvibe");
|
|
351
473
|
const userConfigResult = await loadUserConfig(homePath);
|
|
352
474
|
if (userConfigResult.errors.length > 0) {
|
|
353
475
|
for (const error of userConfigResult.errors) {
|
|
354
476
|
logger.warn({ configPath: userConfigResult.path, error }, "config_error");
|
|
355
477
|
}
|
|
356
478
|
}
|
|
357
|
-
const
|
|
358
|
-
DEFAULT_OPENCODE_BACKEND,
|
|
359
|
-
userConfigResult.config?.agents
|
|
360
|
-
);
|
|
361
|
-
const resolvedDefaultId = userConfigResult.config?.defaultAgentId && backends.some((b) => b.id === userConfigResult.config?.defaultAgentId) ? userConfigResult.config.defaultAgentId : defaultId;
|
|
479
|
+
const backends = mergeBackends(DEFAULT_OPENCODE_BACKEND, userConfigResult.config?.agents);
|
|
362
480
|
const gatewayUrl = await getGatewayUrl();
|
|
363
481
|
return {
|
|
364
482
|
gatewayUrl,
|
|
365
483
|
acpBackends: backends,
|
|
366
|
-
defaultAcpBackendId: resolvedDefaultId,
|
|
367
484
|
clientName: env.MOBVIBE_ACP_CLIENT_NAME ?? "mobvibe-cli",
|
|
368
485
|
clientVersion: env.MOBVIBE_ACP_CLIENT_VERSION ?? "0.0.0",
|
|
369
486
|
homePath,
|
|
370
487
|
logPath: path3.join(homePath, "logs"),
|
|
371
488
|
pidFile: path3.join(homePath, "daemon.pid"),
|
|
489
|
+
walDbPath: path3.join(homePath, "events.db"),
|
|
372
490
|
machineId: env.MOBVIBE_MACHINE_ID ?? generateMachineId(),
|
|
373
|
-
hostname:
|
|
374
|
-
platform:
|
|
491
|
+
hostname: os3.hostname(),
|
|
492
|
+
platform: os3.platform(),
|
|
375
493
|
userConfigPath: userConfigResult.path,
|
|
376
|
-
userConfigErrors: userConfigResult.errors.length > 0 ? userConfigResult.errors :
|
|
494
|
+
userConfigErrors: userConfigResult.errors.length > 0 ? userConfigResult.errors : undefined,
|
|
495
|
+
compaction: {
|
|
496
|
+
...DEFAULT_COMPACTION_CONFIG,
|
|
497
|
+
enabled: env.MOBVIBE_COMPACTION_ENABLED === "true"
|
|
498
|
+
}
|
|
377
499
|
};
|
|
378
500
|
};
|
|
379
501
|
|
|
380
502
|
// src/daemon/daemon.ts
|
|
503
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
381
504
|
import { spawn as spawn2 } from "child_process";
|
|
382
|
-
import
|
|
383
|
-
import
|
|
505
|
+
import fs6 from "fs/promises";
|
|
506
|
+
import path7 from "path";
|
|
507
|
+
import { getSodium as getSodium3, initCrypto as initCrypto2 } from "@mobvibe/shared";
|
|
384
508
|
|
|
385
509
|
// src/acp/session-manager.ts
|
|
386
510
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
387
511
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
388
|
-
import
|
|
512
|
+
import fs4 from "fs/promises";
|
|
513
|
+
import {
|
|
514
|
+
AppError,
|
|
515
|
+
createErrorDetail as createErrorDetail2
|
|
516
|
+
} from "@mobvibe/shared";
|
|
389
517
|
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
518
|
+
// src/wal/compactor.ts
|
|
519
|
+
class WalCompactor {
|
|
520
|
+
config;
|
|
521
|
+
db;
|
|
522
|
+
activeSessionIds = new Set;
|
|
523
|
+
stmtGetAllSessions;
|
|
524
|
+
stmtGetSessionRevisions;
|
|
525
|
+
stmtDeleteAckedEvents;
|
|
526
|
+
stmtDeleteOldRevisionEvents;
|
|
527
|
+
stmtCountEvents;
|
|
528
|
+
stmtLogCompaction;
|
|
529
|
+
constructor(_walStore, config, db) {
|
|
530
|
+
this.config = config;
|
|
531
|
+
this.db = db;
|
|
532
|
+
this.stmtGetAllSessions = this.db.query(`
|
|
533
|
+
SELECT DISTINCT session_id FROM sessions
|
|
534
|
+
`);
|
|
535
|
+
this.stmtGetSessionRevisions = this.db.query(`
|
|
536
|
+
SELECT DISTINCT revision FROM session_events
|
|
537
|
+
WHERE session_id = $sessionId
|
|
538
|
+
ORDER BY revision DESC
|
|
539
|
+
`);
|
|
540
|
+
this.stmtDeleteAckedEvents = this.db.query(`
|
|
541
|
+
DELETE FROM session_events
|
|
542
|
+
WHERE session_id = $sessionId
|
|
543
|
+
AND revision = $revision
|
|
544
|
+
AND acked_at IS NOT NULL
|
|
545
|
+
AND acked_at < $olderThan
|
|
546
|
+
AND id NOT IN (
|
|
547
|
+
SELECT id FROM session_events
|
|
548
|
+
WHERE session_id = $sessionId AND revision = $revision
|
|
549
|
+
ORDER BY seq DESC
|
|
550
|
+
LIMIT $minKeep
|
|
551
|
+
)
|
|
552
|
+
`);
|
|
553
|
+
this.stmtDeleteOldRevisionEvents = this.db.query(`
|
|
554
|
+
DELETE FROM session_events
|
|
555
|
+
WHERE session_id = $sessionId
|
|
556
|
+
AND revision = $revision
|
|
557
|
+
`);
|
|
558
|
+
this.stmtCountEvents = this.db.query(`
|
|
559
|
+
SELECT COUNT(*) as count FROM session_events
|
|
560
|
+
WHERE session_id = $sessionId AND revision = $revision
|
|
561
|
+
`);
|
|
562
|
+
this.stmtLogCompaction = this.db.query(`
|
|
563
|
+
INSERT INTO compaction_log (session_id, revision, operation, events_affected, started_at, completed_at)
|
|
564
|
+
VALUES ($sessionId, $revision, $operation, $eventsAffected, $startedAt, $completedAt)
|
|
565
|
+
`);
|
|
566
|
+
}
|
|
567
|
+
markSessionActive(sessionId) {
|
|
568
|
+
this.activeSessionIds.add(sessionId);
|
|
569
|
+
}
|
|
570
|
+
markSessionInactive(sessionId) {
|
|
571
|
+
this.activeSessionIds.delete(sessionId);
|
|
572
|
+
}
|
|
573
|
+
shouldSkipSession(sessionId) {
|
|
574
|
+
return this.activeSessionIds.has(sessionId);
|
|
575
|
+
}
|
|
576
|
+
async compactAll(options) {
|
|
577
|
+
const startTime = performance.now();
|
|
578
|
+
const stats = [];
|
|
579
|
+
const skipped = [];
|
|
580
|
+
const sessions = this.stmtGetAllSessions.all();
|
|
581
|
+
for (const { session_id: sessionId } of sessions) {
|
|
582
|
+
if (this.shouldSkipSession(sessionId)) {
|
|
583
|
+
skipped.push(sessionId);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const sessionStats = await this.compactSession(sessionId, options);
|
|
588
|
+
stats.push(sessionStats);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
logger.error({ err: error, sessionId }, "compaction_session_error");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const totalDurationMs = performance.now() - startTime;
|
|
594
|
+
const totalDeleted = stats.reduce((sum, s) => sum + s.ackedEventsDeleted + s.oldRevisionsDeleted, 0);
|
|
595
|
+
if (!options?.dryRun && totalDeleted > 0) {
|
|
596
|
+
try {
|
|
597
|
+
this.db.exec("VACUUM");
|
|
598
|
+
logger.info({ totalDeleted }, "compaction_vacuum_complete");
|
|
599
|
+
} catch (error) {
|
|
600
|
+
logger.error({ err: error }, "compaction_vacuum_error");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
logger.info({
|
|
604
|
+
sessionsCompacted: stats.length,
|
|
605
|
+
sessionsSkipped: skipped.length,
|
|
606
|
+
totalDeleted,
|
|
607
|
+
totalDurationMs
|
|
608
|
+
}, "compaction_complete");
|
|
609
|
+
return { stats, totalDurationMs, skipped };
|
|
610
|
+
}
|
|
611
|
+
async compactSession(sessionId, options) {
|
|
612
|
+
const startTime = performance.now();
|
|
613
|
+
let ackedEventsDeleted = 0;
|
|
614
|
+
let oldRevisionsDeleted = 0;
|
|
615
|
+
const revisions = this.stmtGetSessionRevisions.all({
|
|
616
|
+
$sessionId: sessionId
|
|
617
|
+
});
|
|
618
|
+
if (revisions.length === 0) {
|
|
619
|
+
return {
|
|
620
|
+
sessionId,
|
|
621
|
+
ackedEventsDeleted: 0,
|
|
622
|
+
oldRevisionsDeleted: 0,
|
|
623
|
+
durationMs: performance.now() - startTime
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const revisionsToKeep = revisions.slice(0, this.config.keepLatestRevisionsCount).map((r) => r.revision);
|
|
627
|
+
const ackedCutoff = new Date;
|
|
628
|
+
ackedCutoff.setDate(ackedCutoff.getDate() - this.config.ackedEventRetentionDays);
|
|
629
|
+
for (const { revision } of revisions) {
|
|
630
|
+
if (!revisionsToKeep.includes(revision)) {
|
|
631
|
+
const countResult = this.stmtCountEvents.get({
|
|
632
|
+
$sessionId: sessionId,
|
|
633
|
+
$revision: revision
|
|
634
|
+
});
|
|
635
|
+
if (!options?.dryRun) {
|
|
636
|
+
const result = this.stmtDeleteOldRevisionEvents.run({
|
|
637
|
+
$sessionId: sessionId,
|
|
638
|
+
$revision: revision
|
|
639
|
+
});
|
|
640
|
+
oldRevisionsDeleted += result.changes;
|
|
641
|
+
this.logCompaction(sessionId, revision, "delete_old_revision", result.changes);
|
|
642
|
+
} else {
|
|
643
|
+
oldRevisionsDeleted += countResult.count;
|
|
644
|
+
}
|
|
645
|
+
logger.debug({ sessionId, revision, count: countResult.count }, "compaction_delete_old_revision");
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (!options?.dryRun) {
|
|
649
|
+
const result = this.stmtDeleteAckedEvents.run({
|
|
650
|
+
$sessionId: sessionId,
|
|
651
|
+
$revision: revision,
|
|
652
|
+
$olderThan: ackedCutoff.toISOString(),
|
|
653
|
+
$minKeep: this.config.minEventsToKeep
|
|
654
|
+
});
|
|
655
|
+
ackedEventsDeleted += result.changes;
|
|
656
|
+
if (result.changes > 0) {
|
|
657
|
+
this.logCompaction(sessionId, revision, "delete_acked_events", result.changes);
|
|
658
|
+
logger.debug({ sessionId, revision, deleted: result.changes }, "compaction_delete_acked");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const durationMs = performance.now() - startTime;
|
|
663
|
+
if (ackedEventsDeleted > 0 || oldRevisionsDeleted > 0) {
|
|
664
|
+
logger.info({ sessionId, ackedEventsDeleted, oldRevisionsDeleted, durationMs }, "compaction_session_complete");
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
sessionId,
|
|
668
|
+
ackedEventsDeleted,
|
|
669
|
+
oldRevisionsDeleted,
|
|
670
|
+
durationMs
|
|
671
|
+
};
|
|
397
672
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
673
|
+
logCompaction(sessionId, revision, operation, eventsAffected) {
|
|
674
|
+
const now = new Date().toISOString();
|
|
675
|
+
this.stmtLogCompaction.run({
|
|
676
|
+
$sessionId: sessionId,
|
|
677
|
+
$revision: revision,
|
|
678
|
+
$operation: operation,
|
|
679
|
+
$eventsAffected: eventsAffected,
|
|
680
|
+
$startedAt: now,
|
|
681
|
+
$completedAt: now
|
|
682
|
+
});
|
|
407
683
|
}
|
|
408
|
-
}
|
|
684
|
+
}
|
|
685
|
+
// src/wal/migrations.ts
|
|
686
|
+
var MIGRATIONS = [
|
|
687
|
+
{
|
|
688
|
+
version: 1,
|
|
689
|
+
up: `
|
|
690
|
+
-- Sessions table to track session metadata and current revision
|
|
691
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
692
|
+
session_id TEXT PRIMARY KEY,
|
|
693
|
+
machine_id TEXT NOT NULL,
|
|
694
|
+
backend_id TEXT NOT NULL,
|
|
695
|
+
current_revision INTEGER NOT NULL DEFAULT 1,
|
|
696
|
+
cwd TEXT,
|
|
697
|
+
title TEXT,
|
|
698
|
+
created_at TEXT NOT NULL,
|
|
699
|
+
updated_at TEXT NOT NULL
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
-- Session events WAL table
|
|
703
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
704
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
705
|
+
session_id TEXT NOT NULL,
|
|
706
|
+
revision INTEGER NOT NULL,
|
|
707
|
+
seq INTEGER NOT NULL,
|
|
708
|
+
kind TEXT NOT NULL,
|
|
709
|
+
payload TEXT NOT NULL,
|
|
710
|
+
created_at TEXT NOT NULL,
|
|
711
|
+
acked_at TEXT,
|
|
712
|
+
UNIQUE (session_id, revision, seq)
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
-- Index for querying events by session and revision
|
|
716
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session_revision
|
|
717
|
+
ON session_events (session_id, revision, seq);
|
|
718
|
+
|
|
719
|
+
-- Index for querying unacked events
|
|
720
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_unacked
|
|
721
|
+
ON session_events (session_id, revision, acked_at)
|
|
722
|
+
WHERE acked_at IS NULL;
|
|
409
723
|
|
|
724
|
+
-- Schema version tracking
|
|
725
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
726
|
+
version INTEGER PRIMARY KEY
|
|
727
|
+
);
|
|
728
|
+
`
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
version: 2,
|
|
732
|
+
up: `
|
|
733
|
+
-- Discovered sessions table for persisting sessions found via discoverSessions()
|
|
734
|
+
CREATE TABLE IF NOT EXISTS discovered_sessions (
|
|
735
|
+
session_id TEXT PRIMARY KEY,
|
|
736
|
+
backend_id TEXT NOT NULL,
|
|
737
|
+
cwd TEXT,
|
|
738
|
+
title TEXT,
|
|
739
|
+
agent_updated_at TEXT, -- agent-reported update time
|
|
740
|
+
discovered_at TEXT NOT NULL,
|
|
741
|
+
last_verified_at TEXT, -- last time cwd was verified to exist
|
|
742
|
+
is_stale INTEGER DEFAULT 0 -- marked stale when cwd no longer exists
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
CREATE INDEX IF NOT EXISTS idx_discovered_sessions_backend
|
|
746
|
+
ON discovered_sessions (backend_id);
|
|
747
|
+
|
|
748
|
+
-- Add agent_updated_at to sessions table
|
|
749
|
+
ALTER TABLE sessions ADD COLUMN agent_updated_at TEXT;
|
|
750
|
+
`
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
version: 3,
|
|
754
|
+
up: `
|
|
755
|
+
-- Compaction support
|
|
756
|
+
ALTER TABLE session_events ADD COLUMN compacted_at TEXT;
|
|
757
|
+
|
|
758
|
+
-- Index for finding acked events eligible for cleanup
|
|
759
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_acked_at
|
|
760
|
+
ON session_events (session_id, revision, acked_at)
|
|
761
|
+
WHERE acked_at IS NOT NULL;
|
|
762
|
+
|
|
763
|
+
-- Index for finding events by kind (for chunk consolidation)
|
|
764
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_kind
|
|
765
|
+
ON session_events (session_id, revision, kind);
|
|
766
|
+
|
|
767
|
+
-- Compaction operation log
|
|
768
|
+
CREATE TABLE IF NOT EXISTS compaction_log (
|
|
769
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
770
|
+
session_id TEXT NOT NULL,
|
|
771
|
+
revision INTEGER,
|
|
772
|
+
operation TEXT NOT NULL,
|
|
773
|
+
events_affected INTEGER NOT NULL,
|
|
774
|
+
started_at TEXT NOT NULL,
|
|
775
|
+
completed_at TEXT
|
|
776
|
+
);
|
|
777
|
+
`
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
version: 4,
|
|
781
|
+
up: `
|
|
782
|
+
-- Archived sessions table (local archive state)
|
|
783
|
+
CREATE TABLE IF NOT EXISTS archived_session_ids (
|
|
784
|
+
session_id TEXT PRIMARY KEY,
|
|
785
|
+
archived_at TEXT NOT NULL
|
|
786
|
+
);
|
|
787
|
+
`
|
|
788
|
+
}
|
|
789
|
+
];
|
|
790
|
+
function runMigrations(db) {
|
|
791
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
792
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
793
|
+
let currentVersion = 0;
|
|
794
|
+
try {
|
|
795
|
+
const result = db.query("SELECT MAX(version) as version FROM schema_version").get();
|
|
796
|
+
currentVersion = result?.version ?? 0;
|
|
797
|
+
} catch {}
|
|
798
|
+
for (const migration of MIGRATIONS) {
|
|
799
|
+
if (migration.version > currentVersion) {
|
|
800
|
+
db.exec(migration.up);
|
|
801
|
+
db.exec(`INSERT INTO schema_version (version) VALUES (${migration.version})`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// src/wal/seq-generator.ts
|
|
806
|
+
class SeqGenerator {
|
|
807
|
+
sequences = new Map;
|
|
808
|
+
buildKey(sessionId, revision) {
|
|
809
|
+
return `${sessionId}:${revision}`;
|
|
810
|
+
}
|
|
811
|
+
initialize(sessionId, revision, lastSeq) {
|
|
812
|
+
const key = this.buildKey(sessionId, revision);
|
|
813
|
+
this.sequences.set(key, lastSeq);
|
|
814
|
+
}
|
|
815
|
+
next(sessionId, revision) {
|
|
816
|
+
const key = this.buildKey(sessionId, revision);
|
|
817
|
+
const current = this.sequences.get(key) ?? 0;
|
|
818
|
+
const next = current + 1;
|
|
819
|
+
this.sequences.set(key, next);
|
|
820
|
+
return next;
|
|
821
|
+
}
|
|
822
|
+
current(sessionId, revision) {
|
|
823
|
+
const key = this.buildKey(sessionId, revision);
|
|
824
|
+
return this.sequences.get(key) ?? 0;
|
|
825
|
+
}
|
|
826
|
+
reset(sessionId, revision) {
|
|
827
|
+
const key = this.buildKey(sessionId, revision);
|
|
828
|
+
this.sequences.set(key, 0);
|
|
829
|
+
}
|
|
830
|
+
clearSession(sessionId) {
|
|
831
|
+
for (const key of this.sequences.keys()) {
|
|
832
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
833
|
+
this.sequences.delete(key);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// src/wal/wal-store.ts
|
|
839
|
+
import { Database } from "bun:sqlite";
|
|
840
|
+
import fs3 from "fs";
|
|
841
|
+
import path4 from "path";
|
|
842
|
+
var DEFAULT_QUERY_LIMIT = 100;
|
|
843
|
+
|
|
844
|
+
class WalStore {
|
|
845
|
+
db;
|
|
846
|
+
seqGenerator = new SeqGenerator;
|
|
847
|
+
stmtGetSession;
|
|
848
|
+
stmtInsertSession;
|
|
849
|
+
stmtUpdateSession;
|
|
850
|
+
stmtInsertEvent;
|
|
851
|
+
stmtQueryEvents;
|
|
852
|
+
stmtQueryUnackedEvents;
|
|
853
|
+
stmtAckEvents;
|
|
854
|
+
stmtIncrementRevision;
|
|
855
|
+
stmtGetMaxSeq;
|
|
856
|
+
stmtUpsertDiscoveredSession;
|
|
857
|
+
stmtGetDiscoveredSessions;
|
|
858
|
+
stmtGetDiscoveredSessionsByBackend;
|
|
859
|
+
stmtMarkDiscoveredSessionStale;
|
|
860
|
+
stmtDeleteStaleDiscoveredSessions;
|
|
861
|
+
stmtDeleteSessionEvents;
|
|
862
|
+
stmtDeleteSession;
|
|
863
|
+
stmtInsertArchivedSession;
|
|
864
|
+
stmtIsArchived;
|
|
865
|
+
stmtGetArchivedSessionIds;
|
|
866
|
+
constructor(dbPath) {
|
|
867
|
+
const dir = path4.dirname(dbPath);
|
|
868
|
+
if (!fs3.existsSync(dir)) {
|
|
869
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
870
|
+
}
|
|
871
|
+
this.db = new Database(dbPath);
|
|
872
|
+
runMigrations(this.db);
|
|
873
|
+
this.stmtGetSession = this.db.query(`
|
|
874
|
+
SELECT session_id, machine_id, backend_id, current_revision, cwd, title, created_at, updated_at
|
|
875
|
+
FROM sessions
|
|
876
|
+
WHERE session_id = $sessionId
|
|
877
|
+
`);
|
|
878
|
+
this.stmtInsertSession = this.db.query(`
|
|
879
|
+
INSERT INTO sessions (session_id, machine_id, backend_id, current_revision, cwd, title, created_at, updated_at)
|
|
880
|
+
VALUES ($sessionId, $machineId, $backendId, 1, $cwd, $title, $createdAt, $updatedAt)
|
|
881
|
+
`);
|
|
882
|
+
this.stmtUpdateSession = this.db.query(`
|
|
883
|
+
UPDATE sessions
|
|
884
|
+
SET cwd = COALESCE($cwd, cwd),
|
|
885
|
+
title = COALESCE($title, title),
|
|
886
|
+
updated_at = $updatedAt
|
|
887
|
+
WHERE session_id = $sessionId
|
|
888
|
+
`);
|
|
889
|
+
this.stmtInsertEvent = this.db.query(`
|
|
890
|
+
INSERT INTO session_events (session_id, revision, seq, kind, payload, created_at)
|
|
891
|
+
VALUES ($sessionId, $revision, $seq, $kind, $payload, $createdAt)
|
|
892
|
+
`);
|
|
893
|
+
this.stmtQueryEvents = this.db.query(`
|
|
894
|
+
SELECT id, session_id, revision, seq, kind, payload, created_at, acked_at
|
|
895
|
+
FROM session_events
|
|
896
|
+
WHERE session_id = $sessionId
|
|
897
|
+
AND revision = $revision
|
|
898
|
+
AND seq > $afterSeq
|
|
899
|
+
ORDER BY seq ASC
|
|
900
|
+
LIMIT $limit
|
|
901
|
+
`);
|
|
902
|
+
this.stmtQueryUnackedEvents = this.db.query(`
|
|
903
|
+
SELECT id, session_id, revision, seq, kind, payload, created_at, acked_at
|
|
904
|
+
FROM session_events
|
|
905
|
+
WHERE session_id = $sessionId
|
|
906
|
+
AND revision = $revision
|
|
907
|
+
AND acked_at IS NULL
|
|
908
|
+
ORDER BY seq ASC
|
|
909
|
+
`);
|
|
910
|
+
this.stmtAckEvents = this.db.query(`
|
|
911
|
+
UPDATE session_events
|
|
912
|
+
SET acked_at = $ackedAt
|
|
913
|
+
WHERE session_id = $sessionId
|
|
914
|
+
AND revision = $revision
|
|
915
|
+
AND seq <= $upToSeq
|
|
916
|
+
AND acked_at IS NULL
|
|
917
|
+
`);
|
|
918
|
+
this.stmtIncrementRevision = this.db.query(`
|
|
919
|
+
UPDATE sessions
|
|
920
|
+
SET current_revision = current_revision + 1,
|
|
921
|
+
updated_at = $updatedAt
|
|
922
|
+
WHERE session_id = $sessionId
|
|
923
|
+
RETURNING current_revision
|
|
924
|
+
`);
|
|
925
|
+
this.stmtGetMaxSeq = this.db.query(`
|
|
926
|
+
SELECT MAX(seq) as max_seq
|
|
927
|
+
FROM session_events
|
|
928
|
+
WHERE session_id = $sessionId AND revision = $revision
|
|
929
|
+
`);
|
|
930
|
+
this.stmtUpsertDiscoveredSession = this.db.query(`
|
|
931
|
+
INSERT INTO discovered_sessions (
|
|
932
|
+
session_id, backend_id, cwd, title, agent_updated_at,
|
|
933
|
+
discovered_at, last_verified_at, is_stale
|
|
934
|
+
) VALUES (
|
|
935
|
+
$sessionId, $backendId, $cwd, $title, $agentUpdatedAt,
|
|
936
|
+
$discoveredAt, $lastVerifiedAt, 0
|
|
937
|
+
)
|
|
938
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
939
|
+
backend_id = $backendId,
|
|
940
|
+
cwd = COALESCE($cwd, discovered_sessions.cwd),
|
|
941
|
+
title = COALESCE($title, discovered_sessions.title),
|
|
942
|
+
agent_updated_at = COALESCE($agentUpdatedAt, discovered_sessions.agent_updated_at),
|
|
943
|
+
last_verified_at = $lastVerifiedAt,
|
|
944
|
+
is_stale = 0
|
|
945
|
+
`);
|
|
946
|
+
this.stmtGetDiscoveredSessions = this.db.query(`
|
|
947
|
+
SELECT d.session_id, d.backend_id, d.cwd, d.title, d.agent_updated_at,
|
|
948
|
+
d.discovered_at, d.last_verified_at, d.is_stale
|
|
949
|
+
FROM discovered_sessions d
|
|
950
|
+
LEFT JOIN archived_session_ids a ON d.session_id = a.session_id
|
|
951
|
+
WHERE d.is_stale = 0 AND a.session_id IS NULL
|
|
952
|
+
ORDER BY d.discovered_at DESC
|
|
953
|
+
`);
|
|
954
|
+
this.stmtGetDiscoveredSessionsByBackend = this.db.query(`
|
|
955
|
+
SELECT d.session_id, d.backend_id, d.cwd, d.title, d.agent_updated_at,
|
|
956
|
+
d.discovered_at, d.last_verified_at, d.is_stale
|
|
957
|
+
FROM discovered_sessions d
|
|
958
|
+
LEFT JOIN archived_session_ids a ON d.session_id = a.session_id
|
|
959
|
+
WHERE d.backend_id = $backendId AND d.is_stale = 0 AND a.session_id IS NULL
|
|
960
|
+
ORDER BY d.discovered_at DESC
|
|
961
|
+
`);
|
|
962
|
+
this.stmtMarkDiscoveredSessionStale = this.db.query(`
|
|
963
|
+
UPDATE discovered_sessions
|
|
964
|
+
SET is_stale = 1
|
|
965
|
+
WHERE session_id = $sessionId
|
|
966
|
+
`);
|
|
967
|
+
this.stmtDeleteStaleDiscoveredSessions = this.db.query(`
|
|
968
|
+
DELETE FROM discovered_sessions
|
|
969
|
+
WHERE is_stale = 1 AND discovered_at < $olderThan
|
|
970
|
+
`);
|
|
971
|
+
this.stmtDeleteSessionEvents = this.db.query(`
|
|
972
|
+
DELETE FROM session_events WHERE session_id = $sessionId
|
|
973
|
+
`);
|
|
974
|
+
this.stmtDeleteSession = this.db.query(`
|
|
975
|
+
DELETE FROM sessions WHERE session_id = $sessionId
|
|
976
|
+
`);
|
|
977
|
+
this.stmtInsertArchivedSession = this.db.query(`
|
|
978
|
+
INSERT OR IGNORE INTO archived_session_ids (session_id, archived_at)
|
|
979
|
+
VALUES ($sessionId, $archivedAt)
|
|
980
|
+
`);
|
|
981
|
+
this.stmtIsArchived = this.db.query(`
|
|
982
|
+
SELECT 1 FROM archived_session_ids WHERE session_id = $sessionId
|
|
983
|
+
`);
|
|
984
|
+
this.stmtGetArchivedSessionIds = this.db.query(`
|
|
985
|
+
SELECT session_id FROM archived_session_ids
|
|
986
|
+
`);
|
|
987
|
+
}
|
|
988
|
+
ensureSession(params) {
|
|
989
|
+
const now = new Date().toISOString();
|
|
990
|
+
const existing = this.stmtGetSession.get({
|
|
991
|
+
$sessionId: params.sessionId
|
|
992
|
+
});
|
|
993
|
+
logger.debug({ sessionId: params.sessionId, exists: !!existing }, "wal_ensure_session");
|
|
994
|
+
if (existing) {
|
|
995
|
+
this.stmtUpdateSession.run({
|
|
996
|
+
$sessionId: params.sessionId,
|
|
997
|
+
$cwd: params.cwd ?? null,
|
|
998
|
+
$title: params.title ?? null,
|
|
999
|
+
$updatedAt: now
|
|
1000
|
+
});
|
|
1001
|
+
const maxSeq = this.getMaxSeq(params.sessionId, existing.current_revision);
|
|
1002
|
+
this.seqGenerator.initialize(params.sessionId, existing.current_revision, maxSeq);
|
|
1003
|
+
logger.debug({
|
|
1004
|
+
sessionId: params.sessionId,
|
|
1005
|
+
revision: existing.current_revision,
|
|
1006
|
+
maxSeq
|
|
1007
|
+
}, "wal_session_existing");
|
|
1008
|
+
return { revision: existing.current_revision };
|
|
1009
|
+
}
|
|
1010
|
+
this.stmtInsertSession.run({
|
|
1011
|
+
$sessionId: params.sessionId,
|
|
1012
|
+
$machineId: params.machineId,
|
|
1013
|
+
$backendId: params.backendId,
|
|
1014
|
+
$cwd: params.cwd ?? null,
|
|
1015
|
+
$title: params.title ?? null,
|
|
1016
|
+
$createdAt: now,
|
|
1017
|
+
$updatedAt: now
|
|
1018
|
+
});
|
|
1019
|
+
this.seqGenerator.initialize(params.sessionId, 1, 0);
|
|
1020
|
+
logger.info({ sessionId: params.sessionId, revision: 1 }, "wal_session_created");
|
|
1021
|
+
return { revision: 1 };
|
|
1022
|
+
}
|
|
1023
|
+
getSession(sessionId) {
|
|
1024
|
+
const row = this.stmtGetSession.get({
|
|
1025
|
+
$sessionId: sessionId
|
|
1026
|
+
});
|
|
1027
|
+
if (!row)
|
|
1028
|
+
return null;
|
|
1029
|
+
return this.rowToSession(row);
|
|
1030
|
+
}
|
|
1031
|
+
appendEvent(params) {
|
|
1032
|
+
const seq = this.seqGenerator.next(params.sessionId, params.revision);
|
|
1033
|
+
const now = new Date().toISOString();
|
|
1034
|
+
logger.debug({
|
|
1035
|
+
sessionId: params.sessionId,
|
|
1036
|
+
revision: params.revision,
|
|
1037
|
+
seq,
|
|
1038
|
+
kind: params.kind
|
|
1039
|
+
}, "wal_append_event");
|
|
1040
|
+
this.stmtInsertEvent.run({
|
|
1041
|
+
$sessionId: params.sessionId,
|
|
1042
|
+
$revision: params.revision,
|
|
1043
|
+
$seq: seq,
|
|
1044
|
+
$kind: params.kind,
|
|
1045
|
+
$payload: JSON.stringify(params.payload),
|
|
1046
|
+
$createdAt: now
|
|
1047
|
+
});
|
|
1048
|
+
const lastId = this.db.query("SELECT last_insert_rowid() as id").get();
|
|
1049
|
+
logger.info({
|
|
1050
|
+
sessionId: params.sessionId,
|
|
1051
|
+
revision: params.revision,
|
|
1052
|
+
seq,
|
|
1053
|
+
kind: params.kind,
|
|
1054
|
+
eventId: lastId.id
|
|
1055
|
+
}, "wal_event_appended");
|
|
1056
|
+
return {
|
|
1057
|
+
id: lastId.id,
|
|
1058
|
+
sessionId: params.sessionId,
|
|
1059
|
+
revision: params.revision,
|
|
1060
|
+
seq,
|
|
1061
|
+
kind: params.kind,
|
|
1062
|
+
payload: params.payload,
|
|
1063
|
+
createdAt: now
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
queryEvents(params) {
|
|
1067
|
+
logger.debug({
|
|
1068
|
+
sessionId: params.sessionId,
|
|
1069
|
+
revision: params.revision,
|
|
1070
|
+
afterSeq: params.afterSeq ?? 0,
|
|
1071
|
+
limit: params.limit ?? DEFAULT_QUERY_LIMIT
|
|
1072
|
+
}, "wal_query_events");
|
|
1073
|
+
const rows = this.stmtQueryEvents.all({
|
|
1074
|
+
$sessionId: params.sessionId,
|
|
1075
|
+
$revision: params.revision,
|
|
1076
|
+
$afterSeq: params.afterSeq ?? 0,
|
|
1077
|
+
$limit: params.limit ?? DEFAULT_QUERY_LIMIT
|
|
1078
|
+
});
|
|
1079
|
+
logger.debug({
|
|
1080
|
+
sessionId: params.sessionId,
|
|
1081
|
+
revision: params.revision,
|
|
1082
|
+
count: rows.length,
|
|
1083
|
+
seqRange: rows.length > 0 ? `${rows[0].seq}-${rows[rows.length - 1].seq}` : "empty"
|
|
1084
|
+
}, "wal_query_events_result");
|
|
1085
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1086
|
+
}
|
|
1087
|
+
getUnackedEvents(sessionId, revision) {
|
|
1088
|
+
const rows = this.stmtQueryUnackedEvents.all({
|
|
1089
|
+
$sessionId: sessionId,
|
|
1090
|
+
$revision: revision
|
|
1091
|
+
});
|
|
1092
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
1093
|
+
}
|
|
1094
|
+
ackEvents(sessionId, revision, upToSeq) {
|
|
1095
|
+
logger.debug({ sessionId, revision, upToSeq }, "wal_ack_events");
|
|
1096
|
+
const result = this.stmtAckEvents.run({
|
|
1097
|
+
$sessionId: sessionId,
|
|
1098
|
+
$revision: revision,
|
|
1099
|
+
$upToSeq: upToSeq,
|
|
1100
|
+
$ackedAt: new Date().toISOString()
|
|
1101
|
+
});
|
|
1102
|
+
logger.debug({ sessionId, revision, upToSeq, changes: result.changes }, "wal_ack_events_result");
|
|
1103
|
+
}
|
|
1104
|
+
incrementRevision(sessionId) {
|
|
1105
|
+
logger.info({ sessionId }, "wal_increment_revision");
|
|
1106
|
+
const result = this.stmtIncrementRevision.get({
|
|
1107
|
+
$sessionId: sessionId,
|
|
1108
|
+
$updatedAt: new Date().toISOString()
|
|
1109
|
+
});
|
|
1110
|
+
if (!result) {
|
|
1111
|
+
logger.error({ sessionId }, "wal_increment_revision_session_not_found");
|
|
1112
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1113
|
+
}
|
|
1114
|
+
this.seqGenerator.reset(sessionId, result.current_revision);
|
|
1115
|
+
logger.info({ sessionId, newRevision: result.current_revision }, "wal_revision_incremented");
|
|
1116
|
+
return result.current_revision;
|
|
1117
|
+
}
|
|
1118
|
+
getCurrentSeq(sessionId, revision) {
|
|
1119
|
+
return this.seqGenerator.current(sessionId, revision);
|
|
1120
|
+
}
|
|
1121
|
+
saveDiscoveredSessions(sessions) {
|
|
1122
|
+
const now = new Date().toISOString();
|
|
1123
|
+
for (const session of sessions) {
|
|
1124
|
+
this.stmtUpsertDiscoveredSession.run({
|
|
1125
|
+
$sessionId: session.sessionId,
|
|
1126
|
+
$backendId: session.backendId,
|
|
1127
|
+
$cwd: session.cwd ?? null,
|
|
1128
|
+
$title: session.title ?? null,
|
|
1129
|
+
$agentUpdatedAt: session.agentUpdatedAt ?? null,
|
|
1130
|
+
$discoveredAt: session.discoveredAt,
|
|
1131
|
+
$lastVerifiedAt: now
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
getDiscoveredSessions(backendId) {
|
|
1136
|
+
let rows;
|
|
1137
|
+
if (backendId) {
|
|
1138
|
+
rows = this.stmtGetDiscoveredSessionsByBackend.all({
|
|
1139
|
+
$backendId: backendId
|
|
1140
|
+
});
|
|
1141
|
+
} else {
|
|
1142
|
+
rows = this.stmtGetDiscoveredSessions.all();
|
|
1143
|
+
}
|
|
1144
|
+
return rows.map((row) => this.rowToDiscoveredSession(row));
|
|
1145
|
+
}
|
|
1146
|
+
markDiscoveredSessionStale(sessionId) {
|
|
1147
|
+
this.stmtMarkDiscoveredSessionStale.run({
|
|
1148
|
+
$sessionId: sessionId
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
deleteStaleDiscoveredSessions(olderThan) {
|
|
1152
|
+
const result = this.stmtDeleteStaleDiscoveredSessions.run({
|
|
1153
|
+
$olderThan: olderThan.toISOString()
|
|
1154
|
+
});
|
|
1155
|
+
return result.changes;
|
|
1156
|
+
}
|
|
1157
|
+
archiveSession(sessionId) {
|
|
1158
|
+
this.stmtDeleteSessionEvents.run({ $sessionId: sessionId });
|
|
1159
|
+
this.stmtDeleteSession.run({ $sessionId: sessionId });
|
|
1160
|
+
this.stmtInsertArchivedSession.run({
|
|
1161
|
+
$sessionId: sessionId,
|
|
1162
|
+
$archivedAt: new Date().toISOString()
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
bulkArchiveSessions(sessionIds) {
|
|
1166
|
+
let count = 0;
|
|
1167
|
+
for (const sessionId of sessionIds) {
|
|
1168
|
+
this.archiveSession(sessionId);
|
|
1169
|
+
count++;
|
|
1170
|
+
}
|
|
1171
|
+
return count;
|
|
1172
|
+
}
|
|
1173
|
+
isArchived(sessionId) {
|
|
1174
|
+
const row = this.stmtIsArchived.get({ $sessionId: sessionId });
|
|
1175
|
+
return row !== null;
|
|
1176
|
+
}
|
|
1177
|
+
getArchivedSessionIds() {
|
|
1178
|
+
const rows = this.stmtGetArchivedSessionIds.all();
|
|
1179
|
+
return rows.map((r) => r.session_id);
|
|
1180
|
+
}
|
|
1181
|
+
close() {
|
|
1182
|
+
this.db.close();
|
|
1183
|
+
}
|
|
1184
|
+
getMaxSeq(sessionId, revision) {
|
|
1185
|
+
const result = this.stmtGetMaxSeq.get({
|
|
1186
|
+
$sessionId: sessionId,
|
|
1187
|
+
$revision: revision
|
|
1188
|
+
});
|
|
1189
|
+
return result?.max_seq ?? 0;
|
|
1190
|
+
}
|
|
1191
|
+
rowToSession(row) {
|
|
1192
|
+
return {
|
|
1193
|
+
sessionId: row.session_id,
|
|
1194
|
+
machineId: row.machine_id,
|
|
1195
|
+
backendId: row.backend_id,
|
|
1196
|
+
currentRevision: row.current_revision,
|
|
1197
|
+
cwd: row.cwd ?? undefined,
|
|
1198
|
+
title: row.title ?? undefined,
|
|
1199
|
+
createdAt: row.created_at,
|
|
1200
|
+
updatedAt: row.updated_at
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
rowToEvent(row) {
|
|
1204
|
+
return {
|
|
1205
|
+
id: row.id,
|
|
1206
|
+
sessionId: row.session_id,
|
|
1207
|
+
revision: row.revision,
|
|
1208
|
+
seq: row.seq,
|
|
1209
|
+
kind: row.kind,
|
|
1210
|
+
payload: JSON.parse(row.payload),
|
|
1211
|
+
createdAt: row.created_at,
|
|
1212
|
+
ackedAt: row.acked_at ?? undefined
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
rowToDiscoveredSession(row) {
|
|
1216
|
+
return {
|
|
1217
|
+
sessionId: row.session_id,
|
|
1218
|
+
backendId: row.backend_id,
|
|
1219
|
+
cwd: row.cwd ?? undefined,
|
|
1220
|
+
title: row.title ?? undefined,
|
|
1221
|
+
agentUpdatedAt: row.agent_updated_at ?? undefined,
|
|
1222
|
+
discoveredAt: row.discovered_at,
|
|
1223
|
+
lastVerifiedAt: row.last_verified_at ?? undefined,
|
|
1224
|
+
isStale: row.is_stale === 1
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
410
1228
|
// src/acp/acp-connection.ts
|
|
411
1229
|
import { spawn } from "child_process";
|
|
412
1230
|
import { randomUUID } from "crypto";
|
|
413
1231
|
import { EventEmitter } from "events";
|
|
414
|
-
import { Readable, Writable } from "stream";
|
|
1232
|
+
import { Readable, Writable as Writable2 } from "stream";
|
|
415
1233
|
import {
|
|
416
1234
|
ClientSideConnection,
|
|
417
1235
|
ndJsonStream,
|
|
418
1236
|
PROTOCOL_VERSION
|
|
419
1237
|
} from "@agentclientprotocol/sdk";
|
|
1238
|
+
import {
|
|
1239
|
+
createErrorDetail,
|
|
1240
|
+
isProtocolMismatch
|
|
1241
|
+
} from "@mobvibe/shared";
|
|
420
1242
|
var getErrorMessage = (error) => {
|
|
421
1243
|
if (error instanceof Error) {
|
|
422
1244
|
return error.message;
|
|
@@ -520,10 +1342,9 @@ var sliceOutputToLimit = (value, limit) => {
|
|
|
520
1342
|
}
|
|
521
1343
|
return sliced.subarray(start).toString("utf8");
|
|
522
1344
|
};
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
1345
|
+
|
|
1346
|
+
class AcpConnection {
|
|
1347
|
+
options;
|
|
527
1348
|
connection;
|
|
528
1349
|
process;
|
|
529
1350
|
closedPromise;
|
|
@@ -533,11 +1354,14 @@ var AcpConnection = class {
|
|
|
533
1354
|
sessionId;
|
|
534
1355
|
agentInfo;
|
|
535
1356
|
agentCapabilities;
|
|
536
|
-
sessionUpdateEmitter = new EventEmitter
|
|
537
|
-
statusEmitter = new EventEmitter
|
|
538
|
-
terminalOutputEmitter = new EventEmitter
|
|
1357
|
+
sessionUpdateEmitter = new EventEmitter;
|
|
1358
|
+
statusEmitter = new EventEmitter;
|
|
1359
|
+
terminalOutputEmitter = new EventEmitter;
|
|
539
1360
|
permissionHandler;
|
|
540
|
-
terminals =
|
|
1361
|
+
terminals = new Map;
|
|
1362
|
+
constructor(options) {
|
|
1363
|
+
this.options = options;
|
|
1364
|
+
}
|
|
541
1365
|
getStatus() {
|
|
542
1366
|
return {
|
|
543
1367
|
backendId: this.options.backend.id,
|
|
@@ -554,52 +1378,32 @@ var AcpConnection = class {
|
|
|
554
1378
|
getAgentInfo() {
|
|
555
1379
|
return this.agentInfo;
|
|
556
1380
|
}
|
|
557
|
-
/**
|
|
558
|
-
* Get the agent's session capabilities.
|
|
559
|
-
*/
|
|
560
1381
|
getSessionCapabilities() {
|
|
561
1382
|
return {
|
|
562
1383
|
list: this.agentCapabilities?.sessionCapabilities?.list != null,
|
|
563
1384
|
load: this.agentCapabilities?.loadSession === true
|
|
564
1385
|
};
|
|
565
1386
|
}
|
|
566
|
-
/**
|
|
567
|
-
* Check if the agent supports session/list.
|
|
568
|
-
*/
|
|
569
1387
|
supportsSessionList() {
|
|
570
1388
|
return this.agentCapabilities?.sessionCapabilities?.list != null;
|
|
571
1389
|
}
|
|
572
|
-
/**
|
|
573
|
-
* Check if the agent supports session/load.
|
|
574
|
-
*/
|
|
575
1390
|
supportsSessionLoad() {
|
|
576
1391
|
return this.agentCapabilities?.loadSession === true;
|
|
577
1392
|
}
|
|
578
|
-
/**
|
|
579
|
-
* List sessions from the agent (session/list).
|
|
580
|
-
* @param params Optional filter parameters
|
|
581
|
-
* @returns List of session info from the agent
|
|
582
|
-
*/
|
|
583
1393
|
async listSessions(params) {
|
|
584
1394
|
if (!this.supportsSessionList()) {
|
|
585
1395
|
return { sessions: [] };
|
|
586
1396
|
}
|
|
587
1397
|
const connection = await this.ensureReady();
|
|
588
1398
|
const response = await connection.unstable_listSessions({
|
|
589
|
-
cursor: params?.cursor ??
|
|
590
|
-
cwd: params?.cwd ??
|
|
1399
|
+
cursor: params?.cursor ?? undefined,
|
|
1400
|
+
cwd: params?.cwd ?? undefined
|
|
591
1401
|
});
|
|
592
1402
|
return {
|
|
593
1403
|
sessions: response.sessions,
|
|
594
|
-
nextCursor: response.nextCursor ??
|
|
1404
|
+
nextCursor: response.nextCursor ?? undefined
|
|
595
1405
|
};
|
|
596
1406
|
}
|
|
597
|
-
/**
|
|
598
|
-
* Load a historical session with message history replay (session/load).
|
|
599
|
-
* @param sessionId The session ID to load
|
|
600
|
-
* @param cwd The working directory
|
|
601
|
-
* @returns Load session response with modes/models state
|
|
602
|
-
*/
|
|
603
1407
|
async loadSession(sessionId, cwd) {
|
|
604
1408
|
if (!this.supportsSessionLoad()) {
|
|
605
1409
|
throw new Error("Agent does not support session/load capability");
|
|
@@ -644,35 +1448,28 @@ var AcpConnection = class {
|
|
|
644
1448
|
return;
|
|
645
1449
|
}
|
|
646
1450
|
this.updateStatus("connecting");
|
|
647
|
-
this.agentInfo =
|
|
1451
|
+
this.agentInfo = undefined;
|
|
648
1452
|
try {
|
|
649
1453
|
const env = this.options.backend.envOverrides ? { ...process.env, ...this.options.backend.envOverrides } : process.env;
|
|
650
|
-
const child = spawn(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
655
|
-
env
|
|
656
|
-
}
|
|
657
|
-
);
|
|
1454
|
+
const child = spawn(this.options.backend.command, this.options.backend.args, {
|
|
1455
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1456
|
+
env
|
|
1457
|
+
});
|
|
658
1458
|
this.process = child;
|
|
659
|
-
this.sessionId =
|
|
1459
|
+
this.sessionId = undefined;
|
|
660
1460
|
child.stderr.pipe(process.stderr);
|
|
661
|
-
const input =
|
|
1461
|
+
const input = Writable2.toWeb(child.stdin);
|
|
662
1462
|
const output = Readable.toWeb(child.stdout);
|
|
663
1463
|
const stream = ndJsonStream(input, output);
|
|
664
|
-
const connection = new ClientSideConnection(
|
|
665
|
-
() =>
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}),
|
|
674
|
-
stream
|
|
675
|
-
);
|
|
1464
|
+
const connection = new ClientSideConnection(() => buildClient({
|
|
1465
|
+
onSessionUpdate: (notification) => this.emitSessionUpdate(notification),
|
|
1466
|
+
onRequestPermission: (params) => this.handlePermissionRequest(params),
|
|
1467
|
+
onCreateTerminal: (params) => this.createTerminal(params),
|
|
1468
|
+
onTerminalOutput: (params) => this.getTerminalOutput(params),
|
|
1469
|
+
onWaitForTerminalExit: (params) => this.waitForTerminalExit(params),
|
|
1470
|
+
onKillTerminal: (params) => this.killTerminal(params),
|
|
1471
|
+
onReleaseTerminal: (params) => this.releaseTerminal(params)
|
|
1472
|
+
}), stream);
|
|
676
1473
|
this.connection = connection;
|
|
677
1474
|
child.once("error", (error) => {
|
|
678
1475
|
if (this.state === "stopped") {
|
|
@@ -684,16 +1481,10 @@ var AcpConnection = class {
|
|
|
684
1481
|
if (this.state === "stopped") {
|
|
685
1482
|
return;
|
|
686
1483
|
}
|
|
687
|
-
this.updateStatus(
|
|
688
|
-
"error",
|
|
689
|
-
buildProcessExitError(formatExitMessage(code, signal))
|
|
690
|
-
);
|
|
1484
|
+
this.updateStatus("error", buildProcessExitError(formatExitMessage(code, signal)));
|
|
691
1485
|
});
|
|
692
1486
|
this.closedPromise = connection.closed.catch((error) => {
|
|
693
|
-
this.updateStatus(
|
|
694
|
-
"error",
|
|
695
|
-
buildConnectionClosedError(getErrorMessage(error))
|
|
696
|
-
);
|
|
1487
|
+
this.updateStatus("error", buildConnectionClosedError(getErrorMessage(error)));
|
|
697
1488
|
});
|
|
698
1489
|
const initializeResponse = await connection.initialize({
|
|
699
1490
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -703,9 +1494,9 @@ var AcpConnection = class {
|
|
|
703
1494
|
},
|
|
704
1495
|
clientCapabilities: { terminal: true }
|
|
705
1496
|
});
|
|
706
|
-
this.agentInfo = initializeResponse.agentInfo ??
|
|
707
|
-
this.agentCapabilities = initializeResponse.agentCapabilities ??
|
|
708
|
-
this.connectedAt =
|
|
1497
|
+
this.agentInfo = initializeResponse.agentInfo ?? undefined;
|
|
1498
|
+
this.agentCapabilities = initializeResponse.agentCapabilities ?? undefined;
|
|
1499
|
+
this.connectedAt = new Date;
|
|
709
1500
|
this.updateStatus("ready");
|
|
710
1501
|
} catch (error) {
|
|
711
1502
|
this.updateStatus("error", buildConnectError(error));
|
|
@@ -715,10 +1506,7 @@ var AcpConnection = class {
|
|
|
715
1506
|
}
|
|
716
1507
|
async createSession(options) {
|
|
717
1508
|
const connection = await this.ensureReady();
|
|
718
|
-
const response = await this.createSessionInternal(
|
|
719
|
-
connection,
|
|
720
|
-
options?.cwd ?? process.cwd()
|
|
721
|
-
);
|
|
1509
|
+
const response = await this.createSessionInternal(connection, options?.cwd ?? process.cwd());
|
|
722
1510
|
this.sessionId = response.sessionId;
|
|
723
1511
|
return response;
|
|
724
1512
|
}
|
|
@@ -740,9 +1528,7 @@ var AcpConnection = class {
|
|
|
740
1528
|
}
|
|
741
1529
|
async createTerminal(params) {
|
|
742
1530
|
const outputLimit = typeof params.outputByteLimit === "number" && params.outputByteLimit > 0 ? Math.floor(params.outputByteLimit) : 1024 * 1024;
|
|
743
|
-
const resolvedEnv = params.env ? Object.fromEntries(
|
|
744
|
-
params.env.map((envVar) => [envVar.name, envVar.value])
|
|
745
|
-
) : void 0;
|
|
1531
|
+
const resolvedEnv = params.env ? Object.fromEntries(params.env.map((envVar) => [envVar.name, envVar.value])) : undefined;
|
|
746
1532
|
const terminalId = randomUUID();
|
|
747
1533
|
const record = {
|
|
748
1534
|
sessionId: params.sessionId,
|
|
@@ -757,7 +1543,7 @@ var AcpConnection = class {
|
|
|
757
1543
|
};
|
|
758
1544
|
this.terminals.set(terminalId, record);
|
|
759
1545
|
const child = spawn(params.command, params.args ?? [], {
|
|
760
|
-
cwd: params.cwd ??
|
|
1546
|
+
cwd: params.cwd ?? undefined,
|
|
761
1547
|
env: resolvedEnv ? { ...process.env, ...resolvedEnv } : process.env
|
|
762
1548
|
});
|
|
763
1549
|
child.once("error", (error) => {
|
|
@@ -777,8 +1563,7 @@ var AcpConnection = class {
|
|
|
777
1563
|
});
|
|
778
1564
|
});
|
|
779
1565
|
record.process = child;
|
|
780
|
-
let resolveExit = () => {
|
|
781
|
-
};
|
|
1566
|
+
let resolveExit = () => {};
|
|
782
1567
|
record.onExit = new Promise((resolve) => {
|
|
783
1568
|
resolveExit = resolve;
|
|
784
1569
|
});
|
|
@@ -789,20 +1574,14 @@ var AcpConnection = class {
|
|
|
789
1574
|
return;
|
|
790
1575
|
}
|
|
791
1576
|
const combinedOutput = record.output.output + delta;
|
|
792
|
-
record.output.truncated = isOutputOverLimit(
|
|
793
|
-
|
|
794
|
-
record.outputByteLimit
|
|
795
|
-
);
|
|
796
|
-
record.output.output = sliceOutputToLimit(
|
|
797
|
-
combinedOutput,
|
|
798
|
-
record.outputByteLimit
|
|
799
|
-
);
|
|
1577
|
+
record.output.truncated = isOutputOverLimit(combinedOutput, record.outputByteLimit);
|
|
1578
|
+
record.output.output = sliceOutputToLimit(combinedOutput, record.outputByteLimit);
|
|
800
1579
|
this.terminalOutputEmitter.emit("output", {
|
|
801
1580
|
sessionId: record.sessionId,
|
|
802
1581
|
terminalId,
|
|
803
1582
|
delta,
|
|
804
1583
|
truncated: record.output.truncated,
|
|
805
|
-
output: record.output.truncated ? record.output.output :
|
|
1584
|
+
output: record.output.truncated ? record.output.output : undefined,
|
|
806
1585
|
exitStatus: record.output.exitStatus
|
|
807
1586
|
});
|
|
808
1587
|
};
|
|
@@ -863,11 +1642,11 @@ var AcpConnection = class {
|
|
|
863
1642
|
return;
|
|
864
1643
|
}
|
|
865
1644
|
this.updateStatus("stopped");
|
|
866
|
-
this.sessionId =
|
|
867
|
-
this.agentInfo =
|
|
1645
|
+
this.sessionId = undefined;
|
|
1646
|
+
this.agentInfo = undefined;
|
|
868
1647
|
await this.stopProcess();
|
|
869
1648
|
await this.closedPromise;
|
|
870
|
-
this.connection =
|
|
1649
|
+
this.connection = undefined;
|
|
871
1650
|
}
|
|
872
1651
|
async ensureReady() {
|
|
873
1652
|
if (this.state !== "ready" || !this.connection) {
|
|
@@ -899,46 +1678,42 @@ var AcpConnection = class {
|
|
|
899
1678
|
if (!child) {
|
|
900
1679
|
return;
|
|
901
1680
|
}
|
|
902
|
-
this.process =
|
|
1681
|
+
this.process = undefined;
|
|
903
1682
|
if (child.exitCode === null && !child.killed) {
|
|
904
1683
|
child.kill("SIGTERM");
|
|
905
1684
|
}
|
|
906
1685
|
}
|
|
907
|
-
}
|
|
1686
|
+
}
|
|
908
1687
|
|
|
909
1688
|
// src/acp/session-manager.ts
|
|
910
1689
|
var buildPermissionKey = (sessionId, requestId) => `${sessionId}:${requestId}`;
|
|
911
1690
|
var resolveModelState = (models) => {
|
|
912
1691
|
if (!models) {
|
|
913
1692
|
return {
|
|
914
|
-
modelId:
|
|
915
|
-
modelName:
|
|
916
|
-
availableModels:
|
|
1693
|
+
modelId: undefined,
|
|
1694
|
+
modelName: undefined,
|
|
1695
|
+
availableModels: undefined
|
|
917
1696
|
};
|
|
918
1697
|
}
|
|
919
1698
|
const availableModels = models.availableModels?.map((model) => ({
|
|
920
1699
|
id: model.modelId,
|
|
921
1700
|
name: model.name,
|
|
922
|
-
description: model.description ??
|
|
1701
|
+
description: model.description ?? undefined
|
|
923
1702
|
}));
|
|
924
|
-
const modelId = models.currentModelId ??
|
|
925
|
-
const modelName = availableModels?.find(
|
|
926
|
-
(model) => model.id === modelId
|
|
927
|
-
)?.name;
|
|
1703
|
+
const modelId = models.currentModelId ?? undefined;
|
|
1704
|
+
const modelName = availableModels?.find((model) => model.id === modelId)?.name;
|
|
928
1705
|
return { modelId, modelName, availableModels };
|
|
929
1706
|
};
|
|
930
1707
|
var resolveModeState = (modes) => {
|
|
931
1708
|
if (!modes) {
|
|
932
1709
|
return {
|
|
933
|
-
modeId:
|
|
934
|
-
modeName:
|
|
935
|
-
availableModes:
|
|
1710
|
+
modeId: undefined,
|
|
1711
|
+
modeName: undefined,
|
|
1712
|
+
availableModes: undefined
|
|
936
1713
|
};
|
|
937
1714
|
}
|
|
938
|
-
const modeId = modes.currentModeId ??
|
|
939
|
-
const modeName = modes.availableModes?.find(
|
|
940
|
-
(mode) => mode.id === modeId
|
|
941
|
-
)?.name;
|
|
1715
|
+
const modeId = modes.currentModeId ?? undefined;
|
|
1716
|
+
const modeName = modes.availableModes?.find((mode) => mode.id === modeId)?.name;
|
|
942
1717
|
return {
|
|
943
1718
|
modeId,
|
|
944
1719
|
modeName,
|
|
@@ -948,63 +1723,87 @@ var resolveModeState = (modes) => {
|
|
|
948
1723
|
}))
|
|
949
1724
|
};
|
|
950
1725
|
};
|
|
951
|
-
var createCapabilityNotSupportedError = (message) => new AppError(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}),
|
|
958
|
-
409
|
|
959
|
-
);
|
|
1726
|
+
var createCapabilityNotSupportedError = (message) => new AppError(createErrorDetail2({
|
|
1727
|
+
code: "CAPABILITY_NOT_SUPPORTED",
|
|
1728
|
+
message,
|
|
1729
|
+
retryable: false,
|
|
1730
|
+
scope: "session"
|
|
1731
|
+
}), 409);
|
|
960
1732
|
var isValidWorkspacePath = async (cwd) => {
|
|
961
1733
|
try {
|
|
962
|
-
const stats = await
|
|
1734
|
+
const stats = await fs4.stat(cwd);
|
|
963
1735
|
return stats.isDirectory();
|
|
964
1736
|
} catch {
|
|
965
1737
|
return false;
|
|
966
1738
|
}
|
|
967
1739
|
};
|
|
968
|
-
|
|
969
|
-
|
|
1740
|
+
|
|
1741
|
+
class SessionManager {
|
|
1742
|
+
config;
|
|
1743
|
+
sessions = new Map;
|
|
1744
|
+
discoveredSessions = new Map;
|
|
1745
|
+
backendById;
|
|
1746
|
+
permissionRequests = new Map;
|
|
1747
|
+
permissionRequestEmitter = new EventEmitter2;
|
|
1748
|
+
permissionResultEmitter = new EventEmitter2;
|
|
1749
|
+
sessionsChangedEmitter = new EventEmitter2;
|
|
1750
|
+
sessionAttachedEmitter = new EventEmitter2;
|
|
1751
|
+
sessionDetachedEmitter = new EventEmitter2;
|
|
1752
|
+
sessionEventEmitter = new EventEmitter2;
|
|
1753
|
+
walStore;
|
|
1754
|
+
cryptoService;
|
|
1755
|
+
constructor(config, cryptoService) {
|
|
970
1756
|
this.config = config;
|
|
971
|
-
this.backendById = new Map(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1757
|
+
this.backendById = new Map(config.acpBackends.map((backend) => [backend.id, backend]));
|
|
1758
|
+
this.walStore = new WalStore(config.walDbPath);
|
|
1759
|
+
this.cryptoService = cryptoService;
|
|
1760
|
+
}
|
|
1761
|
+
createConnection(backend) {
|
|
1762
|
+
return new AcpConnection({
|
|
1763
|
+
backend,
|
|
1764
|
+
client: {
|
|
1765
|
+
name: this.config.clientName,
|
|
1766
|
+
version: this.config.clientVersion
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
975
1769
|
}
|
|
976
|
-
sessions = /* @__PURE__ */ new Map();
|
|
977
|
-
discoveredSessions = /* @__PURE__ */ new Map();
|
|
978
|
-
backendById;
|
|
979
|
-
defaultBackendId;
|
|
980
|
-
permissionRequests = /* @__PURE__ */ new Map();
|
|
981
|
-
sessionUpdateEmitter = new EventEmitter2();
|
|
982
|
-
sessionErrorEmitter = new EventEmitter2();
|
|
983
|
-
permissionRequestEmitter = new EventEmitter2();
|
|
984
|
-
permissionResultEmitter = new EventEmitter2();
|
|
985
|
-
terminalOutputEmitter = new EventEmitter2();
|
|
986
|
-
sessionsChangedEmitter = new EventEmitter2();
|
|
987
|
-
sessionAttachedEmitter = new EventEmitter2();
|
|
988
|
-
sessionDetachedEmitter = new EventEmitter2();
|
|
989
1770
|
listSessions() {
|
|
990
|
-
return Array.from(this.sessions.values()).map(
|
|
991
|
-
|
|
992
|
-
|
|
1771
|
+
return Array.from(this.sessions.values()).map((record) => this.buildSummary(record));
|
|
1772
|
+
}
|
|
1773
|
+
listAllSessions() {
|
|
1774
|
+
const active = this.listSessions();
|
|
1775
|
+
const merged = new Map(active.map((s) => [s.sessionId, s]));
|
|
1776
|
+
for (const s of this.walStore.getDiscoveredSessions()) {
|
|
1777
|
+
if (s.cwd === undefined)
|
|
1778
|
+
continue;
|
|
1779
|
+
const existing = merged.get(s.sessionId);
|
|
1780
|
+
if (existing) {
|
|
1781
|
+
const discoveredUpdatedAt = s.agentUpdatedAt ?? s.discoveredAt;
|
|
1782
|
+
if (discoveredUpdatedAt > existing.updatedAt) {
|
|
1783
|
+
merged.set(s.sessionId, {
|
|
1784
|
+
...existing,
|
|
1785
|
+
updatedAt: discoveredUpdatedAt
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
} else {
|
|
1789
|
+
merged.set(s.sessionId, {
|
|
1790
|
+
sessionId: s.sessionId,
|
|
1791
|
+
title: s.title ?? `Session ${s.sessionId.slice(0, 8)}`,
|
|
1792
|
+
backendId: s.backendId,
|
|
1793
|
+
backendLabel: s.backendId,
|
|
1794
|
+
cwd: s.cwd,
|
|
1795
|
+
createdAt: s.discoveredAt,
|
|
1796
|
+
updatedAt: s.agentUpdatedAt ?? s.discoveredAt
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return Array.from(merged.values());
|
|
993
1801
|
}
|
|
994
1802
|
getSession(sessionId) {
|
|
995
1803
|
return this.sessions.get(sessionId);
|
|
996
1804
|
}
|
|
997
|
-
|
|
998
|
-
this.
|
|
999
|
-
return () => {
|
|
1000
|
-
this.sessionUpdateEmitter.off("update", listener);
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
onSessionError(listener) {
|
|
1004
|
-
this.sessionErrorEmitter.on("error", listener);
|
|
1005
|
-
return () => {
|
|
1006
|
-
this.sessionErrorEmitter.off("error", listener);
|
|
1007
|
-
};
|
|
1805
|
+
getSessionRevision(sessionId) {
|
|
1806
|
+
return this.sessions.get(sessionId)?.revision;
|
|
1008
1807
|
}
|
|
1009
1808
|
onPermissionRequest(listener) {
|
|
1010
1809
|
this.permissionRequestEmitter.on("request", listener);
|
|
@@ -1018,12 +1817,6 @@ var SessionManager = class {
|
|
|
1018
1817
|
this.permissionResultEmitter.off("result", listener);
|
|
1019
1818
|
};
|
|
1020
1819
|
}
|
|
1021
|
-
onTerminalOutput(listener) {
|
|
1022
|
-
this.terminalOutputEmitter.on("output", listener);
|
|
1023
|
-
return () => {
|
|
1024
|
-
this.terminalOutputEmitter.off("output", listener);
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
1820
|
onSessionsChanged(listener) {
|
|
1028
1821
|
this.sessionsChangedEmitter.on("changed", listener);
|
|
1029
1822
|
return () => {
|
|
@@ -1042,24 +1835,136 @@ var SessionManager = class {
|
|
|
1042
1835
|
this.sessionDetachedEmitter.off("detached", listener);
|
|
1043
1836
|
};
|
|
1044
1837
|
}
|
|
1838
|
+
onSessionEvent(listener) {
|
|
1839
|
+
this.sessionEventEmitter.on("event", listener);
|
|
1840
|
+
return () => {
|
|
1841
|
+
this.sessionEventEmitter.off("event", listener);
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
getSessionEvents(params) {
|
|
1845
|
+
const record = this.sessions.get(params.sessionId);
|
|
1846
|
+
let actualRevision;
|
|
1847
|
+
if (record) {
|
|
1848
|
+
actualRevision = record.revision;
|
|
1849
|
+
} else {
|
|
1850
|
+
const walSession = this.walStore.getSession(params.sessionId);
|
|
1851
|
+
actualRevision = walSession?.currentRevision ?? params.revision;
|
|
1852
|
+
}
|
|
1853
|
+
if (!record && !this.walStore.getSession(params.sessionId)) {
|
|
1854
|
+
return {
|
|
1855
|
+
sessionId: params.sessionId,
|
|
1856
|
+
machineId: this.config.machineId,
|
|
1857
|
+
revision: actualRevision,
|
|
1858
|
+
events: [],
|
|
1859
|
+
hasMore: false
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
if (params.revision !== actualRevision) {
|
|
1863
|
+
return {
|
|
1864
|
+
sessionId: params.sessionId,
|
|
1865
|
+
machineId: this.config.machineId,
|
|
1866
|
+
revision: actualRevision,
|
|
1867
|
+
events: [],
|
|
1868
|
+
hasMore: false
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
const limit = params.limit ?? 100;
|
|
1872
|
+
const events = this.walStore.queryEvents({
|
|
1873
|
+
sessionId: params.sessionId,
|
|
1874
|
+
revision: actualRevision,
|
|
1875
|
+
afterSeq: params.afterSeq,
|
|
1876
|
+
limit: limit + 1
|
|
1877
|
+
});
|
|
1878
|
+
const hasMore = events.length > limit;
|
|
1879
|
+
const resultEvents = hasMore ? events.slice(0, limit) : events;
|
|
1880
|
+
return {
|
|
1881
|
+
sessionId: params.sessionId,
|
|
1882
|
+
machineId: this.config.machineId,
|
|
1883
|
+
revision: actualRevision,
|
|
1884
|
+
events: resultEvents.map((e) => ({
|
|
1885
|
+
sessionId: e.sessionId,
|
|
1886
|
+
machineId: this.config.machineId,
|
|
1887
|
+
revision: e.revision,
|
|
1888
|
+
seq: e.seq,
|
|
1889
|
+
kind: e.kind,
|
|
1890
|
+
createdAt: e.createdAt,
|
|
1891
|
+
payload: e.payload
|
|
1892
|
+
})),
|
|
1893
|
+
nextAfterSeq: resultEvents.length > 0 ? resultEvents[resultEvents.length - 1].seq : undefined,
|
|
1894
|
+
hasMore
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
getUnackedEvents(sessionId, revision) {
|
|
1898
|
+
const events = this.walStore.getUnackedEvents(sessionId, revision);
|
|
1899
|
+
return events.map((e) => ({
|
|
1900
|
+
sessionId: e.sessionId,
|
|
1901
|
+
machineId: this.config.machineId,
|
|
1902
|
+
revision: e.revision,
|
|
1903
|
+
seq: e.seq,
|
|
1904
|
+
kind: e.kind,
|
|
1905
|
+
createdAt: e.createdAt,
|
|
1906
|
+
payload: e.payload
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
ackEvents(sessionId, revision, upToSeq) {
|
|
1910
|
+
this.walStore.ackEvents(sessionId, revision, upToSeq);
|
|
1911
|
+
}
|
|
1912
|
+
recordTurnEnd(sessionId, stopReason) {
|
|
1913
|
+
const record = this.sessions.get(sessionId);
|
|
1914
|
+
if (!record) {
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
record.updatedAt = new Date;
|
|
1918
|
+
this.writeAndEmitEvent(sessionId, record.revision, "turn_end", {
|
|
1919
|
+
stopReason
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
writeAndEmitEvent(sessionId, revision, kind, payload) {
|
|
1923
|
+
logger.debug({ sessionId, revision, kind }, "session_write_and_emit_event_start");
|
|
1924
|
+
const walEvent = this.walStore.appendEvent({
|
|
1925
|
+
sessionId,
|
|
1926
|
+
revision,
|
|
1927
|
+
kind,
|
|
1928
|
+
payload
|
|
1929
|
+
});
|
|
1930
|
+
const event = {
|
|
1931
|
+
sessionId: walEvent.sessionId,
|
|
1932
|
+
machineId: this.config.machineId,
|
|
1933
|
+
revision: walEvent.revision,
|
|
1934
|
+
seq: walEvent.seq,
|
|
1935
|
+
kind: walEvent.kind,
|
|
1936
|
+
createdAt: walEvent.createdAt,
|
|
1937
|
+
payload: walEvent.payload
|
|
1938
|
+
};
|
|
1939
|
+
logger.info({
|
|
1940
|
+
sessionId: event.sessionId,
|
|
1941
|
+
revision: event.revision,
|
|
1942
|
+
seq: event.seq,
|
|
1943
|
+
kind: event.kind
|
|
1944
|
+
}, "session_event_emitting");
|
|
1945
|
+
this.sessionEventEmitter.emit("event", event);
|
|
1946
|
+
logger.debug({ sessionId: event.sessionId, seq: event.seq }, "session_event_emitted");
|
|
1947
|
+
return event;
|
|
1948
|
+
}
|
|
1045
1949
|
emitSessionsChanged(payload) {
|
|
1046
1950
|
this.sessionsChangedEmitter.emit("changed", payload);
|
|
1047
1951
|
}
|
|
1048
|
-
emitSessionAttached(sessionId) {
|
|
1952
|
+
emitSessionAttached(sessionId, force = false) {
|
|
1049
1953
|
const record = this.sessions.get(sessionId);
|
|
1050
1954
|
if (!record) {
|
|
1051
1955
|
return;
|
|
1052
1956
|
}
|
|
1053
|
-
if (record.isAttached) {
|
|
1957
|
+
if (record.isAttached && !force) {
|
|
1054
1958
|
return;
|
|
1055
1959
|
}
|
|
1056
|
-
const attachedAt =
|
|
1960
|
+
const attachedAt = new Date;
|
|
1057
1961
|
record.isAttached = true;
|
|
1058
1962
|
record.attachedAt = attachedAt;
|
|
1059
1963
|
this.sessionAttachedEmitter.emit("attached", {
|
|
1060
1964
|
sessionId,
|
|
1061
1965
|
machineId: this.config.machineId,
|
|
1062
|
-
attachedAt: attachedAt.toISOString()
|
|
1966
|
+
attachedAt: attachedAt.toISOString(),
|
|
1967
|
+
revision: record.revision
|
|
1063
1968
|
});
|
|
1064
1969
|
}
|
|
1065
1970
|
emitSessionDetached(sessionId, reason) {
|
|
@@ -1074,7 +1979,7 @@ var SessionManager = class {
|
|
|
1074
1979
|
this.sessionDetachedEmitter.emit("detached", {
|
|
1075
1980
|
sessionId,
|
|
1076
1981
|
machineId: this.config.machineId,
|
|
1077
|
-
detachedAt:
|
|
1982
|
+
detachedAt: new Date().toISOString(),
|
|
1078
1983
|
reason
|
|
1079
1984
|
});
|
|
1080
1985
|
}
|
|
@@ -1083,69 +1988,72 @@ var SessionManager = class {
|
|
|
1083
1988
|
}
|
|
1084
1989
|
resolvePermissionRequest(sessionId, requestId, outcome) {
|
|
1085
1990
|
const key = buildPermissionKey(sessionId, requestId);
|
|
1086
|
-
const
|
|
1087
|
-
if (!
|
|
1088
|
-
throw new AppError(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}),
|
|
1095
|
-
404
|
|
1096
|
-
);
|
|
1991
|
+
const permRecord = this.permissionRequests.get(key);
|
|
1992
|
+
if (!permRecord) {
|
|
1993
|
+
throw new AppError(createErrorDetail2({
|
|
1994
|
+
code: "REQUEST_VALIDATION_FAILED",
|
|
1995
|
+
message: "Permission request not found",
|
|
1996
|
+
retryable: false,
|
|
1997
|
+
scope: "request"
|
|
1998
|
+
}), 404);
|
|
1097
1999
|
}
|
|
1098
2000
|
const response = { outcome };
|
|
1099
|
-
|
|
2001
|
+
permRecord.resolve(response);
|
|
1100
2002
|
this.permissionRequests.delete(key);
|
|
1101
2003
|
const payload = {
|
|
1102
2004
|
sessionId,
|
|
1103
2005
|
requestId,
|
|
1104
2006
|
outcome
|
|
1105
2007
|
};
|
|
2008
|
+
const sessionRecord = this.sessions.get(sessionId);
|
|
2009
|
+
if (sessionRecord) {
|
|
2010
|
+
this.writeAndEmitEvent(sessionId, sessionRecord.revision, "permission_result", payload);
|
|
2011
|
+
}
|
|
1106
2012
|
this.permissionResultEmitter.emit("result", payload);
|
|
1107
2013
|
return payload;
|
|
1108
2014
|
}
|
|
1109
2015
|
resolveBackend(backendId) {
|
|
1110
|
-
const normalized = backendId
|
|
1111
|
-
|
|
1112
|
-
|
|
2016
|
+
const normalized = backendId.trim();
|
|
2017
|
+
if (!normalized) {
|
|
2018
|
+
throw new AppError(createErrorDetail2({
|
|
2019
|
+
code: "REQUEST_VALIDATION_FAILED",
|
|
2020
|
+
message: "backendId is required",
|
|
2021
|
+
retryable: false,
|
|
2022
|
+
scope: "request"
|
|
2023
|
+
}), 400);
|
|
2024
|
+
}
|
|
2025
|
+
const backend = this.backendById.get(normalized);
|
|
1113
2026
|
if (!backend) {
|
|
1114
|
-
throw new AppError(
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}),
|
|
1121
|
-
400
|
|
1122
|
-
);
|
|
2027
|
+
throw new AppError(createErrorDetail2({
|
|
2028
|
+
code: "REQUEST_VALIDATION_FAILED",
|
|
2029
|
+
message: "Invalid backend ID",
|
|
2030
|
+
retryable: false,
|
|
2031
|
+
scope: "request"
|
|
2032
|
+
}), 400);
|
|
1123
2033
|
}
|
|
1124
2034
|
return backend;
|
|
1125
2035
|
}
|
|
1126
2036
|
async createSession(options) {
|
|
1127
|
-
const backend = this.resolveBackend(options
|
|
1128
|
-
const connection =
|
|
1129
|
-
backend,
|
|
1130
|
-
client: {
|
|
1131
|
-
name: this.config.clientName,
|
|
1132
|
-
version: this.config.clientVersion
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
2037
|
+
const backend = this.resolveBackend(options.backendId);
|
|
2038
|
+
const connection = this.createConnection(backend);
|
|
1135
2039
|
try {
|
|
1136
2040
|
await connection.connect();
|
|
1137
2041
|
const session = await connection.createSession({ cwd: options?.cwd });
|
|
1138
|
-
connection.setPermissionHandler(
|
|
1139
|
-
|
|
1140
|
-
);
|
|
1141
|
-
const now = /* @__PURE__ */ new Date();
|
|
2042
|
+
connection.setPermissionHandler((params) => this.handlePermissionRequest(session.sessionId, params));
|
|
2043
|
+
const now = new Date;
|
|
1142
2044
|
const agentInfo = connection.getAgentInfo();
|
|
1143
|
-
const { modelId, modelName, availableModels } = resolveModelState(
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
2045
|
+
const { modelId, modelName, availableModels } = resolveModelState(session.models);
|
|
2046
|
+
const { modeId, modeName, availableModes } = resolveModeState(session.modes);
|
|
2047
|
+
const { revision } = this.walStore.ensureSession({
|
|
2048
|
+
sessionId: session.sessionId,
|
|
2049
|
+
machineId: this.config.machineId,
|
|
2050
|
+
backendId: backend.id,
|
|
2051
|
+
cwd: options?.cwd,
|
|
2052
|
+
title: options?.title ?? `Session ${this.sessions.size + 1}`
|
|
2053
|
+
});
|
|
2054
|
+
if (this.cryptoService) {
|
|
2055
|
+
this.cryptoService.initSessionDek(session.sessionId);
|
|
2056
|
+
}
|
|
1149
2057
|
const record = {
|
|
1150
2058
|
sessionId: session.sessionId,
|
|
1151
2059
|
title: options?.title ?? `Session ${this.sessions.size + 1}`,
|
|
@@ -1162,24 +2070,25 @@ var SessionManager = class {
|
|
|
1162
2070
|
modeName,
|
|
1163
2071
|
availableModes,
|
|
1164
2072
|
availableModels,
|
|
1165
|
-
availableCommands:
|
|
2073
|
+
availableCommands: undefined,
|
|
2074
|
+
revision
|
|
1166
2075
|
};
|
|
1167
|
-
record.unsubscribe = connection.onSessionUpdate(
|
|
1168
|
-
(
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
2076
|
+
record.unsubscribe = connection.onSessionUpdate((notification) => {
|
|
2077
|
+
logger.debug({
|
|
2078
|
+
sessionId: session.sessionId,
|
|
2079
|
+
updateType: notification.update.sessionUpdate
|
|
2080
|
+
}, "acp_session_update_received");
|
|
2081
|
+
record.updatedAt = new Date;
|
|
2082
|
+
this.writeSessionUpdateToWal(record, notification);
|
|
2083
|
+
this.applySessionUpdateToRecord(record, notification);
|
|
2084
|
+
});
|
|
1174
2085
|
record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
|
|
1175
|
-
|
|
2086
|
+
logger.debug({ sessionId: record.sessionId }, "acp_terminal_output_received");
|
|
2087
|
+
this.writeAndEmitEvent(record.sessionId, record.revision, "terminal_output", event);
|
|
1176
2088
|
});
|
|
1177
2089
|
connection.onStatusChange((status) => {
|
|
1178
2090
|
if (status.error) {
|
|
1179
|
-
this.
|
|
1180
|
-
sessionId: session.sessionId,
|
|
1181
|
-
error: status.error
|
|
1182
|
-
});
|
|
2091
|
+
this.writeAndEmitEvent(record.sessionId, record.revision, "session_error", { error: status.error });
|
|
1183
2092
|
this.emitSessionDetached(session.sessionId, "agent_exit");
|
|
1184
2093
|
}
|
|
1185
2094
|
});
|
|
@@ -1208,7 +2117,6 @@ var SessionManager = class {
|
|
|
1208
2117
|
requestId: record.requestId,
|
|
1209
2118
|
options: record.params.options.map((option) => ({
|
|
1210
2119
|
optionId: option.optionId,
|
|
1211
|
-
// SDK uses 'name', our shared type uses 'label'
|
|
1212
2120
|
label: option.name,
|
|
1213
2121
|
description: option._meta?.description ?? null
|
|
1214
2122
|
})),
|
|
@@ -1228,23 +2136,24 @@ var SessionManager = class {
|
|
|
1228
2136
|
if (existing) {
|
|
1229
2137
|
return existing.promise;
|
|
1230
2138
|
}
|
|
1231
|
-
let resolver = () => {
|
|
1232
|
-
};
|
|
2139
|
+
let resolver = () => {};
|
|
1233
2140
|
const promise = new Promise((resolve) => {
|
|
1234
2141
|
resolver = resolve;
|
|
1235
2142
|
});
|
|
1236
|
-
const
|
|
2143
|
+
const permRecord = {
|
|
1237
2144
|
sessionId,
|
|
1238
2145
|
requestId,
|
|
1239
2146
|
params,
|
|
1240
2147
|
promise,
|
|
1241
2148
|
resolve: resolver
|
|
1242
2149
|
};
|
|
1243
|
-
this.permissionRequests.set(key,
|
|
1244
|
-
this.
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
2150
|
+
this.permissionRequests.set(key, permRecord);
|
|
2151
|
+
const payload = this.buildPermissionRequestPayload(permRecord);
|
|
2152
|
+
const sessionRecord = this.sessions.get(sessionId);
|
|
2153
|
+
if (sessionRecord) {
|
|
2154
|
+
this.writeAndEmitEvent(sessionId, sessionRecord.revision, "permission_request", payload);
|
|
2155
|
+
}
|
|
2156
|
+
this.permissionRequestEmitter.emit("request", payload);
|
|
1248
2157
|
return promise;
|
|
1249
2158
|
}
|
|
1250
2159
|
cancelPermissionRequests(sessionId) {
|
|
@@ -1267,18 +2176,15 @@ var SessionManager = class {
|
|
|
1267
2176
|
updateTitle(sessionId, title) {
|
|
1268
2177
|
const record = this.sessions.get(sessionId);
|
|
1269
2178
|
if (!record) {
|
|
1270
|
-
throw new AppError(
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
}),
|
|
1277
|
-
404
|
|
1278
|
-
);
|
|
2179
|
+
throw new AppError(createErrorDetail2({
|
|
2180
|
+
code: "SESSION_NOT_FOUND",
|
|
2181
|
+
message: "Session not found",
|
|
2182
|
+
retryable: false,
|
|
2183
|
+
scope: "session"
|
|
2184
|
+
}), 404);
|
|
1279
2185
|
}
|
|
1280
2186
|
record.title = title;
|
|
1281
|
-
record.updatedAt =
|
|
2187
|
+
record.updatedAt = new Date;
|
|
1282
2188
|
const summary = this.buildSummary(record);
|
|
1283
2189
|
this.emitSessionsChanged({
|
|
1284
2190
|
added: [],
|
|
@@ -1292,42 +2198,34 @@ var SessionManager = class {
|
|
|
1292
2198
|
if (!record) {
|
|
1293
2199
|
return;
|
|
1294
2200
|
}
|
|
1295
|
-
record.updatedAt =
|
|
2201
|
+
record.updatedAt = new Date;
|
|
1296
2202
|
}
|
|
1297
2203
|
async setSessionMode(sessionId, modeId) {
|
|
1298
2204
|
const record = this.sessions.get(sessionId);
|
|
1299
2205
|
if (!record) {
|
|
1300
|
-
throw new AppError(
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}),
|
|
1307
|
-
404
|
|
1308
|
-
);
|
|
2206
|
+
throw new AppError(createErrorDetail2({
|
|
2207
|
+
code: "SESSION_NOT_FOUND",
|
|
2208
|
+
message: "Session not found",
|
|
2209
|
+
retryable: false,
|
|
2210
|
+
scope: "session"
|
|
2211
|
+
}), 404);
|
|
1309
2212
|
}
|
|
1310
2213
|
if (!record.availableModes || record.availableModes.length === 0) {
|
|
1311
|
-
throw createCapabilityNotSupportedError(
|
|
1312
|
-
"Current agent does not support mode switching"
|
|
1313
|
-
);
|
|
2214
|
+
throw createCapabilityNotSupportedError("Current agent does not support mode switching");
|
|
1314
2215
|
}
|
|
1315
2216
|
const selected = record.availableModes.find((mode) => mode.id === modeId);
|
|
1316
2217
|
if (!selected) {
|
|
1317
|
-
throw new AppError(
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}),
|
|
1324
|
-
400
|
|
1325
|
-
);
|
|
2218
|
+
throw new AppError(createErrorDetail2({
|
|
2219
|
+
code: "REQUEST_VALIDATION_FAILED",
|
|
2220
|
+
message: "Invalid mode ID",
|
|
2221
|
+
retryable: false,
|
|
2222
|
+
scope: "request"
|
|
2223
|
+
}), 400);
|
|
1326
2224
|
}
|
|
1327
2225
|
await record.connection.setSessionMode(sessionId, modeId);
|
|
1328
2226
|
record.modeId = selected.id;
|
|
1329
2227
|
record.modeName = selected.name;
|
|
1330
|
-
record.updatedAt =
|
|
2228
|
+
record.updatedAt = new Date;
|
|
1331
2229
|
const summary = this.buildSummary(record);
|
|
1332
2230
|
this.emitSessionsChanged({
|
|
1333
2231
|
added: [],
|
|
@@ -1339,39 +2237,29 @@ var SessionManager = class {
|
|
|
1339
2237
|
async setSessionModel(sessionId, modelId) {
|
|
1340
2238
|
const record = this.sessions.get(sessionId);
|
|
1341
2239
|
if (!record) {
|
|
1342
|
-
throw new AppError(
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}),
|
|
1349
|
-
404
|
|
1350
|
-
);
|
|
2240
|
+
throw new AppError(createErrorDetail2({
|
|
2241
|
+
code: "SESSION_NOT_FOUND",
|
|
2242
|
+
message: "Session not found",
|
|
2243
|
+
retryable: false,
|
|
2244
|
+
scope: "session"
|
|
2245
|
+
}), 404);
|
|
1351
2246
|
}
|
|
1352
2247
|
if (!record.availableModels || record.availableModels.length === 0) {
|
|
1353
|
-
throw createCapabilityNotSupportedError(
|
|
1354
|
-
"Current agent does not support model switching"
|
|
1355
|
-
);
|
|
2248
|
+
throw createCapabilityNotSupportedError("Current agent does not support model switching");
|
|
1356
2249
|
}
|
|
1357
|
-
const selected = record.availableModels.find(
|
|
1358
|
-
(model) => model.id === modelId
|
|
1359
|
-
);
|
|
2250
|
+
const selected = record.availableModels.find((model) => model.id === modelId);
|
|
1360
2251
|
if (!selected) {
|
|
1361
|
-
throw new AppError(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}),
|
|
1368
|
-
400
|
|
1369
|
-
);
|
|
2252
|
+
throw new AppError(createErrorDetail2({
|
|
2253
|
+
code: "REQUEST_VALIDATION_FAILED",
|
|
2254
|
+
message: "Invalid model ID",
|
|
2255
|
+
retryable: false,
|
|
2256
|
+
scope: "request"
|
|
2257
|
+
}), 400);
|
|
1370
2258
|
}
|
|
1371
2259
|
await record.connection.setSessionModel(sessionId, modelId);
|
|
1372
2260
|
record.modelId = selected.id;
|
|
1373
2261
|
record.modelName = selected.name;
|
|
1374
|
-
record.updatedAt =
|
|
2262
|
+
record.updatedAt = new Date;
|
|
1375
2263
|
const summary = this.buildSummary(record);
|
|
1376
2264
|
this.emitSessionsChanged({
|
|
1377
2265
|
added: [],
|
|
@@ -1418,25 +2306,38 @@ var SessionManager = class {
|
|
|
1418
2306
|
}
|
|
1419
2307
|
async closeAll() {
|
|
1420
2308
|
const sessionIds = Array.from(this.sessions.keys());
|
|
1421
|
-
await Promise.all(
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
2309
|
+
await Promise.all(sessionIds.map((sessionId) => this.closeSession(sessionId)));
|
|
2310
|
+
}
|
|
2311
|
+
async archiveSession(sessionId) {
|
|
2312
|
+
if (this.sessions.has(sessionId)) {
|
|
2313
|
+
await this.closeSession(sessionId);
|
|
2314
|
+
}
|
|
2315
|
+
this.walStore.archiveSession(sessionId);
|
|
2316
|
+
this.discoveredSessions.delete(sessionId);
|
|
2317
|
+
}
|
|
2318
|
+
async bulkArchiveSessions(sessionIds) {
|
|
2319
|
+
await Promise.allSettled(sessionIds.filter((id) => this.sessions.has(id)).map((id) => this.closeSession(id)));
|
|
2320
|
+
const archivedCount = this.walStore.bulkArchiveSessions(sessionIds);
|
|
2321
|
+
for (const id of sessionIds) {
|
|
2322
|
+
this.discoveredSessions.delete(id);
|
|
2323
|
+
}
|
|
2324
|
+
return { archivedCount };
|
|
2325
|
+
}
|
|
2326
|
+
async shutdown() {
|
|
2327
|
+
await this.closeAll();
|
|
2328
|
+
this.walStore.close();
|
|
2329
|
+
}
|
|
2330
|
+
getPersistedDiscoveredSessions(backendId) {
|
|
2331
|
+
return this.walStore.getDiscoveredSessions(backendId).filter((s) => !this.sessions.has(s.sessionId) && s.cwd !== undefined).map((s) => ({
|
|
2332
|
+
sessionId: s.sessionId,
|
|
2333
|
+
cwd: s.cwd,
|
|
2334
|
+
title: s.title,
|
|
2335
|
+
updatedAt: s.agentUpdatedAt
|
|
2336
|
+
}));
|
|
2337
|
+
}
|
|
1431
2338
|
async discoverSessions(options) {
|
|
1432
|
-
const backend = this.resolveBackend(options
|
|
1433
|
-
const connection =
|
|
1434
|
-
backend,
|
|
1435
|
-
client: {
|
|
1436
|
-
name: this.config.clientName,
|
|
1437
|
-
version: this.config.clientVersion
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
2339
|
+
const backend = this.resolveBackend(options.backendId);
|
|
2340
|
+
const connection = this.createConnection(backend);
|
|
1440
2341
|
try {
|
|
1441
2342
|
await connection.connect();
|
|
1442
2343
|
const capabilities = connection.getSessionCapabilities();
|
|
@@ -1448,98 +2349,123 @@ var SessionManager = class {
|
|
|
1448
2349
|
cursor: options?.cursor
|
|
1449
2350
|
});
|
|
1450
2351
|
nextCursor = response.nextCursor;
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
);
|
|
2352
|
+
const archivedIds = new Set(this.walStore.getArchivedSessionIds());
|
|
2353
|
+
const validity = await Promise.all(response.sessions.map(async (session) => ({
|
|
2354
|
+
session,
|
|
2355
|
+
isValid: session.cwd ? await isValidWorkspacePath(session.cwd) : false
|
|
2356
|
+
})));
|
|
2357
|
+
const now = new Date().toISOString();
|
|
2358
|
+
const discoveredRecords = [];
|
|
1457
2359
|
for (const { session, isValid } of validity) {
|
|
1458
2360
|
if (!isValid) {
|
|
1459
2361
|
this.discoveredSessions.delete(session.sessionId);
|
|
2362
|
+
this.walStore.markDiscoveredSessionStale(session.sessionId);
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
if (archivedIds.has(session.sessionId)) {
|
|
1460
2366
|
continue;
|
|
1461
2367
|
}
|
|
1462
2368
|
this.discoveredSessions.set(session.sessionId, {
|
|
1463
2369
|
sessionId: session.sessionId,
|
|
1464
2370
|
cwd: session.cwd,
|
|
1465
|
-
title: session.title ??
|
|
1466
|
-
updatedAt: session.updatedAt ??
|
|
2371
|
+
title: session.title ?? undefined,
|
|
2372
|
+
updatedAt: session.updatedAt ?? undefined
|
|
1467
2373
|
});
|
|
1468
2374
|
sessions.push({
|
|
1469
2375
|
sessionId: session.sessionId,
|
|
1470
2376
|
cwd: session.cwd,
|
|
1471
|
-
title: session.title ??
|
|
1472
|
-
updatedAt: session.updatedAt ??
|
|
2377
|
+
title: session.title ?? undefined,
|
|
2378
|
+
updatedAt: session.updatedAt ?? undefined
|
|
2379
|
+
});
|
|
2380
|
+
discoveredRecords.push({
|
|
2381
|
+
sessionId: session.sessionId,
|
|
2382
|
+
backendId: backend.id,
|
|
2383
|
+
cwd: session.cwd,
|
|
2384
|
+
title: session.title ?? undefined,
|
|
2385
|
+
agentUpdatedAt: session.updatedAt ?? undefined,
|
|
2386
|
+
discoveredAt: now,
|
|
2387
|
+
isStale: false
|
|
1473
2388
|
});
|
|
1474
2389
|
}
|
|
2390
|
+
if (discoveredRecords.length > 0) {
|
|
2391
|
+
this.walStore.saveDiscoveredSessions(discoveredRecords);
|
|
2392
|
+
}
|
|
1475
2393
|
}
|
|
1476
|
-
logger.info(
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
},
|
|
1482
|
-
"sessions_discovered"
|
|
1483
|
-
);
|
|
2394
|
+
logger.info({
|
|
2395
|
+
backendId: backend.id,
|
|
2396
|
+
sessionCount: sessions.length,
|
|
2397
|
+
capabilities
|
|
2398
|
+
}, "sessions_discovered");
|
|
1484
2399
|
return { sessions, capabilities, nextCursor };
|
|
1485
2400
|
} finally {
|
|
1486
2401
|
await connection.disconnect();
|
|
1487
2402
|
}
|
|
1488
2403
|
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Load a historical session from the ACP agent.
|
|
1491
|
-
* This will replay the session's message history.
|
|
1492
|
-
* @param sessionId The session ID to load
|
|
1493
|
-
* @param cwd The working directory
|
|
1494
|
-
* @param backendId Optional backend ID
|
|
1495
|
-
* @returns The loaded session summary
|
|
1496
|
-
*/
|
|
1497
2404
|
async loadSession(sessionId, cwd, backendId) {
|
|
2405
|
+
logger.info({ sessionId, cwd, backendId }, "load_session_start");
|
|
1498
2406
|
const existing = this.sessions.get(sessionId);
|
|
1499
2407
|
if (existing) {
|
|
1500
|
-
|
|
2408
|
+
logger.debug({ sessionId }, "load_session_already_loaded");
|
|
2409
|
+
this.emitSessionAttached(sessionId, true);
|
|
1501
2410
|
return this.buildSummary(existing);
|
|
1502
2411
|
}
|
|
1503
2412
|
const backend = this.resolveBackend(backendId);
|
|
1504
|
-
const connection =
|
|
1505
|
-
backend,
|
|
1506
|
-
client: {
|
|
1507
|
-
name: this.config.clientName,
|
|
1508
|
-
version: this.config.clientVersion
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
2413
|
+
const connection = this.createConnection(backend);
|
|
1511
2414
|
try {
|
|
1512
2415
|
await connection.connect();
|
|
1513
2416
|
if (!connection.supportsSessionLoad()) {
|
|
1514
|
-
throw createCapabilityNotSupportedError(
|
|
1515
|
-
|
|
1516
|
-
|
|
2417
|
+
throw createCapabilityNotSupportedError("Agent does not support session loading");
|
|
2418
|
+
}
|
|
2419
|
+
const existingWalSession = this.walStore.getSession(sessionId);
|
|
2420
|
+
const hasExistingHistory = existingWalSession !== null && this.walStore.queryEvents({
|
|
2421
|
+
sessionId,
|
|
2422
|
+
revision: existingWalSession.currentRevision,
|
|
2423
|
+
afterSeq: 0,
|
|
2424
|
+
limit: 1
|
|
2425
|
+
}).length > 0;
|
|
2426
|
+
let revision;
|
|
2427
|
+
if (hasExistingHistory) {
|
|
2428
|
+
revision = this.walStore.incrementRevision(sessionId);
|
|
2429
|
+
logger.debug({ sessionId, revision }, "load_session_bump_revision");
|
|
2430
|
+
} else {
|
|
2431
|
+
const result = this.walStore.ensureSession({
|
|
2432
|
+
sessionId,
|
|
2433
|
+
machineId: this.config.machineId,
|
|
2434
|
+
backendId: backend.id,
|
|
2435
|
+
cwd
|
|
2436
|
+
});
|
|
2437
|
+
revision = result.revision;
|
|
1517
2438
|
}
|
|
1518
2439
|
const bufferedUpdates = [];
|
|
1519
2440
|
let recordRef;
|
|
1520
|
-
const unsubscribe = connection.onSessionUpdate(
|
|
1521
|
-
(
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2441
|
+
const unsubscribe = connection.onSessionUpdate((notification) => {
|
|
2442
|
+
logger.debug({
|
|
2443
|
+
sessionId,
|
|
2444
|
+
updateType: notification.update.sessionUpdate,
|
|
2445
|
+
hasRecordRef: !!recordRef
|
|
2446
|
+
}, "load_session_update_received");
|
|
2447
|
+
if (recordRef) {
|
|
2448
|
+
this.writeSessionUpdateToWal(recordRef, notification);
|
|
2449
|
+
recordRef.updatedAt = new Date;
|
|
2450
|
+
this.applySessionUpdateToRecord(recordRef, notification);
|
|
2451
|
+
} else {
|
|
2452
|
+
bufferedUpdates.push(notification);
|
|
2453
|
+
logger.debug({ sessionId, bufferedCount: bufferedUpdates.length }, "load_session_buffered");
|
|
1529
2454
|
}
|
|
1530
|
-
);
|
|
2455
|
+
});
|
|
2456
|
+
logger.debug({ sessionId }, "load_session_calling_acp");
|
|
1531
2457
|
const response = await connection.loadSession(sessionId, cwd);
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2458
|
+
logger.debug({
|
|
2459
|
+
sessionId,
|
|
2460
|
+
bufferedCount: bufferedUpdates.length,
|
|
2461
|
+
hasModels: !!response.models,
|
|
2462
|
+
hasModes: !!response.modes
|
|
2463
|
+
}, "load_session_acp_returned");
|
|
2464
|
+
connection.setPermissionHandler((params) => this.handlePermissionRequest(sessionId, params));
|
|
2465
|
+
const now = new Date;
|
|
1536
2466
|
const agentInfo = connection.getAgentInfo();
|
|
1537
|
-
const { modelId, modelName, availableModels } = resolveModelState(
|
|
1538
|
-
|
|
1539
|
-
);
|
|
1540
|
-
const { modeId, modeName, availableModes } = resolveModeState(
|
|
1541
|
-
response.modes
|
|
1542
|
-
);
|
|
2467
|
+
const { modelId, modelName, availableModels } = resolveModelState(response.models);
|
|
2468
|
+
const { modeId, modeName, availableModes } = resolveModeState(response.modes);
|
|
1543
2469
|
const discovered = this.discoveredSessions.get(sessionId);
|
|
1544
2470
|
const record = {
|
|
1545
2471
|
sessionId,
|
|
@@ -1557,11 +2483,18 @@ var SessionManager = class {
|
|
|
1557
2483
|
modeName,
|
|
1558
2484
|
availableModes,
|
|
1559
2485
|
availableModels,
|
|
1560
|
-
availableCommands:
|
|
2486
|
+
availableCommands: undefined,
|
|
2487
|
+
revision
|
|
1561
2488
|
};
|
|
1562
2489
|
recordRef = record;
|
|
1563
2490
|
record.unsubscribe = unsubscribe;
|
|
2491
|
+
if (this.cryptoService) {
|
|
2492
|
+
this.cryptoService.initSessionDek(sessionId);
|
|
2493
|
+
}
|
|
2494
|
+
logger.debug({ sessionId, bufferedCount: bufferedUpdates.length }, "load_session_writing_buffered");
|
|
1564
2495
|
for (const notification of bufferedUpdates) {
|
|
2496
|
+
logger.debug({ sessionId, updateType: notification.update.sessionUpdate }, "load_session_writing_buffered_event");
|
|
2497
|
+
this.writeSessionUpdateToWal(record, notification);
|
|
1565
2498
|
this.applySessionUpdateToRecord(record, notification);
|
|
1566
2499
|
}
|
|
1567
2500
|
this.setupSessionSubscriptions(record, { skipSessionUpdates: true });
|
|
@@ -1573,13 +2506,46 @@ var SessionManager = class {
|
|
|
1573
2506
|
removed: []
|
|
1574
2507
|
});
|
|
1575
2508
|
this.emitSessionAttached(sessionId);
|
|
1576
|
-
logger.info({ sessionId, backendId: backend.id }, "
|
|
2509
|
+
logger.info({ sessionId, backendId: backend.id, revision: record.revision }, "load_session_complete");
|
|
1577
2510
|
return summary;
|
|
1578
2511
|
} catch (error) {
|
|
1579
2512
|
await connection.disconnect();
|
|
1580
2513
|
throw error;
|
|
1581
2514
|
}
|
|
1582
2515
|
}
|
|
2516
|
+
async reloadSession(sessionId, cwd, backendId) {
|
|
2517
|
+
const existing = this.sessions.get(sessionId);
|
|
2518
|
+
if (!existing) {
|
|
2519
|
+
return this.loadSession(sessionId, cwd, backendId);
|
|
2520
|
+
}
|
|
2521
|
+
if (!existing.connection.supportsSessionLoad()) {
|
|
2522
|
+
throw createCapabilityNotSupportedError("Agent does not support session loading");
|
|
2523
|
+
}
|
|
2524
|
+
const newRevision = this.walStore.incrementRevision(sessionId);
|
|
2525
|
+
existing.revision = newRevision;
|
|
2526
|
+
const response = await existing.connection.loadSession(sessionId, cwd);
|
|
2527
|
+
const { modelId, modelName, availableModels } = resolveModelState(response.models);
|
|
2528
|
+
const { modeId, modeName, availableModes } = resolveModeState(response.modes);
|
|
2529
|
+
const agentInfo = existing.connection.getAgentInfo();
|
|
2530
|
+
existing.cwd = cwd;
|
|
2531
|
+
existing.agentName = agentInfo?.title ?? agentInfo?.name ?? existing.agentName;
|
|
2532
|
+
existing.modelId = modelId;
|
|
2533
|
+
existing.modelName = modelName;
|
|
2534
|
+
existing.availableModels = availableModels;
|
|
2535
|
+
existing.modeId = modeId;
|
|
2536
|
+
existing.modeName = modeName;
|
|
2537
|
+
existing.availableModes = availableModes;
|
|
2538
|
+
existing.updatedAt = new Date;
|
|
2539
|
+
const summary = this.buildSummary(existing);
|
|
2540
|
+
this.emitSessionsChanged({
|
|
2541
|
+
added: [],
|
|
2542
|
+
updated: [summary],
|
|
2543
|
+
removed: []
|
|
2544
|
+
});
|
|
2545
|
+
this.emitSessionAttached(sessionId, true);
|
|
2546
|
+
logger.info({ sessionId, backendId, revision: newRevision }, "session_reloaded");
|
|
2547
|
+
return summary;
|
|
2548
|
+
}
|
|
1583
2549
|
applySessionUpdateToRecord(record, notification) {
|
|
1584
2550
|
const update = notification.update;
|
|
1585
2551
|
if (update.sessionUpdate === "current_mode_update") {
|
|
@@ -1601,27 +2567,69 @@ var SessionManager = class {
|
|
|
1601
2567
|
}
|
|
1602
2568
|
}
|
|
1603
2569
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2570
|
+
writeSessionUpdateToWal(record, notification) {
|
|
2571
|
+
const update = notification.update;
|
|
2572
|
+
let kind;
|
|
2573
|
+
logger.debug({
|
|
2574
|
+
sessionId: record.sessionId,
|
|
2575
|
+
revision: record.revision,
|
|
2576
|
+
updateType: update.sessionUpdate
|
|
2577
|
+
}, "write_session_update_to_wal_start");
|
|
2578
|
+
switch (update.sessionUpdate) {
|
|
2579
|
+
case "user_message_chunk":
|
|
2580
|
+
kind = "user_message";
|
|
2581
|
+
break;
|
|
2582
|
+
case "agent_message_chunk":
|
|
2583
|
+
kind = "agent_message_chunk";
|
|
2584
|
+
break;
|
|
2585
|
+
case "agent_thought_chunk":
|
|
2586
|
+
kind = "agent_thought_chunk";
|
|
2587
|
+
break;
|
|
2588
|
+
case "tool_call":
|
|
2589
|
+
kind = "tool_call";
|
|
2590
|
+
break;
|
|
2591
|
+
case "tool_call_update":
|
|
2592
|
+
kind = "tool_call_update";
|
|
2593
|
+
break;
|
|
2594
|
+
case "session_info_update":
|
|
2595
|
+
case "current_mode_update":
|
|
2596
|
+
case "available_commands_update":
|
|
2597
|
+
kind = "session_info_update";
|
|
2598
|
+
break;
|
|
2599
|
+
default:
|
|
2600
|
+
logger.warn({ sessionId: record.sessionId, updateType: update.sessionUpdate }, "unknown_session_update_type_skipped");
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
logger.info({
|
|
2604
|
+
sessionId: record.sessionId,
|
|
2605
|
+
revision: record.revision,
|
|
2606
|
+
updateType: update.sessionUpdate,
|
|
2607
|
+
kind
|
|
2608
|
+
}, "write_session_update_to_wal_mapped");
|
|
2609
|
+
this.writeAndEmitEvent(record.sessionId, record.revision, kind, notification);
|
|
2610
|
+
}
|
|
1607
2611
|
setupSessionSubscriptions(record, options) {
|
|
1608
2612
|
const { sessionId, connection } = record;
|
|
2613
|
+
logger.debug({ sessionId, skipSessionUpdates: options?.skipSessionUpdates }, "setup_session_subscriptions");
|
|
1609
2614
|
if (!options?.skipSessionUpdates) {
|
|
1610
|
-
record.unsubscribe = connection.onSessionUpdate(
|
|
1611
|
-
(
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2615
|
+
record.unsubscribe = connection.onSessionUpdate((notification) => {
|
|
2616
|
+
logger.debug({
|
|
2617
|
+
sessionId,
|
|
2618
|
+
updateType: notification.update.sessionUpdate
|
|
2619
|
+
}, "acp_session_update_received_via_setup");
|
|
2620
|
+
record.updatedAt = new Date;
|
|
2621
|
+
this.writeSessionUpdateToWal(record, notification);
|
|
2622
|
+
this.applySessionUpdateToRecord(record, notification);
|
|
2623
|
+
});
|
|
1617
2624
|
}
|
|
1618
2625
|
record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
|
|
1619
|
-
|
|
2626
|
+
logger.debug({ sessionId }, "acp_terminal_output_received_via_setup");
|
|
2627
|
+
this.writeAndEmitEvent(sessionId, record.revision, "terminal_output", event);
|
|
1620
2628
|
});
|
|
1621
2629
|
connection.onStatusChange((status) => {
|
|
2630
|
+
logger.debug({ sessionId, hasError: !!status.error }, "acp_status_change");
|
|
1622
2631
|
if (status.error) {
|
|
1623
|
-
this.
|
|
1624
|
-
sessionId,
|
|
2632
|
+
this.writeAndEmitEvent(sessionId, record.revision, "session_error", {
|
|
1625
2633
|
error: status.error
|
|
1626
2634
|
});
|
|
1627
2635
|
this.emitSessionDetached(sessionId, "agent_exit");
|
|
@@ -1647,20 +2655,240 @@ var SessionManager = class {
|
|
|
1647
2655
|
modeName: record.modeName,
|
|
1648
2656
|
availableModes: record.availableModes,
|
|
1649
2657
|
availableModels: record.availableModels,
|
|
1650
|
-
availableCommands: record.availableCommands
|
|
2658
|
+
availableCommands: record.availableCommands,
|
|
2659
|
+
revision: record.revision,
|
|
2660
|
+
wrappedDek: this.cryptoService?.getWrappedDek(record.sessionId) ?? undefined
|
|
1651
2661
|
};
|
|
1652
2662
|
}
|
|
1653
|
-
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/e2ee/crypto-service.ts
|
|
2666
|
+
import {
|
|
2667
|
+
deriveAuthKeyPair as deriveAuthKeyPair2,
|
|
2668
|
+
deriveContentKeyPair,
|
|
2669
|
+
encryptPayload,
|
|
2670
|
+
generateDEK,
|
|
2671
|
+
getSodium as getSodium2,
|
|
2672
|
+
wrapDEK
|
|
2673
|
+
} from "@mobvibe/shared";
|
|
2674
|
+
|
|
2675
|
+
class CliCryptoService {
|
|
2676
|
+
authKeyPair;
|
|
2677
|
+
contentKeyPair;
|
|
2678
|
+
sessionDeks = new Map;
|
|
2679
|
+
wrappedDekCache = new Map;
|
|
2680
|
+
constructor(masterSecret) {
|
|
2681
|
+
this.authKeyPair = deriveAuthKeyPair2(masterSecret);
|
|
2682
|
+
this.contentKeyPair = deriveContentKeyPair(masterSecret);
|
|
2683
|
+
}
|
|
2684
|
+
initSessionDek(sessionId) {
|
|
2685
|
+
const dek = generateDEK();
|
|
2686
|
+
const wrappedDek = wrapDEK(dek, this.contentKeyPair.publicKey);
|
|
2687
|
+
this.sessionDeks.set(sessionId, dek);
|
|
2688
|
+
this.wrappedDekCache.set(sessionId, wrappedDek);
|
|
2689
|
+
return { dek, wrappedDek };
|
|
2690
|
+
}
|
|
2691
|
+
setSessionDek(sessionId, dek) {
|
|
2692
|
+
this.sessionDeks.set(sessionId, dek);
|
|
2693
|
+
this.wrappedDekCache.set(sessionId, wrapDEK(dek, this.contentKeyPair.publicKey));
|
|
2694
|
+
}
|
|
2695
|
+
encryptEvent(event) {
|
|
2696
|
+
const dek = this.sessionDeks.get(event.sessionId);
|
|
2697
|
+
if (!dek) {
|
|
2698
|
+
return event;
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
...event,
|
|
2702
|
+
payload: encryptPayload(event.payload, dek)
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
getWrappedDek(sessionId) {
|
|
2706
|
+
return this.wrappedDekCache.get(sessionId) ?? null;
|
|
2707
|
+
}
|
|
2708
|
+
getAuthPublicKeyBase64() {
|
|
2709
|
+
const sodium = getSodium2();
|
|
2710
|
+
return sodium.to_base64(this.authKeyPair.publicKey, sodium.base64_variants.ORIGINAL);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
1654
2713
|
|
|
1655
2714
|
// src/daemon/socket-client.ts
|
|
1656
2715
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1657
|
-
import
|
|
2716
|
+
import fs5 from "fs/promises";
|
|
1658
2717
|
import { homedir } from "os";
|
|
1659
|
-
import
|
|
2718
|
+
import path6 from "path";
|
|
2719
|
+
import { createSignedToken } from "@mobvibe/shared";
|
|
1660
2720
|
import ignore from "ignore";
|
|
1661
2721
|
import { io } from "socket.io-client";
|
|
2722
|
+
|
|
2723
|
+
// src/lib/git-utils.ts
|
|
2724
|
+
import { execFile } from "child_process";
|
|
2725
|
+
import { readFile } from "fs/promises";
|
|
2726
|
+
import path5 from "path";
|
|
2727
|
+
import { promisify } from "util";
|
|
2728
|
+
var execFileAsync = promisify(execFile);
|
|
2729
|
+
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
2730
|
+
async function isGitRepo(cwd) {
|
|
2731
|
+
try {
|
|
2732
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
2733
|
+
cwd,
|
|
2734
|
+
maxBuffer: MAX_BUFFER
|
|
2735
|
+
});
|
|
2736
|
+
return true;
|
|
2737
|
+
} catch {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
async function getGitBranch(cwd) {
|
|
2742
|
+
try {
|
|
2743
|
+
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
|
|
2744
|
+
cwd,
|
|
2745
|
+
maxBuffer: MAX_BUFFER
|
|
2746
|
+
});
|
|
2747
|
+
const branch = stdout.trim();
|
|
2748
|
+
if (branch) {
|
|
2749
|
+
return branch;
|
|
2750
|
+
}
|
|
2751
|
+
const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, maxBuffer: MAX_BUFFER });
|
|
2752
|
+
return hashOut.trim() || undefined;
|
|
2753
|
+
} catch {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
function parseGitStatus(output) {
|
|
2758
|
+
const files = [];
|
|
2759
|
+
const lines = output.split(`
|
|
2760
|
+
`).filter((line) => line.length > 0);
|
|
2761
|
+
for (const line of lines) {
|
|
2762
|
+
const indexStatus = line[0];
|
|
2763
|
+
const workTreeStatus = line[1];
|
|
2764
|
+
const filePath = line.slice(3).split(" -> ").pop()?.trim();
|
|
2765
|
+
if (!filePath) {
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
let status;
|
|
2769
|
+
if (indexStatus === "?" || workTreeStatus === "?") {
|
|
2770
|
+
status = "?";
|
|
2771
|
+
} else if (indexStatus === "!" || workTreeStatus === "!") {
|
|
2772
|
+
status = "!";
|
|
2773
|
+
} else if (indexStatus === "A" || workTreeStatus === "A") {
|
|
2774
|
+
status = "A";
|
|
2775
|
+
} else if (indexStatus === "D" || workTreeStatus === "D") {
|
|
2776
|
+
status = "D";
|
|
2777
|
+
} else if (indexStatus === "R" || workTreeStatus === "R") {
|
|
2778
|
+
status = "R";
|
|
2779
|
+
} else if (indexStatus === "C" || workTreeStatus === "C") {
|
|
2780
|
+
status = "C";
|
|
2781
|
+
} else if (indexStatus === "U" || workTreeStatus === "U") {
|
|
2782
|
+
status = "U";
|
|
2783
|
+
} else if (indexStatus === "M" || workTreeStatus === "M" || indexStatus !== " " || workTreeStatus !== " ") {
|
|
2784
|
+
status = "M";
|
|
2785
|
+
} else {
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
files.push({ path: filePath, status });
|
|
2789
|
+
}
|
|
2790
|
+
return files;
|
|
2791
|
+
}
|
|
2792
|
+
async function getGitStatus(cwd) {
|
|
2793
|
+
try {
|
|
2794
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1"], { cwd, maxBuffer: MAX_BUFFER });
|
|
2795
|
+
return parseGitStatus(stdout);
|
|
2796
|
+
} catch {
|
|
2797
|
+
return [];
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function aggregateDirStatus(files) {
|
|
2801
|
+
const dirStatus = {};
|
|
2802
|
+
const statusPriority = {
|
|
2803
|
+
A: 7,
|
|
2804
|
+
D: 6,
|
|
2805
|
+
M: 5,
|
|
2806
|
+
R: 4,
|
|
2807
|
+
C: 3,
|
|
2808
|
+
U: 2,
|
|
2809
|
+
"?": 1,
|
|
2810
|
+
"!": 0
|
|
2811
|
+
};
|
|
2812
|
+
for (const file of files) {
|
|
2813
|
+
const parts = file.path.split("/");
|
|
2814
|
+
let currentPath = "";
|
|
2815
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
2816
|
+
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
|
2817
|
+
const existing = dirStatus[currentPath];
|
|
2818
|
+
if (!existing || statusPriority[file.status] > statusPriority[existing]) {
|
|
2819
|
+
dirStatus[currentPath] = file.status;
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return dirStatus;
|
|
2824
|
+
}
|
|
2825
|
+
function parseDiffOutput(diffOutput) {
|
|
2826
|
+
const addedLines = [];
|
|
2827
|
+
const modifiedLines = [];
|
|
2828
|
+
const deletedLines = [];
|
|
2829
|
+
const lines = diffOutput.split(`
|
|
2830
|
+
`);
|
|
2831
|
+
let currentLine = 0;
|
|
2832
|
+
let inHunk = false;
|
|
2833
|
+
let pendingDeletionLine = 0;
|
|
2834
|
+
for (const line of lines) {
|
|
2835
|
+
const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
|
2836
|
+
if (hunkMatch) {
|
|
2837
|
+
currentLine = Number.parseInt(hunkMatch[1], 10);
|
|
2838
|
+
inHunk = true;
|
|
2839
|
+
pendingDeletionLine = 0;
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (!inHunk) {
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2846
|
+
addedLines.push(currentLine);
|
|
2847
|
+
currentLine++;
|
|
2848
|
+
pendingDeletionLine = 0;
|
|
2849
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2850
|
+
if (pendingDeletionLine === 0) {
|
|
2851
|
+
pendingDeletionLine = currentLine;
|
|
2852
|
+
}
|
|
2853
|
+
const deletionPos = Math.max(1, currentLine);
|
|
2854
|
+
if (!deletedLines.includes(deletionPos)) {
|
|
2855
|
+
deletedLines.push(deletionPos);
|
|
2856
|
+
}
|
|
2857
|
+
} else if (!line.startsWith("\\")) {
|
|
2858
|
+
currentLine++;
|
|
2859
|
+
pendingDeletionLine = 0;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return { addedLines, modifiedLines, deletedLines };
|
|
2863
|
+
}
|
|
2864
|
+
async function getFileDiff(cwd, filePath) {
|
|
2865
|
+
try {
|
|
2866
|
+
const relativePath = path5.isAbsolute(filePath) ? path5.relative(cwd, filePath) : filePath;
|
|
2867
|
+
const { stdout } = await execFileAsync("git", ["diff", "HEAD", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
2868
|
+
if (!stdout.trim()) {
|
|
2869
|
+
const { stdout: statusOut } = await execFileAsync("git", ["status", "--porcelain=v1", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
2870
|
+
if (statusOut.startsWith("?") || statusOut.startsWith("A")) {
|
|
2871
|
+
const absPath = path5.isAbsolute(filePath) ? filePath : path5.resolve(cwd, relativePath);
|
|
2872
|
+
const content = await readFile(absPath, "utf-8");
|
|
2873
|
+
const lineCount = content.split(`
|
|
2874
|
+
`).filter((l) => l.length > 0).length;
|
|
2875
|
+
return {
|
|
2876
|
+
addedLines: Array.from({ length: lineCount }, (_, i) => i + 1),
|
|
2877
|
+
modifiedLines: [],
|
|
2878
|
+
deletedLines: []
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
return { addedLines: [], modifiedLines: [], deletedLines: [] };
|
|
2882
|
+
}
|
|
2883
|
+
return parseDiffOutput(stdout);
|
|
2884
|
+
} catch {
|
|
2885
|
+
return { addedLines: [], modifiedLines: [], deletedLines: [] };
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/daemon/socket-client.ts
|
|
1662
2890
|
var SESSION_ROOT_NAME = "Working Directory";
|
|
1663
|
-
var MAX_RESOURCE_FILES =
|
|
2891
|
+
var MAX_RESOURCE_FILES = 2000;
|
|
1664
2892
|
var DEFAULT_IGNORES = [
|
|
1665
2893
|
"node_modules",
|
|
1666
2894
|
".git",
|
|
@@ -1678,15 +2906,14 @@ var DEFAULT_IGNORES = [
|
|
|
1678
2906
|
var loadGitignore = async (rootPath) => {
|
|
1679
2907
|
const ig = ignore().add(DEFAULT_IGNORES);
|
|
1680
2908
|
try {
|
|
1681
|
-
const gitignorePath =
|
|
1682
|
-
const content = await
|
|
2909
|
+
const gitignorePath = path6.join(rootPath, ".gitignore");
|
|
2910
|
+
const content = await fs5.readFile(gitignorePath, "utf8");
|
|
1683
2911
|
ig.add(content);
|
|
1684
|
-
} catch {
|
|
1685
|
-
}
|
|
2912
|
+
} catch {}
|
|
1686
2913
|
return ig;
|
|
1687
2914
|
};
|
|
1688
2915
|
var resolveImageMimeType = (filePath) => {
|
|
1689
|
-
const extension =
|
|
2916
|
+
const extension = path6.extname(filePath).toLowerCase();
|
|
1690
2917
|
switch (extension) {
|
|
1691
2918
|
case ".apng":
|
|
1692
2919
|
return "image/apng";
|
|
@@ -1705,31 +2932,28 @@ var resolveImageMimeType = (filePath) => {
|
|
|
1705
2932
|
case ".webp":
|
|
1706
2933
|
return "image/webp";
|
|
1707
2934
|
default:
|
|
1708
|
-
return
|
|
2935
|
+
return;
|
|
1709
2936
|
}
|
|
1710
2937
|
};
|
|
1711
2938
|
var readDirectoryEntries = async (dirPath) => {
|
|
1712
|
-
const entries = await
|
|
1713
|
-
const resolvedEntries = await Promise.all(
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
};
|
|
1731
|
-
})
|
|
1732
|
-
);
|
|
2939
|
+
const entries = await fs5.readdir(dirPath, { withFileTypes: true });
|
|
2940
|
+
const resolvedEntries = await Promise.all(entries.map(async (entry) => {
|
|
2941
|
+
const entryPath = path6.join(dirPath, entry.name);
|
|
2942
|
+
let isDirectory = entry.isDirectory();
|
|
2943
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2944
|
+
try {
|
|
2945
|
+
const stats = await fs5.stat(entryPath);
|
|
2946
|
+
isDirectory = stats.isDirectory();
|
|
2947
|
+
} catch {}
|
|
2948
|
+
}
|
|
2949
|
+
const entryType = isDirectory ? "directory" : "file";
|
|
2950
|
+
return {
|
|
2951
|
+
name: entry.name,
|
|
2952
|
+
path: entryPath,
|
|
2953
|
+
type: entryType,
|
|
2954
|
+
hidden: entry.name.startsWith(".")
|
|
2955
|
+
};
|
|
2956
|
+
}));
|
|
1733
2957
|
return resolvedEntries.sort((left, right) => {
|
|
1734
2958
|
if (left.type !== right.type) {
|
|
1735
2959
|
return left.type === "directory" ? -1 : 1;
|
|
@@ -1738,6 +2962,16 @@ var readDirectoryEntries = async (dirPath) => {
|
|
|
1738
2962
|
});
|
|
1739
2963
|
};
|
|
1740
2964
|
var filterVisibleEntries = (entries) => entries.filter((entry) => !entry.hidden);
|
|
2965
|
+
var resolveWithinCwd = (cwd, requestPath) => {
|
|
2966
|
+
if (path6.isAbsolute(requestPath)) {
|
|
2967
|
+
throw new Error("Absolute paths are not allowed");
|
|
2968
|
+
}
|
|
2969
|
+
const resolved = path6.resolve(cwd, requestPath);
|
|
2970
|
+
if (resolved !== cwd && !resolved.startsWith(`${cwd}/`)) {
|
|
2971
|
+
throw new Error("Path escapes working directory");
|
|
2972
|
+
}
|
|
2973
|
+
return resolved;
|
|
2974
|
+
};
|
|
1741
2975
|
var buildHostFsRoots = async () => {
|
|
1742
2976
|
const homePath = homedir();
|
|
1743
2977
|
return {
|
|
@@ -1745,41 +2979,46 @@ var buildHostFsRoots = async () => {
|
|
|
1745
2979
|
roots: [{ name: "Home", path: homePath }]
|
|
1746
2980
|
};
|
|
1747
2981
|
};
|
|
1748
|
-
|
|
2982
|
+
|
|
2983
|
+
class SocketClient extends EventEmitter3 {
|
|
2984
|
+
options;
|
|
2985
|
+
socket;
|
|
2986
|
+
connected = false;
|
|
2987
|
+
reconnectAttempts = 0;
|
|
2988
|
+
heartbeatInterval;
|
|
1749
2989
|
constructor(options) {
|
|
1750
2990
|
super();
|
|
1751
2991
|
this.options = options;
|
|
2992
|
+
const { cryptoService } = options;
|
|
1752
2993
|
this.socket = io(`${options.config.gatewayUrl}/cli`, {
|
|
1753
2994
|
path: "/socket.io",
|
|
1754
2995
|
reconnection: true,
|
|
1755
2996
|
reconnectionAttempts: Number.POSITIVE_INFINITY,
|
|
1756
|
-
reconnectionDelay:
|
|
1757
|
-
reconnectionDelayMax:
|
|
2997
|
+
reconnectionDelay: 1000,
|
|
2998
|
+
reconnectionDelayMax: 30000,
|
|
1758
2999
|
transports: ["websocket"],
|
|
1759
3000
|
autoConnect: false,
|
|
1760
|
-
|
|
1761
|
-
"x-api-key": options.apiKey
|
|
1762
|
-
}
|
|
3001
|
+
auth: (cb) => cb(createSignedToken(cryptoService.authKeyPair))
|
|
1763
3002
|
});
|
|
1764
3003
|
this.setupEventHandlers();
|
|
1765
3004
|
this.setupRpcHandlers();
|
|
1766
3005
|
this.setupSessionManagerListeners();
|
|
1767
3006
|
}
|
|
1768
|
-
socket;
|
|
1769
|
-
connected = false;
|
|
1770
|
-
reconnectAttempts = 0;
|
|
1771
|
-
heartbeatInterval;
|
|
1772
3007
|
setupEventHandlers() {
|
|
1773
3008
|
this.socket.on("connect", () => {
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
3009
|
+
const wasReconnect = this.reconnectAttempts > 0;
|
|
3010
|
+
logger.info({
|
|
3011
|
+
gatewayUrl: this.options.config.gatewayUrl,
|
|
3012
|
+
wasReconnect
|
|
3013
|
+
}, "gateway_connected");
|
|
1778
3014
|
this.connected = true;
|
|
1779
3015
|
this.reconnectAttempts = 0;
|
|
1780
3016
|
logger.info("gateway_register_start");
|
|
1781
3017
|
this.register();
|
|
1782
3018
|
this.startHeartbeat();
|
|
3019
|
+
if (wasReconnect) {
|
|
3020
|
+
this.replayUnackedEvents();
|
|
3021
|
+
}
|
|
1783
3022
|
this.emit("connected");
|
|
1784
3023
|
});
|
|
1785
3024
|
this.socket.on("disconnect", (reason) => {
|
|
@@ -1791,41 +3030,55 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
1791
3030
|
this.socket.on("connect_error", (error) => {
|
|
1792
3031
|
this.reconnectAttempts++;
|
|
1793
3032
|
if (this.reconnectAttempts <= 3 || this.reconnectAttempts % 10 === 0) {
|
|
1794
|
-
logger.error(
|
|
1795
|
-
{ attempt: this.reconnectAttempts, err: error },
|
|
1796
|
-
"gateway_connect_error"
|
|
1797
|
-
);
|
|
3033
|
+
logger.error({ attempt: this.reconnectAttempts, err: error }, "gateway_connect_error");
|
|
1798
3034
|
}
|
|
1799
3035
|
});
|
|
1800
3036
|
this.socket.on("cli:registered", async (info) => {
|
|
1801
3037
|
logger.info({ machineId: info.machineId }, "gateway_registered");
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
sessions,
|
|
1811
|
-
capabilities,
|
|
1812
|
-
nextCursor
|
|
3038
|
+
for (const backend of this.options.config.acpBackends) {
|
|
3039
|
+
try {
|
|
3040
|
+
let cursor;
|
|
3041
|
+
let page = 0;
|
|
3042
|
+
do {
|
|
3043
|
+
const { sessions, capabilities, nextCursor } = await this.options.sessionManager.discoverSessions({
|
|
3044
|
+
backendId: backend.id,
|
|
3045
|
+
cursor
|
|
1813
3046
|
});
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
"
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
3047
|
+
cursor = nextCursor;
|
|
3048
|
+
if (sessions.length > 0) {
|
|
3049
|
+
this.socket.emit("sessions:discovered", {
|
|
3050
|
+
sessions,
|
|
3051
|
+
capabilities,
|
|
3052
|
+
nextCursor,
|
|
3053
|
+
backendId: backend.id,
|
|
3054
|
+
backendLabel: backend.label
|
|
3055
|
+
});
|
|
3056
|
+
logger.info({
|
|
3057
|
+
count: sessions.length,
|
|
3058
|
+
capabilities,
|
|
3059
|
+
page,
|
|
3060
|
+
backendId: backend.id
|
|
3061
|
+
}, "historical_sessions_discovered");
|
|
3062
|
+
}
|
|
3063
|
+
page += 1;
|
|
3064
|
+
} while (cursor);
|
|
3065
|
+
} catch (error) {
|
|
3066
|
+
logger.warn({ err: error, backendId: backend.id }, "session_discovery_failed");
|
|
3067
|
+
}
|
|
1823
3068
|
}
|
|
1824
3069
|
});
|
|
1825
3070
|
this.socket.on("cli:error", (error) => {
|
|
1826
3071
|
logger.error({ err: error }, "gateway_auth_error");
|
|
1827
3072
|
this.emit("auth_error", error);
|
|
1828
3073
|
});
|
|
3074
|
+
this.socket.on("events:ack", (payload) => {
|
|
3075
|
+
logger.debug({
|
|
3076
|
+
sessionId: payload.sessionId,
|
|
3077
|
+
revision: payload.revision,
|
|
3078
|
+
upToSeq: payload.upToSeq
|
|
3079
|
+
}, "events_acked");
|
|
3080
|
+
this.options.sessionManager.ackEvents(payload.sessionId, payload.revision, payload.upToSeq);
|
|
3081
|
+
});
|
|
1829
3082
|
}
|
|
1830
3083
|
setupRpcHandlers() {
|
|
1831
3084
|
const { sessionManager } = this.options;
|
|
@@ -1835,106 +3088,73 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
1835
3088
|
const session = await sessionManager.createSession(request.params);
|
|
1836
3089
|
this.sendRpcResponse(request.requestId, session);
|
|
1837
3090
|
} catch (error) {
|
|
1838
|
-
logger.error(
|
|
1839
|
-
{ err: error, requestId: request.requestId },
|
|
1840
|
-
"rpc_session_create_error"
|
|
1841
|
-
);
|
|
3091
|
+
logger.error({ err: error, requestId: request.requestId }, "rpc_session_create_error");
|
|
1842
3092
|
this.sendRpcError(request.requestId, error);
|
|
1843
3093
|
}
|
|
1844
3094
|
});
|
|
1845
3095
|
this.socket.on("rpc:session:close", async (request) => {
|
|
1846
3096
|
try {
|
|
1847
|
-
logger.info(
|
|
1848
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
1849
|
-
"rpc_session_close"
|
|
1850
|
-
);
|
|
3097
|
+
logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_close");
|
|
1851
3098
|
await sessionManager.closeSession(request.params.sessionId);
|
|
1852
3099
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
1853
3100
|
} catch (error) {
|
|
1854
|
-
logger.error(
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
},
|
|
1860
|
-
"rpc_session_close_error"
|
|
1861
|
-
);
|
|
3101
|
+
logger.error({
|
|
3102
|
+
err: error,
|
|
3103
|
+
requestId: request.requestId,
|
|
3104
|
+
sessionId: request.params.sessionId
|
|
3105
|
+
}, "rpc_session_close_error");
|
|
1862
3106
|
this.sendRpcError(request.requestId, error);
|
|
1863
3107
|
}
|
|
1864
3108
|
});
|
|
1865
3109
|
this.socket.on("rpc:session:cancel", async (request) => {
|
|
1866
3110
|
try {
|
|
1867
|
-
logger.info(
|
|
1868
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
1869
|
-
"rpc_session_cancel"
|
|
1870
|
-
);
|
|
3111
|
+
logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_cancel");
|
|
1871
3112
|
await sessionManager.cancelSession(request.params.sessionId);
|
|
1872
3113
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
1873
3114
|
} catch (error) {
|
|
1874
|
-
logger.error(
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
},
|
|
1880
|
-
"rpc_session_cancel_error"
|
|
1881
|
-
);
|
|
3115
|
+
logger.error({
|
|
3116
|
+
err: error,
|
|
3117
|
+
requestId: request.requestId,
|
|
3118
|
+
sessionId: request.params.sessionId
|
|
3119
|
+
}, "rpc_session_cancel_error");
|
|
1882
3120
|
this.sendRpcError(request.requestId, error);
|
|
1883
3121
|
}
|
|
1884
3122
|
});
|
|
1885
3123
|
this.socket.on("rpc:session:mode", async (request) => {
|
|
1886
3124
|
try {
|
|
1887
|
-
logger.info(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
"rpc_session_mode"
|
|
1894
|
-
);
|
|
1895
|
-
const session = await sessionManager.setSessionMode(
|
|
1896
|
-
request.params.sessionId,
|
|
1897
|
-
request.params.modeId
|
|
1898
|
-
);
|
|
3125
|
+
logger.info({
|
|
3126
|
+
requestId: request.requestId,
|
|
3127
|
+
sessionId: request.params.sessionId,
|
|
3128
|
+
modeId: request.params.modeId
|
|
3129
|
+
}, "rpc_session_mode");
|
|
3130
|
+
const session = await sessionManager.setSessionMode(request.params.sessionId, request.params.modeId);
|
|
1899
3131
|
this.sendRpcResponse(request.requestId, session);
|
|
1900
3132
|
} catch (error) {
|
|
1901
|
-
logger.error(
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
},
|
|
1908
|
-
"rpc_session_mode_error"
|
|
1909
|
-
);
|
|
3133
|
+
logger.error({
|
|
3134
|
+
err: error,
|
|
3135
|
+
requestId: request.requestId,
|
|
3136
|
+
sessionId: request.params.sessionId,
|
|
3137
|
+
modeId: request.params.modeId
|
|
3138
|
+
}, "rpc_session_mode_error");
|
|
1910
3139
|
this.sendRpcError(request.requestId, error);
|
|
1911
3140
|
}
|
|
1912
3141
|
});
|
|
1913
3142
|
this.socket.on("rpc:session:model", async (request) => {
|
|
1914
3143
|
try {
|
|
1915
|
-
logger.info(
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
"rpc_session_model"
|
|
1922
|
-
);
|
|
1923
|
-
const session = await sessionManager.setSessionModel(
|
|
1924
|
-
request.params.sessionId,
|
|
1925
|
-
request.params.modelId
|
|
1926
|
-
);
|
|
3144
|
+
logger.info({
|
|
3145
|
+
requestId: request.requestId,
|
|
3146
|
+
sessionId: request.params.sessionId,
|
|
3147
|
+
modelId: request.params.modelId
|
|
3148
|
+
}, "rpc_session_model");
|
|
3149
|
+
const session = await sessionManager.setSessionModel(request.params.sessionId, request.params.modelId);
|
|
1927
3150
|
this.sendRpcResponse(request.requestId, session);
|
|
1928
3151
|
} catch (error) {
|
|
1929
|
-
logger.error(
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
},
|
|
1936
|
-
"rpc_session_model_error"
|
|
1937
|
-
);
|
|
3152
|
+
logger.error({
|
|
3153
|
+
err: error,
|
|
3154
|
+
requestId: request.requestId,
|
|
3155
|
+
sessionId: request.params.sessionId,
|
|
3156
|
+
modelId: request.params.modelId
|
|
3157
|
+
}, "rpc_session_model_error");
|
|
1938
3158
|
this.sendRpcError(request.requestId, error);
|
|
1939
3159
|
}
|
|
1940
3160
|
});
|
|
@@ -1942,97 +3162,71 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
1942
3162
|
const requestStart = process.hrtime.bigint();
|
|
1943
3163
|
try {
|
|
1944
3164
|
const { sessionId, prompt } = request.params;
|
|
1945
|
-
logger.info(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
requestId: request.requestId,
|
|
1956
|
-
sessionId,
|
|
1957
|
-
promptBlocks: prompt.length
|
|
1958
|
-
},
|
|
1959
|
-
"rpc_message_send_start"
|
|
1960
|
-
);
|
|
3165
|
+
logger.info({
|
|
3166
|
+
requestId: request.requestId,
|
|
3167
|
+
sessionId,
|
|
3168
|
+
promptBlocks: prompt.length
|
|
3169
|
+
}, "rpc_message_send");
|
|
3170
|
+
logger.debug({
|
|
3171
|
+
requestId: request.requestId,
|
|
3172
|
+
sessionId,
|
|
3173
|
+
promptBlocks: prompt.length
|
|
3174
|
+
}, "rpc_message_send_start");
|
|
1961
3175
|
const record = sessionManager.getSession(sessionId);
|
|
1962
3176
|
if (!record) {
|
|
1963
3177
|
throw new Error("Session not found");
|
|
1964
3178
|
}
|
|
1965
3179
|
sessionManager.touchSession(sessionId);
|
|
1966
|
-
const result = await record.connection.prompt(
|
|
1967
|
-
sessionId,
|
|
1968
|
-
prompt
|
|
1969
|
-
);
|
|
3180
|
+
const result = await record.connection.prompt(sessionId, prompt);
|
|
1970
3181
|
sessionManager.touchSession(sessionId);
|
|
3182
|
+
sessionManager.recordTurnEnd(sessionId, result.stopReason);
|
|
1971
3183
|
this.sendRpcResponse(request.requestId, {
|
|
1972
3184
|
stopReason: result.stopReason
|
|
1973
3185
|
});
|
|
1974
3186
|
const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
|
|
1975
|
-
logger.info(
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
requestId: request.requestId,
|
|
1987
|
-
sessionId,
|
|
1988
|
-
durationMs
|
|
1989
|
-
},
|
|
1990
|
-
"rpc_message_send_finish"
|
|
1991
|
-
);
|
|
3187
|
+
logger.info({
|
|
3188
|
+
requestId: request.requestId,
|
|
3189
|
+
sessionId,
|
|
3190
|
+
stopReason: result.stopReason,
|
|
3191
|
+
durationMs
|
|
3192
|
+
}, "rpc_message_send_complete");
|
|
3193
|
+
logger.debug({
|
|
3194
|
+
requestId: request.requestId,
|
|
3195
|
+
sessionId,
|
|
3196
|
+
durationMs
|
|
3197
|
+
}, "rpc_message_send_finish");
|
|
1992
3198
|
} catch (error) {
|
|
1993
3199
|
const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
|
|
1994
|
-
logger.error(
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
},
|
|
2002
|
-
"rpc_message_send_error"
|
|
2003
|
-
);
|
|
3200
|
+
logger.error({
|
|
3201
|
+
err: error,
|
|
3202
|
+
requestId: request.requestId,
|
|
3203
|
+
sessionId: request.params.sessionId,
|
|
3204
|
+
promptBlocks: request.params.prompt.length,
|
|
3205
|
+
durationMs
|
|
3206
|
+
}, "rpc_message_send_error");
|
|
2004
3207
|
this.sendRpcError(request.requestId, error);
|
|
2005
3208
|
}
|
|
2006
3209
|
});
|
|
2007
3210
|
this.socket.on("rpc:permission:decision", async (request) => {
|
|
2008
3211
|
try {
|
|
2009
3212
|
const { sessionId, requestId, outcome } = request.params;
|
|
2010
|
-
logger.info(
|
|
2011
|
-
{ requestId: request.requestId, sessionId, outcome },
|
|
2012
|
-
"rpc_permission_decision"
|
|
2013
|
-
);
|
|
3213
|
+
logger.info({ requestId: request.requestId, sessionId, outcome }, "rpc_permission_decision");
|
|
2014
3214
|
sessionManager.resolvePermissionRequest(sessionId, requestId, outcome);
|
|
2015
3215
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
2016
3216
|
} catch (error) {
|
|
2017
|
-
logger.error(
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
},
|
|
2025
|
-
"rpc_permission_decision_error"
|
|
2026
|
-
);
|
|
3217
|
+
logger.error({
|
|
3218
|
+
err: error,
|
|
3219
|
+
requestId: request.requestId,
|
|
3220
|
+
sessionId: request.params.sessionId,
|
|
3221
|
+
permissionRequestId: request.params.requestId,
|
|
3222
|
+
outcome: request.params.outcome
|
|
3223
|
+
}, "rpc_permission_decision_error");
|
|
2027
3224
|
this.sendRpcError(request.requestId, error);
|
|
2028
3225
|
}
|
|
2029
3226
|
});
|
|
2030
3227
|
this.socket.on("rpc:fs:roots", async (request) => {
|
|
2031
3228
|
try {
|
|
2032
|
-
logger.debug(
|
|
2033
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
2034
|
-
"rpc_fs_roots"
|
|
2035
|
-
);
|
|
3229
|
+
logger.debug({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_fs_roots");
|
|
2036
3230
|
const record = sessionManager.getSession(request.params.sessionId);
|
|
2037
3231
|
if (!record || !record.cwd) {
|
|
2038
3232
|
throw new Error("Session not found or no working directory");
|
|
@@ -2043,105 +3237,81 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2043
3237
|
};
|
|
2044
3238
|
this.sendRpcResponse(request.requestId, { root });
|
|
2045
3239
|
} catch (error) {
|
|
2046
|
-
logger.error(
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
},
|
|
2052
|
-
"rpc_fs_roots_error"
|
|
2053
|
-
);
|
|
3240
|
+
logger.error({
|
|
3241
|
+
err: error,
|
|
3242
|
+
requestId: request.requestId,
|
|
3243
|
+
sessionId: request.params.sessionId
|
|
3244
|
+
}, "rpc_fs_roots_error");
|
|
2054
3245
|
this.sendRpcError(request.requestId, error);
|
|
2055
3246
|
}
|
|
2056
3247
|
});
|
|
2057
3248
|
this.socket.on("rpc:hostfs:roots", async (request) => {
|
|
2058
3249
|
try {
|
|
2059
|
-
logger.debug(
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
},
|
|
2064
|
-
"rpc_hostfs_roots"
|
|
2065
|
-
);
|
|
3250
|
+
logger.debug({
|
|
3251
|
+
requestId: request.requestId,
|
|
3252
|
+
machineId: request.params.machineId
|
|
3253
|
+
}, "rpc_hostfs_roots");
|
|
2066
3254
|
const result = await buildHostFsRoots();
|
|
2067
3255
|
this.sendRpcResponse(request.requestId, result);
|
|
2068
3256
|
} catch (error) {
|
|
2069
|
-
logger.error(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
},
|
|
2075
|
-
"rpc_hostfs_roots_error"
|
|
2076
|
-
);
|
|
3257
|
+
logger.error({
|
|
3258
|
+
err: error,
|
|
3259
|
+
requestId: request.requestId,
|
|
3260
|
+
machineId: request.params.machineId
|
|
3261
|
+
}, "rpc_hostfs_roots_error");
|
|
2077
3262
|
this.sendRpcError(request.requestId, error);
|
|
2078
3263
|
}
|
|
2079
3264
|
});
|
|
2080
3265
|
this.socket.on("rpc:hostfs:entries", async (request) => {
|
|
2081
3266
|
try {
|
|
2082
3267
|
const { path: requestPath, machineId } = request.params;
|
|
2083
|
-
logger.debug(
|
|
2084
|
-
{ requestId: request.requestId, machineId, path: requestPath },
|
|
2085
|
-
"rpc_hostfs_entries"
|
|
2086
|
-
);
|
|
3268
|
+
logger.debug({ requestId: request.requestId, machineId, path: requestPath }, "rpc_hostfs_entries");
|
|
2087
3269
|
const entries = await readDirectoryEntries(requestPath);
|
|
2088
3270
|
this.sendRpcResponse(request.requestId, {
|
|
2089
3271
|
path: requestPath,
|
|
2090
3272
|
entries: filterVisibleEntries(entries)
|
|
2091
3273
|
});
|
|
2092
3274
|
} catch (error) {
|
|
2093
|
-
logger.error(
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
},
|
|
2099
|
-
"rpc_hostfs_entries_error"
|
|
2100
|
-
);
|
|
3275
|
+
logger.error({
|
|
3276
|
+
err: error,
|
|
3277
|
+
requestId: request.requestId,
|
|
3278
|
+
machineId: request.params.machineId
|
|
3279
|
+
}, "rpc_hostfs_entries_error");
|
|
2101
3280
|
this.sendRpcError(request.requestId, error);
|
|
2102
3281
|
}
|
|
2103
3282
|
});
|
|
2104
3283
|
this.socket.on("rpc:fs:entries", async (request) => {
|
|
2105
3284
|
try {
|
|
2106
3285
|
const { sessionId, path: requestPath } = request.params;
|
|
2107
|
-
logger.debug(
|
|
2108
|
-
{ requestId: request.requestId, sessionId, path: requestPath },
|
|
2109
|
-
"rpc_fs_entries"
|
|
2110
|
-
);
|
|
3286
|
+
logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_entries");
|
|
2111
3287
|
const record = sessionManager.getSession(sessionId);
|
|
2112
3288
|
if (!record || !record.cwd) {
|
|
2113
3289
|
throw new Error("Session not found or no working directory");
|
|
2114
3290
|
}
|
|
2115
|
-
const resolved = requestPath ?
|
|
3291
|
+
const resolved = requestPath ? resolveWithinCwd(record.cwd, requestPath) : record.cwd;
|
|
2116
3292
|
const entries = await readDirectoryEntries(resolved);
|
|
2117
3293
|
this.sendRpcResponse(request.requestId, { path: resolved, entries });
|
|
2118
3294
|
} catch (error) {
|
|
2119
|
-
logger.error(
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
},
|
|
2125
|
-
"rpc_fs_entries_error"
|
|
2126
|
-
);
|
|
3295
|
+
logger.error({
|
|
3296
|
+
err: error,
|
|
3297
|
+
requestId: request.requestId,
|
|
3298
|
+
sessionId: request.params.sessionId
|
|
3299
|
+
}, "rpc_fs_entries_error");
|
|
2127
3300
|
this.sendRpcError(request.requestId, error);
|
|
2128
3301
|
}
|
|
2129
3302
|
});
|
|
2130
3303
|
this.socket.on("rpc:fs:file", async (request) => {
|
|
2131
3304
|
try {
|
|
2132
3305
|
const { sessionId, path: requestPath } = request.params;
|
|
2133
|
-
logger.debug(
|
|
2134
|
-
{ requestId: request.requestId, sessionId, path: requestPath },
|
|
2135
|
-
"rpc_fs_file"
|
|
2136
|
-
);
|
|
3306
|
+
logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_file");
|
|
2137
3307
|
const record = sessionManager.getSession(sessionId);
|
|
2138
3308
|
if (!record || !record.cwd) {
|
|
2139
3309
|
throw new Error("Session not found or no working directory");
|
|
2140
3310
|
}
|
|
2141
|
-
const resolved =
|
|
3311
|
+
const resolved = resolveWithinCwd(record.cwd, requestPath);
|
|
2142
3312
|
const mimeType = resolveImageMimeType(resolved);
|
|
2143
3313
|
if (mimeType) {
|
|
2144
|
-
const buffer = await
|
|
3314
|
+
const buffer = await fs5.readFile(resolved);
|
|
2145
3315
|
const preview2 = {
|
|
2146
3316
|
path: resolved,
|
|
2147
3317
|
previewType: "image",
|
|
@@ -2151,7 +3321,7 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2151
3321
|
this.sendRpcResponse(request.requestId, preview2);
|
|
2152
3322
|
return;
|
|
2153
3323
|
}
|
|
2154
|
-
const content = await
|
|
3324
|
+
const content = await fs5.readFile(resolved, "utf8");
|
|
2155
3325
|
const preview = {
|
|
2156
3326
|
path: resolved,
|
|
2157
3327
|
previewType: "code",
|
|
@@ -2159,24 +3329,18 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2159
3329
|
};
|
|
2160
3330
|
this.sendRpcResponse(request.requestId, preview);
|
|
2161
3331
|
} catch (error) {
|
|
2162
|
-
logger.error(
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
},
|
|
2168
|
-
"rpc_fs_file_error"
|
|
2169
|
-
);
|
|
3332
|
+
logger.error({
|
|
3333
|
+
err: error,
|
|
3334
|
+
requestId: request.requestId,
|
|
3335
|
+
sessionId: request.params.sessionId
|
|
3336
|
+
}, "rpc_fs_file_error");
|
|
2170
3337
|
this.sendRpcError(request.requestId, error);
|
|
2171
3338
|
}
|
|
2172
3339
|
});
|
|
2173
3340
|
this.socket.on("rpc:fs:resources", async (request) => {
|
|
2174
3341
|
try {
|
|
2175
3342
|
const { sessionId } = request.params;
|
|
2176
|
-
logger.debug(
|
|
2177
|
-
{ requestId: request.requestId, sessionId },
|
|
2178
|
-
"rpc_fs_resources"
|
|
2179
|
-
);
|
|
3343
|
+
logger.debug({ requestId: request.requestId, sessionId }, "rpc_fs_resources");
|
|
2180
3344
|
const record = sessionManager.getSession(sessionId);
|
|
2181
3345
|
if (!record || !record.cwd) {
|
|
2182
3346
|
throw new Error("Session not found or no working directory");
|
|
@@ -2187,24 +3351,18 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2187
3351
|
entries
|
|
2188
3352
|
});
|
|
2189
3353
|
} catch (error) {
|
|
2190
|
-
logger.error(
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
},
|
|
2196
|
-
"rpc_fs_resources_error"
|
|
2197
|
-
);
|
|
3354
|
+
logger.error({
|
|
3355
|
+
err: error,
|
|
3356
|
+
requestId: request.requestId,
|
|
3357
|
+
sessionId: request.params.sessionId
|
|
3358
|
+
}, "rpc_fs_resources_error");
|
|
2198
3359
|
this.sendRpcError(request.requestId, error);
|
|
2199
3360
|
}
|
|
2200
3361
|
});
|
|
2201
3362
|
this.socket.on("rpc:sessions:discover", async (request) => {
|
|
2202
3363
|
try {
|
|
2203
3364
|
const { cwd, backendId, cursor } = request.params;
|
|
2204
|
-
logger.info(
|
|
2205
|
-
{ requestId: request.requestId, cwd, backendId, cursor },
|
|
2206
|
-
"rpc_sessions_discover"
|
|
2207
|
-
);
|
|
3365
|
+
logger.info({ requestId: request.requestId, cwd, backendId, cursor }, "rpc_sessions_discover");
|
|
2208
3366
|
const result = await sessionManager.discoverSessions({
|
|
2209
3367
|
cwd,
|
|
2210
3368
|
backendId,
|
|
@@ -2212,53 +3370,177 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2212
3370
|
});
|
|
2213
3371
|
this.sendRpcResponse(request.requestId, result);
|
|
2214
3372
|
} catch (error) {
|
|
2215
|
-
logger.error(
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
},
|
|
2220
|
-
"rpc_sessions_discover_error"
|
|
2221
|
-
);
|
|
3373
|
+
logger.error({
|
|
3374
|
+
err: error,
|
|
3375
|
+
requestId: request.requestId
|
|
3376
|
+
}, "rpc_sessions_discover_error");
|
|
2222
3377
|
this.sendRpcError(request.requestId, error);
|
|
2223
3378
|
}
|
|
2224
3379
|
});
|
|
2225
3380
|
this.socket.on("rpc:session:load", async (request) => {
|
|
2226
3381
|
try {
|
|
2227
|
-
const { sessionId, cwd } = request.params;
|
|
2228
|
-
logger.info(
|
|
2229
|
-
|
|
2230
|
-
"rpc_session_load"
|
|
2231
|
-
);
|
|
2232
|
-
const session = await sessionManager.loadSession(sessionId, cwd);
|
|
3382
|
+
const { sessionId, cwd, backendId } = request.params;
|
|
3383
|
+
logger.info({ requestId: request.requestId, sessionId, cwd, backendId }, "rpc_session_load");
|
|
3384
|
+
const session = await sessionManager.loadSession(sessionId, cwd, backendId);
|
|
2233
3385
|
this.sendRpcResponse(request.requestId, session);
|
|
2234
3386
|
} catch (error) {
|
|
2235
|
-
logger.error(
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
},
|
|
2241
|
-
"rpc_session_load_error"
|
|
2242
|
-
);
|
|
3387
|
+
logger.error({
|
|
3388
|
+
err: error,
|
|
3389
|
+
requestId: request.requestId,
|
|
3390
|
+
sessionId: request.params.sessionId
|
|
3391
|
+
}, "rpc_session_load_error");
|
|
2243
3392
|
this.sendRpcError(request.requestId, error);
|
|
2244
3393
|
}
|
|
2245
3394
|
});
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
this.
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
3395
|
+
this.socket.on("rpc:session:reload", async (request) => {
|
|
3396
|
+
try {
|
|
3397
|
+
const { sessionId, cwd, backendId } = request.params;
|
|
3398
|
+
logger.info({ requestId: request.requestId, sessionId, cwd, backendId }, "rpc_session_reload");
|
|
3399
|
+
const session = await sessionManager.reloadSession(sessionId, cwd, backendId);
|
|
3400
|
+
this.sendRpcResponse(request.requestId, session);
|
|
3401
|
+
} catch (error) {
|
|
3402
|
+
logger.error({
|
|
3403
|
+
err: error,
|
|
3404
|
+
requestId: request.requestId,
|
|
3405
|
+
sessionId: request.params.sessionId
|
|
3406
|
+
}, "rpc_session_reload_error");
|
|
3407
|
+
this.sendRpcError(request.requestId, error);
|
|
2255
3408
|
}
|
|
2256
3409
|
});
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
3410
|
+
this.socket.on("rpc:git:status", async (request) => {
|
|
3411
|
+
try {
|
|
3412
|
+
const { sessionId } = request.params;
|
|
3413
|
+
logger.debug({ requestId: request.requestId, sessionId }, "rpc_git_status");
|
|
3414
|
+
const record = sessionManager.getSession(sessionId);
|
|
3415
|
+
if (!record || !record.cwd) {
|
|
3416
|
+
throw new Error("Session not found or no working directory");
|
|
3417
|
+
}
|
|
3418
|
+
const isRepo = await isGitRepo(record.cwd);
|
|
3419
|
+
if (!isRepo) {
|
|
3420
|
+
this.sendRpcResponse(request.requestId, {
|
|
3421
|
+
isGitRepo: false,
|
|
3422
|
+
files: [],
|
|
3423
|
+
dirStatus: {}
|
|
3424
|
+
});
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
const [branch, files] = await Promise.all([
|
|
3428
|
+
getGitBranch(record.cwd),
|
|
3429
|
+
getGitStatus(record.cwd)
|
|
3430
|
+
]);
|
|
3431
|
+
const dirStatus = aggregateDirStatus(files);
|
|
3432
|
+
this.sendRpcResponse(request.requestId, {
|
|
3433
|
+
isGitRepo: true,
|
|
3434
|
+
branch,
|
|
3435
|
+
files,
|
|
3436
|
+
dirStatus
|
|
3437
|
+
});
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
logger.error({
|
|
3440
|
+
err: error,
|
|
3441
|
+
requestId: request.requestId,
|
|
3442
|
+
sessionId: request.params.sessionId
|
|
3443
|
+
}, "rpc_git_status_error");
|
|
3444
|
+
this.sendRpcError(request.requestId, error);
|
|
3445
|
+
}
|
|
3446
|
+
});
|
|
3447
|
+
this.socket.on("rpc:git:fileDiff", async (request) => {
|
|
3448
|
+
try {
|
|
3449
|
+
const { sessionId, path: filePath } = request.params;
|
|
3450
|
+
logger.debug({ requestId: request.requestId, sessionId, path: filePath }, "rpc_git_file_diff");
|
|
3451
|
+
const record = sessionManager.getSession(sessionId);
|
|
3452
|
+
if (!record || !record.cwd) {
|
|
3453
|
+
throw new Error("Session not found or no working directory");
|
|
3454
|
+
}
|
|
3455
|
+
resolveWithinCwd(record.cwd, filePath);
|
|
3456
|
+
const isRepo = await isGitRepo(record.cwd);
|
|
3457
|
+
if (!isRepo) {
|
|
3458
|
+
this.sendRpcResponse(request.requestId, {
|
|
3459
|
+
isGitRepo: false,
|
|
3460
|
+
path: filePath,
|
|
3461
|
+
addedLines: [],
|
|
3462
|
+
modifiedLines: []
|
|
3463
|
+
});
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
const { addedLines, modifiedLines } = await getFileDiff(record.cwd, filePath);
|
|
3467
|
+
this.sendRpcResponse(request.requestId, {
|
|
3468
|
+
isGitRepo: true,
|
|
3469
|
+
path: filePath,
|
|
3470
|
+
addedLines,
|
|
3471
|
+
modifiedLines
|
|
3472
|
+
});
|
|
3473
|
+
} catch (error) {
|
|
3474
|
+
logger.error({
|
|
3475
|
+
err: error,
|
|
3476
|
+
requestId: request.requestId,
|
|
3477
|
+
sessionId: request.params.sessionId
|
|
3478
|
+
}, "rpc_git_file_diff_error");
|
|
3479
|
+
this.sendRpcError(request.requestId, error);
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
this.socket.on("rpc:session:archive", async (request) => {
|
|
3483
|
+
try {
|
|
3484
|
+
const { sessionId } = request.params;
|
|
3485
|
+
logger.info({ requestId: request.requestId, sessionId }, "rpc_session_archive");
|
|
3486
|
+
await sessionManager.archiveSession(sessionId);
|
|
3487
|
+
this.sendRpcResponse(request.requestId, { ok: true });
|
|
3488
|
+
} catch (error) {
|
|
3489
|
+
logger.error({
|
|
3490
|
+
err: error,
|
|
3491
|
+
requestId: request.requestId,
|
|
3492
|
+
sessionId: request.params.sessionId
|
|
3493
|
+
}, "rpc_session_archive_error");
|
|
3494
|
+
this.sendRpcError(request.requestId, error);
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
this.socket.on("rpc:session:archive-all", async (request) => {
|
|
3498
|
+
try {
|
|
3499
|
+
const { sessionIds } = request.params;
|
|
3500
|
+
logger.info({
|
|
3501
|
+
requestId: request.requestId,
|
|
3502
|
+
count: sessionIds.length
|
|
3503
|
+
}, "rpc_session_archive_all");
|
|
3504
|
+
const result = await sessionManager.bulkArchiveSessions(sessionIds);
|
|
3505
|
+
this.sendRpcResponse(request.requestId, result);
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
logger.error({
|
|
3508
|
+
err: error,
|
|
3509
|
+
requestId: request.requestId
|
|
3510
|
+
}, "rpc_session_archive_all_error");
|
|
3511
|
+
this.sendRpcError(request.requestId, error);
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
this.socket.on("rpc:session:events", (request) => {
|
|
3515
|
+
try {
|
|
3516
|
+
const { sessionId, revision, afterSeq, limit } = request.params;
|
|
3517
|
+
logger.debug({
|
|
3518
|
+
requestId: request.requestId,
|
|
3519
|
+
sessionId,
|
|
3520
|
+
revision,
|
|
3521
|
+
afterSeq,
|
|
3522
|
+
limit
|
|
3523
|
+
}, "rpc_session_events");
|
|
3524
|
+
const result = sessionManager.getSessionEvents({
|
|
3525
|
+
sessionId,
|
|
3526
|
+
revision,
|
|
3527
|
+
afterSeq,
|
|
3528
|
+
limit
|
|
3529
|
+
});
|
|
3530
|
+
result.events = result.events.map((e) => this.options.cryptoService.encryptEvent(e));
|
|
3531
|
+
this.sendRpcResponse(request.requestId, result);
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
logger.error({
|
|
3534
|
+
err: error,
|
|
3535
|
+
requestId: request.requestId,
|
|
3536
|
+
sessionId: request.params.sessionId
|
|
3537
|
+
}, "rpc_session_events_error");
|
|
3538
|
+
this.sendRpcError(request.requestId, error);
|
|
2260
3539
|
}
|
|
2261
3540
|
});
|
|
3541
|
+
}
|
|
3542
|
+
setupSessionManagerListeners() {
|
|
3543
|
+
const { sessionManager } = this.options;
|
|
2262
3544
|
sessionManager.onPermissionRequest((payload) => {
|
|
2263
3545
|
if (this.connected) {
|
|
2264
3546
|
this.socket.emit("permission:request", payload);
|
|
@@ -2269,21 +3551,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2269
3551
|
this.socket.emit("permission:result", payload);
|
|
2270
3552
|
}
|
|
2271
3553
|
});
|
|
2272
|
-
sessionManager.onTerminalOutput((event) => {
|
|
2273
|
-
if (this.connected) {
|
|
2274
|
-
this.socket.emit("terminal:output", event);
|
|
2275
|
-
}
|
|
2276
|
-
});
|
|
2277
3554
|
sessionManager.onSessionsChanged((payload) => {
|
|
2278
3555
|
if (this.connected) {
|
|
2279
|
-
logger.info(
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
},
|
|
2285
|
-
"sessions_changed_emit"
|
|
2286
|
-
);
|
|
3556
|
+
logger.info({
|
|
3557
|
+
added: payload.added.length,
|
|
3558
|
+
updated: payload.updated.length,
|
|
3559
|
+
removed: payload.removed.length
|
|
3560
|
+
}, "sessions_changed_emit");
|
|
2287
3561
|
this.socket.emit("sessions:changed", payload);
|
|
2288
3562
|
}
|
|
2289
3563
|
});
|
|
@@ -2297,13 +3571,42 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2297
3571
|
this.socket.emit("session:detached", payload);
|
|
2298
3572
|
}
|
|
2299
3573
|
});
|
|
3574
|
+
sessionManager.onSessionEvent((event) => {
|
|
3575
|
+
logger.info({
|
|
3576
|
+
sessionId: event.sessionId,
|
|
3577
|
+
revision: event.revision,
|
|
3578
|
+
seq: event.seq,
|
|
3579
|
+
kind: event.kind,
|
|
3580
|
+
connected: this.connected
|
|
3581
|
+
}, "session_event_received_from_manager");
|
|
3582
|
+
if (this.connected) {
|
|
3583
|
+
const encrypted = this.options.cryptoService.encryptEvent(event);
|
|
3584
|
+
logger.debug({
|
|
3585
|
+
sessionId: event.sessionId,
|
|
3586
|
+
revision: event.revision,
|
|
3587
|
+
seq: event.seq,
|
|
3588
|
+
kind: event.kind
|
|
3589
|
+
}, "session_event_emitting_to_gateway");
|
|
3590
|
+
this.socket.emit("session:event", encrypted);
|
|
3591
|
+
logger.debug({
|
|
3592
|
+
sessionId: event.sessionId,
|
|
3593
|
+
seq: event.seq
|
|
3594
|
+
}, "session_event_emitted_to_gateway");
|
|
3595
|
+
} else {
|
|
3596
|
+
logger.warn({
|
|
3597
|
+
sessionId: event.sessionId,
|
|
3598
|
+
seq: event.seq,
|
|
3599
|
+
kind: event.kind
|
|
3600
|
+
}, "session_event_dropped_not_connected");
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
2300
3603
|
}
|
|
2301
3604
|
async listSessionResources(rootPath) {
|
|
2302
3605
|
const ig = await loadGitignore(rootPath);
|
|
2303
3606
|
const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
|
|
2304
3607
|
return allFiles.map((filePath) => ({
|
|
2305
|
-
name:
|
|
2306
|
-
relativePath:
|
|
3608
|
+
name: path6.basename(filePath),
|
|
3609
|
+
relativePath: path6.relative(rootPath, filePath),
|
|
2307
3610
|
path: filePath
|
|
2308
3611
|
}));
|
|
2309
3612
|
}
|
|
@@ -2311,13 +3614,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2311
3614
|
if (collected.length >= MAX_RESOURCE_FILES) {
|
|
2312
3615
|
return collected;
|
|
2313
3616
|
}
|
|
2314
|
-
const entries = await
|
|
3617
|
+
const entries = await fs5.readdir(rootPath, { withFileTypes: true });
|
|
2315
3618
|
for (const entry of entries) {
|
|
2316
3619
|
if (collected.length >= MAX_RESOURCE_FILES) {
|
|
2317
3620
|
break;
|
|
2318
3621
|
}
|
|
2319
|
-
const entryPath =
|
|
2320
|
-
const relativePath =
|
|
3622
|
+
const entryPath = path6.join(rootPath, entry.name);
|
|
3623
|
+
const relativePath = path6.relative(baseDir, entryPath);
|
|
2321
3624
|
const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
|
|
2322
3625
|
if (ig.ignores(checkPath)) {
|
|
2323
3626
|
continue;
|
|
@@ -2337,16 +3640,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2337
3640
|
}
|
|
2338
3641
|
sendRpcError(requestId, error) {
|
|
2339
3642
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2340
|
-
const detail = error instanceof Error ? error.stack :
|
|
2341
|
-
logger.error(
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
},
|
|
2348
|
-
"rpc_response_error_sent"
|
|
2349
|
-
);
|
|
3643
|
+
const detail = error instanceof Error ? error.stack : undefined;
|
|
3644
|
+
logger.error({
|
|
3645
|
+
requestId,
|
|
3646
|
+
err: error,
|
|
3647
|
+
message,
|
|
3648
|
+
detail
|
|
3649
|
+
}, "rpc_response_error_sent");
|
|
2350
3650
|
const response = {
|
|
2351
3651
|
requestId,
|
|
2352
3652
|
error: {
|
|
@@ -2369,28 +3669,45 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2369
3669
|
backends: config.acpBackends.map((backend) => ({
|
|
2370
3670
|
backendId: backend.id,
|
|
2371
3671
|
backendLabel: backend.label
|
|
2372
|
-
}))
|
|
2373
|
-
defaultBackendId: config.defaultAcpBackendId
|
|
3672
|
+
}))
|
|
2374
3673
|
});
|
|
2375
3674
|
logger.info({ machineId: config.machineId }, "cli_register_sessions_list");
|
|
2376
|
-
this.socket.emit("sessions:list", sessionManager.
|
|
3675
|
+
this.socket.emit("sessions:list", sessionManager.listAllSessions());
|
|
2377
3676
|
}
|
|
2378
3677
|
startHeartbeat() {
|
|
2379
3678
|
this.stopHeartbeat();
|
|
2380
3679
|
this.heartbeatInterval = setInterval(() => {
|
|
2381
3680
|
if (this.connected) {
|
|
2382
3681
|
this.socket.emit("cli:heartbeat");
|
|
2383
|
-
this.socket.emit(
|
|
2384
|
-
"sessions:list",
|
|
2385
|
-
this.options.sessionManager.listSessions()
|
|
2386
|
-
);
|
|
3682
|
+
this.socket.emit("sessions:list", this.options.sessionManager.listAllSessions());
|
|
2387
3683
|
}
|
|
2388
|
-
},
|
|
3684
|
+
}, 30000);
|
|
2389
3685
|
}
|
|
2390
3686
|
stopHeartbeat() {
|
|
2391
3687
|
if (this.heartbeatInterval) {
|
|
2392
3688
|
clearInterval(this.heartbeatInterval);
|
|
2393
|
-
this.heartbeatInterval =
|
|
3689
|
+
this.heartbeatInterval = undefined;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
replayUnackedEvents() {
|
|
3693
|
+
const { sessionManager } = this.options;
|
|
3694
|
+
const sessions = sessionManager.listSessions();
|
|
3695
|
+
for (const session of sessions) {
|
|
3696
|
+
const revision = sessionManager.getSessionRevision(session.sessionId);
|
|
3697
|
+
if (revision === undefined)
|
|
3698
|
+
continue;
|
|
3699
|
+
const unackedEvents = sessionManager.getUnackedEvents(session.sessionId, revision);
|
|
3700
|
+
if (unackedEvents.length > 0) {
|
|
3701
|
+
logger.info({
|
|
3702
|
+
sessionId: session.sessionId,
|
|
3703
|
+
revision,
|
|
3704
|
+
count: unackedEvents.length
|
|
3705
|
+
}, "replaying_unacked_events");
|
|
3706
|
+
for (const event of unackedEvents) {
|
|
3707
|
+
const encrypted = this.options.cryptoService.encryptEvent(event);
|
|
3708
|
+
this.socket.emit("session:event", encrypted);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
2394
3711
|
}
|
|
2395
3712
|
}
|
|
2396
3713
|
connect() {
|
|
@@ -2403,20 +3720,21 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2403
3720
|
isConnected() {
|
|
2404
3721
|
return this.connected;
|
|
2405
3722
|
}
|
|
2406
|
-
}
|
|
3723
|
+
}
|
|
2407
3724
|
|
|
2408
3725
|
// src/daemon/daemon.ts
|
|
2409
|
-
|
|
3726
|
+
class DaemonManager {
|
|
3727
|
+
config;
|
|
2410
3728
|
constructor(config) {
|
|
2411
3729
|
this.config = config;
|
|
2412
3730
|
}
|
|
2413
3731
|
async ensureHomeDirectory() {
|
|
2414
|
-
await
|
|
2415
|
-
await
|
|
3732
|
+
await fs6.mkdir(this.config.homePath, { recursive: true });
|
|
3733
|
+
await fs6.mkdir(this.config.logPath, { recursive: true });
|
|
2416
3734
|
}
|
|
2417
3735
|
async getPid() {
|
|
2418
3736
|
try {
|
|
2419
|
-
const content = await
|
|
3737
|
+
const content = await fs6.readFile(this.config.pidFile, "utf8");
|
|
2420
3738
|
const pid = Number.parseInt(content.trim(), 10);
|
|
2421
3739
|
if (Number.isNaN(pid)) {
|
|
2422
3740
|
return null;
|
|
@@ -2433,13 +3751,12 @@ var DaemonManager = class {
|
|
|
2433
3751
|
}
|
|
2434
3752
|
}
|
|
2435
3753
|
async writePidFile(pid) {
|
|
2436
|
-
await
|
|
3754
|
+
await fs6.writeFile(this.config.pidFile, String(pid), "utf8");
|
|
2437
3755
|
}
|
|
2438
3756
|
async removePidFile() {
|
|
2439
3757
|
try {
|
|
2440
|
-
await
|
|
2441
|
-
} catch {
|
|
2442
|
-
}
|
|
3758
|
+
await fs6.unlink(this.config.pidFile);
|
|
3759
|
+
} catch {}
|
|
2443
3760
|
}
|
|
2444
3761
|
async status() {
|
|
2445
3762
|
const pid = await this.getPid();
|
|
@@ -2478,7 +3795,7 @@ var DaemonManager = class {
|
|
|
2478
3795
|
logger.info({ pid }, "daemon_stop_sigterm");
|
|
2479
3796
|
process.kill(pid, "SIGTERM");
|
|
2480
3797
|
const startTime = Date.now();
|
|
2481
|
-
const timeout =
|
|
3798
|
+
const timeout = 5000;
|
|
2482
3799
|
while (Date.now() - startTime < timeout) {
|
|
2483
3800
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2484
3801
|
try {
|
|
@@ -2504,10 +3821,7 @@ var DaemonManager = class {
|
|
|
2504
3821
|
}
|
|
2505
3822
|
}
|
|
2506
3823
|
async spawnBackground() {
|
|
2507
|
-
const logFile =
|
|
2508
|
-
this.config.logPath,
|
|
2509
|
-
`${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
|
|
2510
|
-
);
|
|
3824
|
+
const logFile = path7.join(this.config.logPath, `${new Date().toISOString().replace(/[:.]/g, "-")}-daemon.log`);
|
|
2511
3825
|
const args = process.argv.slice(1).filter((arg) => arg !== "--foreground" && arg !== "-f");
|
|
2512
3826
|
args.push("--foreground");
|
|
2513
3827
|
const child = spawn2(process.argv[0], args, {
|
|
@@ -2522,22 +3836,18 @@ var DaemonManager = class {
|
|
|
2522
3836
|
logger.error("daemon_spawn_failed");
|
|
2523
3837
|
throw new Error("Failed to spawn daemon process");
|
|
2524
3838
|
}
|
|
2525
|
-
const logStream = await
|
|
3839
|
+
const logStream = await fs6.open(logFile, "a");
|
|
2526
3840
|
const fileHandle = logStream;
|
|
2527
3841
|
child.stdout?.on("data", (data) => {
|
|
2528
|
-
fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {
|
|
2529
|
-
});
|
|
3842
|
+
fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {});
|
|
2530
3843
|
});
|
|
2531
3844
|
child.stderr?.on("data", (data) => {
|
|
2532
|
-
fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {
|
|
2533
|
-
});
|
|
3845
|
+
fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {});
|
|
2534
3846
|
});
|
|
2535
3847
|
child.on("exit", (code, signal) => {
|
|
2536
3848
|
fileHandle.write(`[exit] Process exited with code ${code}, signal ${signal}
|
|
2537
|
-
`).catch(() => {
|
|
2538
|
-
});
|
|
2539
|
-
fileHandle.close().catch(() => {
|
|
2540
|
-
});
|
|
3849
|
+
`).catch(() => {});
|
|
3850
|
+
fileHandle.close().catch(() => {});
|
|
2541
3851
|
});
|
|
2542
3852
|
child.unref();
|
|
2543
3853
|
logger.info({ pid: child.pid }, "daemon_started");
|
|
@@ -2550,22 +3860,47 @@ var DaemonManager = class {
|
|
|
2550
3860
|
logger.info({ pid }, "daemon_starting");
|
|
2551
3861
|
logger.info({ gatewayUrl: this.config.gatewayUrl }, "daemon_gateway_url");
|
|
2552
3862
|
logger.info({ machineId: this.config.machineId }, "daemon_machine_id");
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
);
|
|
2559
|
-
logger.warn("daemon_exit_missing_api_key");
|
|
3863
|
+
await initCrypto2();
|
|
3864
|
+
const masterSecretBase64 = await getMasterSecret();
|
|
3865
|
+
if (!masterSecretBase64) {
|
|
3866
|
+
logger.error("daemon_master_secret_missing");
|
|
3867
|
+
console.error(`[mobvibe-cli] No credentials found. Run 'mobvibe login' to authenticate.`);
|
|
3868
|
+
logger.warn("daemon_exit_missing_master_secret");
|
|
2560
3869
|
process.exit(1);
|
|
2561
3870
|
}
|
|
2562
|
-
|
|
2563
|
-
const
|
|
3871
|
+
const sodium = getSodium3();
|
|
3872
|
+
const masterSecret = sodium.from_base64(masterSecretBase64, sodium.base64_variants.ORIGINAL);
|
|
3873
|
+
const cryptoService = new CliCryptoService(masterSecret);
|
|
3874
|
+
logger.info("daemon_crypto_initialized");
|
|
3875
|
+
const sessionManager = new SessionManager(this.config, cryptoService);
|
|
2564
3876
|
const socketClient = new SocketClient({
|
|
2565
3877
|
config: this.config,
|
|
2566
3878
|
sessionManager,
|
|
2567
|
-
|
|
3879
|
+
cryptoService
|
|
2568
3880
|
});
|
|
3881
|
+
let compactor;
|
|
3882
|
+
let compactionInterval;
|
|
3883
|
+
let compactorWalStore;
|
|
3884
|
+
let compactorDb;
|
|
3885
|
+
if (this.config.compaction.enabled) {
|
|
3886
|
+
compactorWalStore = new WalStore(this.config.walDbPath);
|
|
3887
|
+
compactorDb = new Database2(this.config.walDbPath);
|
|
3888
|
+
compactor = new WalCompactor(compactorWalStore, this.config.compaction, compactorDb);
|
|
3889
|
+
if (this.config.compaction.runOnStartup) {
|
|
3890
|
+
logger.info("compaction_startup_start");
|
|
3891
|
+
compactor.compactAll().catch((error) => {
|
|
3892
|
+
logger.error({ err: error }, "compaction_startup_error");
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
const intervalMs = this.config.compaction.runIntervalHours * 60 * 60 * 1000;
|
|
3896
|
+
compactionInterval = setInterval(() => {
|
|
3897
|
+
logger.info("compaction_scheduled_start");
|
|
3898
|
+
compactor?.compactAll().catch((error) => {
|
|
3899
|
+
logger.error({ err: error }, "compaction_scheduled_error");
|
|
3900
|
+
});
|
|
3901
|
+
}, intervalMs);
|
|
3902
|
+
logger.info({ intervalHours: this.config.compaction.runIntervalHours }, "compaction_scheduled");
|
|
3903
|
+
}
|
|
2569
3904
|
let shuttingDown = false;
|
|
2570
3905
|
const shutdown = async (signal) => {
|
|
2571
3906
|
if (shuttingDown) {
|
|
@@ -2575,8 +3910,17 @@ var DaemonManager = class {
|
|
|
2575
3910
|
shuttingDown = true;
|
|
2576
3911
|
logger.info({ signal }, "daemon_shutdown_start");
|
|
2577
3912
|
try {
|
|
3913
|
+
if (compactionInterval) {
|
|
3914
|
+
clearInterval(compactionInterval);
|
|
3915
|
+
}
|
|
3916
|
+
if (compactorWalStore) {
|
|
3917
|
+
compactorWalStore.close();
|
|
3918
|
+
}
|
|
3919
|
+
if (compactorDb) {
|
|
3920
|
+
compactorDb.close();
|
|
3921
|
+
}
|
|
2578
3922
|
socketClient.disconnect();
|
|
2579
|
-
await sessionManager.
|
|
3923
|
+
await sessionManager.shutdown();
|
|
2580
3924
|
await this.removePidFile();
|
|
2581
3925
|
logger.info({ signal }, "daemon_shutdown_complete");
|
|
2582
3926
|
} catch (error) {
|
|
@@ -2594,18 +3938,17 @@ var DaemonManager = class {
|
|
|
2594
3938
|
});
|
|
2595
3939
|
});
|
|
2596
3940
|
socketClient.connect();
|
|
2597
|
-
await new Promise(() => {
|
|
2598
|
-
});
|
|
3941
|
+
await new Promise(() => {});
|
|
2599
3942
|
}
|
|
2600
3943
|
async logs(options) {
|
|
2601
|
-
const files = await
|
|
3944
|
+
const files = await fs6.readdir(this.config.logPath);
|
|
2602
3945
|
const logFiles = files.filter((f) => f.endsWith("-daemon.log")).sort().reverse();
|
|
2603
3946
|
if (logFiles.length === 0) {
|
|
2604
3947
|
logger.warn("daemon_logs_empty");
|
|
2605
3948
|
console.log("No log files found");
|
|
2606
3949
|
return;
|
|
2607
3950
|
}
|
|
2608
|
-
const latestLog =
|
|
3951
|
+
const latestLog = path7.join(this.config.logPath, logFiles[0]);
|
|
2609
3952
|
logger.info({ logFile: latestLog }, "daemon_logs_latest");
|
|
2610
3953
|
console.log(`Log file: ${latestLog}
|
|
2611
3954
|
`);
|
|
@@ -2617,16 +3960,18 @@ var DaemonManager = class {
|
|
|
2617
3960
|
tail.on("close", () => resolve());
|
|
2618
3961
|
});
|
|
2619
3962
|
} else {
|
|
2620
|
-
const content = await
|
|
2621
|
-
const lines = content.split(
|
|
3963
|
+
const content = await fs6.readFile(latestLog, "utf8");
|
|
3964
|
+
const lines = content.split(`
|
|
3965
|
+
`);
|
|
2622
3966
|
const count = options?.lines ?? 50;
|
|
2623
|
-
console.log(lines.slice(-count).join(
|
|
3967
|
+
console.log(lines.slice(-count).join(`
|
|
3968
|
+
`));
|
|
2624
3969
|
}
|
|
2625
3970
|
}
|
|
2626
|
-
}
|
|
3971
|
+
}
|
|
2627
3972
|
|
|
2628
3973
|
// src/index.ts
|
|
2629
|
-
var program = new Command
|
|
3974
|
+
var program = new Command;
|
|
2630
3975
|
program.name("mobvibe").description("Mobvibe CLI - Connect local ACP backends to the gateway").version("0.0.0");
|
|
2631
3976
|
program.command("start").description("Start the mobvibe daemon").option("--gateway <url>", "Gateway URL", process.env.MOBVIBE_GATEWAY_URL).option("--foreground", "Run in foreground instead of detaching").action(async (options) => {
|
|
2632
3977
|
if (options.gateway) {
|
|
@@ -2648,10 +3993,10 @@ program.command("status").description("Show daemon status").action(async () => {
|
|
|
2648
3993
|
if (status.running) {
|
|
2649
3994
|
logger.info({ pid: status.pid }, "daemon_status_running");
|
|
2650
3995
|
console.log(`Daemon is running (PID ${status.pid})`);
|
|
2651
|
-
if (status.connected !==
|
|
3996
|
+
if (status.connected !== undefined) {
|
|
2652
3997
|
console.log(`Connected to gateway: ${status.connected ? "yes" : "no"}`);
|
|
2653
3998
|
}
|
|
2654
|
-
if (status.sessionCount !==
|
|
3999
|
+
if (status.sessionCount !== undefined) {
|
|
2655
4000
|
console.log(`Active sessions: ${status.sessionCount}`);
|
|
2656
4001
|
}
|
|
2657
4002
|
} else {
|
|
@@ -2681,6 +4026,97 @@ program.command("logout").description("Remove stored credentials").action(async
|
|
|
2681
4026
|
program.command("auth-status").description("Show authentication status").action(async () => {
|
|
2682
4027
|
await loginStatus();
|
|
2683
4028
|
});
|
|
4029
|
+
var e2eeCmd = program.command("e2ee").description("E2EE key management");
|
|
4030
|
+
e2eeCmd.command("show").description("Display the master secret for pairing other devices").action(async () => {
|
|
4031
|
+
const credentials = await loadCredentials();
|
|
4032
|
+
if (!credentials) {
|
|
4033
|
+
console.error("Not logged in. Run 'mobvibe login' first.");
|
|
4034
|
+
process.exit(1);
|
|
4035
|
+
}
|
|
4036
|
+
const base64url = credentials.masterSecret.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4037
|
+
const pairingUrl = `mobvibe://pair?secret=${base64url}`;
|
|
4038
|
+
const QRCode = await import("qrcode");
|
|
4039
|
+
const qrText = await QRCode.toString(pairingUrl, {
|
|
4040
|
+
type: "terminal",
|
|
4041
|
+
small: true
|
|
4042
|
+
});
|
|
4043
|
+
console.log(qrText);
|
|
4044
|
+
console.log("Master secret (for pairing WebUI/Tauri devices):");
|
|
4045
|
+
console.log(` ${credentials.masterSecret}`);
|
|
4046
|
+
console.log(`
|
|
4047
|
+
Scan the QR code with your phone, or paste the secret into WebUI Settings > E2EE > Pair.`);
|
|
4048
|
+
});
|
|
4049
|
+
e2eeCmd.command("status").description("Show E2EE key status").action(async () => {
|
|
4050
|
+
const { initCrypto: initCrypto3, deriveAuthKeyPair: deriveAuthKeyPair3, deriveContentKeyPair: deriveContentKeyPair2, getSodium: getSodium4 } = await import("@mobvibe/shared");
|
|
4051
|
+
const credentials = await loadCredentials();
|
|
4052
|
+
if (!credentials) {
|
|
4053
|
+
console.log("Status: Not logged in");
|
|
4054
|
+
console.log("Run 'mobvibe login' to authenticate.");
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
await initCrypto3();
|
|
4058
|
+
const sodium = getSodium4();
|
|
4059
|
+
const masterSecret = sodium.from_base64(credentials.masterSecret, sodium.base64_variants.ORIGINAL);
|
|
4060
|
+
const authKp = deriveAuthKeyPair3(masterSecret);
|
|
4061
|
+
const contentKp = deriveContentKeyPair2(masterSecret);
|
|
4062
|
+
const authPub = sodium.to_base64(authKp.publicKey, sodium.base64_variants.ORIGINAL);
|
|
4063
|
+
const contentPub = sodium.to_base64(contentKp.publicKey, sodium.base64_variants.ORIGINAL);
|
|
4064
|
+
console.log("Status: E2EE enabled");
|
|
4065
|
+
console.log(`Auth public key: ${authPub.slice(0, 16)}...`);
|
|
4066
|
+
console.log(`Content public key: ${contentPub.slice(0, 16)}...`);
|
|
4067
|
+
console.log(`Saved: ${new Date(credentials.createdAt).toLocaleString()}`);
|
|
4068
|
+
});
|
|
4069
|
+
program.command("compact").description("Compact the WAL database to reclaim space").option("--session <id>", "Compact a specific session only").option("--dry-run", "Show what would be deleted without actually deleting").option("-v, --verbose", "Show detailed output").action(async (options) => {
|
|
4070
|
+
const config = await getCliConfig();
|
|
4071
|
+
if (!config.compaction.enabled && !options.dryRun) {
|
|
4072
|
+
console.log("Compaction is disabled in configuration.");
|
|
4073
|
+
console.log("Set MOBVIBE_COMPACTION_ENABLED=true to enable.");
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const walStore = new WalStore(config.walDbPath);
|
|
4077
|
+
const db = new Database3(config.walDbPath);
|
|
4078
|
+
const compactor = new WalCompactor(walStore, config.compaction, db);
|
|
4079
|
+
console.log(options.dryRun ? "Dry run - no changes will be made" : "Starting compaction...");
|
|
4080
|
+
try {
|
|
4081
|
+
if (options.session) {
|
|
4082
|
+
const stats = await compactor.compactSession(options.session, {
|
|
4083
|
+
dryRun: options.dryRun
|
|
4084
|
+
});
|
|
4085
|
+
console.log(`Session ${options.session}:`);
|
|
4086
|
+
console.log(` Acked events deleted: ${stats.ackedEventsDeleted}`);
|
|
4087
|
+
console.log(` Old revisions deleted: ${stats.oldRevisionsDeleted}`);
|
|
4088
|
+
console.log(` Duration: ${stats.durationMs.toFixed(2)}ms`);
|
|
4089
|
+
} else {
|
|
4090
|
+
const result = await compactor.compactAll({
|
|
4091
|
+
dryRun: options.dryRun
|
|
4092
|
+
});
|
|
4093
|
+
if (options.verbose) {
|
|
4094
|
+
for (const stats of result.stats) {
|
|
4095
|
+
if (stats.ackedEventsDeleted > 0 || stats.oldRevisionsDeleted > 0) {
|
|
4096
|
+
console.log(`
|
|
4097
|
+
Session ${stats.sessionId}:`);
|
|
4098
|
+
console.log(` Acked events deleted: ${stats.ackedEventsDeleted}`);
|
|
4099
|
+
console.log(` Old revisions deleted: ${stats.oldRevisionsDeleted}`);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
if (result.skipped.length > 0) {
|
|
4103
|
+
console.log(`
|
|
4104
|
+
Skipped (active sessions): ${result.skipped.join(", ")}`);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
const totalDeleted = result.stats.reduce((sum, s) => sum + s.ackedEventsDeleted + s.oldRevisionsDeleted, 0);
|
|
4108
|
+
console.log(`
|
|
4109
|
+
Summary:`);
|
|
4110
|
+
console.log(` Sessions processed: ${result.stats.length}`);
|
|
4111
|
+
console.log(` Sessions skipped: ${result.skipped.length}`);
|
|
4112
|
+
console.log(` Total events ${options.dryRun ? "to delete" : "deleted"}: ${totalDeleted}`);
|
|
4113
|
+
console.log(` Duration: ${result.totalDurationMs.toFixed(2)}ms`);
|
|
4114
|
+
}
|
|
4115
|
+
} finally {
|
|
4116
|
+
walStore.close();
|
|
4117
|
+
db.close();
|
|
4118
|
+
}
|
|
4119
|
+
});
|
|
2684
4120
|
async function run() {
|
|
2685
4121
|
await program.parseAsync(process.argv);
|
|
2686
4122
|
}
|
|
@@ -2692,4 +4128,5 @@ run().catch((error) => {
|
|
|
2692
4128
|
export {
|
|
2693
4129
|
run
|
|
2694
4130
|
};
|
|
2695
|
-
|
|
4131
|
+
|
|
4132
|
+
//# debugId=3BEAA052FAC1A9B764756E2164756E21
|