@devness/useai 0.5.29 → 0.5.30

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.
Files changed (2) hide show
  1. package/dist/index.js +72 -44
  2. 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.29";
2684
+ VERSION = "0.5.30";
2685
2685
  }
2686
2686
  });
2687
2687
 
@@ -36341,11 +36341,17 @@ var init_session_state = __esm({
36341
36341
  conversationIndex;
36342
36342
  /** MCP transport session ID (set in daemon mode). Survives reset(). */
36343
36343
  mcpSessionId;
36344
+ /** Self-reported model ID from useai_start (e.g. "claude-opus-4-6"). */
36345
+ modelId;
36346
+ /** Token estimates for the useai_start tool call. */
36347
+ startCallTokensEst;
36344
36348
  constructor() {
36345
36349
  this.sessionId = generateSessionId();
36346
36350
  this.conversationId = generateSessionId();
36347
36351
  this.conversationIndex = 0;
36348
36352
  this.mcpSessionId = null;
36353
+ this.modelId = null;
36354
+ this.startCallTokensEst = null;
36349
36355
  this.sessionStartTime = Date.now();
36350
36356
  this.heartbeatCount = 0;
36351
36357
  this.sessionRecordCount = 0;
@@ -36370,6 +36376,8 @@ var init_session_state = __esm({
36370
36376
  this.sessionTitle = null;
36371
36377
  this.sessionPrivateTitle = null;
36372
36378
  this.sessionPromptWordCount = null;
36379
+ this.modelId = null;
36380
+ this.startCallTokensEst = null;
36373
36381
  this.detectProject();
36374
36382
  }
36375
36383
  detectProject() {
@@ -36393,6 +36401,9 @@ var init_session_state = __esm({
36393
36401
  setPromptWordCount(count) {
36394
36402
  this.sessionPromptWordCount = count;
36395
36403
  }
36404
+ setModel(id) {
36405
+ this.modelId = id;
36406
+ }
36396
36407
  incrementHeartbeat() {
36397
36408
  this.heartbeatCount++;
36398
36409
  }
@@ -36447,25 +36458,6 @@ function writeMcpMapping(mcpSessionId, useaiSessionId) {
36447
36458
  map[mcpSessionId] = useaiSessionId;
36448
36459
  writeJson(mcpMapPath(), map);
36449
36460
  }
36450
- function removeMcpMapping(mcpSessionId) {
36451
- if (!mcpSessionId) return;
36452
- const map = readMcpMap();
36453
- if (mcpSessionId in map) {
36454
- delete map[mcpSessionId];
36455
- writeJson(mcpMapPath(), map);
36456
- }
36457
- }
36458
- function removeMcpMappingByUseaiId(useaiSessionId) {
36459
- const map = readMcpMap();
36460
- let changed = false;
36461
- for (const [mcpId, sessionId] of Object.entries(map)) {
36462
- if (sessionId === useaiSessionId) {
36463
- delete map[mcpId];
36464
- changed = true;
36465
- }
36466
- }
36467
- if (changed) writeJson(mcpMapPath(), map);
36468
- }
36469
36461
  var init_mcp_map = __esm({
36470
36462
  "src/mcp-map.ts"() {
36471
36463
  "use strict";
@@ -36510,15 +36502,17 @@ function registerTools(server2, session2, opts) {
36510
36502
  task_type: external_exports.enum(["coding", "debugging", "testing", "planning", "reviewing", "documenting", "learning", "other"]).optional().describe("What kind of task is the developer working on?"),
36511
36503
  title: external_exports.string().optional().describe(`Short public session title derived from the user's prompt. No project names, file paths, or identifying details. Example: "Fix authentication bug"`),
36512
36504
  private_title: external_exports.string().optional().describe('Detailed session title for private records. Can include project names and specifics. Example: "Fix JWT refresh in UseAI login flow"'),
36513
- project: external_exports.string().optional().describe('Project name for this session. Typically the root directory name of the codebase being worked on. Example: "goodpass", "useai"')
36505
+ project: external_exports.string().optional().describe('Project name for this session. Typically the root directory name of the codebase being worked on. Example: "goodpass", "useai"'),
36506
+ model: external_exports.string().optional().describe('The AI model ID running this session. Example: "claude-opus-4-6", "claude-sonnet-4-6"')
36514
36507
  },
36515
- async ({ task_type, title, private_title, project }) => {
36508
+ async ({ task_type, title, private_title, project, model }) => {
36516
36509
  if (session2.sessionRecordCount > 0 && opts?.sealBeforeReset) {
36517
36510
  opts.sealBeforeReset();
36518
36511
  }
36519
36512
  session2.reset();
36520
36513
  resolveClient(server2, session2);
36521
36514
  if (project) session2.setProject(project);
36515
+ if (model) session2.setModel(model);
36522
36516
  session2.setTaskType(task_type ?? "coding");
36523
36517
  session2.setTitle(title ?? null);
36524
36518
  session2.setPrivateTitle(private_title ?? null);
@@ -36532,13 +36526,20 @@ function registerTools(server2, session2, opts) {
36532
36526
  };
36533
36527
  if (title) chainData.title = title;
36534
36528
  if (private_title) chainData.private_title = private_title;
36529
+ if (model) chainData.model = model;
36535
36530
  const record2 = session2.appendToChain("session_start", chainData);
36536
36531
  writeMcpMapping(session2.mcpSessionId, session2.sessionId);
36532
+ const responseText = `useai session started \u2014 ${session2.sessionTaskType} on ${session2.clientName} \xB7 ${session2.sessionId.slice(0, 8)} \xB7 conv ${session2.conversationId.slice(0, 8)}#${session2.conversationIndex} \xB7 ${session2.signingAvailable ? "signed" : "unsigned"}`;
36533
+ const paramsJson = JSON.stringify({ task_type, title, private_title, project, model });
36534
+ session2.startCallTokensEst = {
36535
+ output: Math.ceil(paramsJson.length / 4),
36536
+ input: Math.ceil(responseText.length / 4)
36537
+ };
36537
36538
  return {
36538
36539
  content: [
36539
36540
  {
36540
36541
  type: "text",
36541
- text: `useai session started \u2014 ${session2.sessionTaskType} on ${session2.clientName} \xB7 ${session2.sessionId.slice(0, 8)} \xB7 conv ${session2.conversationId.slice(0, 8)}#${session2.conversationIndex} \xB7 ${session2.signingAvailable ? "signed" : "unsigned"}`
36542
+ text: responseText
36542
36543
  }
36543
36544
  ]
36544
36545
  };
@@ -36639,14 +36640,18 @@ function registerTools(server2, session2, opts) {
36639
36640
  writeJson(MILESTONES_FILE, allMilestones);
36640
36641
  }
36641
36642
  }
36643
+ const endParamsJson = JSON.stringify({ task_type, languages, files_touched_count, milestones: milestonesInput, evaluation });
36644
+ const endOutputTokensEst = Math.ceil(endParamsJson.length / 4);
36642
36645
  const endRecord = session2.appendToChain("session_end", {
36643
36646
  duration_seconds: duration3,
36644
36647
  task_type: finalTaskType,
36645
36648
  languages: languages ?? [],
36646
36649
  files_touched: files_touched_count ?? 0,
36647
36650
  heartbeat_count: session2.heartbeatCount,
36648
- ...evaluation ? { evaluation } : {}
36651
+ ...evaluation ? { evaluation } : {},
36652
+ ...session2.modelId ? { model: session2.modelId } : {}
36649
36653
  });
36654
+ const startEst = session2.startCallTokensEst ?? { input: 0, output: 0 };
36650
36655
  const sealData = JSON.stringify({
36651
36656
  session_id: session2.sessionId,
36652
36657
  conversation_id: session2.conversationId,
@@ -36659,6 +36664,7 @@ function registerTools(server2, session2, opts) {
36659
36664
  title: session2.sessionTitle ?? void 0,
36660
36665
  private_title: session2.sessionPrivateTitle ?? void 0,
36661
36666
  prompt_word_count: session2.sessionPromptWordCount ?? void 0,
36667
+ model: session2.modelId ?? void 0,
36662
36668
  evaluation: evaluation ?? void 0,
36663
36669
  started_at: new Date(session2.sessionStartTime).toISOString(),
36664
36670
  ended_at: now,
@@ -36683,6 +36689,17 @@ function registerTools(server2, session2, opts) {
36683
36689
  }
36684
36690
  } catch {
36685
36691
  }
36692
+ const durationStr = formatDuration(duration3);
36693
+ const langStr = languages && languages.length > 0 ? ` using ${languages.join(", ")}` : "";
36694
+ const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
36695
+ const evalStr = evaluation ? ` \xB7 eval: ${evaluation.task_outcome} (prompt: ${evaluation.prompt_quality}/5)` : "";
36696
+ const responseText = `Session ended: ${durationStr} ${finalTaskType}${langStr}${milestoneStr}${evalStr}`;
36697
+ const endInputTokensEst = Math.ceil(responseText.length / 4);
36698
+ const toolOverhead = {
36699
+ start: { input_tokens_est: startEst.input, output_tokens_est: startEst.output },
36700
+ end: { input_tokens_est: endInputTokensEst, output_tokens_est: endOutputTokensEst },
36701
+ total_tokens_est: startEst.input + startEst.output + endInputTokensEst + endOutputTokensEst
36702
+ };
36686
36703
  const seal = {
36687
36704
  session_id: session2.sessionId,
36688
36705
  conversation_id: session2.conversationId,
@@ -36695,7 +36712,9 @@ function registerTools(server2, session2, opts) {
36695
36712
  title: session2.sessionTitle ?? void 0,
36696
36713
  private_title: session2.sessionPrivateTitle ?? void 0,
36697
36714
  prompt_word_count: session2.sessionPromptWordCount ?? void 0,
36715
+ model: session2.modelId ?? void 0,
36698
36716
  evaluation: evaluation ?? void 0,
36717
+ tool_overhead: toolOverhead,
36699
36718
  started_at: new Date(session2.sessionStartTime).toISOString(),
36700
36719
  ended_at: now,
36701
36720
  duration_seconds: duration3,
@@ -36708,16 +36727,11 @@ function registerTools(server2, session2, opts) {
36708
36727
  const sessions2 = getSessions().filter((s) => s.session_id !== seal.session_id);
36709
36728
  sessions2.push(seal);
36710
36729
  writeJson(SESSIONS_FILE, sessions2);
36711
- removeMcpMapping(session2.mcpSessionId);
36712
- const durationStr = formatDuration(duration3);
36713
- const langStr = languages && languages.length > 0 ? ` using ${languages.join(", ")}` : "";
36714
- const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
36715
- const evalStr = evaluation ? ` \xB7 eval: ${evaluation.task_outcome} (prompt: ${evaluation.prompt_quality}/5)` : "";
36716
36730
  return {
36717
36731
  content: [
36718
36732
  {
36719
36733
  type: "text",
36720
- text: `Session ended: ${durationStr} ${finalTaskType}${langStr}${milestoneStr}${evalStr}`
36734
+ text: responseText
36721
36735
  }
36722
36736
  ]
36723
36737
  };
@@ -37167,7 +37181,6 @@ function sealOrphanFile(sessionId) {
37167
37181
  renameSync3(filePath, join10(SEALED_DIR, `${sessionId}.jsonl`));
37168
37182
  } catch {
37169
37183
  }
37170
- removeMcpMappingByUseaiId(sessionId);
37171
37184
  const convId = startData["conversation_id"] ?? void 0;
37172
37185
  const convIdx = startData["conversation_index"] ?? void 0;
37173
37186
  const seal = {
@@ -37418,6 +37431,17 @@ function parseBody(req) {
37418
37431
  req.on("error", reject);
37419
37432
  });
37420
37433
  }
37434
+ function detectClientFromHeaders(req) {
37435
+ const ua = (req.headers["user-agent"] ?? "").toLowerCase();
37436
+ if (ua.includes("claude-code") || ua.includes("claudecode")) return "claude-code";
37437
+ if (ua.includes("cursor")) return "cursor";
37438
+ if (ua.includes("windsurf") || ua.includes("codeium")) return "windsurf";
37439
+ if (ua.includes("vscode") || ua.includes("visual studio code")) return "vscode";
37440
+ if (ua.includes("codex")) return "codex";
37441
+ if (ua.includes("gemini")) return "gemini-cli";
37442
+ if (ua.includes("zed")) return "zed";
37443
+ return "unknown";
37444
+ }
37421
37445
  function sendJsonRpcResult(res, rpcId, text) {
37422
37446
  res.writeHead(200, { "Content-Type": "application/json" });
37423
37447
  res.end(JSON.stringify({
@@ -37450,21 +37474,22 @@ function readChainMetadata(useaiSessionId) {
37450
37474
  return null;
37451
37475
  }
37452
37476
  }
37453
- function recoverStartSession(staleMcpSessionId, args, rpcId, res) {
37477
+ function recoverStartSession(staleMcpSessionId, args, rpcId, res, req) {
37454
37478
  const map = readMcpMap();
37455
37479
  const prevSessionId = map[staleMcpSessionId];
37456
- if (!prevSessionId) return false;
37457
- const prevActivePath = join10(ACTIVE_DIR, `${prevSessionId}.jsonl`);
37458
- if (existsSync11(prevActivePath)) {
37480
+ const prevActivePath = prevSessionId ? join10(ACTIVE_DIR, `${prevSessionId}.jsonl`) : null;
37481
+ if (prevActivePath && existsSync11(prevActivePath)) {
37459
37482
  sealOrphanFile(prevSessionId);
37460
37483
  }
37461
- const meta = readChainMetadata(prevSessionId);
37462
- const client = meta?.client ?? "unknown";
37484
+ const meta = prevSessionId ? readChainMetadata(prevSessionId) : null;
37485
+ const chainClient = meta?.client;
37486
+ const client = chainClient && chainClient !== "unknown" ? chainClient : detectClientFromHeaders(req);
37463
37487
  const newSessionId = randomUUID4();
37464
37488
  const taskType = args["task_type"] ?? "coding";
37465
37489
  const title = args["title"];
37466
37490
  const privateTitle = args["private_title"];
37467
37491
  const project = args["project"];
37492
+ const model = args["model"];
37468
37493
  const convId = meta?.convId ?? randomUUID4();
37469
37494
  const chainData = {
37470
37495
  client,
@@ -37476,6 +37501,7 @@ function recoverStartSession(staleMcpSessionId, args, rpcId, res) {
37476
37501
  };
37477
37502
  if (title) chainData["title"] = title;
37478
37503
  if (privateTitle) chainData["private_title"] = privateTitle;
37504
+ if (model) chainData["model"] = model;
37479
37505
  const record2 = buildChainRecord("session_start", newSessionId, chainData, "GENESIS", daemonSigningKey);
37480
37506
  const chainPath = join10(ACTIVE_DIR, `${newSessionId}.jsonl`);
37481
37507
  appendFileSync2(chainPath, JSON.stringify(record2) + "\n");
@@ -37544,6 +37570,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37544
37570
  const sessionTitle = startData["title"] ?? void 0;
37545
37571
  const sessionPrivateTitle = startData["private_title"] ?? void 0;
37546
37572
  const sessionProject = startData["project"] ?? void 0;
37573
+ const sessionModel = startData["model"] ?? void 0;
37547
37574
  const convId = startData["conversation_id"] ?? void 0;
37548
37575
  const convIdx = startData["conversation_index"] ?? void 0;
37549
37576
  const taskType = args["task_type"] ?? startData["task_type"] ?? "coding";
@@ -37593,6 +37620,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37593
37620
  project: sessionProject,
37594
37621
  title: sessionTitle,
37595
37622
  private_title: sessionPrivateTitle,
37623
+ model: sessionModel,
37596
37624
  evaluation: evaluation ?? void 0,
37597
37625
  started_at: startTime,
37598
37626
  ended_at: now,
@@ -37604,7 +37632,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37604
37632
  seal_signature: ""
37605
37633
  };
37606
37634
  upsertSessionSeal(richSeal);
37607
- removeMcpMapping(staleMcpSessionId);
37608
37635
  const durationStr2 = formatDuration(duration3);
37609
37636
  sendJsonRpcResult(
37610
37637
  res,
@@ -37616,7 +37643,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37616
37643
  }
37617
37644
  const lastRecord = JSON.parse(lines[lines.length - 1]);
37618
37645
  if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") {
37619
- removeMcpMapping(staleMcpSessionId);
37620
37646
  sendJsonRpcResult(res, rpcId, "Session already ended.");
37621
37647
  return true;
37622
37648
  }
@@ -37675,7 +37701,8 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37675
37701
  files_touched: filesTouched,
37676
37702
  heartbeat_count: heartbeatCount,
37677
37703
  recovered: true,
37678
- ...evaluation ? { evaluation } : {}
37704
+ ...evaluation ? { evaluation } : {},
37705
+ ...sessionModel ? { model: sessionModel } : {}
37679
37706
  }, chainTip, daemonSigningKey);
37680
37707
  appendFileSync2(activePath, JSON.stringify(endRecord) + "\n");
37681
37708
  recordCount++;
@@ -37690,6 +37717,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37690
37717
  project: sessionProject,
37691
37718
  title: sessionTitle,
37692
37719
  private_title: sessionPrivateTitle,
37720
+ model: sessionModel,
37693
37721
  evaluation: evaluation ?? void 0,
37694
37722
  started_at: startTime,
37695
37723
  ended_at: now,
@@ -37724,6 +37752,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37724
37752
  project: sessionProject,
37725
37753
  title: sessionTitle,
37726
37754
  private_title: sessionPrivateTitle,
37755
+ model: sessionModel,
37727
37756
  evaluation: evaluation ?? void 0,
37728
37757
  started_at: startTime,
37729
37758
  ended_at: now,
@@ -37734,7 +37763,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37734
37763
  chain_end_hash: endRecord.hash,
37735
37764
  seal_signature: sealSignature
37736
37765
  });
37737
- removeMcpMapping(staleMcpSessionId);
37738
37766
  const durationStr = formatDuration(duration3);
37739
37767
  sendJsonRpcResult(
37740
37768
  res,
@@ -37744,7 +37772,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37744
37772
  console.log(`Recovered useai_end for stale session ${useaiSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
37745
37773
  return true;
37746
37774
  }
37747
- function tryRecoverStaleSession(staleMcpSessionId, body, res) {
37775
+ function tryRecoverStaleSession(staleMcpSessionId, body, res, req) {
37748
37776
  try {
37749
37777
  const rpc = body;
37750
37778
  if (rpc?.method !== "tools/call") return false;
@@ -37753,7 +37781,7 @@ function tryRecoverStaleSession(staleMcpSessionId, body, res) {
37753
37781
  const rpcId = rpc.id;
37754
37782
  switch (toolName) {
37755
37783
  case "useai_start":
37756
- return recoverStartSession(staleMcpSessionId, args, rpcId, res);
37784
+ return recoverStartSession(staleMcpSessionId, args, rpcId, res, req);
37757
37785
  case "useai_heartbeat":
37758
37786
  return recoverHeartbeat(staleMcpSessionId, rpcId, res);
37759
37787
  case "useai_end":
@@ -37932,7 +37960,7 @@ async function startDaemon(port) {
37932
37960
  resetIdleTimer(sid);
37933
37961
  await sessions.get(sid).transport.handleRequest(req, res, body);
37934
37962
  } else if (sid && !sessions.has(sid)) {
37935
- if (!tryRecoverStaleSession(sid, body, res)) {
37963
+ if (!tryRecoverStaleSession(sid, body, res, req)) {
37936
37964
  res.writeHead(404, { "Content-Type": "application/json" });
37937
37965
  res.end(JSON.stringify({
37938
37966
  jsonrpc: "2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.5.29",
3
+ "version": "0.5.30",
4
4
  "description": "Track your AI-assisted development workflow. MCP server that records usage metrics across all your AI tools.",
5
5
  "keywords": [
6
6
  "mcp",