@devness/useai 0.5.26 → 0.5.28
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/dist/index.js +391 -129
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2681,7 +2681,7 @@ var VERSION;
|
|
|
2681
2681
|
var init_version = __esm({
|
|
2682
2682
|
"../shared/dist/constants/version.js"() {
|
|
2683
2683
|
"use strict";
|
|
2684
|
-
VERSION = "0.5.
|
|
2684
|
+
VERSION = "0.5.28";
|
|
2685
2685
|
}
|
|
2686
2686
|
});
|
|
2687
2687
|
|
|
@@ -17070,13 +17070,13 @@ function showManualHints(installedTools) {
|
|
|
17070
17070
|
for (const { name, hint } of hints) {
|
|
17071
17071
|
console.log(` ${source_default.bold(name)}: ${hint}`);
|
|
17072
17072
|
}
|
|
17073
|
-
console.log(
|
|
17073
|
+
console.log();
|
|
17074
17074
|
for (const line of USEAI_INSTRUCTIONS_TEXT.split("\n")) {
|
|
17075
|
-
console.log(
|
|
17075
|
+
console.log(` ${line}`);
|
|
17076
17076
|
}
|
|
17077
|
-
console.log(
|
|
17077
|
+
console.log();
|
|
17078
17078
|
}
|
|
17079
|
-
async function daemonInstallFlow(tools, explicit) {
|
|
17079
|
+
async function daemonInstallFlow(tools, autoYes, explicit) {
|
|
17080
17080
|
console.log(source_default.dim(" Ensuring UseAI daemon is running..."));
|
|
17081
17081
|
const daemonOk = await ensureDaemon();
|
|
17082
17082
|
let useDaemon = true;
|
|
@@ -17099,30 +17099,105 @@ async function daemonInstallFlow(tools, explicit) {
|
|
|
17099
17099
|
}
|
|
17100
17100
|
}
|
|
17101
17101
|
}
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17102
|
+
if (explicit) {
|
|
17103
|
+
console.log();
|
|
17104
|
+
let configuredCount2 = 0;
|
|
17105
|
+
for (const tool of tools) {
|
|
17106
|
+
try {
|
|
17107
|
+
configureToolDaemon(tool, useDaemon);
|
|
17108
|
+
configuredCount2++;
|
|
17109
|
+
} catch (e) {
|
|
17110
|
+
console.log(source_default.red(` \u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
|
|
17111
|
+
}
|
|
17112
|
+
}
|
|
17113
|
+
installHooksAndFinish(tools, configuredCount2, useDaemon);
|
|
17105
17114
|
return;
|
|
17106
17115
|
}
|
|
17107
|
-
|
|
17116
|
+
console.log(source_default.dim("\n Scanning for AI tools...\n"));
|
|
17117
|
+
const detected = tools.filter((t) => t.detect());
|
|
17118
|
+
if (detected.length === 0) {
|
|
17119
|
+
console.log(source_default.red(" No AI tools detected on this machine."));
|
|
17120
|
+
return;
|
|
17121
|
+
}
|
|
17122
|
+
const alreadyConfigured = detected.filter((t) => t.isConfigured());
|
|
17123
|
+
const unconfigured = detected.filter((t) => !t.isConfigured());
|
|
17124
|
+
console.log(` Found ${source_default.bold(String(detected.length))} AI tool${detected.length === 1 ? "" : "s"} on this machine:
|
|
17125
|
+
`);
|
|
17126
|
+
for (const tool of alreadyConfigured) {
|
|
17127
|
+
console.log(source_default.green(` \u2705 ${tool.name}`) + source_default.dim(" (already configured)"));
|
|
17128
|
+
}
|
|
17129
|
+
for (const tool of unconfigured) {
|
|
17130
|
+
console.log(source_default.dim(` \u2610 ${tool.name}`));
|
|
17131
|
+
}
|
|
17108
17132
|
console.log();
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
}
|
|
17115
|
-
tool.install();
|
|
17116
|
-
console.log(source_default.green(` \u2713 ${tool.name.padEnd(18)} \u2192 ${source_default.dim("stdio (no URL support)")}`));
|
|
17117
|
-
} else {
|
|
17118
|
-
tool.install();
|
|
17119
|
-
console.log(source_default.green(` \u2713 ${tool.name.padEnd(18)} \u2192 ${source_default.dim("stdio")}`));
|
|
17133
|
+
if (unconfigured.length === 0) {
|
|
17134
|
+
console.log(source_default.green(" All detected tools are already configured."));
|
|
17135
|
+
for (const tool of alreadyConfigured) {
|
|
17136
|
+
try {
|
|
17137
|
+
configureToolDaemon(tool, useDaemon);
|
|
17138
|
+
} catch {
|
|
17120
17139
|
}
|
|
17140
|
+
}
|
|
17141
|
+
installHooksAndFinish(alreadyConfigured, alreadyConfigured.length, useDaemon);
|
|
17142
|
+
return;
|
|
17143
|
+
}
|
|
17144
|
+
let toInstall;
|
|
17145
|
+
if (autoYes) {
|
|
17146
|
+
toInstall = unconfigured;
|
|
17147
|
+
} else {
|
|
17148
|
+
let selected;
|
|
17149
|
+
try {
|
|
17150
|
+
selected = await dist_default4({
|
|
17151
|
+
message: "Select tools to configure:",
|
|
17152
|
+
choices: unconfigured.map((t) => ({
|
|
17153
|
+
name: t.name,
|
|
17154
|
+
value: t.id,
|
|
17155
|
+
checked: true
|
|
17156
|
+
}))
|
|
17157
|
+
});
|
|
17158
|
+
} catch {
|
|
17159
|
+
console.log("\n");
|
|
17160
|
+
return;
|
|
17161
|
+
}
|
|
17162
|
+
toInstall = unconfigured.filter((t) => selected.includes(t.id));
|
|
17163
|
+
}
|
|
17164
|
+
if (toInstall.length === 0) {
|
|
17165
|
+
console.log(source_default.dim(" No tools selected."));
|
|
17166
|
+
return;
|
|
17167
|
+
}
|
|
17168
|
+
console.log(`
|
|
17169
|
+
Configuring ${toInstall.length} tool${toInstall.length === 1 ? "" : "s"}...
|
|
17170
|
+
`);
|
|
17171
|
+
let configuredCount = 0;
|
|
17172
|
+
for (const tool of toInstall) {
|
|
17173
|
+
try {
|
|
17174
|
+
configureToolDaemon(tool, useDaemon);
|
|
17121
17175
|
configuredCount++;
|
|
17122
17176
|
} catch (e) {
|
|
17123
17177
|
console.log(source_default.red(` \u2717 ${tool.name.padEnd(18)} \u2014 ${e.message}`));
|
|
17124
17178
|
}
|
|
17125
17179
|
}
|
|
17180
|
+
for (const tool of alreadyConfigured) {
|
|
17181
|
+
try {
|
|
17182
|
+
configureToolDaemon(tool, useDaemon);
|
|
17183
|
+
} catch {
|
|
17184
|
+
}
|
|
17185
|
+
}
|
|
17186
|
+
installHooksAndFinish([...toInstall, ...alreadyConfigured], configuredCount, useDaemon);
|
|
17187
|
+
}
|
|
17188
|
+
function configureToolDaemon(tool, useDaemon) {
|
|
17189
|
+
if (useDaemon && tool.supportsUrl) {
|
|
17190
|
+
tool.installHttp();
|
|
17191
|
+
console.log(source_default.green(` \u2713 ${tool.name.padEnd(18)} \u2192 ${source_default.dim("HTTP (daemon)")}`));
|
|
17192
|
+
} else if (useDaemon && !tool.supportsUrl) {
|
|
17193
|
+
tool.install();
|
|
17194
|
+
console.log(source_default.green(` \u2713 ${tool.name.padEnd(18)} \u2192 ${source_default.dim("stdio (no URL support)")}`));
|
|
17195
|
+
} else {
|
|
17196
|
+
tool.install();
|
|
17197
|
+
console.log(source_default.green(` \u2713 ${tool.name.padEnd(18)} \u2192 ${source_default.dim("stdio")}`));
|
|
17198
|
+
}
|
|
17199
|
+
}
|
|
17200
|
+
function installHooksAndFinish(allTools, configuredCount, useDaemon) {
|
|
17126
17201
|
try {
|
|
17127
17202
|
const hooksInstalled = installClaudeCodeHooks();
|
|
17128
17203
|
if (hooksInstalled) {
|
|
@@ -17131,7 +17206,7 @@ async function daemonInstallFlow(tools, explicit) {
|
|
|
17131
17206
|
} catch {
|
|
17132
17207
|
console.log(source_default.yellow(" \u26A0 Could not install Claude Code hooks"));
|
|
17133
17208
|
}
|
|
17134
|
-
showManualHints(
|
|
17209
|
+
showManualHints(allTools);
|
|
17135
17210
|
const mode = useDaemon ? "daemon mode" : "stdio mode";
|
|
17136
17211
|
console.log(`
|
|
17137
17212
|
Done! UseAI configured in ${source_default.bold(String(configuredCount))} tool${configuredCount === 1 ? "" : "s"} (${mode}).`);
|
|
@@ -17301,7 +17376,7 @@ async function runSetup(args) {
|
|
|
17301
17376
|
} else if (isStdio) {
|
|
17302
17377
|
await shared.installFlow(tools, autoYes, explicit);
|
|
17303
17378
|
} else {
|
|
17304
|
-
await daemonInstallFlow(tools, explicit);
|
|
17379
|
+
await daemonInstallFlow(tools, autoYes, explicit);
|
|
17305
17380
|
}
|
|
17306
17381
|
}
|
|
17307
17382
|
var shared;
|
|
@@ -37343,32 +37418,95 @@ function parseBody(req) {
|
|
|
37343
37418
|
req.on("error", reject);
|
|
37344
37419
|
});
|
|
37345
37420
|
}
|
|
37346
|
-
function
|
|
37421
|
+
function sendJsonRpcResult(res, rpcId, text) {
|
|
37422
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
37423
|
+
res.end(JSON.stringify({
|
|
37424
|
+
jsonrpc: "2.0",
|
|
37425
|
+
result: { content: [{ type: "text", text }] },
|
|
37426
|
+
id: rpcId
|
|
37427
|
+
}));
|
|
37428
|
+
}
|
|
37429
|
+
function readChainMetadata(useaiSessionId) {
|
|
37430
|
+
const activePath = join10(ACTIVE_DIR, `${useaiSessionId}.jsonl`);
|
|
37431
|
+
const sealedPath = join10(SEALED_DIR, `${useaiSessionId}.jsonl`);
|
|
37432
|
+
const chainPath = existsSync11(activePath) ? activePath : existsSync11(sealedPath) ? sealedPath : null;
|
|
37433
|
+
if (!chainPath) return null;
|
|
37434
|
+
try {
|
|
37435
|
+
const firstLine = readFileSync5(chainPath, "utf-8").split("\n")[0];
|
|
37436
|
+
if (!firstLine) return null;
|
|
37437
|
+
const record2 = JSON.parse(firstLine);
|
|
37438
|
+
const d = record2.data;
|
|
37439
|
+
return {
|
|
37440
|
+
client: d["client"] ?? "unknown",
|
|
37441
|
+
startTime: record2.timestamp,
|
|
37442
|
+
taskType: d["task_type"] ?? "coding",
|
|
37443
|
+
title: d["title"] ?? void 0,
|
|
37444
|
+
privateTitle: d["private_title"] ?? void 0,
|
|
37445
|
+
project: d["project"] ?? void 0,
|
|
37446
|
+
convId: d["conversation_id"] ?? void 0,
|
|
37447
|
+
convIdx: d["conversation_index"] ?? void 0
|
|
37448
|
+
};
|
|
37449
|
+
} catch {
|
|
37450
|
+
return null;
|
|
37451
|
+
}
|
|
37452
|
+
}
|
|
37453
|
+
function recoverStartSession(staleMcpSessionId, args, rpcId, res) {
|
|
37454
|
+
const map = readMcpMap();
|
|
37455
|
+
const prevSessionId = map[staleMcpSessionId];
|
|
37456
|
+
if (prevSessionId) {
|
|
37457
|
+
const prevActivePath = join10(ACTIVE_DIR, `${prevSessionId}.jsonl`);
|
|
37458
|
+
if (existsSync11(prevActivePath)) {
|
|
37459
|
+
sealOrphanFile(prevSessionId);
|
|
37460
|
+
}
|
|
37461
|
+
}
|
|
37462
|
+
let client = "unknown";
|
|
37463
|
+
if (prevSessionId) {
|
|
37464
|
+
const meta = readChainMetadata(prevSessionId);
|
|
37465
|
+
if (meta) client = meta.client;
|
|
37466
|
+
}
|
|
37467
|
+
const newSessionId = randomUUID4();
|
|
37468
|
+
const taskType = args["task_type"] ?? "coding";
|
|
37469
|
+
const title = args["title"];
|
|
37470
|
+
const privateTitle = args["private_title"];
|
|
37471
|
+
const project = args["project"];
|
|
37472
|
+
const convId = prevSessionId ? readChainMetadata(prevSessionId)?.convId ?? randomUUID4() : randomUUID4();
|
|
37473
|
+
const chainData = {
|
|
37474
|
+
client,
|
|
37475
|
+
task_type: taskType,
|
|
37476
|
+
project,
|
|
37477
|
+
conversation_id: convId,
|
|
37478
|
+
version: VERSION,
|
|
37479
|
+
recovered: true
|
|
37480
|
+
};
|
|
37481
|
+
if (title) chainData["title"] = title;
|
|
37482
|
+
if (privateTitle) chainData["private_title"] = privateTitle;
|
|
37483
|
+
const record2 = buildChainRecord("session_start", newSessionId, chainData, "GENESIS", daemonSigningKey);
|
|
37484
|
+
const chainPath = join10(ACTIVE_DIR, `${newSessionId}.jsonl`);
|
|
37485
|
+
appendFileSync2(chainPath, JSON.stringify(record2) + "\n");
|
|
37486
|
+
writeMcpMapping(staleMcpSessionId, newSessionId);
|
|
37487
|
+
sendJsonRpcResult(
|
|
37488
|
+
res,
|
|
37489
|
+
rpcId,
|
|
37490
|
+
`useai session started \u2014 ${taskType} on ${client} \xB7 ${newSessionId.slice(0, 8)} \xB7 conv ${convId.slice(0, 8)} \xB7 recovered \xB7 ${daemonSigningKey ? "signed" : "unsigned"}`
|
|
37491
|
+
);
|
|
37492
|
+
console.log(`Recovered useai_start: new session ${newSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
|
|
37493
|
+
return true;
|
|
37494
|
+
}
|
|
37495
|
+
function recoverHeartbeat(staleMcpSessionId, rpcId, res) {
|
|
37496
|
+
const map = readMcpMap();
|
|
37497
|
+
const useaiSessionId = map[staleMcpSessionId];
|
|
37498
|
+
if (!useaiSessionId) return false;
|
|
37499
|
+
const chainPath = join10(ACTIVE_DIR, `${useaiSessionId}.jsonl`);
|
|
37500
|
+
if (!existsSync11(chainPath)) {
|
|
37501
|
+
sendJsonRpcResult(res, rpcId, "Session already ended (recovered).");
|
|
37502
|
+
return true;
|
|
37503
|
+
}
|
|
37347
37504
|
try {
|
|
37348
|
-
const rpc = body;
|
|
37349
|
-
if (rpc?.method !== "tools/call" || rpc?.params?.name !== "useai_end") return false;
|
|
37350
|
-
const args = rpc.params?.arguments ?? {};
|
|
37351
|
-
const rpcId = rpc.id;
|
|
37352
|
-
const map = readMcpMap();
|
|
37353
|
-
const useaiSessionId = map[staleMcpSessionId];
|
|
37354
|
-
if (!useaiSessionId) return false;
|
|
37355
|
-
const chainPath = join10(ACTIVE_DIR, `${useaiSessionId}.jsonl`);
|
|
37356
|
-
if (!existsSync11(chainPath)) return false;
|
|
37357
37505
|
const content = readFileSync5(chainPath, "utf-8").trim();
|
|
37358
|
-
if (!content) return false;
|
|
37359
37506
|
const lines = content.split("\n").filter(Boolean);
|
|
37360
37507
|
if (lines.length === 0) return false;
|
|
37361
37508
|
const firstRecord = JSON.parse(lines[0]);
|
|
37362
37509
|
const lastRecord = JSON.parse(lines[lines.length - 1]);
|
|
37363
|
-
if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") return false;
|
|
37364
|
-
const startData = firstRecord.data;
|
|
37365
|
-
const client = startData["client"] ?? "unknown";
|
|
37366
|
-
const startTime = firstRecord.timestamp;
|
|
37367
|
-
const sessionTitle = startData["title"] ?? void 0;
|
|
37368
|
-
const sessionPrivateTitle = startData["private_title"] ?? void 0;
|
|
37369
|
-
const sessionProject = startData["project"] ?? void 0;
|
|
37370
|
-
const convId = startData["conversation_id"] ?? void 0;
|
|
37371
|
-
const convIdx = startData["conversation_index"] ?? void 0;
|
|
37372
37510
|
let heartbeatCount = 0;
|
|
37373
37511
|
for (const line of lines) {
|
|
37374
37512
|
try {
|
|
@@ -37376,34 +37514,58 @@ function tryRecoverEndSession(staleMcpSessionId, body, res) {
|
|
|
37376
37514
|
} catch {
|
|
37377
37515
|
}
|
|
37378
37516
|
}
|
|
37379
|
-
|
|
37380
|
-
const duration3 = Math.round((Date.now() - new Date(
|
|
37381
|
-
const
|
|
37382
|
-
|
|
37383
|
-
|
|
37384
|
-
|
|
37385
|
-
|
|
37386
|
-
|
|
37387
|
-
|
|
37388
|
-
|
|
37517
|
+
heartbeatCount++;
|
|
37518
|
+
const duration3 = Math.round((Date.now() - new Date(firstRecord.timestamp).getTime()) / 1e3);
|
|
37519
|
+
const record2 = buildChainRecord("heartbeat", useaiSessionId, {
|
|
37520
|
+
heartbeat_number: heartbeatCount,
|
|
37521
|
+
cumulative_seconds: duration3,
|
|
37522
|
+
recovered: true
|
|
37523
|
+
}, lastRecord.hash, daemonSigningKey);
|
|
37524
|
+
appendFileSync2(chainPath, JSON.stringify(record2) + "\n");
|
|
37525
|
+
sendJsonRpcResult(res, rpcId, `Heartbeat recorded. Session active for ${formatDuration(duration3)}.`);
|
|
37526
|
+
return true;
|
|
37527
|
+
} catch {
|
|
37528
|
+
return false;
|
|
37529
|
+
}
|
|
37530
|
+
}
|
|
37531
|
+
function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
|
|
37532
|
+
const map = readMcpMap();
|
|
37533
|
+
const useaiSessionId = map[staleMcpSessionId];
|
|
37534
|
+
if (!useaiSessionId) return false;
|
|
37535
|
+
const activePath = join10(ACTIVE_DIR, `${useaiSessionId}.jsonl`);
|
|
37536
|
+
const sealedPath = join10(SEALED_DIR, `${useaiSessionId}.jsonl`);
|
|
37537
|
+
const chainPath = existsSync11(activePath) ? activePath : existsSync11(sealedPath) ? sealedPath : null;
|
|
37538
|
+
if (!chainPath) return false;
|
|
37539
|
+
const isAlreadySealed = chainPath === sealedPath;
|
|
37540
|
+
const content = readFileSync5(chainPath, "utf-8").trim();
|
|
37541
|
+
if (!content) return false;
|
|
37542
|
+
const lines = content.split("\n").filter(Boolean);
|
|
37543
|
+
if (lines.length === 0) return false;
|
|
37544
|
+
const firstRecord = JSON.parse(lines[0]);
|
|
37545
|
+
const startData = firstRecord.data;
|
|
37546
|
+
const client = startData["client"] ?? "unknown";
|
|
37547
|
+
const startTime = firstRecord.timestamp;
|
|
37548
|
+
const sessionTitle = startData["title"] ?? void 0;
|
|
37549
|
+
const sessionPrivateTitle = startData["private_title"] ?? void 0;
|
|
37550
|
+
const sessionProject = startData["project"] ?? void 0;
|
|
37551
|
+
const convId = startData["conversation_id"] ?? void 0;
|
|
37552
|
+
const convIdx = startData["conversation_index"] ?? void 0;
|
|
37553
|
+
const taskType = args["task_type"] ?? startData["task_type"] ?? "coding";
|
|
37554
|
+
const languages = args["languages"] ?? [];
|
|
37555
|
+
const filesTouched = args["files_touched_count"] ?? 0;
|
|
37556
|
+
const milestonesInput = args["milestones"];
|
|
37557
|
+
const evaluation = args["evaluation"];
|
|
37558
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
37559
|
+
const duration3 = Math.round((Date.now() - new Date(startTime).getTime()) / 1e3);
|
|
37560
|
+
if (isAlreadySealed) {
|
|
37561
|
+
let milestoneCount2 = 0;
|
|
37389
37562
|
if (milestonesInput && milestonesInput.length > 0) {
|
|
37390
37563
|
const config2 = readJson(CONFIG_FILE, { milestone_tracking: true, auto_sync: true });
|
|
37391
37564
|
if (config2.milestone_tracking) {
|
|
37392
37565
|
const durationMinutes = Math.round(duration3 / 60);
|
|
37393
37566
|
const allMilestones = readJson(MILESTONES_FILE, []);
|
|
37394
37567
|
for (const m of milestonesInput) {
|
|
37395
|
-
|
|
37396
|
-
title: m.title,
|
|
37397
|
-
private_title: m.private_title,
|
|
37398
|
-
category: m.category,
|
|
37399
|
-
complexity: m.complexity ?? "medium",
|
|
37400
|
-
duration_minutes: durationMinutes,
|
|
37401
|
-
languages
|
|
37402
|
-
}, chainTip, daemonSigningKey);
|
|
37403
|
-
appendFileSync2(chainPath, JSON.stringify(mRecord) + "\n");
|
|
37404
|
-
chainTip = mRecord.hash;
|
|
37405
|
-
recordCount++;
|
|
37406
|
-
const milestone = {
|
|
37568
|
+
allMilestones.push({
|
|
37407
37569
|
id: `m_${randomUUID4().slice(0, 8)}`,
|
|
37408
37570
|
session_id: useaiSessionId,
|
|
37409
37571
|
title: m.title,
|
|
@@ -37417,26 +37579,14 @@ function tryRecoverEndSession(staleMcpSessionId, body, res) {
|
|
|
37417
37579
|
created_at: now,
|
|
37418
37580
|
published: false,
|
|
37419
37581
|
published_at: null,
|
|
37420
|
-
chain_hash:
|
|
37421
|
-
};
|
|
37422
|
-
|
|
37423
|
-
milestoneCount++;
|
|
37582
|
+
chain_hash: ""
|
|
37583
|
+
});
|
|
37584
|
+
milestoneCount2++;
|
|
37424
37585
|
}
|
|
37425
37586
|
writeJson(MILESTONES_FILE, allMilestones);
|
|
37426
37587
|
}
|
|
37427
37588
|
}
|
|
37428
|
-
const
|
|
37429
|
-
duration_seconds: duration3,
|
|
37430
|
-
task_type: taskType,
|
|
37431
|
-
languages,
|
|
37432
|
-
files_touched: filesTouched,
|
|
37433
|
-
heartbeat_count: heartbeatCount,
|
|
37434
|
-
recovered: true,
|
|
37435
|
-
...evaluation ? { evaluation } : {}
|
|
37436
|
-
}, chainTip, daemonSigningKey);
|
|
37437
|
-
appendFileSync2(chainPath, JSON.stringify(endRecord) + "\n");
|
|
37438
|
-
recordCount++;
|
|
37439
|
-
const sealData = JSON.stringify({
|
|
37589
|
+
const richSeal = {
|
|
37440
37590
|
session_id: useaiSessionId,
|
|
37441
37591
|
conversation_id: convId,
|
|
37442
37592
|
conversation_index: convIdx,
|
|
@@ -37451,60 +37601,172 @@ function tryRecoverEndSession(staleMcpSessionId, body, res) {
|
|
|
37451
37601
|
started_at: startTime,
|
|
37452
37602
|
ended_at: now,
|
|
37453
37603
|
duration_seconds: duration3,
|
|
37454
|
-
heartbeat_count:
|
|
37455
|
-
record_count:
|
|
37456
|
-
|
|
37457
|
-
chain_end_hash:
|
|
37458
|
-
|
|
37459
|
-
|
|
37460
|
-
|
|
37461
|
-
|
|
37604
|
+
heartbeat_count: 0,
|
|
37605
|
+
record_count: lines.length,
|
|
37606
|
+
chain_start_hash: firstRecord.prev_hash,
|
|
37607
|
+
chain_end_hash: "",
|
|
37608
|
+
seal_signature: ""
|
|
37609
|
+
};
|
|
37610
|
+
upsertSessionSeal(richSeal);
|
|
37611
|
+
removeMcpMapping(staleMcpSessionId);
|
|
37612
|
+
const durationStr2 = formatDuration(duration3);
|
|
37613
|
+
sendJsonRpcResult(
|
|
37614
|
+
res,
|
|
37615
|
+
rpcId,
|
|
37616
|
+
`Session ended (recovered): ${durationStr2} ${taskType}` + (milestoneCount2 > 0 ? ` \xB7 ${milestoneCount2} milestone${milestoneCount2 > 1 ? "s" : ""} recorded` : "") + (evaluation ? ` \xB7 eval: ${evaluation.task_outcome}` : "")
|
|
37462
37617
|
);
|
|
37463
|
-
|
|
37618
|
+
console.log(`Recovered useai_end for already-sealed session ${useaiSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
|
|
37619
|
+
return true;
|
|
37620
|
+
}
|
|
37621
|
+
const lastRecord = JSON.parse(lines[lines.length - 1]);
|
|
37622
|
+
if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") {
|
|
37623
|
+
removeMcpMapping(staleMcpSessionId);
|
|
37624
|
+
sendJsonRpcResult(res, rpcId, "Session already ended.");
|
|
37625
|
+
return true;
|
|
37626
|
+
}
|
|
37627
|
+
let heartbeatCount = 0;
|
|
37628
|
+
for (const line of lines) {
|
|
37629
|
+
try {
|
|
37630
|
+
if (JSON.parse(line).type === "heartbeat") heartbeatCount++;
|
|
37631
|
+
} catch {
|
|
37632
|
+
}
|
|
37633
|
+
}
|
|
37634
|
+
let chainTip = lastRecord.hash;
|
|
37635
|
+
let recordCount = lines.length;
|
|
37636
|
+
let milestoneCount = 0;
|
|
37637
|
+
if (milestonesInput && milestonesInput.length > 0) {
|
|
37638
|
+
const config2 = readJson(CONFIG_FILE, { milestone_tracking: true, auto_sync: true });
|
|
37639
|
+
if (config2.milestone_tracking) {
|
|
37640
|
+
const durationMinutes = Math.round(duration3 / 60);
|
|
37641
|
+
const allMilestones = readJson(MILESTONES_FILE, []);
|
|
37642
|
+
for (const m of milestonesInput) {
|
|
37643
|
+
const mRecord = buildChainRecord("milestone", useaiSessionId, {
|
|
37644
|
+
title: m.title,
|
|
37645
|
+
private_title: m.private_title,
|
|
37646
|
+
category: m.category,
|
|
37647
|
+
complexity: m.complexity ?? "medium",
|
|
37648
|
+
duration_minutes: durationMinutes,
|
|
37649
|
+
languages
|
|
37650
|
+
}, chainTip, daemonSigningKey);
|
|
37651
|
+
appendFileSync2(activePath, JSON.stringify(mRecord) + "\n");
|
|
37652
|
+
chainTip = mRecord.hash;
|
|
37653
|
+
recordCount++;
|
|
37654
|
+
allMilestones.push({
|
|
37655
|
+
id: `m_${randomUUID4().slice(0, 8)}`,
|
|
37656
|
+
session_id: useaiSessionId,
|
|
37657
|
+
title: m.title,
|
|
37658
|
+
private_title: m.private_title,
|
|
37659
|
+
project: sessionProject,
|
|
37660
|
+
category: m.category,
|
|
37661
|
+
complexity: m.complexity ?? "medium",
|
|
37662
|
+
duration_minutes: durationMinutes,
|
|
37663
|
+
languages,
|
|
37664
|
+
client,
|
|
37665
|
+
created_at: now,
|
|
37666
|
+
published: false,
|
|
37667
|
+
published_at: null,
|
|
37668
|
+
chain_hash: mRecord.hash
|
|
37669
|
+
});
|
|
37670
|
+
milestoneCount++;
|
|
37671
|
+
}
|
|
37672
|
+
writeJson(MILESTONES_FILE, allMilestones);
|
|
37673
|
+
}
|
|
37674
|
+
}
|
|
37675
|
+
const endRecord = buildChainRecord("session_end", useaiSessionId, {
|
|
37676
|
+
duration_seconds: duration3,
|
|
37677
|
+
task_type: taskType,
|
|
37678
|
+
languages,
|
|
37679
|
+
files_touched: filesTouched,
|
|
37680
|
+
heartbeat_count: heartbeatCount,
|
|
37681
|
+
recovered: true,
|
|
37682
|
+
...evaluation ? { evaluation } : {}
|
|
37683
|
+
}, chainTip, daemonSigningKey);
|
|
37684
|
+
appendFileSync2(activePath, JSON.stringify(endRecord) + "\n");
|
|
37685
|
+
recordCount++;
|
|
37686
|
+
const sealData = JSON.stringify({
|
|
37687
|
+
session_id: useaiSessionId,
|
|
37688
|
+
conversation_id: convId,
|
|
37689
|
+
conversation_index: convIdx,
|
|
37690
|
+
client,
|
|
37691
|
+
task_type: taskType,
|
|
37692
|
+
languages,
|
|
37693
|
+
files_touched: filesTouched,
|
|
37694
|
+
project: sessionProject,
|
|
37695
|
+
title: sessionTitle,
|
|
37696
|
+
private_title: sessionPrivateTitle,
|
|
37697
|
+
evaluation: evaluation ?? void 0,
|
|
37698
|
+
started_at: startTime,
|
|
37699
|
+
ended_at: now,
|
|
37700
|
+
duration_seconds: duration3,
|
|
37701
|
+
heartbeat_count: heartbeatCount,
|
|
37702
|
+
record_count: recordCount + 1,
|
|
37703
|
+
chain_end_hash: endRecord.hash
|
|
37704
|
+
});
|
|
37705
|
+
const sealSignature = signHash(
|
|
37706
|
+
createHash4("sha256").update(sealData).digest("hex"),
|
|
37707
|
+
daemonSigningKey
|
|
37708
|
+
);
|
|
37709
|
+
appendFileSync2(activePath, JSON.stringify(
|
|
37710
|
+
buildChainRecord("session_seal", useaiSessionId, {
|
|
37464
37711
|
seal: sealData,
|
|
37465
37712
|
seal_signature: sealSignature,
|
|
37466
37713
|
recovered: true
|
|
37467
|
-
}, endRecord.hash, daemonSigningKey)
|
|
37468
|
-
|
|
37469
|
-
|
|
37470
|
-
|
|
37471
|
-
|
|
37714
|
+
}, endRecord.hash, daemonSigningKey)
|
|
37715
|
+
) + "\n");
|
|
37716
|
+
try {
|
|
37717
|
+
renameSync3(activePath, sealedPath);
|
|
37718
|
+
} catch {
|
|
37719
|
+
}
|
|
37720
|
+
upsertSessionSeal({
|
|
37721
|
+
session_id: useaiSessionId,
|
|
37722
|
+
conversation_id: convId,
|
|
37723
|
+
conversation_index: convIdx,
|
|
37724
|
+
client,
|
|
37725
|
+
task_type: taskType,
|
|
37726
|
+
languages,
|
|
37727
|
+
files_touched: filesTouched,
|
|
37728
|
+
project: sessionProject,
|
|
37729
|
+
title: sessionTitle,
|
|
37730
|
+
private_title: sessionPrivateTitle,
|
|
37731
|
+
evaluation: evaluation ?? void 0,
|
|
37732
|
+
started_at: startTime,
|
|
37733
|
+
ended_at: now,
|
|
37734
|
+
duration_seconds: duration3,
|
|
37735
|
+
heartbeat_count: heartbeatCount,
|
|
37736
|
+
record_count: recordCount + 1,
|
|
37737
|
+
chain_start_hash: firstRecord.prev_hash,
|
|
37738
|
+
chain_end_hash: endRecord.hash,
|
|
37739
|
+
seal_signature: sealSignature
|
|
37740
|
+
});
|
|
37741
|
+
removeMcpMapping(staleMcpSessionId);
|
|
37742
|
+
const durationStr = formatDuration(duration3);
|
|
37743
|
+
sendJsonRpcResult(
|
|
37744
|
+
res,
|
|
37745
|
+
rpcId,
|
|
37746
|
+
`Session ended (recovered): ${durationStr} ${taskType}` + (milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "") + (evaluation ? ` \xB7 eval: ${evaluation.task_outcome}` : "")
|
|
37747
|
+
);
|
|
37748
|
+
console.log(`Recovered useai_end for stale session ${useaiSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
|
|
37749
|
+
return true;
|
|
37750
|
+
}
|
|
37751
|
+
function tryRecoverStaleSession(staleMcpSessionId, body, res) {
|
|
37752
|
+
try {
|
|
37753
|
+
const rpc = body;
|
|
37754
|
+
if (rpc?.method !== "tools/call") return false;
|
|
37755
|
+
const toolName = rpc.params?.name;
|
|
37756
|
+
const args = rpc.params?.arguments ?? {};
|
|
37757
|
+
const rpcId = rpc.id;
|
|
37758
|
+
switch (toolName) {
|
|
37759
|
+
case "useai_start":
|
|
37760
|
+
return recoverStartSession(staleMcpSessionId, args, rpcId, res);
|
|
37761
|
+
case "useai_heartbeat":
|
|
37762
|
+
return recoverHeartbeat(staleMcpSessionId, rpcId, res);
|
|
37763
|
+
case "useai_end":
|
|
37764
|
+
return recoverEndSession(staleMcpSessionId, args, rpcId, res);
|
|
37765
|
+
default:
|
|
37766
|
+
return false;
|
|
37472
37767
|
}
|
|
37473
|
-
const seal = {
|
|
37474
|
-
session_id: useaiSessionId,
|
|
37475
|
-
conversation_id: convId,
|
|
37476
|
-
conversation_index: convIdx,
|
|
37477
|
-
client,
|
|
37478
|
-
task_type: taskType,
|
|
37479
|
-
languages,
|
|
37480
|
-
files_touched: filesTouched,
|
|
37481
|
-
project: sessionProject,
|
|
37482
|
-
title: sessionTitle,
|
|
37483
|
-
private_title: sessionPrivateTitle,
|
|
37484
|
-
evaluation: evaluation ?? void 0,
|
|
37485
|
-
started_at: startTime,
|
|
37486
|
-
ended_at: now,
|
|
37487
|
-
duration_seconds: duration3,
|
|
37488
|
-
heartbeat_count: heartbeatCount,
|
|
37489
|
-
record_count: recordCount + 1,
|
|
37490
|
-
chain_start_hash: firstRecord.prev_hash,
|
|
37491
|
-
chain_end_hash: endRecord.hash,
|
|
37492
|
-
seal_signature: sealSignature
|
|
37493
|
-
};
|
|
37494
|
-
upsertSessionSeal(seal);
|
|
37495
|
-
removeMcpMapping(staleMcpSessionId);
|
|
37496
|
-
const durationMin = Math.round(duration3 / 60);
|
|
37497
|
-
const responseText = `Session ended (recovered): ${durationMin}m ${taskType}` + (milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "") + (evaluation ? ` \xB7 eval: ${evaluation.task_outcome}` : "");
|
|
37498
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
37499
|
-
res.end(JSON.stringify({
|
|
37500
|
-
jsonrpc: "2.0",
|
|
37501
|
-
result: { content: [{ type: "text", text: responseText }] },
|
|
37502
|
-
id: rpcId
|
|
37503
|
-
}));
|
|
37504
|
-
console.log(`Recovered useai_end for stale session ${useaiSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
|
|
37505
|
-
return true;
|
|
37506
37768
|
} catch (err) {
|
|
37507
|
-
console.error("
|
|
37769
|
+
console.error("Stale session recovery failed:", err.message);
|
|
37508
37770
|
return false;
|
|
37509
37771
|
}
|
|
37510
37772
|
}
|
|
@@ -37674,7 +37936,7 @@ async function startDaemon(port) {
|
|
|
37674
37936
|
resetIdleTimer(sid);
|
|
37675
37937
|
await sessions.get(sid).transport.handleRequest(req, res, body);
|
|
37676
37938
|
} else if (sid && !sessions.has(sid)) {
|
|
37677
|
-
if (!
|
|
37939
|
+
if (!tryRecoverStaleSession(sid, body, res)) {
|
|
37678
37940
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
37679
37941
|
res.end(JSON.stringify({
|
|
37680
37942
|
jsonrpc: "2.0",
|