@devness/useai 0.6.11 → 0.6.12

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 +169 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -684,7 +684,7 @@ var VERSION;
684
684
  var init_version = __esm({
685
685
  "../shared/dist/constants/version.js"() {
686
686
  "use strict";
687
- VERSION = "0.6.11";
687
+ VERSION = "0.6.12";
688
688
  }
689
689
  });
690
690
 
@@ -34589,6 +34589,12 @@ var init_session_state = __esm({
34589
34589
  modelId;
34590
34590
  /** Token estimates for the useai_start tool call. */
34591
34591
  startCallTokensEst;
34592
+ /** Whether a UseAI session is actively in-progress (between useai_start and useai_end). */
34593
+ inProgress;
34594
+ /** Timestamp when the current session was marked in-progress. */
34595
+ inProgressSince;
34596
+ /** Session ID that was auto-sealed by seal-active hook (for useai_end fallback). */
34597
+ autoSealedSessionId;
34592
34598
  constructor() {
34593
34599
  this.sessionId = generateSessionId();
34594
34600
  this.conversationId = generateSessionId();
@@ -34596,6 +34602,9 @@ var init_session_state = __esm({
34596
34602
  this.mcpSessionId = null;
34597
34603
  this.modelId = null;
34598
34604
  this.startCallTokensEst = null;
34605
+ this.inProgress = false;
34606
+ this.inProgressSince = null;
34607
+ this.autoSealedSessionId = null;
34599
34608
  this.sessionStartTime = Date.now();
34600
34609
  this.heartbeatCount = 0;
34601
34610
  this.sessionRecordCount = 0;
@@ -34622,6 +34631,8 @@ var init_session_state = __esm({
34622
34631
  this.sessionPromptWordCount = null;
34623
34632
  this.modelId = null;
34624
34633
  this.startCallTokensEst = null;
34634
+ this.inProgress = false;
34635
+ this.inProgressSince = null;
34625
34636
  this.detectProject();
34626
34637
  }
34627
34638
  detectProject() {
@@ -34715,7 +34726,7 @@ __export(register_tools_exports, {
34715
34726
  registerTools: () => registerTools
34716
34727
  });
34717
34728
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
34718
- import { existsSync as existsSync8, renameSync as renameSync2 } from "fs";
34729
+ import { existsSync as existsSync8, readFileSync as readFileSync5, renameSync as renameSync2 } from "fs";
34719
34730
  import { join as join7 } from "path";
34720
34731
  function coerceJsonString(schema) {
34721
34732
  return external_exports.preprocess((val) => {
@@ -34750,6 +34761,130 @@ function resolveClient2(server2, session2) {
34750
34761
  }
34751
34762
  session2.setClient(detectClient());
34752
34763
  }
34764
+ function enrichAutoSealedSession(sealedSessionId, session2, args) {
34765
+ const sealedPath = join7(SEALED_DIR, `${sealedSessionId}.jsonl`);
34766
+ const activePath = join7(ACTIVE_DIR, `${sealedSessionId}.jsonl`);
34767
+ const chainPath = existsSync8(sealedPath) ? sealedPath : existsSync8(activePath) ? activePath : null;
34768
+ if (!chainPath) {
34769
+ return "No active session to end (already sealed or never started).";
34770
+ }
34771
+ let startData = {};
34772
+ let duration3 = 0;
34773
+ let endedAt = (/* @__PURE__ */ new Date()).toISOString();
34774
+ let startedAt2 = endedAt;
34775
+ try {
34776
+ const content = readFileSync5(chainPath, "utf-8").trim();
34777
+ const lines = content.split("\n").filter(Boolean);
34778
+ if (lines.length > 0) {
34779
+ const firstRecord = JSON.parse(lines[0]);
34780
+ startData = firstRecord.data;
34781
+ startedAt2 = firstRecord.timestamp;
34782
+ const lastRecord = JSON.parse(lines[lines.length - 1]);
34783
+ if (lastRecord.type === "session_seal" && lastRecord.data["seal"]) {
34784
+ try {
34785
+ const sealObj = JSON.parse(lastRecord.data["seal"]);
34786
+ duration3 = sealObj.duration_seconds ?? Math.round((new Date(lastRecord.timestamp).getTime() - new Date(startedAt2).getTime()) / 1e3);
34787
+ endedAt = sealObj.ended_at ?? lastRecord.timestamp;
34788
+ } catch {
34789
+ duration3 = Math.round((new Date(lastRecord.timestamp).getTime() - new Date(startedAt2).getTime()) / 1e3);
34790
+ endedAt = lastRecord.timestamp;
34791
+ }
34792
+ } else {
34793
+ duration3 = Math.round((new Date(lastRecord.timestamp).getTime() - new Date(startedAt2).getTime()) / 1e3);
34794
+ endedAt = lastRecord.timestamp;
34795
+ }
34796
+ }
34797
+ } catch {
34798
+ return "No active session to end (chain file unreadable).";
34799
+ }
34800
+ const taskType = args.task_type ?? startData.task_type ?? "coding";
34801
+ const languages = args.languages ?? [];
34802
+ const filesTouched = args.files_touched_count ?? 0;
34803
+ let milestoneCount = 0;
34804
+ if (args.milestones && args.milestones.length > 0) {
34805
+ const config2 = getConfig();
34806
+ if (config2.milestone_tracking) {
34807
+ const durationMinutes = Math.round(duration3 / 60);
34808
+ const allMilestones = getMilestones();
34809
+ for (const m of args.milestones) {
34810
+ allMilestones.push({
34811
+ id: `m_${randomUUID3().slice(0, 8)}`,
34812
+ session_id: sealedSessionId,
34813
+ title: m.title,
34814
+ private_title: m.private_title,
34815
+ project: startData.project ?? session2.project ?? void 0,
34816
+ category: m.category,
34817
+ complexity: m.complexity ?? "medium",
34818
+ duration_minutes: durationMinutes,
34819
+ languages,
34820
+ client: startData.client ?? session2.clientName,
34821
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
34822
+ published: false,
34823
+ published_at: null,
34824
+ chain_hash: ""
34825
+ });
34826
+ milestoneCount++;
34827
+ }
34828
+ writeJson(MILESTONES_FILE, allMilestones);
34829
+ }
34830
+ }
34831
+ let sessionScore;
34832
+ let frameworkId;
34833
+ if (args.evaluation) {
34834
+ const config2 = getConfig();
34835
+ const framework = getFramework(config2.evaluation_framework);
34836
+ sessionScore = Math.round(framework.computeSessionScore(args.evaluation));
34837
+ frameworkId = framework.id;
34838
+ }
34839
+ const richSeal = {
34840
+ session_id: sealedSessionId,
34841
+ conversation_id: startData.conversation_id,
34842
+ conversation_index: startData.conversation_index,
34843
+ client: startData.client ?? session2.clientName,
34844
+ task_type: taskType,
34845
+ languages,
34846
+ files_touched: filesTouched,
34847
+ project: startData.project ?? session2.project ?? void 0,
34848
+ title: startData.title ?? void 0,
34849
+ private_title: startData.private_title ?? void 0,
34850
+ model: startData.model ?? session2.modelId ?? void 0,
34851
+ evaluation: args.evaluation ?? void 0,
34852
+ session_score: sessionScore,
34853
+ evaluation_framework: frameworkId,
34854
+ started_at: startedAt2,
34855
+ ended_at: endedAt,
34856
+ duration_seconds: duration3,
34857
+ heartbeat_count: 0,
34858
+ record_count: 0,
34859
+ chain_start_hash: "",
34860
+ chain_end_hash: "",
34861
+ seal_signature: ""
34862
+ };
34863
+ const allSessions = getSessions();
34864
+ const existingIdx = allSessions.findIndex((s) => s.session_id === sealedSessionId);
34865
+ if (existingIdx >= 0) {
34866
+ const existing = allSessions[existingIdx];
34867
+ allSessions[existingIdx] = {
34868
+ ...existing,
34869
+ // Enrich with data from useai_end call
34870
+ task_type: taskType,
34871
+ languages,
34872
+ files_touched: filesTouched,
34873
+ evaluation: args.evaluation ?? existing.evaluation,
34874
+ session_score: sessionScore ?? existing.session_score,
34875
+ evaluation_framework: frameworkId ?? existing.evaluation_framework
34876
+ };
34877
+ } else {
34878
+ allSessions.push(richSeal);
34879
+ }
34880
+ writeJson(SESSIONS_FILE, allSessions);
34881
+ const durationStr = formatDuration(duration3);
34882
+ const langStr = languages.length > 0 ? ` using ${languages.join(", ")}` : "";
34883
+ const milestoneStr = milestoneCount > 0 ? ` \xB7 ${milestoneCount} milestone${milestoneCount > 1 ? "s" : ""} recorded` : "";
34884
+ const evalStr = args.evaluation ? ` \xB7 eval: ${args.evaluation.task_outcome} (prompt: ${args.evaluation.prompt_quality}/5)` : "";
34885
+ const scoreStr = sessionScore !== void 0 ? ` \xB7 score: ${sessionScore}/100 (${frameworkId})` : "";
34886
+ return `Session ended (enriched auto-seal): ${durationStr} ${taskType}${langStr}${milestoneStr}${evalStr}${scoreStr}`;
34887
+ }
34753
34888
  function registerTools(server2, session2, opts) {
34754
34889
  server2.tool(
34755
34890
  "useai_start",
@@ -34768,6 +34903,7 @@ function registerTools(server2, session2, opts) {
34768
34903
  opts.sealBeforeReset();
34769
34904
  }
34770
34905
  session2.reset();
34906
+ session2.autoSealedSessionId = null;
34771
34907
  resolveClient2(server2, session2);
34772
34908
  if (conversation_id) {
34773
34909
  if (conversation_id !== prevConvId) {
@@ -34795,6 +34931,8 @@ function registerTools(server2, session2, opts) {
34795
34931
  if (private_title) chainData.private_title = private_title;
34796
34932
  if (model) chainData.model = model;
34797
34933
  const record2 = session2.appendToChain("session_start", chainData);
34934
+ session2.inProgress = true;
34935
+ session2.inProgressSince = Date.now();
34798
34936
  writeMcpMapping(session2.mcpSessionId, session2.sessionId);
34799
34937
  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"}`;
34800
34938
  const paramsJson = JSON.stringify({ task_type, title, private_title, project, model });
@@ -34862,6 +35000,17 @@ function registerTools(server2, session2, opts) {
34862
35000
  },
34863
35001
  async ({ task_type, languages, files_touched_count, milestones: milestonesInput, evaluation }) => {
34864
35002
  if (session2.sessionRecordCount === 0) {
35003
+ if (session2.autoSealedSessionId) {
35004
+ const enrichResult = enrichAutoSealedSession(
35005
+ session2.autoSealedSessionId,
35006
+ session2,
35007
+ { task_type, languages, files_touched_count, milestones: milestonesInput, evaluation }
35008
+ );
35009
+ session2.autoSealedSessionId = null;
35010
+ session2.inProgress = false;
35011
+ session2.inProgressSince = null;
35012
+ return { content: [{ type: "text", text: enrichResult }] };
35013
+ }
34865
35014
  return {
34866
35015
  content: [{ type: "text", text: "No active session to end (already sealed or never started)." }]
34867
35016
  };
@@ -35009,6 +35158,8 @@ function registerTools(server2, session2, opts) {
35009
35158
  const sessions2 = getSessions().filter((s) => s.session_id !== seal.session_id);
35010
35159
  sessions2.push(seal);
35011
35160
  writeJson(SESSIONS_FILE, sessions2);
35161
+ session2.inProgress = false;
35162
+ session2.inProgressSince = null;
35012
35163
  return {
35013
35164
  content: [
35014
35165
  {
@@ -35521,7 +35672,7 @@ __export(daemon_exports, {
35521
35672
  });
35522
35673
  import { createServer } from "http";
35523
35674
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
35524
- import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync5, appendFileSync as appendFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, statSync } from "fs";
35675
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync6, appendFileSync as appendFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, statSync } from "fs";
35525
35676
  import { join as join9 } from "path";
35526
35677
  function getActiveUseaiSessionIds() {
35527
35678
  const ids = /* @__PURE__ */ new Set();
@@ -35534,7 +35685,7 @@ function sealOrphanFile(sessionId) {
35534
35685
  const filePath = join9(ACTIVE_DIR, `${sessionId}.jsonl`);
35535
35686
  if (!existsSync10(filePath)) return;
35536
35687
  try {
35537
- const content = readFileSync5(filePath, "utf-8").trim();
35688
+ const content = readFileSync6(filePath, "utf-8").trim();
35538
35689
  if (!content) return;
35539
35690
  const lines = content.split("\n").filter(Boolean);
35540
35691
  if (lines.length === 0) return;
@@ -35772,8 +35923,10 @@ function autoSealSession(active) {
35772
35923
  upsertSessionSeal(seal);
35773
35924
  }
35774
35925
  function sealSessionData(active) {
35926
+ const sealedId = active.session.sessionId;
35775
35927
  autoSealSession(active);
35776
35928
  active.session.reset();
35929
+ active.session.autoSealedSessionId = sealedId;
35777
35930
  }
35778
35931
  function resetIdleTimer(sessionId) {
35779
35932
  const active = sessions.get(sessionId);
@@ -35877,7 +36030,7 @@ function readChainMetadata(useaiSessionId) {
35877
36030
  const chainPath = existsSync10(activePath) ? activePath : existsSync10(sealedPath) ? sealedPath : null;
35878
36031
  if (!chainPath) return null;
35879
36032
  try {
35880
- const firstLine = readFileSync5(chainPath, "utf-8").split("\n")[0];
36033
+ const firstLine = readFileSync6(chainPath, "utf-8").split("\n")[0];
35881
36034
  if (!firstLine) return null;
35882
36035
  const record2 = JSON.parse(firstLine);
35883
36036
  const d = record2.data;
@@ -35946,7 +36099,7 @@ function recoverHeartbeat(staleMcpSessionId, rpcId, res) {
35946
36099
  return true;
35947
36100
  }
35948
36101
  try {
35949
- const content = readFileSync5(chainPath, "utf-8").trim();
36102
+ const content = readFileSync6(chainPath, "utf-8").trim();
35950
36103
  const lines = content.split("\n").filter(Boolean);
35951
36104
  if (lines.length === 0) return false;
35952
36105
  const firstRecord = JSON.parse(lines[0]);
@@ -35981,7 +36134,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
35981
36134
  const chainPath = existsSync10(activePath) ? activePath : existsSync10(sealedPath) ? sealedPath : null;
35982
36135
  if (!chainPath) return false;
35983
36136
  const isAlreadySealed = chainPath === sealedPath;
35984
- const content = readFileSync5(chainPath, "utf-8").trim();
36137
+ const content = readFileSync6(chainPath, "utf-8").trim();
35985
36138
  if (!content) return false;
35986
36139
  const lines = content.split("\n").filter(Boolean);
35987
36140
  if (lines.length === 0) return false;
@@ -36387,8 +36540,14 @@ async function startDaemon(port) {
36387
36540
  }
36388
36541
  if (url.pathname === "/api/seal-active" && req.method === "POST") {
36389
36542
  let sealed = 0;
36543
+ let skipped = 0;
36390
36544
  for (const [, active] of sessions) {
36391
36545
  if (active.session.sessionRecordCount > 0 && !isSessionAlreadySealed(active.session)) {
36546
+ const { inProgress, inProgressSince } = active.session;
36547
+ if (inProgress && inProgressSince && Date.now() - inProgressSince < SEAL_GRACE_MS) {
36548
+ skipped++;
36549
+ continue;
36550
+ }
36392
36551
  sealSessionData(active);
36393
36552
  sealed++;
36394
36553
  }
@@ -36397,7 +36556,7 @@ async function startDaemon(port) {
36397
36556
  "Content-Type": "application/json",
36398
36557
  "Access-Control-Allow-Origin": "*"
36399
36558
  });
36400
- res.end(JSON.stringify({ sealed }));
36559
+ res.end(JSON.stringify({ sealed, skipped }));
36401
36560
  return;
36402
36561
  }
36403
36562
  if ((url.pathname.startsWith("/api/local/") || url.pathname === "/api/seal-active") && req.method === "OPTIONS") {
@@ -36554,7 +36713,7 @@ async function startDaemon(port) {
36554
36713
  console.log(`UseAI daemon listening on http://127.0.0.1:${listenPort}`);
36555
36714
  console.log(`PID: ${process.pid}`);
36556
36715
  }
36557
- var IDLE_TIMEOUT_MS, sessions, daemonSigningKey, ORPHAN_SWEEP_INTERVAL_MS, startedAt, updateCheckCache, UPDATE_CHECK_TTL_MS;
36716
+ var IDLE_TIMEOUT_MS, SEAL_GRACE_MS, sessions, daemonSigningKey, ORPHAN_SWEEP_INTERVAL_MS, startedAt, updateCheckCache, UPDATE_CHECK_TTL_MS;
36558
36717
  var init_daemon2 = __esm({
36559
36718
  "src/daemon.ts"() {
36560
36719
  "use strict";
@@ -36568,6 +36727,7 @@ var init_daemon2 = __esm({
36568
36727
  init_html();
36569
36728
  init_local_api();
36570
36729
  IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
36730
+ SEAL_GRACE_MS = 30 * 60 * 1e3;
36571
36731
  sessions = /* @__PURE__ */ new Map();
36572
36732
  daemonSigningKey = null;
36573
36733
  ORPHAN_SWEEP_INTERVAL_MS = 15 * 60 * 1e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
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",