@devness/useai 0.6.12 → 0.6.14

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 +152 -13
  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.12";
687
+ VERSION = "0.6.14";
688
688
  }
689
689
  });
690
690
 
@@ -34595,6 +34595,8 @@ var init_session_state = __esm({
34595
34595
  inProgressSince;
34596
34596
  /** Session ID that was auto-sealed by seal-active hook (for useai_end fallback). */
34597
34597
  autoSealedSessionId;
34598
+ /** Saved parent session state when a child (subagent) session is active. */
34599
+ parentState;
34598
34600
  constructor() {
34599
34601
  this.sessionId = generateSessionId();
34600
34602
  this.conversationId = generateSessionId();
@@ -34605,6 +34607,7 @@ var init_session_state = __esm({
34605
34607
  this.inProgress = false;
34606
34608
  this.inProgressSince = null;
34607
34609
  this.autoSealedSessionId = null;
34610
+ this.parentState = null;
34608
34611
  this.sessionStartTime = Date.now();
34609
34612
  this.heartbeatCount = 0;
34610
34613
  this.sessionRecordCount = 0;
@@ -34665,6 +34668,56 @@ var init_session_state = __esm({
34665
34668
  getSessionDuration() {
34666
34669
  return Math.round((Date.now() - this.sessionStartTime) / 1e3);
34667
34670
  }
34671
+ /**
34672
+ * Save the current session state as the parent, so it can be restored
34673
+ * after a child (subagent) session finishes.
34674
+ */
34675
+ saveParentState() {
34676
+ this.parentState = {
34677
+ sessionId: this.sessionId,
34678
+ sessionStartTime: this.sessionStartTime,
34679
+ heartbeatCount: this.heartbeatCount,
34680
+ sessionRecordCount: this.sessionRecordCount,
34681
+ chainTipHash: this.chainTipHash,
34682
+ conversationId: this.conversationId,
34683
+ conversationIndex: this.conversationIndex,
34684
+ sessionTaskType: this.sessionTaskType,
34685
+ sessionTitle: this.sessionTitle,
34686
+ sessionPrivateTitle: this.sessionPrivateTitle,
34687
+ sessionPromptWordCount: this.sessionPromptWordCount,
34688
+ project: this.project,
34689
+ modelId: this.modelId,
34690
+ startCallTokensEst: this.startCallTokensEst,
34691
+ inProgress: this.inProgress,
34692
+ inProgressSince: this.inProgressSince
34693
+ };
34694
+ }
34695
+ /**
34696
+ * Restore the parent session state after a child session ends.
34697
+ * Returns true if parent state was restored, false if no parent state was saved.
34698
+ */
34699
+ restoreParentState() {
34700
+ if (!this.parentState) return false;
34701
+ const p = this.parentState;
34702
+ this.sessionId = p.sessionId;
34703
+ this.sessionStartTime = p.sessionStartTime;
34704
+ this.heartbeatCount = p.heartbeatCount;
34705
+ this.sessionRecordCount = p.sessionRecordCount;
34706
+ this.chainTipHash = p.chainTipHash;
34707
+ this.conversationId = p.conversationId;
34708
+ this.conversationIndex = p.conversationIndex;
34709
+ this.sessionTaskType = p.sessionTaskType;
34710
+ this.sessionTitle = p.sessionTitle;
34711
+ this.sessionPrivateTitle = p.sessionPrivateTitle;
34712
+ this.sessionPromptWordCount = p.sessionPromptWordCount;
34713
+ this.project = p.project;
34714
+ this.modelId = p.modelId;
34715
+ this.startCallTokensEst = p.startCallTokensEst;
34716
+ this.inProgress = p.inProgress;
34717
+ this.inProgressSince = p.inProgressSince;
34718
+ this.parentState = null;
34719
+ return true;
34720
+ }
34668
34721
  initializeKeystore() {
34669
34722
  ensureDir();
34670
34723
  if (existsSync7(KEYSTORE_FILE)) {
@@ -34899,8 +34952,14 @@ function registerTools(server2, session2, opts) {
34899
34952
  },
34900
34953
  async ({ task_type, title, private_title, project, model, conversation_id }) => {
34901
34954
  const prevConvId = session2.conversationId;
34902
- if (session2.sessionRecordCount > 0 && opts?.sealBeforeReset) {
34903
- opts.sealBeforeReset();
34955
+ const isChildSession = session2.inProgress && session2.sessionRecordCount > 0;
34956
+ const parentSessionId = isChildSession ? session2.sessionId : null;
34957
+ if (isChildSession) {
34958
+ session2.saveParentState();
34959
+ } else {
34960
+ if (session2.sessionRecordCount > 0 && opts?.sealBeforeReset) {
34961
+ opts.sealBeforeReset();
34962
+ }
34904
34963
  }
34905
34964
  session2.reset();
34906
34965
  session2.autoSealedSessionId = null;
@@ -34910,7 +34969,7 @@ function registerTools(server2, session2, opts) {
34910
34969
  session2.conversationId = conversation_id;
34911
34970
  session2.conversationIndex = 0;
34912
34971
  }
34913
- } else {
34972
+ } else if (!isChildSession) {
34914
34973
  session2.conversationId = generateSessionId();
34915
34974
  session2.conversationIndex = 0;
34916
34975
  }
@@ -34930,11 +34989,13 @@ function registerTools(server2, session2, opts) {
34930
34989
  if (title) chainData.title = title;
34931
34990
  if (private_title) chainData.private_title = private_title;
34932
34991
  if (model) chainData.model = model;
34992
+ if (parentSessionId) chainData.parent_session_id = parentSessionId;
34933
34993
  const record2 = session2.appendToChain("session_start", chainData);
34934
34994
  session2.inProgress = true;
34935
34995
  session2.inProgressSince = Date.now();
34936
34996
  writeMcpMapping(session2.mcpSessionId, session2.sessionId);
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"}`;
34997
+ const childSuffix = parentSessionId ? ` \xB7 child of ${parentSessionId.slice(0, 8)}` : "";
34998
+ 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}${childSuffix} \xB7 ${session2.signingAvailable ? "signed" : "unsigned"}`;
34938
34999
  const paramsJson = JSON.stringify({ task_type, title, private_title, project, model });
34939
35000
  session2.startCallTokensEst = {
34940
35001
  output: Math.ceil(paramsJson.length / 4),
@@ -34972,17 +35033,18 @@ function registerTools(server2, session2, opts) {
34972
35033
  );
34973
35034
  server2.tool(
34974
35035
  "useai_end",
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.',
35036
+ 'End the current AI coding session and record milestones. Each milestone is an object with required fields: "title" (generic, no project names), "category" (e.g. "feature", "bugfix", "investigation", "analysis", "refactor", "test", "docs"), and optional "private_title" and "complexity". Example: [{"title": "Implemented auth flow", "category": "feature"}]. 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.',
34976
35037
  {
35038
+ session_id: external_exports.string().optional().describe("Session ID to end. If omitted, ends the current active session. Pass the session_id from your useai_start response to explicitly target your own session (important when subagents may have started their own sessions on the same connection)."),
34977
35039
  task_type: taskTypeSchema.optional().describe("What kind of task was the developer working on?"),
34978
35040
  languages: coerceJsonString(external_exports.array(external_exports.string())).optional().describe("Programming languages used (e.g. ['typescript', 'python'])"),
34979
35041
  files_touched_count: coerceJsonString(external_exports.number()).optional().describe("Approximate number of files created or modified (count only, no names)"),
34980
35042
  milestones: coerceJsonString(external_exports.array(external_exports.object({
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'"),
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'"),
34983
- category: milestoneCategorySchema.describe("Type of work completed"),
34984
- complexity: complexitySchema.optional().describe("How complex was this task?")
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."),
35043
+ title: external_exports.string().describe("PRIVACY-CRITICAL: Generic description of what was accomplished. NEVER include project names, file paths, class names, or identifying details. GOOD: 'Implemented user authentication'. BAD: 'Fixed bug in Acme auth'."),
35044
+ private_title: external_exports.string().optional().describe("Detailed description for the user's private records. CAN include project names and specifics."),
35045
+ category: milestoneCategorySchema.describe("Required. Type of work: feature, bugfix, refactor, test, docs, investigation, analysis, research, setup, deployment, performance, cleanup, chore, security, migration, design, devops, config, other"),
35046
+ complexity: complexitySchema.optional().describe("Optional. simple, medium, or complex. Defaults to medium.")
35047
+ }))).optional().describe('Array of milestone objects. Each MUST have "title" (string) and "category" (string). Example: [{"title": "Implemented auth flow", "category": "feature"}, {"title": "Fixed race condition", "category": "bugfix"}]'),
34986
35048
  evaluation: coerceJsonString(external_exports.object({
34987
35049
  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"),
34988
35050
  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."),
@@ -34998,7 +35060,79 @@ function registerTools(server2, session2, opts) {
34998
35060
  tools_leveraged: external_exports.number().min(0).describe("Count of distinct AI capabilities used (code gen, debugging, refactoring, testing, docs, etc.)")
34999
35061
  })).optional().describe("AI-assessed evaluation of this session. Score honestly based on the actual interaction.")
35000
35062
  },
35001
- async ({ task_type, languages, files_touched_count, milestones: milestonesInput, evaluation }) => {
35063
+ async ({ session_id: targetSessionId, task_type, languages, files_touched_count, milestones: milestonesInput, evaluation }) => {
35064
+ if (targetSessionId && session2.parentState && session2.parentState.sessionId.startsWith(targetSessionId)) {
35065
+ if (session2.sessionRecordCount > 0) {
35066
+ const childDuration = session2.getSessionDuration();
35067
+ const childNow = (/* @__PURE__ */ new Date()).toISOString();
35068
+ const childEndRecord = session2.appendToChain("session_end", {
35069
+ duration_seconds: childDuration,
35070
+ task_type: session2.sessionTaskType,
35071
+ languages: [],
35072
+ files_touched: 0,
35073
+ heartbeat_count: session2.heartbeatCount,
35074
+ auto_sealed: true,
35075
+ parent_ended: true
35076
+ });
35077
+ const childSealData = JSON.stringify({
35078
+ session_id: session2.sessionId,
35079
+ parent_session_id: session2.parentState.sessionId,
35080
+ conversation_id: session2.conversationId,
35081
+ conversation_index: session2.conversationIndex,
35082
+ client: session2.clientName,
35083
+ task_type: session2.sessionTaskType,
35084
+ languages: [],
35085
+ files_touched: 0,
35086
+ project: session2.project ?? void 0,
35087
+ title: session2.sessionTitle ?? void 0,
35088
+ private_title: session2.sessionPrivateTitle ?? void 0,
35089
+ model: session2.modelId ?? void 0,
35090
+ started_at: new Date(session2.sessionStartTime).toISOString(),
35091
+ ended_at: childNow,
35092
+ duration_seconds: childDuration,
35093
+ heartbeat_count: session2.heartbeatCount,
35094
+ record_count: session2.sessionRecordCount,
35095
+ chain_end_hash: childEndRecord.hash
35096
+ });
35097
+ const childSealSig = signHash(
35098
+ createHash3("sha256").update(childSealData).digest("hex"),
35099
+ session2.signingKey
35100
+ );
35101
+ session2.appendToChain("session_seal", { seal: childSealData, seal_signature: childSealSig });
35102
+ const childActivePath = join7(ACTIVE_DIR, `${session2.sessionId}.jsonl`);
35103
+ const childSealedPath = join7(SEALED_DIR, `${session2.sessionId}.jsonl`);
35104
+ try {
35105
+ if (existsSync8(childActivePath)) renameSync2(childActivePath, childSealedPath);
35106
+ } catch {
35107
+ }
35108
+ const childSeal = {
35109
+ session_id: session2.sessionId,
35110
+ parent_session_id: session2.parentState.sessionId,
35111
+ conversation_id: session2.conversationId,
35112
+ conversation_index: session2.conversationIndex,
35113
+ client: session2.clientName,
35114
+ task_type: session2.sessionTaskType,
35115
+ languages: [],
35116
+ files_touched: 0,
35117
+ project: session2.project ?? void 0,
35118
+ title: session2.sessionTitle ?? void 0,
35119
+ private_title: session2.sessionPrivateTitle ?? void 0,
35120
+ model: session2.modelId ?? void 0,
35121
+ started_at: new Date(session2.sessionStartTime).toISOString(),
35122
+ ended_at: childNow,
35123
+ duration_seconds: childDuration,
35124
+ heartbeat_count: session2.heartbeatCount,
35125
+ record_count: session2.sessionRecordCount,
35126
+ chain_start_hash: "GENESIS",
35127
+ chain_end_hash: childEndRecord.hash,
35128
+ seal_signature: childSealSig
35129
+ };
35130
+ const childSessions = getSessions().filter((s) => s.session_id !== childSeal.session_id);
35131
+ childSessions.push(childSeal);
35132
+ writeJson(SESSIONS_FILE, childSessions);
35133
+ }
35134
+ session2.restoreParentState();
35135
+ }
35002
35136
  if (session2.sessionRecordCount === 0) {
35003
35137
  if (session2.autoSealedSessionId) {
35004
35138
  const enrichResult = enrichAutoSealedSession(
@@ -35078,8 +35212,10 @@ function registerTools(server2, session2, opts) {
35078
35212
  ...session2.modelId ? { model: session2.modelId } : {}
35079
35213
  });
35080
35214
  const startEst = session2.startCallTokensEst ?? { input: 0, output: 0 };
35215
+ const parentId = session2.parentState?.sessionId;
35081
35216
  const sealData = JSON.stringify({
35082
35217
  session_id: session2.sessionId,
35218
+ ...parentId ? { parent_session_id: parentId } : {},
35083
35219
  conversation_id: session2.conversationId,
35084
35220
  conversation_index: session2.conversationIndex,
35085
35221
  client: session2.clientName,
@@ -35131,6 +35267,7 @@ function registerTools(server2, session2, opts) {
35131
35267
  };
35132
35268
  const seal = {
35133
35269
  session_id: session2.sessionId,
35270
+ ...parentId ? { parent_session_id: parentId } : {},
35134
35271
  conversation_id: session2.conversationId,
35135
35272
  conversation_index: session2.conversationIndex,
35136
35273
  client: session2.clientName,
@@ -35160,11 +35297,13 @@ function registerTools(server2, session2, opts) {
35160
35297
  writeJson(SESSIONS_FILE, sessions2);
35161
35298
  session2.inProgress = false;
35162
35299
  session2.inProgressSince = null;
35300
+ const restoredParent = session2.restoreParentState();
35301
+ const parentRestoredStr = restoredParent ? ` \xB7 parent ${session2.sessionId.slice(0, 8)} restored` : "";
35163
35302
  return {
35164
35303
  content: [
35165
35304
  {
35166
35305
  type: "text",
35167
- text: responseText
35306
+ text: responseText + parentRestoredStr
35168
35307
  }
35169
35308
  ]
35170
35309
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devness/useai",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
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",