@devness/useai 0.5.28 → 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 +74 -50
  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.28";
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,26 +37474,23 @@ 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) {
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;
37480
+ const prevActivePath = prevSessionId ? join10(ACTIVE_DIR, `${prevSessionId}.jsonl`) : null;
37481
+ if (prevActivePath && existsSync11(prevActivePath)) {
37482
+ sealOrphanFile(prevSessionId);
37466
37483
  }
37484
+ const meta = prevSessionId ? readChainMetadata(prevSessionId) : null;
37485
+ const chainClient = meta?.client;
37486
+ const client = chainClient && chainClient !== "unknown" ? chainClient : detectClientFromHeaders(req);
37467
37487
  const newSessionId = randomUUID4();
37468
37488
  const taskType = args["task_type"] ?? "coding";
37469
37489
  const title = args["title"];
37470
37490
  const privateTitle = args["private_title"];
37471
37491
  const project = args["project"];
37472
- const convId = prevSessionId ? readChainMetadata(prevSessionId)?.convId ?? randomUUID4() : randomUUID4();
37492
+ const model = args["model"];
37493
+ const convId = meta?.convId ?? randomUUID4();
37473
37494
  const chainData = {
37474
37495
  client,
37475
37496
  task_type: taskType,
@@ -37480,6 +37501,7 @@ function recoverStartSession(staleMcpSessionId, args, rpcId, res) {
37480
37501
  };
37481
37502
  if (title) chainData["title"] = title;
37482
37503
  if (privateTitle) chainData["private_title"] = privateTitle;
37504
+ if (model) chainData["model"] = model;
37483
37505
  const record2 = buildChainRecord("session_start", newSessionId, chainData, "GENESIS", daemonSigningKey);
37484
37506
  const chainPath = join10(ACTIVE_DIR, `${newSessionId}.jsonl`);
37485
37507
  appendFileSync2(chainPath, JSON.stringify(record2) + "\n");
@@ -37548,6 +37570,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37548
37570
  const sessionTitle = startData["title"] ?? void 0;
37549
37571
  const sessionPrivateTitle = startData["private_title"] ?? void 0;
37550
37572
  const sessionProject = startData["project"] ?? void 0;
37573
+ const sessionModel = startData["model"] ?? void 0;
37551
37574
  const convId = startData["conversation_id"] ?? void 0;
37552
37575
  const convIdx = startData["conversation_index"] ?? void 0;
37553
37576
  const taskType = args["task_type"] ?? startData["task_type"] ?? "coding";
@@ -37597,6 +37620,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37597
37620
  project: sessionProject,
37598
37621
  title: sessionTitle,
37599
37622
  private_title: sessionPrivateTitle,
37623
+ model: sessionModel,
37600
37624
  evaluation: evaluation ?? void 0,
37601
37625
  started_at: startTime,
37602
37626
  ended_at: now,
@@ -37608,7 +37632,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37608
37632
  seal_signature: ""
37609
37633
  };
37610
37634
  upsertSessionSeal(richSeal);
37611
- removeMcpMapping(staleMcpSessionId);
37612
37635
  const durationStr2 = formatDuration(duration3);
37613
37636
  sendJsonRpcResult(
37614
37637
  res,
@@ -37620,7 +37643,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37620
37643
  }
37621
37644
  const lastRecord = JSON.parse(lines[lines.length - 1]);
37622
37645
  if (lastRecord.type === "session_end" || lastRecord.type === "session_seal") {
37623
- removeMcpMapping(staleMcpSessionId);
37624
37646
  sendJsonRpcResult(res, rpcId, "Session already ended.");
37625
37647
  return true;
37626
37648
  }
@@ -37679,7 +37701,8 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37679
37701
  files_touched: filesTouched,
37680
37702
  heartbeat_count: heartbeatCount,
37681
37703
  recovered: true,
37682
- ...evaluation ? { evaluation } : {}
37704
+ ...evaluation ? { evaluation } : {},
37705
+ ...sessionModel ? { model: sessionModel } : {}
37683
37706
  }, chainTip, daemonSigningKey);
37684
37707
  appendFileSync2(activePath, JSON.stringify(endRecord) + "\n");
37685
37708
  recordCount++;
@@ -37694,6 +37717,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37694
37717
  project: sessionProject,
37695
37718
  title: sessionTitle,
37696
37719
  private_title: sessionPrivateTitle,
37720
+ model: sessionModel,
37697
37721
  evaluation: evaluation ?? void 0,
37698
37722
  started_at: startTime,
37699
37723
  ended_at: now,
@@ -37728,6 +37752,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37728
37752
  project: sessionProject,
37729
37753
  title: sessionTitle,
37730
37754
  private_title: sessionPrivateTitle,
37755
+ model: sessionModel,
37731
37756
  evaluation: evaluation ?? void 0,
37732
37757
  started_at: startTime,
37733
37758
  ended_at: now,
@@ -37738,7 +37763,6 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37738
37763
  chain_end_hash: endRecord.hash,
37739
37764
  seal_signature: sealSignature
37740
37765
  });
37741
- removeMcpMapping(staleMcpSessionId);
37742
37766
  const durationStr = formatDuration(duration3);
37743
37767
  sendJsonRpcResult(
37744
37768
  res,
@@ -37748,7 +37772,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
37748
37772
  console.log(`Recovered useai_end for stale session ${useaiSessionId.slice(0, 8)} (MCP ${staleMcpSessionId.slice(0, 8)})`);
37749
37773
  return true;
37750
37774
  }
37751
- function tryRecoverStaleSession(staleMcpSessionId, body, res) {
37775
+ function tryRecoverStaleSession(staleMcpSessionId, body, res, req) {
37752
37776
  try {
37753
37777
  const rpc = body;
37754
37778
  if (rpc?.method !== "tools/call") return false;
@@ -37757,7 +37781,7 @@ function tryRecoverStaleSession(staleMcpSessionId, body, res) {
37757
37781
  const rpcId = rpc.id;
37758
37782
  switch (toolName) {
37759
37783
  case "useai_start":
37760
- return recoverStartSession(staleMcpSessionId, args, rpcId, res);
37784
+ return recoverStartSession(staleMcpSessionId, args, rpcId, res, req);
37761
37785
  case "useai_heartbeat":
37762
37786
  return recoverHeartbeat(staleMcpSessionId, rpcId, res);
37763
37787
  case "useai_end":
@@ -37936,7 +37960,7 @@ async function startDaemon(port) {
37936
37960
  resetIdleTimer(sid);
37937
37961
  await sessions.get(sid).transport.handleRequest(req, res, body);
37938
37962
  } else if (sid && !sessions.has(sid)) {
37939
- if (!tryRecoverStaleSession(sid, body, res)) {
37963
+ if (!tryRecoverStaleSession(sid, body, res, req)) {
37940
37964
  res.writeHead(404, { "Content-Type": "application/json" });
37941
37965
  res.end(JSON.stringify({
37942
37966
  jsonrpc: "2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.5.28",
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",