@mobvibe/cli 0.1.7 → 0.1.9
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 +2089 -978
- 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;
|
|
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;
|
|
409
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,6 +1835,117 @@ 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
|
}
|
|
@@ -1053,13 +1957,14 @@ var SessionManager = class {
|
|
|
1053
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) {
|
|
2408
|
+
logger.debug({ sessionId }, "load_session_already_loaded");
|
|
1500
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,34 +2506,26 @@ 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
|
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Reload a historical session from the ACP agent.
|
|
1585
|
-
* Replays session history even if the session is already loaded.
|
|
1586
|
-
*/
|
|
1587
2516
|
async reloadSession(sessionId, cwd, backendId) {
|
|
1588
2517
|
const existing = this.sessions.get(sessionId);
|
|
1589
2518
|
if (!existing) {
|
|
1590
2519
|
return this.loadSession(sessionId, cwd, backendId);
|
|
1591
2520
|
}
|
|
1592
2521
|
if (!existing.connection.supportsSessionLoad()) {
|
|
1593
|
-
throw createCapabilityNotSupportedError(
|
|
1594
|
-
"Agent does not support session loading"
|
|
1595
|
-
);
|
|
2522
|
+
throw createCapabilityNotSupportedError("Agent does not support session loading");
|
|
1596
2523
|
}
|
|
2524
|
+
const newRevision = this.walStore.incrementRevision(sessionId);
|
|
2525
|
+
existing.revision = newRevision;
|
|
1597
2526
|
const response = await existing.connection.loadSession(sessionId, cwd);
|
|
1598
|
-
const { modelId, modelName, availableModels } = resolveModelState(
|
|
1599
|
-
|
|
1600
|
-
);
|
|
1601
|
-
const { modeId, modeName, availableModes } = resolveModeState(
|
|
1602
|
-
response.modes
|
|
1603
|
-
);
|
|
2527
|
+
const { modelId, modelName, availableModels } = resolveModelState(response.models);
|
|
2528
|
+
const { modeId, modeName, availableModes } = resolveModeState(response.modes);
|
|
1604
2529
|
const agentInfo = existing.connection.getAgentInfo();
|
|
1605
2530
|
existing.cwd = cwd;
|
|
1606
2531
|
existing.agentName = agentInfo?.title ?? agentInfo?.name ?? existing.agentName;
|
|
@@ -1610,7 +2535,7 @@ var SessionManager = class {
|
|
|
1610
2535
|
existing.modeId = modeId;
|
|
1611
2536
|
existing.modeName = modeName;
|
|
1612
2537
|
existing.availableModes = availableModes;
|
|
1613
|
-
existing.updatedAt =
|
|
2538
|
+
existing.updatedAt = new Date;
|
|
1614
2539
|
const summary = this.buildSummary(existing);
|
|
1615
2540
|
this.emitSessionsChanged({
|
|
1616
2541
|
added: [],
|
|
@@ -1618,7 +2543,7 @@ var SessionManager = class {
|
|
|
1618
2543
|
removed: []
|
|
1619
2544
|
});
|
|
1620
2545
|
this.emitSessionAttached(sessionId, true);
|
|
1621
|
-
logger.info({ sessionId, backendId }, "session_reloaded");
|
|
2546
|
+
logger.info({ sessionId, backendId, revision: newRevision }, "session_reloaded");
|
|
1622
2547
|
return summary;
|
|
1623
2548
|
}
|
|
1624
2549
|
applySessionUpdateToRecord(record, notification) {
|
|
@@ -1642,27 +2567,69 @@ var SessionManager = class {
|
|
|
1642
2567
|
}
|
|
1643
2568
|
}
|
|
1644
2569
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
+
}
|
|
1648
2611
|
setupSessionSubscriptions(record, options) {
|
|
1649
2612
|
const { sessionId, connection } = record;
|
|
2613
|
+
logger.debug({ sessionId, skipSessionUpdates: options?.skipSessionUpdates }, "setup_session_subscriptions");
|
|
1650
2614
|
if (!options?.skipSessionUpdates) {
|
|
1651
|
-
record.unsubscribe = connection.onSessionUpdate(
|
|
1652
|
-
(
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
+
});
|
|
1658
2624
|
}
|
|
1659
2625
|
record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
|
|
1660
|
-
|
|
2626
|
+
logger.debug({ sessionId }, "acp_terminal_output_received_via_setup");
|
|
2627
|
+
this.writeAndEmitEvent(sessionId, record.revision, "terminal_output", event);
|
|
1661
2628
|
});
|
|
1662
2629
|
connection.onStatusChange((status) => {
|
|
2630
|
+
logger.debug({ sessionId, hasError: !!status.error }, "acp_status_change");
|
|
1663
2631
|
if (status.error) {
|
|
1664
|
-
this.
|
|
1665
|
-
sessionId,
|
|
2632
|
+
this.writeAndEmitEvent(sessionId, record.revision, "session_error", {
|
|
1666
2633
|
error: status.error
|
|
1667
2634
|
});
|
|
1668
2635
|
this.emitSessionDetached(sessionId, "agent_exit");
|
|
@@ -1688,28 +2655,81 @@ var SessionManager = class {
|
|
|
1688
2655
|
modeName: record.modeName,
|
|
1689
2656
|
availableModes: record.availableModes,
|
|
1690
2657
|
availableModels: record.availableModels,
|
|
1691
|
-
availableCommands: record.availableCommands
|
|
2658
|
+
availableCommands: record.availableCommands,
|
|
2659
|
+
revision: record.revision,
|
|
2660
|
+
wrappedDek: this.cryptoService?.getWrappedDek(record.sessionId) ?? undefined
|
|
1692
2661
|
};
|
|
1693
2662
|
}
|
|
1694
|
-
}
|
|
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
|
+
}
|
|
1695
2713
|
|
|
1696
2714
|
// src/daemon/socket-client.ts
|
|
1697
2715
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
1698
|
-
import
|
|
2716
|
+
import fs5 from "fs/promises";
|
|
1699
2717
|
import { homedir } from "os";
|
|
1700
|
-
import
|
|
2718
|
+
import path6 from "path";
|
|
2719
|
+
import { createSignedToken } from "@mobvibe/shared";
|
|
1701
2720
|
import ignore from "ignore";
|
|
1702
2721
|
import { io } from "socket.io-client";
|
|
1703
2722
|
|
|
1704
2723
|
// src/lib/git-utils.ts
|
|
1705
|
-
import {
|
|
1706
|
-
import
|
|
2724
|
+
import { execFile } from "child_process";
|
|
2725
|
+
import { readFile } from "fs/promises";
|
|
2726
|
+
import path5 from "path";
|
|
1707
2727
|
import { promisify } from "util";
|
|
1708
|
-
var
|
|
2728
|
+
var execFileAsync = promisify(execFile);
|
|
1709
2729
|
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
1710
2730
|
async function isGitRepo(cwd) {
|
|
1711
2731
|
try {
|
|
1712
|
-
await
|
|
2732
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1713
2733
|
cwd,
|
|
1714
2734
|
maxBuffer: MAX_BUFFER
|
|
1715
2735
|
});
|
|
@@ -1720,7 +2740,7 @@ async function isGitRepo(cwd) {
|
|
|
1720
2740
|
}
|
|
1721
2741
|
async function getGitBranch(cwd) {
|
|
1722
2742
|
try {
|
|
1723
|
-
const { stdout } = await
|
|
2743
|
+
const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
|
|
1724
2744
|
cwd,
|
|
1725
2745
|
maxBuffer: MAX_BUFFER
|
|
1726
2746
|
});
|
|
@@ -1728,18 +2748,16 @@ async function getGitBranch(cwd) {
|
|
|
1728
2748
|
if (branch) {
|
|
1729
2749
|
return branch;
|
|
1730
2750
|
}
|
|
1731
|
-
const { stdout: hashOut } = await
|
|
1732
|
-
|
|
1733
|
-
maxBuffer: MAX_BUFFER
|
|
1734
|
-
});
|
|
1735
|
-
return hashOut.trim() || void 0;
|
|
2751
|
+
const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, maxBuffer: MAX_BUFFER });
|
|
2752
|
+
return hashOut.trim() || undefined;
|
|
1736
2753
|
} catch {
|
|
1737
|
-
return
|
|
2754
|
+
return;
|
|
1738
2755
|
}
|
|
1739
2756
|
}
|
|
1740
2757
|
function parseGitStatus(output) {
|
|
1741
2758
|
const files = [];
|
|
1742
|
-
const lines = output.split(
|
|
2759
|
+
const lines = output.split(`
|
|
2760
|
+
`).filter((line) => line.length > 0);
|
|
1743
2761
|
for (const line of lines) {
|
|
1744
2762
|
const indexStatus = line[0];
|
|
1745
2763
|
const workTreeStatus = line[1];
|
|
@@ -1773,10 +2791,7 @@ function parseGitStatus(output) {
|
|
|
1773
2791
|
}
|
|
1774
2792
|
async function getGitStatus(cwd) {
|
|
1775
2793
|
try {
|
|
1776
|
-
const { stdout } = await
|
|
1777
|
-
cwd,
|
|
1778
|
-
maxBuffer: MAX_BUFFER
|
|
1779
|
-
});
|
|
2794
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain=v1"], { cwd, maxBuffer: MAX_BUFFER });
|
|
1780
2795
|
return parseGitStatus(stdout);
|
|
1781
2796
|
} catch {
|
|
1782
2797
|
return [];
|
|
@@ -1797,7 +2812,7 @@ function aggregateDirStatus(files) {
|
|
|
1797
2812
|
for (const file of files) {
|
|
1798
2813
|
const parts = file.path.split("/");
|
|
1799
2814
|
let currentPath = "";
|
|
1800
|
-
for (let i = 0;
|
|
2815
|
+
for (let i = 0;i < parts.length - 1; i++) {
|
|
1801
2816
|
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
|
|
1802
2817
|
const existing = dirStatus[currentPath];
|
|
1803
2818
|
if (!existing || statusPriority[file.status] > statusPriority[existing]) {
|
|
@@ -1811,7 +2826,8 @@ function parseDiffOutput(diffOutput) {
|
|
|
1811
2826
|
const addedLines = [];
|
|
1812
2827
|
const modifiedLines = [];
|
|
1813
2828
|
const deletedLines = [];
|
|
1814
|
-
const lines = diffOutput.split(
|
|
2829
|
+
const lines = diffOutput.split(`
|
|
2830
|
+
`);
|
|
1815
2831
|
let currentLine = 0;
|
|
1816
2832
|
let inHunk = false;
|
|
1817
2833
|
let pendingDeletionLine = 0;
|
|
@@ -1847,22 +2863,15 @@ function parseDiffOutput(diffOutput) {
|
|
|
1847
2863
|
}
|
|
1848
2864
|
async function getFileDiff(cwd, filePath) {
|
|
1849
2865
|
try {
|
|
1850
|
-
const relativePath =
|
|
1851
|
-
const { stdout } = await
|
|
1852
|
-
cwd,
|
|
1853
|
-
maxBuffer: MAX_BUFFER
|
|
1854
|
-
});
|
|
2866
|
+
const relativePath = path5.isAbsolute(filePath) ? path5.relative(cwd, filePath) : filePath;
|
|
2867
|
+
const { stdout } = await execFileAsync("git", ["diff", "HEAD", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
1855
2868
|
if (!stdout.trim()) {
|
|
1856
|
-
const { stdout: statusOut } = await
|
|
1857
|
-
`git status --porcelain=v1 -- "${relativePath}"`,
|
|
1858
|
-
{ cwd, maxBuffer: MAX_BUFFER }
|
|
1859
|
-
);
|
|
2869
|
+
const { stdout: statusOut } = await execFileAsync("git", ["status", "--porcelain=v1", "--", relativePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
1860
2870
|
if (statusOut.startsWith("?") || statusOut.startsWith("A")) {
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
const lineCount = Number.parseInt(wcOut.trim(), 10) || 0;
|
|
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;
|
|
1866
2875
|
return {
|
|
1867
2876
|
addedLines: Array.from({ length: lineCount }, (_, i) => i + 1),
|
|
1868
2877
|
modifiedLines: [],
|
|
@@ -1879,7 +2888,7 @@ async function getFileDiff(cwd, filePath) {
|
|
|
1879
2888
|
|
|
1880
2889
|
// src/daemon/socket-client.ts
|
|
1881
2890
|
var SESSION_ROOT_NAME = "Working Directory";
|
|
1882
|
-
var MAX_RESOURCE_FILES =
|
|
2891
|
+
var MAX_RESOURCE_FILES = 2000;
|
|
1883
2892
|
var DEFAULT_IGNORES = [
|
|
1884
2893
|
"node_modules",
|
|
1885
2894
|
".git",
|
|
@@ -1897,15 +2906,14 @@ var DEFAULT_IGNORES = [
|
|
|
1897
2906
|
var loadGitignore = async (rootPath) => {
|
|
1898
2907
|
const ig = ignore().add(DEFAULT_IGNORES);
|
|
1899
2908
|
try {
|
|
1900
|
-
const gitignorePath =
|
|
1901
|
-
const content = await
|
|
2909
|
+
const gitignorePath = path6.join(rootPath, ".gitignore");
|
|
2910
|
+
const content = await fs5.readFile(gitignorePath, "utf8");
|
|
1902
2911
|
ig.add(content);
|
|
1903
|
-
} catch {
|
|
1904
|
-
}
|
|
2912
|
+
} catch {}
|
|
1905
2913
|
return ig;
|
|
1906
2914
|
};
|
|
1907
2915
|
var resolveImageMimeType = (filePath) => {
|
|
1908
|
-
const extension =
|
|
2916
|
+
const extension = path6.extname(filePath).toLowerCase();
|
|
1909
2917
|
switch (extension) {
|
|
1910
2918
|
case ".apng":
|
|
1911
2919
|
return "image/apng";
|
|
@@ -1924,31 +2932,28 @@ var resolveImageMimeType = (filePath) => {
|
|
|
1924
2932
|
case ".webp":
|
|
1925
2933
|
return "image/webp";
|
|
1926
2934
|
default:
|
|
1927
|
-
return
|
|
2935
|
+
return;
|
|
1928
2936
|
}
|
|
1929
2937
|
};
|
|
1930
2938
|
var readDirectoryEntries = async (dirPath) => {
|
|
1931
|
-
const entries = await
|
|
1932
|
-
const resolvedEntries = await Promise.all(
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
};
|
|
1950
|
-
})
|
|
1951
|
-
);
|
|
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
|
+
}));
|
|
1952
2957
|
return resolvedEntries.sort((left, right) => {
|
|
1953
2958
|
if (left.type !== right.type) {
|
|
1954
2959
|
return left.type === "directory" ? -1 : 1;
|
|
@@ -1957,6 +2962,16 @@ var readDirectoryEntries = async (dirPath) => {
|
|
|
1957
2962
|
});
|
|
1958
2963
|
};
|
|
1959
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
|
+
};
|
|
1960
2975
|
var buildHostFsRoots = async () => {
|
|
1961
2976
|
const homePath = homedir();
|
|
1962
2977
|
return {
|
|
@@ -1964,41 +2979,46 @@ var buildHostFsRoots = async () => {
|
|
|
1964
2979
|
roots: [{ name: "Home", path: homePath }]
|
|
1965
2980
|
};
|
|
1966
2981
|
};
|
|
1967
|
-
|
|
2982
|
+
|
|
2983
|
+
class SocketClient extends EventEmitter3 {
|
|
2984
|
+
options;
|
|
2985
|
+
socket;
|
|
2986
|
+
connected = false;
|
|
2987
|
+
reconnectAttempts = 0;
|
|
2988
|
+
heartbeatInterval;
|
|
1968
2989
|
constructor(options) {
|
|
1969
2990
|
super();
|
|
1970
2991
|
this.options = options;
|
|
2992
|
+
const { cryptoService } = options;
|
|
1971
2993
|
this.socket = io(`${options.config.gatewayUrl}/cli`, {
|
|
1972
2994
|
path: "/socket.io",
|
|
1973
2995
|
reconnection: true,
|
|
1974
2996
|
reconnectionAttempts: Number.POSITIVE_INFINITY,
|
|
1975
|
-
reconnectionDelay:
|
|
1976
|
-
reconnectionDelayMax:
|
|
2997
|
+
reconnectionDelay: 1000,
|
|
2998
|
+
reconnectionDelayMax: 30000,
|
|
1977
2999
|
transports: ["websocket"],
|
|
1978
3000
|
autoConnect: false,
|
|
1979
|
-
|
|
1980
|
-
"x-api-key": options.apiKey
|
|
1981
|
-
}
|
|
3001
|
+
auth: (cb) => cb(createSignedToken(cryptoService.authKeyPair))
|
|
1982
3002
|
});
|
|
1983
3003
|
this.setupEventHandlers();
|
|
1984
3004
|
this.setupRpcHandlers();
|
|
1985
3005
|
this.setupSessionManagerListeners();
|
|
1986
3006
|
}
|
|
1987
|
-
socket;
|
|
1988
|
-
connected = false;
|
|
1989
|
-
reconnectAttempts = 0;
|
|
1990
|
-
heartbeatInterval;
|
|
1991
3007
|
setupEventHandlers() {
|
|
1992
3008
|
this.socket.on("connect", () => {
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
3009
|
+
const wasReconnect = this.reconnectAttempts > 0;
|
|
3010
|
+
logger.info({
|
|
3011
|
+
gatewayUrl: this.options.config.gatewayUrl,
|
|
3012
|
+
wasReconnect
|
|
3013
|
+
}, "gateway_connected");
|
|
1997
3014
|
this.connected = true;
|
|
1998
3015
|
this.reconnectAttempts = 0;
|
|
1999
3016
|
logger.info("gateway_register_start");
|
|
2000
3017
|
this.register();
|
|
2001
3018
|
this.startHeartbeat();
|
|
3019
|
+
if (wasReconnect) {
|
|
3020
|
+
this.replayUnackedEvents();
|
|
3021
|
+
}
|
|
2002
3022
|
this.emit("connected");
|
|
2003
3023
|
});
|
|
2004
3024
|
this.socket.on("disconnect", (reason) => {
|
|
@@ -2010,41 +3030,55 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2010
3030
|
this.socket.on("connect_error", (error) => {
|
|
2011
3031
|
this.reconnectAttempts++;
|
|
2012
3032
|
if (this.reconnectAttempts <= 3 || this.reconnectAttempts % 10 === 0) {
|
|
2013
|
-
logger.error(
|
|
2014
|
-
{ attempt: this.reconnectAttempts, err: error },
|
|
2015
|
-
"gateway_connect_error"
|
|
2016
|
-
);
|
|
3033
|
+
logger.error({ attempt: this.reconnectAttempts, err: error }, "gateway_connect_error");
|
|
2017
3034
|
}
|
|
2018
3035
|
});
|
|
2019
3036
|
this.socket.on("cli:registered", async (info) => {
|
|
2020
3037
|
logger.info({ machineId: info.machineId }, "gateway_registered");
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
sessions,
|
|
2030
|
-
capabilities,
|
|
2031
|
-
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
|
|
2032
3046
|
});
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
"
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
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
|
+
}
|
|
2042
3068
|
}
|
|
2043
3069
|
});
|
|
2044
3070
|
this.socket.on("cli:error", (error) => {
|
|
2045
3071
|
logger.error({ err: error }, "gateway_auth_error");
|
|
2046
3072
|
this.emit("auth_error", error);
|
|
2047
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
|
+
});
|
|
2048
3082
|
}
|
|
2049
3083
|
setupRpcHandlers() {
|
|
2050
3084
|
const { sessionManager } = this.options;
|
|
@@ -2054,106 +3088,73 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2054
3088
|
const session = await sessionManager.createSession(request.params);
|
|
2055
3089
|
this.sendRpcResponse(request.requestId, session);
|
|
2056
3090
|
} catch (error) {
|
|
2057
|
-
logger.error(
|
|
2058
|
-
{ err: error, requestId: request.requestId },
|
|
2059
|
-
"rpc_session_create_error"
|
|
2060
|
-
);
|
|
3091
|
+
logger.error({ err: error, requestId: request.requestId }, "rpc_session_create_error");
|
|
2061
3092
|
this.sendRpcError(request.requestId, error);
|
|
2062
3093
|
}
|
|
2063
3094
|
});
|
|
2064
3095
|
this.socket.on("rpc:session:close", async (request) => {
|
|
2065
3096
|
try {
|
|
2066
|
-
logger.info(
|
|
2067
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
2068
|
-
"rpc_session_close"
|
|
2069
|
-
);
|
|
3097
|
+
logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_close");
|
|
2070
3098
|
await sessionManager.closeSession(request.params.sessionId);
|
|
2071
3099
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
2072
3100
|
} catch (error) {
|
|
2073
|
-
logger.error(
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
},
|
|
2079
|
-
"rpc_session_close_error"
|
|
2080
|
-
);
|
|
3101
|
+
logger.error({
|
|
3102
|
+
err: error,
|
|
3103
|
+
requestId: request.requestId,
|
|
3104
|
+
sessionId: request.params.sessionId
|
|
3105
|
+
}, "rpc_session_close_error");
|
|
2081
3106
|
this.sendRpcError(request.requestId, error);
|
|
2082
3107
|
}
|
|
2083
3108
|
});
|
|
2084
3109
|
this.socket.on("rpc:session:cancel", async (request) => {
|
|
2085
3110
|
try {
|
|
2086
|
-
logger.info(
|
|
2087
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
2088
|
-
"rpc_session_cancel"
|
|
2089
|
-
);
|
|
3111
|
+
logger.info({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_session_cancel");
|
|
2090
3112
|
await sessionManager.cancelSession(request.params.sessionId);
|
|
2091
3113
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
2092
3114
|
} catch (error) {
|
|
2093
|
-
logger.error(
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
},
|
|
2099
|
-
"rpc_session_cancel_error"
|
|
2100
|
-
);
|
|
3115
|
+
logger.error({
|
|
3116
|
+
err: error,
|
|
3117
|
+
requestId: request.requestId,
|
|
3118
|
+
sessionId: request.params.sessionId
|
|
3119
|
+
}, "rpc_session_cancel_error");
|
|
2101
3120
|
this.sendRpcError(request.requestId, error);
|
|
2102
3121
|
}
|
|
2103
3122
|
});
|
|
2104
3123
|
this.socket.on("rpc:session:mode", async (request) => {
|
|
2105
3124
|
try {
|
|
2106
|
-
logger.info(
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
"rpc_session_mode"
|
|
2113
|
-
);
|
|
2114
|
-
const session = await sessionManager.setSessionMode(
|
|
2115
|
-
request.params.sessionId,
|
|
2116
|
-
request.params.modeId
|
|
2117
|
-
);
|
|
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);
|
|
2118
3131
|
this.sendRpcResponse(request.requestId, session);
|
|
2119
3132
|
} catch (error) {
|
|
2120
|
-
logger.error(
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
},
|
|
2127
|
-
"rpc_session_mode_error"
|
|
2128
|
-
);
|
|
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");
|
|
2129
3139
|
this.sendRpcError(request.requestId, error);
|
|
2130
3140
|
}
|
|
2131
3141
|
});
|
|
2132
3142
|
this.socket.on("rpc:session:model", async (request) => {
|
|
2133
3143
|
try {
|
|
2134
|
-
logger.info(
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
"rpc_session_model"
|
|
2141
|
-
);
|
|
2142
|
-
const session = await sessionManager.setSessionModel(
|
|
2143
|
-
request.params.sessionId,
|
|
2144
|
-
request.params.modelId
|
|
2145
|
-
);
|
|
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);
|
|
2146
3150
|
this.sendRpcResponse(request.requestId, session);
|
|
2147
3151
|
} catch (error) {
|
|
2148
|
-
logger.error(
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
},
|
|
2155
|
-
"rpc_session_model_error"
|
|
2156
|
-
);
|
|
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");
|
|
2157
3158
|
this.sendRpcError(request.requestId, error);
|
|
2158
3159
|
}
|
|
2159
3160
|
});
|
|
@@ -2161,97 +3162,71 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2161
3162
|
const requestStart = process.hrtime.bigint();
|
|
2162
3163
|
try {
|
|
2163
3164
|
const { sessionId, prompt } = request.params;
|
|
2164
|
-
logger.info(
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
requestId: request.requestId,
|
|
2175
|
-
sessionId,
|
|
2176
|
-
promptBlocks: prompt.length
|
|
2177
|
-
},
|
|
2178
|
-
"rpc_message_send_start"
|
|
2179
|
-
);
|
|
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");
|
|
2180
3175
|
const record = sessionManager.getSession(sessionId);
|
|
2181
3176
|
if (!record) {
|
|
2182
3177
|
throw new Error("Session not found");
|
|
2183
3178
|
}
|
|
2184
3179
|
sessionManager.touchSession(sessionId);
|
|
2185
|
-
const result = await record.connection.prompt(
|
|
2186
|
-
sessionId,
|
|
2187
|
-
prompt
|
|
2188
|
-
);
|
|
3180
|
+
const result = await record.connection.prompt(sessionId, prompt);
|
|
2189
3181
|
sessionManager.touchSession(sessionId);
|
|
3182
|
+
sessionManager.recordTurnEnd(sessionId, result.stopReason);
|
|
2190
3183
|
this.sendRpcResponse(request.requestId, {
|
|
2191
3184
|
stopReason: result.stopReason
|
|
2192
3185
|
});
|
|
2193
3186
|
const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
|
|
2194
|
-
logger.info(
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
requestId: request.requestId,
|
|
2206
|
-
sessionId,
|
|
2207
|
-
durationMs
|
|
2208
|
-
},
|
|
2209
|
-
"rpc_message_send_finish"
|
|
2210
|
-
);
|
|
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");
|
|
2211
3198
|
} catch (error) {
|
|
2212
3199
|
const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
|
|
2213
|
-
logger.error(
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
},
|
|
2221
|
-
"rpc_message_send_error"
|
|
2222
|
-
);
|
|
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");
|
|
2223
3207
|
this.sendRpcError(request.requestId, error);
|
|
2224
3208
|
}
|
|
2225
3209
|
});
|
|
2226
3210
|
this.socket.on("rpc:permission:decision", async (request) => {
|
|
2227
3211
|
try {
|
|
2228
3212
|
const { sessionId, requestId, outcome } = request.params;
|
|
2229
|
-
logger.info(
|
|
2230
|
-
{ requestId: request.requestId, sessionId, outcome },
|
|
2231
|
-
"rpc_permission_decision"
|
|
2232
|
-
);
|
|
3213
|
+
logger.info({ requestId: request.requestId, sessionId, outcome }, "rpc_permission_decision");
|
|
2233
3214
|
sessionManager.resolvePermissionRequest(sessionId, requestId, outcome);
|
|
2234
3215
|
this.sendRpcResponse(request.requestId, { ok: true });
|
|
2235
3216
|
} catch (error) {
|
|
2236
|
-
logger.error(
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
},
|
|
2244
|
-
"rpc_permission_decision_error"
|
|
2245
|
-
);
|
|
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");
|
|
2246
3224
|
this.sendRpcError(request.requestId, error);
|
|
2247
3225
|
}
|
|
2248
3226
|
});
|
|
2249
3227
|
this.socket.on("rpc:fs:roots", async (request) => {
|
|
2250
3228
|
try {
|
|
2251
|
-
logger.debug(
|
|
2252
|
-
{ requestId: request.requestId, sessionId: request.params.sessionId },
|
|
2253
|
-
"rpc_fs_roots"
|
|
2254
|
-
);
|
|
3229
|
+
logger.debug({ requestId: request.requestId, sessionId: request.params.sessionId }, "rpc_fs_roots");
|
|
2255
3230
|
const record = sessionManager.getSession(request.params.sessionId);
|
|
2256
3231
|
if (!record || !record.cwd) {
|
|
2257
3232
|
throw new Error("Session not found or no working directory");
|
|
@@ -2262,105 +3237,81 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2262
3237
|
};
|
|
2263
3238
|
this.sendRpcResponse(request.requestId, { root });
|
|
2264
3239
|
} catch (error) {
|
|
2265
|
-
logger.error(
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
},
|
|
2271
|
-
"rpc_fs_roots_error"
|
|
2272
|
-
);
|
|
3240
|
+
logger.error({
|
|
3241
|
+
err: error,
|
|
3242
|
+
requestId: request.requestId,
|
|
3243
|
+
sessionId: request.params.sessionId
|
|
3244
|
+
}, "rpc_fs_roots_error");
|
|
2273
3245
|
this.sendRpcError(request.requestId, error);
|
|
2274
3246
|
}
|
|
2275
3247
|
});
|
|
2276
3248
|
this.socket.on("rpc:hostfs:roots", async (request) => {
|
|
2277
3249
|
try {
|
|
2278
|
-
logger.debug(
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
},
|
|
2283
|
-
"rpc_hostfs_roots"
|
|
2284
|
-
);
|
|
3250
|
+
logger.debug({
|
|
3251
|
+
requestId: request.requestId,
|
|
3252
|
+
machineId: request.params.machineId
|
|
3253
|
+
}, "rpc_hostfs_roots");
|
|
2285
3254
|
const result = await buildHostFsRoots();
|
|
2286
3255
|
this.sendRpcResponse(request.requestId, result);
|
|
2287
3256
|
} catch (error) {
|
|
2288
|
-
logger.error(
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
},
|
|
2294
|
-
"rpc_hostfs_roots_error"
|
|
2295
|
-
);
|
|
3257
|
+
logger.error({
|
|
3258
|
+
err: error,
|
|
3259
|
+
requestId: request.requestId,
|
|
3260
|
+
machineId: request.params.machineId
|
|
3261
|
+
}, "rpc_hostfs_roots_error");
|
|
2296
3262
|
this.sendRpcError(request.requestId, error);
|
|
2297
3263
|
}
|
|
2298
3264
|
});
|
|
2299
3265
|
this.socket.on("rpc:hostfs:entries", async (request) => {
|
|
2300
3266
|
try {
|
|
2301
3267
|
const { path: requestPath, machineId } = request.params;
|
|
2302
|
-
logger.debug(
|
|
2303
|
-
{ requestId: request.requestId, machineId, path: requestPath },
|
|
2304
|
-
"rpc_hostfs_entries"
|
|
2305
|
-
);
|
|
3268
|
+
logger.debug({ requestId: request.requestId, machineId, path: requestPath }, "rpc_hostfs_entries");
|
|
2306
3269
|
const entries = await readDirectoryEntries(requestPath);
|
|
2307
3270
|
this.sendRpcResponse(request.requestId, {
|
|
2308
3271
|
path: requestPath,
|
|
2309
3272
|
entries: filterVisibleEntries(entries)
|
|
2310
3273
|
});
|
|
2311
3274
|
} catch (error) {
|
|
2312
|
-
logger.error(
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
},
|
|
2318
|
-
"rpc_hostfs_entries_error"
|
|
2319
|
-
);
|
|
3275
|
+
logger.error({
|
|
3276
|
+
err: error,
|
|
3277
|
+
requestId: request.requestId,
|
|
3278
|
+
machineId: request.params.machineId
|
|
3279
|
+
}, "rpc_hostfs_entries_error");
|
|
2320
3280
|
this.sendRpcError(request.requestId, error);
|
|
2321
3281
|
}
|
|
2322
3282
|
});
|
|
2323
3283
|
this.socket.on("rpc:fs:entries", async (request) => {
|
|
2324
3284
|
try {
|
|
2325
3285
|
const { sessionId, path: requestPath } = request.params;
|
|
2326
|
-
logger.debug(
|
|
2327
|
-
{ requestId: request.requestId, sessionId, path: requestPath },
|
|
2328
|
-
"rpc_fs_entries"
|
|
2329
|
-
);
|
|
3286
|
+
logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_entries");
|
|
2330
3287
|
const record = sessionManager.getSession(sessionId);
|
|
2331
3288
|
if (!record || !record.cwd) {
|
|
2332
3289
|
throw new Error("Session not found or no working directory");
|
|
2333
3290
|
}
|
|
2334
|
-
const resolved = requestPath ?
|
|
3291
|
+
const resolved = requestPath ? resolveWithinCwd(record.cwd, requestPath) : record.cwd;
|
|
2335
3292
|
const entries = await readDirectoryEntries(resolved);
|
|
2336
3293
|
this.sendRpcResponse(request.requestId, { path: resolved, entries });
|
|
2337
3294
|
} catch (error) {
|
|
2338
|
-
logger.error(
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
},
|
|
2344
|
-
"rpc_fs_entries_error"
|
|
2345
|
-
);
|
|
3295
|
+
logger.error({
|
|
3296
|
+
err: error,
|
|
3297
|
+
requestId: request.requestId,
|
|
3298
|
+
sessionId: request.params.sessionId
|
|
3299
|
+
}, "rpc_fs_entries_error");
|
|
2346
3300
|
this.sendRpcError(request.requestId, error);
|
|
2347
3301
|
}
|
|
2348
3302
|
});
|
|
2349
3303
|
this.socket.on("rpc:fs:file", async (request) => {
|
|
2350
3304
|
try {
|
|
2351
3305
|
const { sessionId, path: requestPath } = request.params;
|
|
2352
|
-
logger.debug(
|
|
2353
|
-
{ requestId: request.requestId, sessionId, path: requestPath },
|
|
2354
|
-
"rpc_fs_file"
|
|
2355
|
-
);
|
|
3306
|
+
logger.debug({ requestId: request.requestId, sessionId, path: requestPath }, "rpc_fs_file");
|
|
2356
3307
|
const record = sessionManager.getSession(sessionId);
|
|
2357
3308
|
if (!record || !record.cwd) {
|
|
2358
3309
|
throw new Error("Session not found or no working directory");
|
|
2359
3310
|
}
|
|
2360
|
-
const resolved =
|
|
3311
|
+
const resolved = resolveWithinCwd(record.cwd, requestPath);
|
|
2361
3312
|
const mimeType = resolveImageMimeType(resolved);
|
|
2362
3313
|
if (mimeType) {
|
|
2363
|
-
const buffer = await
|
|
3314
|
+
const buffer = await fs5.readFile(resolved);
|
|
2364
3315
|
const preview2 = {
|
|
2365
3316
|
path: resolved,
|
|
2366
3317
|
previewType: "image",
|
|
@@ -2370,7 +3321,7 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2370
3321
|
this.sendRpcResponse(request.requestId, preview2);
|
|
2371
3322
|
return;
|
|
2372
3323
|
}
|
|
2373
|
-
const content = await
|
|
3324
|
+
const content = await fs5.readFile(resolved, "utf8");
|
|
2374
3325
|
const preview = {
|
|
2375
3326
|
path: resolved,
|
|
2376
3327
|
previewType: "code",
|
|
@@ -2378,24 +3329,18 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2378
3329
|
};
|
|
2379
3330
|
this.sendRpcResponse(request.requestId, preview);
|
|
2380
3331
|
} catch (error) {
|
|
2381
|
-
logger.error(
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
},
|
|
2387
|
-
"rpc_fs_file_error"
|
|
2388
|
-
);
|
|
3332
|
+
logger.error({
|
|
3333
|
+
err: error,
|
|
3334
|
+
requestId: request.requestId,
|
|
3335
|
+
sessionId: request.params.sessionId
|
|
3336
|
+
}, "rpc_fs_file_error");
|
|
2389
3337
|
this.sendRpcError(request.requestId, error);
|
|
2390
3338
|
}
|
|
2391
3339
|
});
|
|
2392
3340
|
this.socket.on("rpc:fs:resources", async (request) => {
|
|
2393
3341
|
try {
|
|
2394
3342
|
const { sessionId } = request.params;
|
|
2395
|
-
logger.debug(
|
|
2396
|
-
{ requestId: request.requestId, sessionId },
|
|
2397
|
-
"rpc_fs_resources"
|
|
2398
|
-
);
|
|
3343
|
+
logger.debug({ requestId: request.requestId, sessionId }, "rpc_fs_resources");
|
|
2399
3344
|
const record = sessionManager.getSession(sessionId);
|
|
2400
3345
|
if (!record || !record.cwd) {
|
|
2401
3346
|
throw new Error("Session not found or no working directory");
|
|
@@ -2406,24 +3351,18 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2406
3351
|
entries
|
|
2407
3352
|
});
|
|
2408
3353
|
} catch (error) {
|
|
2409
|
-
logger.error(
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
},
|
|
2415
|
-
"rpc_fs_resources_error"
|
|
2416
|
-
);
|
|
3354
|
+
logger.error({
|
|
3355
|
+
err: error,
|
|
3356
|
+
requestId: request.requestId,
|
|
3357
|
+
sessionId: request.params.sessionId
|
|
3358
|
+
}, "rpc_fs_resources_error");
|
|
2417
3359
|
this.sendRpcError(request.requestId, error);
|
|
2418
3360
|
}
|
|
2419
3361
|
});
|
|
2420
3362
|
this.socket.on("rpc:sessions:discover", async (request) => {
|
|
2421
3363
|
try {
|
|
2422
3364
|
const { cwd, backendId, cursor } = request.params;
|
|
2423
|
-
logger.info(
|
|
2424
|
-
{ requestId: request.requestId, cwd, backendId, cursor },
|
|
2425
|
-
"rpc_sessions_discover"
|
|
2426
|
-
);
|
|
3365
|
+
logger.info({ requestId: request.requestId, cwd, backendId, cursor }, "rpc_sessions_discover");
|
|
2427
3366
|
const result = await sessionManager.discoverSessions({
|
|
2428
3367
|
cwd,
|
|
2429
3368
|
backendId,
|
|
@@ -2431,65 +3370,47 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2431
3370
|
});
|
|
2432
3371
|
this.sendRpcResponse(request.requestId, result);
|
|
2433
3372
|
} catch (error) {
|
|
2434
|
-
logger.error(
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
},
|
|
2439
|
-
"rpc_sessions_discover_error"
|
|
2440
|
-
);
|
|
3373
|
+
logger.error({
|
|
3374
|
+
err: error,
|
|
3375
|
+
requestId: request.requestId
|
|
3376
|
+
}, "rpc_sessions_discover_error");
|
|
2441
3377
|
this.sendRpcError(request.requestId, error);
|
|
2442
3378
|
}
|
|
2443
3379
|
});
|
|
2444
3380
|
this.socket.on("rpc:session:load", async (request) => {
|
|
2445
3381
|
try {
|
|
2446
|
-
const { sessionId, cwd } = request.params;
|
|
2447
|
-
logger.info(
|
|
2448
|
-
|
|
2449
|
-
"rpc_session_load"
|
|
2450
|
-
);
|
|
2451
|
-
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);
|
|
2452
3385
|
this.sendRpcResponse(request.requestId, session);
|
|
2453
3386
|
} catch (error) {
|
|
2454
|
-
logger.error(
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
},
|
|
2460
|
-
"rpc_session_load_error"
|
|
2461
|
-
);
|
|
3387
|
+
logger.error({
|
|
3388
|
+
err: error,
|
|
3389
|
+
requestId: request.requestId,
|
|
3390
|
+
sessionId: request.params.sessionId
|
|
3391
|
+
}, "rpc_session_load_error");
|
|
2462
3392
|
this.sendRpcError(request.requestId, error);
|
|
2463
3393
|
}
|
|
2464
3394
|
});
|
|
2465
3395
|
this.socket.on("rpc:session:reload", async (request) => {
|
|
2466
3396
|
try {
|
|
2467
|
-
const { sessionId, cwd } = request.params;
|
|
2468
|
-
logger.info(
|
|
2469
|
-
|
|
2470
|
-
"rpc_session_reload"
|
|
2471
|
-
);
|
|
2472
|
-
const session = await sessionManager.reloadSession(sessionId, cwd);
|
|
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);
|
|
2473
3400
|
this.sendRpcResponse(request.requestId, session);
|
|
2474
3401
|
} catch (error) {
|
|
2475
|
-
logger.error(
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
},
|
|
2481
|
-
"rpc_session_reload_error"
|
|
2482
|
-
);
|
|
3402
|
+
logger.error({
|
|
3403
|
+
err: error,
|
|
3404
|
+
requestId: request.requestId,
|
|
3405
|
+
sessionId: request.params.sessionId
|
|
3406
|
+
}, "rpc_session_reload_error");
|
|
2483
3407
|
this.sendRpcError(request.requestId, error);
|
|
2484
3408
|
}
|
|
2485
3409
|
});
|
|
2486
3410
|
this.socket.on("rpc:git:status", async (request) => {
|
|
2487
3411
|
try {
|
|
2488
3412
|
const { sessionId } = request.params;
|
|
2489
|
-
logger.debug(
|
|
2490
|
-
{ requestId: request.requestId, sessionId },
|
|
2491
|
-
"rpc_git_status"
|
|
2492
|
-
);
|
|
3413
|
+
logger.debug({ requestId: request.requestId, sessionId }, "rpc_git_status");
|
|
2493
3414
|
const record = sessionManager.getSession(sessionId);
|
|
2494
3415
|
if (!record || !record.cwd) {
|
|
2495
3416
|
throw new Error("Session not found or no working directory");
|
|
@@ -2515,28 +3436,23 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2515
3436
|
dirStatus
|
|
2516
3437
|
});
|
|
2517
3438
|
} catch (error) {
|
|
2518
|
-
logger.error(
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
},
|
|
2524
|
-
"rpc_git_status_error"
|
|
2525
|
-
);
|
|
3439
|
+
logger.error({
|
|
3440
|
+
err: error,
|
|
3441
|
+
requestId: request.requestId,
|
|
3442
|
+
sessionId: request.params.sessionId
|
|
3443
|
+
}, "rpc_git_status_error");
|
|
2526
3444
|
this.sendRpcError(request.requestId, error);
|
|
2527
3445
|
}
|
|
2528
3446
|
});
|
|
2529
3447
|
this.socket.on("rpc:git:fileDiff", async (request) => {
|
|
2530
3448
|
try {
|
|
2531
3449
|
const { sessionId, path: filePath } = request.params;
|
|
2532
|
-
logger.debug(
|
|
2533
|
-
{ requestId: request.requestId, sessionId, path: filePath },
|
|
2534
|
-
"rpc_git_file_diff"
|
|
2535
|
-
);
|
|
3450
|
+
logger.debug({ requestId: request.requestId, sessionId, path: filePath }, "rpc_git_file_diff");
|
|
2536
3451
|
const record = sessionManager.getSession(sessionId);
|
|
2537
3452
|
if (!record || !record.cwd) {
|
|
2538
3453
|
throw new Error("Session not found or no working directory");
|
|
2539
3454
|
}
|
|
3455
|
+
resolveWithinCwd(record.cwd, filePath);
|
|
2540
3456
|
const isRepo = await isGitRepo(record.cwd);
|
|
2541
3457
|
if (!isRepo) {
|
|
2542
3458
|
this.sendRpcResponse(request.requestId, {
|
|
@@ -2547,10 +3463,7 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2547
3463
|
});
|
|
2548
3464
|
return;
|
|
2549
3465
|
}
|
|
2550
|
-
const { addedLines, modifiedLines } = await getFileDiff(
|
|
2551
|
-
record.cwd,
|
|
2552
|
-
filePath
|
|
2553
|
-
);
|
|
3466
|
+
const { addedLines, modifiedLines } = await getFileDiff(record.cwd, filePath);
|
|
2554
3467
|
this.sendRpcResponse(request.requestId, {
|
|
2555
3468
|
isGitRepo: true,
|
|
2556
3469
|
path: filePath,
|
|
@@ -2558,33 +3471,76 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2558
3471
|
modifiedLines
|
|
2559
3472
|
});
|
|
2560
3473
|
} catch (error) {
|
|
2561
|
-
logger.error(
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
},
|
|
2567
|
-
"rpc_git_file_diff_error"
|
|
2568
|
-
);
|
|
3474
|
+
logger.error({
|
|
3475
|
+
err: error,
|
|
3476
|
+
requestId: request.requestId,
|
|
3477
|
+
sessionId: request.params.sessionId
|
|
3478
|
+
}, "rpc_git_file_diff_error");
|
|
2569
3479
|
this.sendRpcError(request.requestId, error);
|
|
2570
3480
|
}
|
|
2571
3481
|
});
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
this.
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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);
|
|
2581
3495
|
}
|
|
2582
3496
|
});
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
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);
|
|
2586
3539
|
}
|
|
2587
3540
|
});
|
|
3541
|
+
}
|
|
3542
|
+
setupSessionManagerListeners() {
|
|
3543
|
+
const { sessionManager } = this.options;
|
|
2588
3544
|
sessionManager.onPermissionRequest((payload) => {
|
|
2589
3545
|
if (this.connected) {
|
|
2590
3546
|
this.socket.emit("permission:request", payload);
|
|
@@ -2595,21 +3551,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2595
3551
|
this.socket.emit("permission:result", payload);
|
|
2596
3552
|
}
|
|
2597
3553
|
});
|
|
2598
|
-
sessionManager.onTerminalOutput((event) => {
|
|
2599
|
-
if (this.connected) {
|
|
2600
|
-
this.socket.emit("terminal:output", event);
|
|
2601
|
-
}
|
|
2602
|
-
});
|
|
2603
3554
|
sessionManager.onSessionsChanged((payload) => {
|
|
2604
3555
|
if (this.connected) {
|
|
2605
|
-
logger.info(
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
},
|
|
2611
|
-
"sessions_changed_emit"
|
|
2612
|
-
);
|
|
3556
|
+
logger.info({
|
|
3557
|
+
added: payload.added.length,
|
|
3558
|
+
updated: payload.updated.length,
|
|
3559
|
+
removed: payload.removed.length
|
|
3560
|
+
}, "sessions_changed_emit");
|
|
2613
3561
|
this.socket.emit("sessions:changed", payload);
|
|
2614
3562
|
}
|
|
2615
3563
|
});
|
|
@@ -2623,13 +3571,42 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2623
3571
|
this.socket.emit("session:detached", payload);
|
|
2624
3572
|
}
|
|
2625
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
|
+
});
|
|
2626
3603
|
}
|
|
2627
3604
|
async listSessionResources(rootPath) {
|
|
2628
3605
|
const ig = await loadGitignore(rootPath);
|
|
2629
3606
|
const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
|
|
2630
3607
|
return allFiles.map((filePath) => ({
|
|
2631
|
-
name:
|
|
2632
|
-
relativePath:
|
|
3608
|
+
name: path6.basename(filePath),
|
|
3609
|
+
relativePath: path6.relative(rootPath, filePath),
|
|
2633
3610
|
path: filePath
|
|
2634
3611
|
}));
|
|
2635
3612
|
}
|
|
@@ -2637,13 +3614,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2637
3614
|
if (collected.length >= MAX_RESOURCE_FILES) {
|
|
2638
3615
|
return collected;
|
|
2639
3616
|
}
|
|
2640
|
-
const entries = await
|
|
3617
|
+
const entries = await fs5.readdir(rootPath, { withFileTypes: true });
|
|
2641
3618
|
for (const entry of entries) {
|
|
2642
3619
|
if (collected.length >= MAX_RESOURCE_FILES) {
|
|
2643
3620
|
break;
|
|
2644
3621
|
}
|
|
2645
|
-
const entryPath =
|
|
2646
|
-
const relativePath =
|
|
3622
|
+
const entryPath = path6.join(rootPath, entry.name);
|
|
3623
|
+
const relativePath = path6.relative(baseDir, entryPath);
|
|
2647
3624
|
const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
|
|
2648
3625
|
if (ig.ignores(checkPath)) {
|
|
2649
3626
|
continue;
|
|
@@ -2663,16 +3640,13 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2663
3640
|
}
|
|
2664
3641
|
sendRpcError(requestId, error) {
|
|
2665
3642
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2666
|
-
const detail = error instanceof Error ? error.stack :
|
|
2667
|
-
logger.error(
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
},
|
|
2674
|
-
"rpc_response_error_sent"
|
|
2675
|
-
);
|
|
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");
|
|
2676
3650
|
const response = {
|
|
2677
3651
|
requestId,
|
|
2678
3652
|
error: {
|
|
@@ -2695,28 +3669,45 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2695
3669
|
backends: config.acpBackends.map((backend) => ({
|
|
2696
3670
|
backendId: backend.id,
|
|
2697
3671
|
backendLabel: backend.label
|
|
2698
|
-
}))
|
|
2699
|
-
defaultBackendId: config.defaultAcpBackendId
|
|
3672
|
+
}))
|
|
2700
3673
|
});
|
|
2701
3674
|
logger.info({ machineId: config.machineId }, "cli_register_sessions_list");
|
|
2702
|
-
this.socket.emit("sessions:list", sessionManager.
|
|
3675
|
+
this.socket.emit("sessions:list", sessionManager.listAllSessions());
|
|
2703
3676
|
}
|
|
2704
3677
|
startHeartbeat() {
|
|
2705
3678
|
this.stopHeartbeat();
|
|
2706
3679
|
this.heartbeatInterval = setInterval(() => {
|
|
2707
3680
|
if (this.connected) {
|
|
2708
3681
|
this.socket.emit("cli:heartbeat");
|
|
2709
|
-
this.socket.emit(
|
|
2710
|
-
"sessions:list",
|
|
2711
|
-
this.options.sessionManager.listSessions()
|
|
2712
|
-
);
|
|
3682
|
+
this.socket.emit("sessions:list", this.options.sessionManager.listAllSessions());
|
|
2713
3683
|
}
|
|
2714
|
-
},
|
|
3684
|
+
}, 30000);
|
|
2715
3685
|
}
|
|
2716
3686
|
stopHeartbeat() {
|
|
2717
3687
|
if (this.heartbeatInterval) {
|
|
2718
3688
|
clearInterval(this.heartbeatInterval);
|
|
2719
|
-
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
|
+
}
|
|
2720
3711
|
}
|
|
2721
3712
|
}
|
|
2722
3713
|
connect() {
|
|
@@ -2729,20 +3720,21 @@ var SocketClient = class extends EventEmitter3 {
|
|
|
2729
3720
|
isConnected() {
|
|
2730
3721
|
return this.connected;
|
|
2731
3722
|
}
|
|
2732
|
-
}
|
|
3723
|
+
}
|
|
2733
3724
|
|
|
2734
3725
|
// src/daemon/daemon.ts
|
|
2735
|
-
|
|
3726
|
+
class DaemonManager {
|
|
3727
|
+
config;
|
|
2736
3728
|
constructor(config) {
|
|
2737
3729
|
this.config = config;
|
|
2738
3730
|
}
|
|
2739
3731
|
async ensureHomeDirectory() {
|
|
2740
|
-
await
|
|
2741
|
-
await
|
|
3732
|
+
await fs6.mkdir(this.config.homePath, { recursive: true });
|
|
3733
|
+
await fs6.mkdir(this.config.logPath, { recursive: true });
|
|
2742
3734
|
}
|
|
2743
3735
|
async getPid() {
|
|
2744
3736
|
try {
|
|
2745
|
-
const content = await
|
|
3737
|
+
const content = await fs6.readFile(this.config.pidFile, "utf8");
|
|
2746
3738
|
const pid = Number.parseInt(content.trim(), 10);
|
|
2747
3739
|
if (Number.isNaN(pid)) {
|
|
2748
3740
|
return null;
|
|
@@ -2759,13 +3751,12 @@ var DaemonManager = class {
|
|
|
2759
3751
|
}
|
|
2760
3752
|
}
|
|
2761
3753
|
async writePidFile(pid) {
|
|
2762
|
-
await
|
|
3754
|
+
await fs6.writeFile(this.config.pidFile, String(pid), "utf8");
|
|
2763
3755
|
}
|
|
2764
3756
|
async removePidFile() {
|
|
2765
3757
|
try {
|
|
2766
|
-
await
|
|
2767
|
-
} catch {
|
|
2768
|
-
}
|
|
3758
|
+
await fs6.unlink(this.config.pidFile);
|
|
3759
|
+
} catch {}
|
|
2769
3760
|
}
|
|
2770
3761
|
async status() {
|
|
2771
3762
|
const pid = await this.getPid();
|
|
@@ -2804,7 +3795,7 @@ var DaemonManager = class {
|
|
|
2804
3795
|
logger.info({ pid }, "daemon_stop_sigterm");
|
|
2805
3796
|
process.kill(pid, "SIGTERM");
|
|
2806
3797
|
const startTime = Date.now();
|
|
2807
|
-
const timeout =
|
|
3798
|
+
const timeout = 5000;
|
|
2808
3799
|
while (Date.now() - startTime < timeout) {
|
|
2809
3800
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2810
3801
|
try {
|
|
@@ -2830,10 +3821,7 @@ var DaemonManager = class {
|
|
|
2830
3821
|
}
|
|
2831
3822
|
}
|
|
2832
3823
|
async spawnBackground() {
|
|
2833
|
-
const logFile =
|
|
2834
|
-
this.config.logPath,
|
|
2835
|
-
`${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
|
|
2836
|
-
);
|
|
3824
|
+
const logFile = path7.join(this.config.logPath, `${new Date().toISOString().replace(/[:.]/g, "-")}-daemon.log`);
|
|
2837
3825
|
const args = process.argv.slice(1).filter((arg) => arg !== "--foreground" && arg !== "-f");
|
|
2838
3826
|
args.push("--foreground");
|
|
2839
3827
|
const child = spawn2(process.argv[0], args, {
|
|
@@ -2848,22 +3836,18 @@ var DaemonManager = class {
|
|
|
2848
3836
|
logger.error("daemon_spawn_failed");
|
|
2849
3837
|
throw new Error("Failed to spawn daemon process");
|
|
2850
3838
|
}
|
|
2851
|
-
const logStream = await
|
|
3839
|
+
const logStream = await fs6.open(logFile, "a");
|
|
2852
3840
|
const fileHandle = logStream;
|
|
2853
3841
|
child.stdout?.on("data", (data) => {
|
|
2854
|
-
fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {
|
|
2855
|
-
});
|
|
3842
|
+
fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {});
|
|
2856
3843
|
});
|
|
2857
3844
|
child.stderr?.on("data", (data) => {
|
|
2858
|
-
fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {
|
|
2859
|
-
});
|
|
3845
|
+
fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {});
|
|
2860
3846
|
});
|
|
2861
3847
|
child.on("exit", (code, signal) => {
|
|
2862
3848
|
fileHandle.write(`[exit] Process exited with code ${code}, signal ${signal}
|
|
2863
|
-
`).catch(() => {
|
|
2864
|
-
});
|
|
2865
|
-
fileHandle.close().catch(() => {
|
|
2866
|
-
});
|
|
3849
|
+
`).catch(() => {});
|
|
3850
|
+
fileHandle.close().catch(() => {});
|
|
2867
3851
|
});
|
|
2868
3852
|
child.unref();
|
|
2869
3853
|
logger.info({ pid: child.pid }, "daemon_started");
|
|
@@ -2876,22 +3860,47 @@ var DaemonManager = class {
|
|
|
2876
3860
|
logger.info({ pid }, "daemon_starting");
|
|
2877
3861
|
logger.info({ gatewayUrl: this.config.gatewayUrl }, "daemon_gateway_url");
|
|
2878
3862
|
logger.info({ machineId: this.config.machineId }, "daemon_machine_id");
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
);
|
|
2885
|
-
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");
|
|
2886
3869
|
process.exit(1);
|
|
2887
3870
|
}
|
|
2888
|
-
|
|
2889
|
-
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);
|
|
2890
3876
|
const socketClient = new SocketClient({
|
|
2891
3877
|
config: this.config,
|
|
2892
3878
|
sessionManager,
|
|
2893
|
-
|
|
3879
|
+
cryptoService
|
|
2894
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
|
+
}
|
|
2895
3904
|
let shuttingDown = false;
|
|
2896
3905
|
const shutdown = async (signal) => {
|
|
2897
3906
|
if (shuttingDown) {
|
|
@@ -2901,8 +3910,17 @@ var DaemonManager = class {
|
|
|
2901
3910
|
shuttingDown = true;
|
|
2902
3911
|
logger.info({ signal }, "daemon_shutdown_start");
|
|
2903
3912
|
try {
|
|
3913
|
+
if (compactionInterval) {
|
|
3914
|
+
clearInterval(compactionInterval);
|
|
3915
|
+
}
|
|
3916
|
+
if (compactorWalStore) {
|
|
3917
|
+
compactorWalStore.close();
|
|
3918
|
+
}
|
|
3919
|
+
if (compactorDb) {
|
|
3920
|
+
compactorDb.close();
|
|
3921
|
+
}
|
|
2904
3922
|
socketClient.disconnect();
|
|
2905
|
-
await sessionManager.
|
|
3923
|
+
await sessionManager.shutdown();
|
|
2906
3924
|
await this.removePidFile();
|
|
2907
3925
|
logger.info({ signal }, "daemon_shutdown_complete");
|
|
2908
3926
|
} catch (error) {
|
|
@@ -2920,18 +3938,17 @@ var DaemonManager = class {
|
|
|
2920
3938
|
});
|
|
2921
3939
|
});
|
|
2922
3940
|
socketClient.connect();
|
|
2923
|
-
await new Promise(() => {
|
|
2924
|
-
});
|
|
3941
|
+
await new Promise(() => {});
|
|
2925
3942
|
}
|
|
2926
3943
|
async logs(options) {
|
|
2927
|
-
const files = await
|
|
3944
|
+
const files = await fs6.readdir(this.config.logPath);
|
|
2928
3945
|
const logFiles = files.filter((f) => f.endsWith("-daemon.log")).sort().reverse();
|
|
2929
3946
|
if (logFiles.length === 0) {
|
|
2930
3947
|
logger.warn("daemon_logs_empty");
|
|
2931
3948
|
console.log("No log files found");
|
|
2932
3949
|
return;
|
|
2933
3950
|
}
|
|
2934
|
-
const latestLog =
|
|
3951
|
+
const latestLog = path7.join(this.config.logPath, logFiles[0]);
|
|
2935
3952
|
logger.info({ logFile: latestLog }, "daemon_logs_latest");
|
|
2936
3953
|
console.log(`Log file: ${latestLog}
|
|
2937
3954
|
`);
|
|
@@ -2943,16 +3960,18 @@ var DaemonManager = class {
|
|
|
2943
3960
|
tail.on("close", () => resolve());
|
|
2944
3961
|
});
|
|
2945
3962
|
} else {
|
|
2946
|
-
const content = await
|
|
2947
|
-
const lines = content.split(
|
|
3963
|
+
const content = await fs6.readFile(latestLog, "utf8");
|
|
3964
|
+
const lines = content.split(`
|
|
3965
|
+
`);
|
|
2948
3966
|
const count = options?.lines ?? 50;
|
|
2949
|
-
console.log(lines.slice(-count).join(
|
|
3967
|
+
console.log(lines.slice(-count).join(`
|
|
3968
|
+
`));
|
|
2950
3969
|
}
|
|
2951
3970
|
}
|
|
2952
|
-
}
|
|
3971
|
+
}
|
|
2953
3972
|
|
|
2954
3973
|
// src/index.ts
|
|
2955
|
-
var program = new Command
|
|
3974
|
+
var program = new Command;
|
|
2956
3975
|
program.name("mobvibe").description("Mobvibe CLI - Connect local ACP backends to the gateway").version("0.0.0");
|
|
2957
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) => {
|
|
2958
3977
|
if (options.gateway) {
|
|
@@ -2974,10 +3993,10 @@ program.command("status").description("Show daemon status").action(async () => {
|
|
|
2974
3993
|
if (status.running) {
|
|
2975
3994
|
logger.info({ pid: status.pid }, "daemon_status_running");
|
|
2976
3995
|
console.log(`Daemon is running (PID ${status.pid})`);
|
|
2977
|
-
if (status.connected !==
|
|
3996
|
+
if (status.connected !== undefined) {
|
|
2978
3997
|
console.log(`Connected to gateway: ${status.connected ? "yes" : "no"}`);
|
|
2979
3998
|
}
|
|
2980
|
-
if (status.sessionCount !==
|
|
3999
|
+
if (status.sessionCount !== undefined) {
|
|
2981
4000
|
console.log(`Active sessions: ${status.sessionCount}`);
|
|
2982
4001
|
}
|
|
2983
4002
|
} else {
|
|
@@ -3007,6 +4026,97 @@ program.command("logout").description("Remove stored credentials").action(async
|
|
|
3007
4026
|
program.command("auth-status").description("Show authentication status").action(async () => {
|
|
3008
4027
|
await loginStatus();
|
|
3009
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
|
+
});
|
|
3010
4120
|
async function run() {
|
|
3011
4121
|
await program.parseAsync(process.argv);
|
|
3012
4122
|
}
|
|
@@ -3018,4 +4128,5 @@ run().catch((error) => {
|
|
|
3018
4128
|
export {
|
|
3019
4129
|
run
|
|
3020
4130
|
};
|
|
3021
|
-
|
|
4131
|
+
|
|
4132
|
+
//# debugId=3BEAA052FAC1A9B764756E2164756E21
|