@devness/useai 0.6.10 → 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 +187 -15
  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.10";
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,8 +34726,20 @@ __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";
34731
+ function coerceJsonString(schema) {
34732
+ return external_exports.preprocess((val) => {
34733
+ if (typeof val === "string") {
34734
+ try {
34735
+ return JSON.parse(val);
34736
+ } catch {
34737
+ return val;
34738
+ }
34739
+ }
34740
+ return val;
34741
+ }, schema);
34742
+ }
34720
34743
  function getConfig() {
34721
34744
  return readJson(CONFIG_FILE, {
34722
34745
  milestone_tracking: true,
@@ -34738,6 +34761,130 @@ function resolveClient2(server2, session2) {
34738
34761
  }
34739
34762
  session2.setClient(detectClient());
34740
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
+ }
34741
34888
  function registerTools(server2, session2, opts) {
34742
34889
  server2.tool(
34743
34890
  "useai_start",
@@ -34756,6 +34903,7 @@ function registerTools(server2, session2, opts) {
34756
34903
  opts.sealBeforeReset();
34757
34904
  }
34758
34905
  session2.reset();
34906
+ session2.autoSealedSessionId = null;
34759
34907
  resolveClient2(server2, session2);
34760
34908
  if (conversation_id) {
34761
34909
  if (conversation_id !== prevConvId) {
@@ -34783,6 +34931,8 @@ function registerTools(server2, session2, opts) {
34783
34931
  if (private_title) chainData.private_title = private_title;
34784
34932
  if (model) chainData.model = model;
34785
34933
  const record2 = session2.appendToChain("session_start", chainData);
34934
+ session2.inProgress = true;
34935
+ session2.inProgressSince = Date.now();
34786
34936
  writeMcpMapping(session2.mcpSessionId, session2.sessionId);
34787
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"}`;
34788
34938
  const paramsJson = JSON.stringify({ task_type, title, private_title, project, model });
@@ -34825,15 +34975,15 @@ function registerTools(server2, session2, opts) {
34825
34975
  'End the current AI coding session and record milestones. Each milestone needs TWO titles: (1) a generic public "title" safe for public display (NEVER include project names, file names, class names, or any identifying details), and (2) an optional detailed "private_title" for the user\'s own records that CAN include project names, file names, and specific details. GOOD title: "Implemented user authentication". GOOD private_title: "Added JWT auth to UseAI API server". BAD title: "Fixed bug in Acme auth service". Also provide an `evaluation` object assessing the session: prompt_quality (1-5), context_provided (1-5), task_outcome (completed/partial/abandoned/blocked), iteration_count, independence_level (1-5), scope_quality (1-5), and tools_leveraged count. Score honestly based on the actual interaction. For any scored metric < 5 or non-completed outcome, you MUST provide a *_reason field explaining what was lacking and a concrete tip for the user to improve next time. Only skip *_reason for a perfect 5.',
34826
34976
  {
34827
34977
  task_type: taskTypeSchema.optional().describe("What kind of task was the developer working on?"),
34828
- languages: external_exports.array(external_exports.string()).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
34829
- files_touched_count: external_exports.number().optional().describe("Approximate number of files created or modified (count only, no names)"),
34830
- milestones: external_exports.array(external_exports.object({
34978
+ languages: coerceJsonString(external_exports.array(external_exports.string())).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
34979
+ files_touched_count: coerceJsonString(external_exports.number()).optional().describe("Approximate number of files created or modified (count only, no names)"),
34980
+ milestones: coerceJsonString(external_exports.array(external_exports.object({
34831
34981
  title: external_exports.string().describe("PRIVACY-CRITICAL: Generic description of what was accomplished. NEVER include project names, repo names, product names, package names, file names, file paths, class names, API endpoints, database names, company names, or ANY identifier that could reveal which codebase this work was done in. Write as if describing the work to a stranger. GOOD: 'Implemented user authentication', 'Fixed race condition in background worker', 'Added unit tests for data validation', 'Refactored state management layer'. BAD: 'Fixed bug in Acme auth', 'Investigated ProjectX pipeline', 'Updated UserService.ts in src/services/', 'Added tests for coverit MCP tool'"),
34832
34982
  private_title: external_exports.string().optional().describe("Detailed description for the user's private records. CAN include project names, file names, and specific details. Example: 'Added private/public milestone support to UseAI MCP server'"),
34833
34983
  category: milestoneCategorySchema.describe("Type of work completed"),
34834
34984
  complexity: complexitySchema.optional().describe("How complex was this task?")
34835
- })).optional().describe("What was accomplished this session? List each distinct piece of work completed. Provide both a generic public title and an optional detailed private_title."),
34836
- evaluation: external_exports.object({
34985
+ }))).optional().describe("What was accomplished this session? List each distinct piece of work completed. Provide both a generic public title and an optional detailed private_title."),
34986
+ evaluation: coerceJsonString(external_exports.object({
34837
34987
  prompt_quality: external_exports.number().min(1).max(5).describe("How clear, specific, and complete was the initial prompt? 1=vague/ambiguous, 5=crystal clear with acceptance criteria"),
34838
34988
  prompt_quality_reason: external_exports.string().optional().describe("Required if prompt_quality < 5. Explain what was vague/missing and how the user could phrase it better next time."),
34839
34989
  context_provided: external_exports.number().min(1).max(5).describe("Did the user provide relevant context (files, errors, constraints)? 1=no context, 5=comprehensive context"),
@@ -34846,10 +34996,21 @@ function registerTools(server2, session2, opts) {
34846
34996
  scope_quality: external_exports.number().min(1).max(5).describe("Was the task well-scoped? 1=vague or impossibly broad, 5=precise and achievable"),
34847
34997
  scope_quality_reason: external_exports.string().optional().describe("Required if scope_quality < 5. How was the scope too broad/vague and how could it be better defined?"),
34848
34998
  tools_leveraged: external_exports.number().min(0).describe("Count of distinct AI capabilities used (code gen, debugging, refactoring, testing, docs, etc.)")
34849
- }).optional().describe("AI-assessed evaluation of this session. Score honestly based on the actual interaction.")
34999
+ })).optional().describe("AI-assessed evaluation of this session. Score honestly based on the actual interaction.")
34850
35000
  },
34851
35001
  async ({ task_type, languages, files_touched_count, milestones: milestonesInput, evaluation }) => {
34852
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
+ }
34853
35014
  return {
34854
35015
  content: [{ type: "text", text: "No active session to end (already sealed or never started)." }]
34855
35016
  };
@@ -34997,6 +35158,8 @@ function registerTools(server2, session2, opts) {
34997
35158
  const sessions2 = getSessions().filter((s) => s.session_id !== seal.session_id);
34998
35159
  sessions2.push(seal);
34999
35160
  writeJson(SESSIONS_FILE, sessions2);
35161
+ session2.inProgress = false;
35162
+ session2.inProgressSince = null;
35000
35163
  return {
35001
35164
  content: [
35002
35165
  {
@@ -35509,7 +35672,7 @@ __export(daemon_exports, {
35509
35672
  });
35510
35673
  import { createServer } from "http";
35511
35674
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
35512
- 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";
35513
35676
  import { join as join9 } from "path";
35514
35677
  function getActiveUseaiSessionIds() {
35515
35678
  const ids = /* @__PURE__ */ new Set();
@@ -35522,7 +35685,7 @@ function sealOrphanFile(sessionId) {
35522
35685
  const filePath = join9(ACTIVE_DIR, `${sessionId}.jsonl`);
35523
35686
  if (!existsSync10(filePath)) return;
35524
35687
  try {
35525
- const content = readFileSync5(filePath, "utf-8").trim();
35688
+ const content = readFileSync6(filePath, "utf-8").trim();
35526
35689
  if (!content) return;
35527
35690
  const lines = content.split("\n").filter(Boolean);
35528
35691
  if (lines.length === 0) return;
@@ -35760,8 +35923,10 @@ function autoSealSession(active) {
35760
35923
  upsertSessionSeal(seal);
35761
35924
  }
35762
35925
  function sealSessionData(active) {
35926
+ const sealedId = active.session.sessionId;
35763
35927
  autoSealSession(active);
35764
35928
  active.session.reset();
35929
+ active.session.autoSealedSessionId = sealedId;
35765
35930
  }
35766
35931
  function resetIdleTimer(sessionId) {
35767
35932
  const active = sessions.get(sessionId);
@@ -35865,7 +36030,7 @@ function readChainMetadata(useaiSessionId) {
35865
36030
  const chainPath = existsSync10(activePath) ? activePath : existsSync10(sealedPath) ? sealedPath : null;
35866
36031
  if (!chainPath) return null;
35867
36032
  try {
35868
- const firstLine = readFileSync5(chainPath, "utf-8").split("\n")[0];
36033
+ const firstLine = readFileSync6(chainPath, "utf-8").split("\n")[0];
35869
36034
  if (!firstLine) return null;
35870
36035
  const record2 = JSON.parse(firstLine);
35871
36036
  const d = record2.data;
@@ -35934,7 +36099,7 @@ function recoverHeartbeat(staleMcpSessionId, rpcId, res) {
35934
36099
  return true;
35935
36100
  }
35936
36101
  try {
35937
- const content = readFileSync5(chainPath, "utf-8").trim();
36102
+ const content = readFileSync6(chainPath, "utf-8").trim();
35938
36103
  const lines = content.split("\n").filter(Boolean);
35939
36104
  if (lines.length === 0) return false;
35940
36105
  const firstRecord = JSON.parse(lines[0]);
@@ -35969,7 +36134,7 @@ function recoverEndSession(staleMcpSessionId, args, rpcId, res) {
35969
36134
  const chainPath = existsSync10(activePath) ? activePath : existsSync10(sealedPath) ? sealedPath : null;
35970
36135
  if (!chainPath) return false;
35971
36136
  const isAlreadySealed = chainPath === sealedPath;
35972
- const content = readFileSync5(chainPath, "utf-8").trim();
36137
+ const content = readFileSync6(chainPath, "utf-8").trim();
35973
36138
  if (!content) return false;
35974
36139
  const lines = content.split("\n").filter(Boolean);
35975
36140
  if (lines.length === 0) return false;
@@ -36375,8 +36540,14 @@ async function startDaemon(port) {
36375
36540
  }
36376
36541
  if (url.pathname === "/api/seal-active" && req.method === "POST") {
36377
36542
  let sealed = 0;
36543
+ let skipped = 0;
36378
36544
  for (const [, active] of sessions) {
36379
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
+ }
36380
36551
  sealSessionData(active);
36381
36552
  sealed++;
36382
36553
  }
@@ -36385,7 +36556,7 @@ async function startDaemon(port) {
36385
36556
  "Content-Type": "application/json",
36386
36557
  "Access-Control-Allow-Origin": "*"
36387
36558
  });
36388
- res.end(JSON.stringify({ sealed }));
36559
+ res.end(JSON.stringify({ sealed, skipped }));
36389
36560
  return;
36390
36561
  }
36391
36562
  if ((url.pathname.startsWith("/api/local/") || url.pathname === "/api/seal-active") && req.method === "OPTIONS") {
@@ -36542,7 +36713,7 @@ async function startDaemon(port) {
36542
36713
  console.log(`UseAI daemon listening on http://127.0.0.1:${listenPort}`);
36543
36714
  console.log(`PID: ${process.pid}`);
36544
36715
  }
36545
- 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;
36546
36717
  var init_daemon2 = __esm({
36547
36718
  "src/daemon.ts"() {
36548
36719
  "use strict";
@@ -36556,6 +36727,7 @@ var init_daemon2 = __esm({
36556
36727
  init_html();
36557
36728
  init_local_api();
36558
36729
  IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
36730
+ SEAL_GRACE_MS = 30 * 60 * 1e3;
36559
36731
  sessions = /* @__PURE__ */ new Map();
36560
36732
  daemonSigningKey = null;
36561
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.10",
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",