context-mode 1.0.89 → 1.0.90

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 (128) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +184 -60
  6. package/build/adapters/antigravity/index.d.ts +3 -5
  7. package/build/adapters/antigravity/index.js +7 -35
  8. package/build/adapters/base.d.ts +27 -0
  9. package/build/adapters/base.js +59 -0
  10. package/build/adapters/claude-code/index.d.ts +9 -25
  11. package/build/adapters/claude-code/index.js +12 -140
  12. package/build/adapters/claude-code-base.d.ts +49 -0
  13. package/build/adapters/claude-code-base.js +113 -0
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +21 -14
  16. package/build/adapters/codex/hooks.js +22 -15
  17. package/build/adapters/codex/index.d.ts +6 -10
  18. package/build/adapters/codex/index.js +13 -43
  19. package/build/adapters/copilot-base.d.ts +78 -0
  20. package/build/adapters/copilot-base.js +281 -0
  21. package/build/adapters/cursor/index.d.ts +3 -5
  22. package/build/adapters/cursor/index.js +6 -34
  23. package/build/adapters/detect.d.ts +7 -0
  24. package/build/adapters/detect.js +57 -56
  25. package/build/adapters/gemini-cli/index.d.ts +3 -5
  26. package/build/adapters/gemini-cli/index.js +7 -35
  27. package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
  28. package/build/adapters/jetbrains-copilot/config.js +8 -0
  29. package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
  30. package/build/adapters/jetbrains-copilot/hooks.js +82 -0
  31. package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
  32. package/build/adapters/jetbrains-copilot/index.js +119 -0
  33. package/build/adapters/kiro/hooks.d.ts +14 -0
  34. package/build/adapters/kiro/hooks.js +23 -0
  35. package/build/adapters/kiro/index.d.ts +3 -5
  36. package/build/adapters/kiro/index.js +10 -38
  37. package/build/adapters/openclaw/index.d.ts +3 -4
  38. package/build/adapters/openclaw/index.js +6 -22
  39. package/build/adapters/opencode/index.d.ts +2 -3
  40. package/build/adapters/opencode/index.js +5 -16
  41. package/build/adapters/qwen-code/index.d.ts +39 -0
  42. package/build/adapters/qwen-code/index.js +199 -0
  43. package/build/adapters/types.d.ts +1 -1
  44. package/build/adapters/vscode-copilot/index.d.ts +16 -46
  45. package/build/adapters/vscode-copilot/index.js +29 -320
  46. package/build/adapters/zed/index.d.ts +3 -5
  47. package/build/adapters/zed/index.js +7 -35
  48. package/build/cli.js +13 -0
  49. package/build/lifecycle.d.ts +23 -0
  50. package/build/lifecycle.js +54 -13
  51. package/build/opencode-plugin.d.ts +19 -7
  52. package/build/opencode-plugin.js +19 -7
  53. package/build/runtime.js +24 -9
  54. package/build/security.d.ts +17 -1
  55. package/build/security.js +40 -6
  56. package/build/server.js +41 -9
  57. package/build/session/analytics.d.ts +8 -7
  58. package/build/session/analytics.js +95 -75
  59. package/build/session/db.d.ts +10 -1
  60. package/build/session/db.js +67 -8
  61. package/build/session/extract.js +10 -2
  62. package/build/session/project-attribution.d.ts +73 -0
  63. package/build/session/project-attribution.js +231 -0
  64. package/build/store.d.ts +4 -0
  65. package/build/store.js +58 -9
  66. package/build/types.d.ts +8 -0
  67. package/cli.bundle.mjs +135 -121
  68. package/configs/antigravity/GEMINI.md +31 -36
  69. package/configs/claude-code/CLAUDE.md +31 -37
  70. package/configs/codex/AGENTS.md +35 -49
  71. package/configs/cursor/context-mode.mdc +24 -25
  72. package/configs/gemini-cli/GEMINI.md +30 -36
  73. package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
  74. package/configs/jetbrains-copilot/hooks.json +16 -0
  75. package/configs/jetbrains-copilot/mcp.json +8 -0
  76. package/configs/kilo/AGENTS.md +30 -36
  77. package/configs/kiro/KIRO.md +30 -36
  78. package/configs/kiro/agent.json +1 -1
  79. package/configs/openclaw/AGENTS.md +30 -36
  80. package/configs/opencode/AGENTS.md +30 -36
  81. package/configs/pi/AGENTS.md +31 -36
  82. package/configs/qwen-code/QWEN.md +63 -0
  83. package/configs/vscode-copilot/copilot-instructions.md +30 -36
  84. package/configs/zed/AGENTS.md +31 -36
  85. package/hooks/codex/posttooluse.mjs +7 -7
  86. package/hooks/codex/pretooluse.mjs +3 -3
  87. package/hooks/codex/sessionstart.mjs +2 -1
  88. package/hooks/core/formatters.mjs +24 -0
  89. package/hooks/core/routing.mjs +40 -15
  90. package/hooks/core/tool-naming.mjs +2 -0
  91. package/hooks/cursor/posttooluse.mjs +7 -7
  92. package/hooks/cursor/pretooluse.mjs +3 -3
  93. package/hooks/cursor/sessionstart.mjs +2 -1
  94. package/hooks/cursor/stop.mjs +2 -2
  95. package/hooks/ensure-deps.mjs +22 -10
  96. package/hooks/gemini-cli/aftertool.mjs +8 -8
  97. package/hooks/gemini-cli/beforetool.mjs +3 -2
  98. package/hooks/gemini-cli/precompress.mjs +2 -2
  99. package/hooks/gemini-cli/sessionstart.mjs +12 -4
  100. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  101. package/hooks/jetbrains-copilot/precompact.mjs +54 -0
  102. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  103. package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
  104. package/hooks/kiro/posttooluse.mjs +6 -7
  105. package/hooks/kiro/pretooluse.mjs +3 -2
  106. package/hooks/posttooluse.mjs +8 -8
  107. package/hooks/precompact.mjs +3 -4
  108. package/hooks/pretooluse.mjs +5 -4
  109. package/hooks/routing-block.mjs +35 -33
  110. package/hooks/session-attribution.bundle.mjs +1 -0
  111. package/hooks/session-db.bundle.mjs +27 -8
  112. package/hooks/session-extract.bundle.mjs +2 -1
  113. package/hooks/session-helpers.mjs +44 -3
  114. package/hooks/session-loaders.mjs +37 -0
  115. package/hooks/sessionstart.mjs +5 -5
  116. package/hooks/userpromptsubmit.mjs +26 -9
  117. package/hooks/vscode-copilot/posttooluse.mjs +8 -8
  118. package/hooks/vscode-copilot/precompact.mjs +2 -2
  119. package/hooks/vscode-copilot/pretooluse.mjs +3 -2
  120. package/hooks/vscode-copilot/sessionstart.mjs +2 -2
  121. package/insight/server.mjs +237 -25
  122. package/insight/src/lib/api.ts +2 -1
  123. package/insight/src/routes/index.tsx +16 -3
  124. package/insight/src/routes/search.tsx +1 -1
  125. package/openclaw.plugin.json +1 -1
  126. package/package.json +11 -2
  127. package/server.bundle.mjs +94 -80
  128. package/skills/ctx-insight/SKILL.md +1 -1
@@ -18,81 +18,83 @@ export function createRoutingBlock(t, options = {}) {
18
18
  return `
19
19
  <context_window_protection>
20
20
  <priority_instructions>
21
- Raw tool output floods your context window. You MUST use context-mode MCP tools to keep raw data in the sandbox.
21
+ Raw tool output floods context window. MUST use context-mode MCP tools. Keep raw data in sandbox.
22
22
  </priority_instructions>
23
23
 
24
24
  <tool_selection_hierarchy>
25
25
  1. GATHER: ${t("ctx_batch_execute")}(commands, queries)
26
- - Primary tool for research. Runs all commands, auto-indexes, and searches.
27
- - ONE call replaces many individual steps.
28
- - Each command: {label: "descriptive section header", command: "shell command"}
29
- - label becomes the FTS5 chunk title — use descriptive labels for better search.
26
+ - Primary research tool. Runs commands, auto-indexes, searches. ONE call replaces many steps.
27
+ - Each command: {label: "section header", command: "shell command"}
28
+ - label becomes FTS5 chunk title descriptive labels improve search.
30
29
  2. FOLLOW-UP: ${t("ctx_search")}(queries: ["q1", "q2", ...])
31
- - Use for all follow-up questions. ONE call, many queries.
30
+ - All follow-up questions. ONE call, many queries.
32
31
  3. PROCESSING: ${t("ctx_execute")}(language, code) | ${t("ctx_execute_file")}(path, language, code)
33
- - Use for API calls, log analysis, and data processing.
32
+ - API calls, log analysis, data processing.
34
33
  </tool_selection_hierarchy>
35
34
 
36
35
  <forbidden_actions>
37
- - DO NOT use Bash for commands producing >20 lines of output.
38
- - DO NOT use Read for analysis (use execute_file). Read IS correct for files you intend to Edit.
39
- - DO NOT use WebFetch (use ${t("ctx_fetch_and_index")} instead).
40
- - Bash is ONLY for git/mkdir/rm/mv/navigation.
41
- - DO NOT use ${t("ctx_execute")} or ${t("ctx_execute_file")} to create, modify, or overwrite files.
42
- ctx_execute is for data analysis, log processing, and computation only.
36
+ - NO Bash for commands producing >20 lines output.
37
+ - NO Read for analysis use execute_file. Read IS correct for files you intend to Edit.
38
+ - NO WebFetch use ${t("ctx_fetch_and_index")}.
39
+ - Bash ONLY for git/mkdir/rm/mv/navigation.
40
+ - NO ${t("ctx_execute")} or ${t("ctx_execute_file")} for file creation/modification.
41
+ ctx_execute is for analysis, processing, computation only.
43
42
  </forbidden_actions>
44
43
 
45
44
  <file_writing_policy>
46
- ALWAYS use the native Write tool to create files and Edit tool to modify files.
47
- NEVER use ${t("ctx_execute")}, ${t("ctx_execute_file")}, or Bash to write file content.
48
- This applies to all file types: code, configs, plans, specs, YAML, JSON, markdown.
45
+ ALWAYS use native Write/Edit tools for file creation/modification.
46
+ NEVER use ${t("ctx_execute")}, ${t("ctx_execute_file")}, or Bash to write files.
47
+ Applies to all file types: code, configs, plans, specs, YAML, JSON, markdown.
49
48
  </file_writing_policy>
50
49
 
51
50
  <output_constraints>
52
- <word_limit>Keep your final response under 500 words.</word_limit>
51
+ <communication_style>
52
+ Terse like caveman. Technical substance exact. Only fluff die.
53
+ Use fragments when clear. Short synonyms (fix not "implement a solution for").
54
+ Technical terms exact. Code blocks unchanged.
55
+ Auto-expand for: security warnings, irreversible actions, user confusion.
56
+ </communication_style>
53
57
  <artifact_policy>
54
- Write artifacts (code, configs, PRDs) to FILES using the native Write tool. NEVER return them as inline text.
55
- Use Edit tool for modifications to existing files.
58
+ Write artifacts (code, configs, PRDs) to FILES. NEVER inline.
56
59
  Return only: file path + 1-line description.
57
60
  </artifact_policy>
58
61
  <response_format>
59
- Your response must be a concise summary:
62
+ Concise summary:
60
63
  - Actions taken (2-3 bullets)
61
64
  - File paths created/modified
62
- - Knowledge base source labels (so parent can search)
63
65
  - Key findings
64
66
  </response_format>
65
67
  </output_constraints>
66
68
  ${includeCommands ? `
67
69
  <ctx_commands>
68
- When the user says "ctx stats", "ctx-stats", "/ctx-stats", or asks about context savings:
69
- → Call the stats MCP tool and display the full output verbatim.
70
+ "ctx stats" | "ctx-stats" | "/ctx-stats" | context savings question
71
+ → Call stats MCP tool, display full output verbatim.
70
72
 
71
- When the user says "ctx doctor", "ctx-doctor", "/ctx-doctor", or asks to diagnose context-mode:
72
- → Call the doctor MCP tool, execute the returned shell command, display results as a checklist.
73
+ "ctx doctor" | "ctx-doctor" | "/ctx-doctor" | diagnose context-mode
74
+ → Call doctor MCP tool, run returned shell command, display as checklist.
73
75
 
74
- When the user says "ctx upgrade", "ctx-upgrade", "/ctx-upgrade", or asks to update context-mode:
75
- → Call the upgrade MCP tool, execute the returned shell command, display results as a checklist.
76
+ "ctx upgrade" | "ctx-upgrade" | "/ctx-upgrade" | update context-mode
77
+ → Call upgrade MCP tool, run returned shell command, display as checklist.
76
78
 
77
- When the user says "ctx purge", "ctx-purge", "/ctx-purge", or asks to wipe/reset the knowledge base:
78
- → Call the purge MCP tool with confirm: true. Warn the user this is irreversible.
79
+ "ctx purge" | "ctx-purge" | "/ctx-purge" | wipe/reset knowledge base
80
+ → Call purge MCP tool with confirm: true. Warn: irreversible.
79
81
 
80
- After /clear or /compact: knowledge base and session stats are preserved. Inform the user: "context-mode knowledge base preserved. Use \`ctx purge\` if you want to start fresh."
82
+ After /clear or /compact: knowledge base preserved. Tell user: "context-mode knowledge base preserved. Use \`ctx purge\` to start fresh."
81
83
  </ctx_commands>
82
84
  ` : ''}
83
85
  </context_window_protection>`;
84
86
  }
85
87
 
86
88
  export function createReadGuidance(t) {
87
- return '<context_guidance>\n <tip>\n If you are reading this file to Edit it, Read is the correct tool — Edit needs file content in context.\n If you are reading to analyze or explore, use ' + t("ctx_execute_file") + '(path, language, code) instead — only your printed summary will enter the context.\n </tip>\n</context_guidance>';
89
+ return '<context_guidance>\n <tip>\n Reading to Edit? Read is correct — Edit needs content in context.\n Reading to analyze/explore? Use ' + t("ctx_execute_file") + '(path, language, code) — only printed summary enters context.\n </tip>\n</context_guidance>';
88
90
  }
89
91
 
90
92
  export function createGrepGuidance(t) {
91
- return '<context_guidance>\n <tip>\n This operation may flood your context window. To stay efficient:\n - Use ' + t("ctx_execute") + '(language: "shell", code: "...") to run searches in the sandbox.\n - Only your final printed summary will enter the context.\n </tip>\n</context_guidance>';
93
+ return '<context_guidance>\n <tip>\n May flood context. Use ' + t("ctx_execute") + '(language: "shell", code: "...") to run searches in sandbox. Only printed summary enters context.\n </tip>\n</context_guidance>';
92
94
  }
93
95
 
94
96
  export function createBashGuidance(t) {
95
- return '<context_guidance>\n <tip>\n This Bash command may produce large output. To stay efficient:\n - Use ' + t("ctx_batch_execute") + '(commands, queries) for multiple commands\n - Use ' + t("ctx_execute") + '(language: "shell", code: "...") to run in sandbox\n - Only your final printed summary will enter the context.\n - Bash is best for: git, mkdir, rm, mv, navigation, and short-output commands only.\n </tip>\n</context_guidance>';
97
+ return '<context_guidance>\n <tip>\n May produce large output. Use ' + t("ctx_batch_execute") + '(commands, queries) for multiple commands, ' + t("ctx_execute") + '(language: "shell", code: "...") for single. Only printed summary enters context. Bash only for: git, mkdir, rm, mv, navigation.\n </tip>\n</context_guidance>';
96
98
  }
97
99
 
98
100
  // ── Backward compat: static exports defaulting to claude-code ──
@@ -0,0 +1 @@
1
+ import{dirname as d,isAbsolute as a,normalize as g,resolve as j,sep as y}from"node:path";function e(r){let n=g(r).replace(/\\/g,"/");return n.length<=1?n:n.replace(/\/+$/,"")}function u(r,n){return!r||!n?!1:r===n?!0:r.startsWith(`${n}/`)}function D(r){if(!r||r.length===0)return[];let n=r.filter(i=>typeof i=="string"&&i.trim().length>0).map(i=>e(i));return Array.from(new Set(n)).sort((i,s)=>s.length-i.length)}function m(r){let n=" in ",t=r.lastIndexOf(n);if(t<0)return null;let i=r.slice(t+n.length).trim();return i.length>0?i:null}function w(r){return r?r.includes("/")||r.includes("\\")||r.startsWith(".")||/^[A-Za-z]:[\\/]/.test(r):!1}function P(r){if(r.type==="cwd")return{rawPath:r.data,fromCwdEvent:!0};if(r.type==="file_search"){let t=m(r.data);if(t)return{rawPath:t,fromCwdEvent:!1}}return new Set(["file_read","file_write","file_edit","file_glob","rule"]).has(r.type)&&w(r.data)?{rawPath:r.data,fromCwdEvent:!1}:null}function b(r,n){if(!r||r.includes("*")&&!a(r)&&!/^[A-Za-z]:[\\/]/.test(r))return null;if(a(r)||/^[A-Za-z]:[\\/]/.test(r))return e(r);let t=n.lastKnownProjectDir||n.inputProjectDir||n.sessionOriginDir||null;return t?e(j(t,r)):null}function h(r,n,t){let i=D(t.workspaceRoots),s=t.sessionOriginDir?e(t.sessionOriginDir):"",o=t.inputProjectDir?e(t.inputProjectDir):"",c=t.lastKnownProjectDir?e(t.lastKnownProjectDir):"",f=i.find(p=>u(r,p));return f?{projectDir:f,source:"workspace_root",confidence:.98}:o&&u(r,o)?{projectDir:o,source:"input_cwd",confidence:.88}:s&&u(r,s)?{projectDir:s,source:"session_origin",confidence:.82}:c&&u(r,c)?{projectDir:c,source:"last_seen",confidence:.76}:n.type==="cwd"?{projectDir:r,source:"cwd_event",confidence:.9}:{projectDir:new Set(["file_read","file_write","file_edit","rule"]).has(n.type)?e(d(r)):r,source:"event_path",confidence:.7}}function l(r){return r.inputProjectDir?{projectDir:e(r.inputProjectDir),source:"input_cwd",confidence:.45}:r.lastKnownProjectDir?{projectDir:e(r.lastKnownProjectDir),source:"last_seen",confidence:.4}:r.sessionOriginDir?{projectDir:e(r.sessionOriginDir),source:"session_origin",confidence:.35}:{projectDir:"",source:"unknown",confidence:0}}function A(r,n){try{let t=P(r);if(!t)return l(n);let i=b(t.rawPath,n);return i?h(i,r,n):l(n)}catch{return l(n)}}function z(r,n){let t=[],i=n.lastKnownProjectDir?e(n.lastKnownProjectDir):"";for(let s of r){let o=A(s,{...n,lastKnownProjectDir:i||n.lastKnownProjectDir||null});t.push(o),o.projectDir&&o.confidence>=.55&&(i=o.projectDir)}return t}function C(r){let n=Math.max(0,Math.min(1,r));return Math.round(n*100)}function E(r){return r>=.8}function K(r){return e(r)}var O=1;export{O as PROJECT_ATTRIBUTION_VERSION,C as confidenceToPercent,E as isHighConfidenceAttribution,K as normalizeProjectDir,A as resolveProjectAttribution,z as resolveProjectAttributions};
@@ -1,4 +1,4 @@
1
- import{createRequire as R}from"node:module";import{existsSync as v,unlinkSync as T,renameSync as L}from"node:fs";import{tmpdir as N}from"node:os";import{join as w}from"node:path";var l=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){let s="",e=null;for(let o=0;o<t.length;o++){let a=t[o];if(e)s+=a,a===e&&(e=null);else if(a==="'"||a==='"')s+=a,e=a;else if(a===";"){let u=s.trim();u&&this.#t.prepare(u).run(),s=""}else s+=a}let r=s.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>{let r=s.get(...e);return r===null?void 0:r},all:(...e)=>s.all(...e),iterate:(...e)=>s.iterate(...e)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},p=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){return this.#t.exec(t),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>s.get(...e),all:(...e)=>s.all(...e),iterate:(...e)=>typeof s.iterate=="function"?s.iterate(...e):s.all(...e)[Symbol.iterator]()}}transaction(t){return(...s)=>{this.#t.exec("BEGIN");try{let e=t(...s);return this.#t.exec("COMMIT"),e}catch(e){throw this.#t.exec("ROLLBACK"),e}}}close(){this.#t.close()}},c=null;function O(){if(!c){let n=R(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;c=function(e,r){let o=new t(e,{readonly:r?.readonly,create:!0}),a=new l(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(process.platform==="linux")try{let{DatabaseSync:t}=n(["node","sqlite"].join(":"));c=function(e,r){let o=new t(e,{readOnly:r?.readonly??!1});return new p(o)}}catch{c=n("better-sqlite3")}else c=n("better-sqlite3")}return c}function g(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function h(n){if(!v(n))for(let t of["-wal","-shm"])try{T(n+t)}catch{}}function D(n){for(let t of["","-wal","-shm"])try{T(n+t)}catch{}}function _(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function y(n="context-mode"){return w(N(),`${n}-${process.pid}.db`)}function b(n,t=[100,500,2e3]){let s;for(let e=0;e<=t.length;e++)try{return n()}catch(r){let o=r instanceof Error?r.message:String(r);if(!o.includes("SQLITE_BUSY")&&!o.includes("database is locked"))throw r;if(s=r instanceof Error?r:new Error(o),e<t.length){let a=t[e],u=Date.now();for(;Date.now()-u<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${s?.message}`)}function I(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function C(n){let t=Date.now();for(let s of["","-wal","-shm"])try{L(n+s,`${n}${s}.corrupt-${t}`)}catch{}}var d=Symbol.for("__context_mode_live_dbs__"),m=(()=>{let n=globalThis;return n[d]||(n[d]=new Set,process.on("exit",()=>{for(let t of n[d])_(t);n[d].clear()})),n[d]})(),E=class{#t;#e;constructor(t){let s=O();this.#t=t,h(t);let e;try{e=new s(t,{timeout:3e4}),g(e)}catch(r){let o=r instanceof Error?r.message:String(r);if(I(o)){C(t),h(t);try{e=new s(t,{timeout:3e4}),g(e)}catch(a){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw r}this.#e=e,m.add(this.#e),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){m.delete(this.#e),_(this.#e)}withRetry(t){return b(t)}cleanup(){m.delete(this.#e),_(this.#e),D(this.#t)}};import{createHash as S}from"node:crypto";import{execFileSync as A}from"node:child_process";function Y(){let n=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(n!==void 0)return n?`__${n}`:"";try{let t=process.cwd(),s=A("git",["worktree","list","--porcelain"],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();if(s&&t!==s)return`__${S("sha256").update(t).digest("hex").slice(0,8)}`}catch{}return""}var x=1e3,M=5,i={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions"},f=class extends E{constructor(t){super(t?.dbPath??y("session"))}stmt(t){return this.stmts.get(t)}initSchema(){try{let s=this.db.pragma("table_xinfo(session_events)").find(e=>e.name==="data_hash");s&&s.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
1
+ import{createRequire as b}from"node:module";import{existsSync as N,unlinkSync as y,renameSync as D}from"node:fs";import{tmpdir as O}from"node:os";import{join as w}from"node:path";var p=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){let s="",e=null;for(let o=0;o<t.length;o++){let a=t[o];if(e)s+=a,a===e&&(e=null);else if(a==="'"||a==='"')s+=a,e=a;else if(a===";"){let c=s.trim();c&&this.#t.prepare(c).run(),s=""}else s+=a}let r=s.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>{let r=s.get(...e);return r===null?void 0:r},all:(...e)=>s.all(...e),iterate:(...e)=>s.iterate(...e)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},_=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){return this.#t.exec(t),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>s.get(...e),all:(...e)=>s.all(...e),iterate:(...e)=>typeof s.iterate=="function"?s.iterate(...e):s.all(...e)[Symbol.iterator]()}}transaction(t){return(...s)=>{this.#t.exec("BEGIN");try{let e=t(...s);return this.#t.exec("COMMIT"),e}catch(e){throw this.#t.exec("ROLLBACK"),e}}}close(){this.#t.close()}},u=null;function A(){if(!u){let n=b(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;u=function(e,r){let o=new t(e,{readonly:r?.readonly,create:!0}),a=new p(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(process.platform==="linux")try{let{DatabaseSync:t}=n(["node","sqlite"].join(":"));u=function(e,r){let o=new t(e,{readOnly:r?.readonly??!1});return new _(o)}}catch{u=n("better-sqlite3")}else u=n("better-sqlite3")}return u}function g(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function h(n){if(!N(n))for(let t of["-wal","-shm"])try{y(n+t)}catch{}}function C(n){for(let t of["","-wal","-shm"])try{y(n+t)}catch{}}function l(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function f(n="context-mode"){return w(O(),`${n}-${process.pid}.db`)}function I(n,t=[100,500,2e3]){let s;for(let e=0;e<=t.length;e++)try{return n()}catch(r){let o=r instanceof Error?r.message:String(r);if(!o.includes("SQLITE_BUSY")&&!o.includes("database is locked"))throw r;if(s=r instanceof Error?r:new Error(o),e<t.length){let a=t[e],c=Date.now();for(;Date.now()-c<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${s?.message}`)}function U(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function x(n){let t=Date.now();for(let s of["","-wal","-shm"])try{D(n+s,`${n}${s}.corrupt-${t}`)}catch{}}var d=Symbol.for("__context_mode_live_dbs__"),m=(()=>{let n=globalThis;return n[d]||(n[d]=new Set,process.on("exit",()=>{for(let t of n[d])l(t);n[d].clear()})),n[d]})(),E=class{#t;#e;constructor(t){let s=A();this.#t=t,h(t);let e;try{e=new s(t,{timeout:3e4}),g(e)}catch(r){let o=r instanceof Error?r.message:String(r);if(U(o)){x(t),h(t);try{e=new s(t,{timeout:3e4}),g(e)}catch(a){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw r}this.#e=e,m.add(this.#e),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){m.delete(this.#e),l(this.#e)}withRetry(t){return I(t)}cleanup(){m.delete(this.#e),l(this.#e),C(this.#t)}};import{createHash as R}from"node:crypto";import{execFileSync as M}from"node:child_process";function K(){let n=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(n!==void 0)return n?`__${n}`:"";try{let t=process.cwd(),s=M("git",["worktree","list","--porcelain"],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();if(s&&t!==s)return`__${R("sha256").update(t).digest("hex").slice(0,8)}`}catch{}return""}var k=1e3,F=5,i={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions"},L=class extends E{constructor(t){super(t?.dbPath??f("session"))}stmt(t){return this.stmts.get(t)}initSchema(){try{let s=this.db.pragma("table_xinfo(session_events)").find(e=>e.name==="data_hash");s&&s.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
2
2
  CREATE TABLE IF NOT EXISTS session_events (
3
3
  id INTEGER PRIMARY KEY AUTOINCREMENT,
4
4
  session_id TEXT NOT NULL,
@@ -6,6 +6,9 @@ import{createRequire as R}from"node:module";import{existsSync as v,unlinkSync as
6
6
  category TEXT NOT NULL,
7
7
  priority INTEGER NOT NULL DEFAULT 2,
8
8
  data TEXT NOT NULL,
9
+ project_dir TEXT NOT NULL DEFAULT '',
10
+ attribution_source TEXT NOT NULL DEFAULT 'unknown',
11
+ attribution_confidence REAL NOT NULL DEFAULT 0,
9
12
  source_hook TEXT NOT NULL,
10
13
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
11
14
  data_hash TEXT NOT NULL DEFAULT ''
@@ -32,12 +35,28 @@ import{createRequire as R}from"node:module";import{existsSync as v,unlinkSync as
32
35
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
36
  consumed INTEGER NOT NULL DEFAULT 0
34
37
  );
35
- `)}prepareStatements(){this.stmts=new Map;let t=(s,e)=>{this.stmts.set(s,this.db.prepare(e))};t(i.insertEvent,`INSERT INTO session_events (session_id, type, category, priority, data, source_hook, data_hash)
36
- VALUES (?, ?, ?, ?, ?, ?, ?)`),t(i.getEvents,`SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash
37
- FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByType,`SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash
38
- FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByPriority,`SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash
39
- FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByTypeAndPriority,`SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash
40
- FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(i.getEventCount,"SELECT COUNT(*) AS cnt FROM session_events WHERE session_id = ?"),t(i.checkDuplicate,`SELECT 1 FROM (
38
+ `);try{let t=this.db.pragma("table_xinfo(session_events)"),s=new Set(t.map(e=>e.name));s.has("project_dir")||this.db.exec("ALTER TABLE session_events ADD COLUMN project_dir TEXT NOT NULL DEFAULT ''"),s.has("attribution_source")||this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_source TEXT NOT NULL DEFAULT 'unknown'"),s.has("attribution_confidence")||this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_confidence REAL NOT NULL DEFAULT 0"),this.db.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)")}catch{}}prepareStatements(){this.stmts=new Map;let t=(s,e)=>{this.stmts.set(s,this.db.prepare(e))};t(i.insertEvent,`INSERT INTO session_events (
39
+ session_id, type, category, priority, data,
40
+ project_dir, attribution_source, attribution_confidence,
41
+ source_hook, data_hash
42
+ )
43
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),t(i.getEvents,`SELECT id, session_id, type, category, priority, data,
44
+ project_dir, attribution_source, attribution_confidence,
45
+ source_hook, created_at, data_hash
46
+ FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByType,`SELECT id, session_id, type, category, priority, data,
47
+ project_dir, attribution_source, attribution_confidence,
48
+ source_hook, created_at, data_hash
49
+ FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByPriority,`SELECT id, session_id, type, category, priority, data,
50
+ project_dir, attribution_source, attribution_confidence,
51
+ source_hook, created_at, data_hash
52
+ FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(i.getEventsByTypeAndPriority,`SELECT id, session_id, type, category, priority, data,
53
+ project_dir, attribution_source, attribution_confidence,
54
+ source_hook, created_at, data_hash
55
+ FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(i.getEventCount,"SELECT COUNT(*) AS cnt FROM session_events WHERE session_id = ?"),t(i.getLatestAttributedProject,`SELECT project_dir
56
+ FROM session_events
57
+ WHERE session_id = ? AND project_dir != ''
58
+ ORDER BY id DESC
59
+ LIMIT 1`),t(i.checkDuplicate,`SELECT 1 FROM (
41
60
  SELECT type, data_hash FROM session_events
42
61
  WHERE session_id = ? ORDER BY id DESC LIMIT ?
43
62
  ) AS recent
@@ -54,4 +73,4 @@ import{createRequire as R}from"node:module";import{existsSync as v,unlinkSync as
54
73
  snapshot = excluded.snapshot,
55
74
  event_count = excluded.event_count,
56
75
  created_at = datetime('now'),
57
- consumed = 0`),t(i.getResume,"SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?"),t(i.markResumeConsumed,"UPDATE session_resume SET consumed = 1 WHERE session_id = ?"),t(i.deleteEvents,"DELETE FROM session_events WHERE session_id = ?"),t(i.deleteMeta,"DELETE FROM session_meta WHERE session_id = ?"),t(i.deleteResume,"DELETE FROM session_resume WHERE session_id = ?"),t(i.getOldSessions,"SELECT session_id FROM session_meta WHERE started_at < datetime('now', ? || ' days')")}insertEvent(t,s,e="PostToolUse"){let r=S("sha256").update(s.data).digest("hex").slice(0,16).toUpperCase(),o=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,M,s.type,r))return;this.stmt(i.getEventCount).get(t).cnt>=x&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,s.type,s.category,s.priority,s.data,e,r),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>o())}getEvents(t,s){let e=s?.limit??1e3,r=s?.type,o=s?.minPriority;return r&&o!==void 0?this.stmt(i.getEventsByTypeAndPriority).all(t,r,o,e):r?this.stmt(i.getEventsByType).all(t,r,e):o!==void 0?this.stmt(i.getEventsByPriority).all(t,o,e):this.stmt(i.getEvents).all(t,e)}getEventCount(t){return this.stmt(i.getEventCount).get(t).cnt}ensureSession(t,s){this.stmt(i.ensureSession).run(t,s)}getSessionStats(t){return this.stmt(i.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(i.incrementCompactCount).run(t)}upsertResume(t,s,e){this.stmt(i.upsertResume).run(t,s,e??0)}getResume(t){return this.stmt(i.getResume).get(t)??null}markResumeConsumed(t){this.stmt(i.markResumeConsumed).run(t)}deleteSession(t){this.db.transaction(()=>{this.stmt(i.deleteEvents).run(t),this.stmt(i.deleteResume).run(t),this.stmt(i.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let s=`-${t}`,e=this.stmt(i.getOldSessions).all(s);for(let{session_id:r}of e)this.deleteSession(r);return e.length}};export{f as SessionDB,Y as getWorktreeSuffix};
76
+ consumed = 0`),t(i.getResume,"SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?"),t(i.markResumeConsumed,"UPDATE session_resume SET consumed = 1 WHERE session_id = ?"),t(i.deleteEvents,"DELETE FROM session_events WHERE session_id = ?"),t(i.deleteMeta,"DELETE FROM session_meta WHERE session_id = ?"),t(i.deleteResume,"DELETE FROM session_resume WHERE session_id = ?"),t(i.getOldSessions,"SELECT session_id FROM session_meta WHERE started_at < datetime('now', ? || ' days')")}insertEvent(t,s,e="PostToolUse",r){let o=R("sha256").update(s.data).digest("hex").slice(0,16).toUpperCase(),a=String(r?.projectDir??s.project_dir??"").trim(),c=String(r?.source??s.attribution_source??"unknown"),T=Number(r?.confidence??s.attribution_confidence??0),S=Number.isFinite(T)?Math.max(0,Math.min(1,T)):0,v=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,F,s.type,o))return;this.stmt(i.getEventCount).get(t).cnt>=k&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,s.type,s.category,s.priority,s.data,a,c,S,e,o),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>v())}getEvents(t,s){let e=s?.limit??1e3,r=s?.type,o=s?.minPriority;return r&&o!==void 0?this.stmt(i.getEventsByTypeAndPriority).all(t,r,o,e):r?this.stmt(i.getEventsByType).all(t,r,e):o!==void 0?this.stmt(i.getEventsByPriority).all(t,o,e):this.stmt(i.getEvents).all(t,e)}getEventCount(t){return this.stmt(i.getEventCount).get(t).cnt}getLatestAttributedProjectDir(t){return this.stmt(i.getLatestAttributedProject).get(t)?.project_dir||null}ensureSession(t,s){this.stmt(i.ensureSession).run(t,s)}getSessionStats(t){return this.stmt(i.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(i.incrementCompactCount).run(t)}upsertResume(t,s,e){this.stmt(i.upsertResume).run(t,s,e??0)}getResume(t){return this.stmt(i.getResume).get(t)??null}markResumeConsumed(t){this.stmt(i.markResumeConsumed).run(t)}deleteSession(t){this.db.transaction(()=>{this.stmt(i.deleteEvents).run(t),this.stmt(i.deleteResume).run(t),this.stmt(i.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let s=`-${t}`,e=this.stmt(i.getOldSessions).all(s);for(let{session_id:r}of e)this.deleteSession(r);return e.length}};export{L as SessionDB,K as getWorktreeSuffix};
@@ -1 +1,2 @@
1
- function o(t){return t==null?"":String(t)}function l(t){return t==null?"":typeof t=="string"?t:JSON.stringify(t)}function c(t){let{tool_name:e,tool_input:n,tool_response:s}=t,r=[];if(e==="Read"){let i=String(n.file_path??"");return/CLAUDE\.md$|\.claude[\\/]/i.test(i)&&(r.push({type:"rule",category:"rule",data:o(i),priority:1}),s&&s.length>0&&r.push({type:"rule_content",category:"rule",data:o(s),priority:1})),r.push({type:"file_read",category:"file",data:o(i),priority:1}),r}if(e==="Edit"){let i=String(n.file_path??"");return r.push({type:"file_edit",category:"file",data:o(i),priority:1}),r}if(e==="NotebookEdit"){let i=String(n.notebook_path??"");return r.push({type:"file_edit",category:"file",data:o(i),priority:1}),r}if(e==="Write"){let i=String(n.file_path??"");return r.push({type:"file_write",category:"file",data:o(i),priority:1}),r}if(e==="Glob"){let i=String(n.pattern??"");return r.push({type:"file_glob",category:"file",data:o(i),priority:3}),r}if(e==="Grep"){let i=String(n.pattern??""),a=String(n.path??"");return r.push({type:"file_search",category:"file",data:o(`${i} in ${a}`),priority:3}),r}return r}function u(t){if(t.tool_name!=="Bash")return[];let n=String(t.tool_input.command??"").match(/\bcd\s+("([^"]+)"|'([^']+)'|(\S+))/);if(!n)return[];let s=n[2]??n[3]??n[4]??"";return[{type:"cwd",category:"cwd",data:o(s),priority:2}]}function d(t){let{tool_name:e,tool_input:n,tool_response:s,tool_output:r}=t,i=String(s??""),a=r?.isError===!0;return!(e==="Bash"&&/exit code [1-9]|error:|Error:|FAIL|failed/i.test(i))&&!a?[]:[{type:"error_tool",category:"error",data:o(i),priority:2}]}var g=[{pattern:/\bgit\s+checkout\b/,operation:"branch"},{pattern:/\bgit\s+commit\b/,operation:"commit"},{pattern:/\bgit\s+merge\s+\S+/,operation:"merge"},{pattern:/\bgit\s+rebase\b/,operation:"rebase"},{pattern:/\bgit\s+stash\b/,operation:"stash"},{pattern:/\bgit\s+push\b/,operation:"push"},{pattern:/\bgit\s+pull\b/,operation:"pull"},{pattern:/\bgit\s+log\b/,operation:"log"},{pattern:/\bgit\s+diff\b/,operation:"diff"},{pattern:/\bgit\s+status\b/,operation:"status"},{pattern:/\bgit\s+branch\b/,operation:"branch"},{pattern:/\bgit\s+reset\b/,operation:"reset"},{pattern:/\bgit\s+add\b/,operation:"add"},{pattern:/\bgit\s+cherry-pick\b/,operation:"cherry-pick"},{pattern:/\bgit\s+tag\b/,operation:"tag"},{pattern:/\bgit\s+fetch\b/,operation:"fetch"},{pattern:/\bgit\s+clone\b/,operation:"clone"},{pattern:/\bgit\s+worktree\b/,operation:"worktree"}];function b(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??""),n=g.find(s=>s.pattern.test(e));return n?[{type:"git",category:"git",data:o(n.operation),priority:2}]:[]}function y(t){return new Set(["TodoWrite","TaskCreate","TaskUpdate"]).has(t.tool_name)?[{type:t.tool_name==="TaskUpdate"?"task_update":t.tool_name==="TaskCreate"?"task_create":"task",category:"task",data:o(JSON.stringify(t.tool_input)),priority:1}]:[]}function f(t){if(t.tool_name==="EnterPlanMode")return[{type:"plan_enter",category:"plan",data:"entered plan mode",priority:2}];if(t.tool_name==="ExitPlanMode"){let e=[],n=t.tool_input.allowedPrompts,s=Array.isArray(n)&&n.length>0?`exited plan mode (allowed: ${l(n.map(i=>typeof i=="object"&&i!==null&&"prompt"in i?String(i.prompt):String(i)).join(", "))})`:"exited plan mode";e.push({type:"plan_exit",category:"plan",data:o(s),priority:2});let r=String(t.tool_response??"").toLowerCase();return r.includes("approved")||r.includes("approve")?e.push({type:"plan_approved",category:"plan",data:"plan approved by user",priority:1}):(r.includes("rejected")||r.includes("decline")||r.includes("denied"))&&e.push({type:"plan_rejected",category:"plan",data:o(`plan rejected: ${t.tool_response??""}`),priority:2}),e}if(t.tool_name==="Write"||t.tool_name==="Edit"){let e=String(t.tool_input.file_path??"");if(/[/\\]\.claude[/\\]plans[/\\]/.test(e))return[{type:"plan_file_write",category:"plan",data:o(`plan file: ${e.split(/[/\\]/).pop()??e}`),priority:2}]}return[]}var h=[/\bsource\s+\S*activate\b/,/\bexport\s+\w+=/,/\bnvm\s+use\b/,/\bpyenv\s+(shell|local|global)\b/,/\bconda\s+activate\b/,/\brbenv\s+(shell|local|global)\b/,/\bnpm\s+install\b/,/\bnpm\s+ci\b/,/\bpip\s+install\b/,/\bbun\s+install\b/,/\byarn\s+(add|install)\b/,/\bpnpm\s+(add|install)\b/,/\bcargo\s+(install|add)\b/,/\bgo\s+(install|get)\b/,/\brustup\b/,/\basdf\b/,/\bvolta\b/,/\bdeno\s+install\b/];function _(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??"");if(!h.some(r=>r.test(e)))return[];let s=e.replace(/\bexport\s+(\w+)=\S*/g,"export $1=***");return[{type:"env",category:"env",data:o(s),priority:2}]}function m(t){if(t.tool_name!=="Skill")return[];let e=String(t.tool_input.skill??"");return[{type:"skill",category:"skill",data:o(e),priority:3}]}function S(t){if(t.tool_name!=="Agent")return[];let e=o(String(t.tool_input.prompt??t.tool_input.description??"")),n=t.tool_response?o(String(t.tool_response)):"",s=n.length>0;return[{type:s?"subagent_completed":"subagent_launched",category:"subagent",data:o(s?`[completed] ${e} \u2192 ${n}`:`[launched] ${e}`),priority:s?2:3}]}function k(t){let{tool_name:e,tool_input:n}=t;if(!e.startsWith("mcp__"))return[];let s=e.split("__"),r=s[s.length-1]||e,i=Object.values(n).find(p=>typeof p=="string"),a=i?`: ${o(String(i))}`:"";return[{type:"mcp",category:"mcp",data:o(`${r}${a}`),priority:3}]}function E(t){if(t.tool_name!=="AskUserQuestion")return[];let e=t.tool_input.questions,n=Array.isArray(e)&&e.length>0?String(e[0].question??""):"",s=o(String(t.tool_response??"")),r=n?`Q: ${o(n)} \u2192 A: ${s}`:`answer: ${s}`;return[{type:"decision_question",category:"decision",data:o(r),priority:2}]}function v(t){if(t.tool_name!=="EnterWorktree")return[];let e=String(t.tool_input.name??"unnamed");return[{type:"worktree",category:"env",data:o(`entered worktree: ${e}`),priority:2}]}var x=[/\b(don'?t|do not|never|always|instead|rather|prefer)\b/i,/\b(use|switch to|go with|pick|choose)\s+\w+\s+(instead|over|not)\b/i,/\b(no,?\s+(use|do|try|make))\b/i,/\b(hayır|hayir|evet|böyle|boyle|degil|değil|yerine|kullan)\b/i];function w(t){return x.some(n=>n.test(t))?[{type:"decision",category:"decision",data:o(t),priority:2}]:[]}var T=[/\b(act as|you are|behave like|pretend|role of|persona)\b/i,/\b(senior|staff|principal|lead)\s+(engineer|developer|architect)\b/i,/\b(gibi davran|rolünde|olarak çalış)\b/i];function R(t){return T.some(n=>n.test(t))?[{type:"role",category:"role",data:o(t),priority:3}]:[]}var A=[{mode:"investigate",pattern:/\b(why|how does|explain|understand|what is|analyze|debug|look into)\b/i},{mode:"implement",pattern:/\b(create|add|build|implement|write|make|develop|fix)\b/i},{mode:"discuss",pattern:/\b(think about|consider|should we|what if|pros and cons|opinion)\b/i},{mode:"review",pattern:/\b(review|check|audit|verify|test|validate)\b/i}];function I(t){let e=A.find(({pattern:n})=>n.test(t));return e?[{type:"intent",category:"intent",data:o(e.mode),priority:4}]:[]}function $(t){return t.length<=1024?[]:[{type:"data",category:"data",data:o(t),priority:4}]}function P(t){try{let e=[];return e.push(...c(t)),e.push(...u(t)),e.push(...d(t)),e.push(...b(t)),e.push(..._(t)),e.push(...y(t)),e.push(...f(t)),e.push(...m(t)),e.push(...S(t)),e.push(...k(t)),e.push(...E(t)),e.push(...v(t)),e}catch{return[]}}function H(t){try{let e=[];return e.push(...w(t)),e.push(...R(t)),e.push(...I(t)),e.push(...$(t)),e}catch{return[]}}export{P as extractEvents,H as extractUserEvents};
1
+ function o(t){return t==null?"":String(t)}function u(t){return t==null?"":typeof t=="string"?t:JSON.stringify(t)}function d(t){let{tool_name:e,tool_input:n,tool_response:i}=t,r=[];if(e==="Read"){let s=String(n.file_path??"");return/CLAUDE\.md$|\.claude[\\/]/i.test(s)&&(r.push({type:"rule",category:"rule",data:o(s),priority:1}),i&&i.length>0&&r.push({type:"rule_content",category:"rule",data:o(i),priority:1})),r.push({type:"file_read",category:"file",data:o(s),priority:1}),r}if(e==="Edit"){let s=String(n.file_path??"");return r.push({type:"file_edit",category:"file",data:o(s),priority:1}),r}if(e==="NotebookEdit"){let s=String(n.notebook_path??"");return r.push({type:"file_edit",category:"file",data:o(s),priority:1}),r}if(e==="Write"){let s=String(n.file_path??"");return r.push({type:"file_write",category:"file",data:o(s),priority:1}),r}if(e==="Glob"){let s=String(n.pattern??"");return r.push({type:"file_glob",category:"file",data:o(s),priority:3}),r}if(e==="Grep"){let s=String(n.pattern??""),a=String(n.path??"");return r.push({type:"file_search",category:"file",data:o(`${s} in ${a}`),priority:3}),r}return r}function g(t){if(t.tool_name!=="Bash")return[];let n=String(t.tool_input.command??"").match(/\bcd\s+("([^"]+)"|'([^']+)'|(\S+))/);if(!n)return[];let i=n[2]??n[3]??n[4]??"";return[{type:"cwd",category:"cwd",data:o(i),priority:2}]}function b(t){let{tool_name:e,tool_input:n,tool_response:i,tool_output:r}=t,s=String(i??""),a=r?.isError===!0;return!(e==="Bash"&&/exit code [1-9]|error:|Error:|FAIL|failed/i.test(s))&&!a?[]:[{type:"error_tool",category:"error",data:o(s),priority:2}]}var y=[{pattern:/\bgit\s+checkout\b/,operation:"branch"},{pattern:/\bgit\s+commit\b/,operation:"commit"},{pattern:/\bgit\s+merge\s+\S+/,operation:"merge"},{pattern:/\bgit\s+rebase\b/,operation:"rebase"},{pattern:/\bgit\s+stash\b/,operation:"stash"},{pattern:/\bgit\s+push\b/,operation:"push"},{pattern:/\bgit\s+pull\b/,operation:"pull"},{pattern:/\bgit\s+log\b/,operation:"log"},{pattern:/\bgit\s+diff\b/,operation:"diff"},{pattern:/\bgit\s+status\b/,operation:"status"},{pattern:/\bgit\s+branch\b/,operation:"branch"},{pattern:/\bgit\s+reset\b/,operation:"reset"},{pattern:/\bgit\s+add\b/,operation:"add"},{pattern:/\bgit\s+cherry-pick\b/,operation:"cherry-pick"},{pattern:/\bgit\s+tag\b/,operation:"tag"},{pattern:/\bgit\s+fetch\b/,operation:"fetch"},{pattern:/\bgit\s+clone\b/,operation:"clone"},{pattern:/\bgit\s+worktree\b/,operation:"worktree"}];function f(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??""),n=y.find(i=>i.pattern.test(e));return n?[{type:"git",category:"git",data:o(n.operation),priority:2}]:[]}function h(t){return new Set(["TodoWrite","TaskCreate","TaskUpdate"]).has(t.tool_name)?[{type:t.tool_name==="TaskUpdate"?"task_update":t.tool_name==="TaskCreate"?"task_create":"task",category:"task",data:o(JSON.stringify(t.tool_input)),priority:1}]:[]}function _(t){if(t.tool_name==="EnterPlanMode")return[{type:"plan_enter",category:"plan",data:"entered plan mode",priority:2}];if(t.tool_name==="ExitPlanMode"){let e=[],n=t.tool_input.allowedPrompts,i=Array.isArray(n)&&n.length>0?`exited plan mode (allowed: ${u(n.map(s=>typeof s=="object"&&s!==null&&"prompt"in s?String(s.prompt):String(s)).join(", "))})`:"exited plan mode";e.push({type:"plan_exit",category:"plan",data:o(i),priority:2});let r=String(t.tool_response??"").toLowerCase();return r.includes("approved")||r.includes("approve")?e.push({type:"plan_approved",category:"plan",data:"plan approved by user",priority:1}):(r.includes("rejected")||r.includes("decline")||r.includes("denied"))&&e.push({type:"plan_rejected",category:"plan",data:o(`plan rejected: ${t.tool_response??""}`),priority:2}),e}if(t.tool_name==="Write"||t.tool_name==="Edit"){let e=String(t.tool_input.file_path??"");if(/[/\\]\.claude[/\\]plans[/\\]/.test(e))return[{type:"plan_file_write",category:"plan",data:o(`plan file: ${e.split(/[/\\]/).pop()??e}`),priority:2}]}return[]}var m=[/\bsource\s+\S*activate\b/,/\bexport\s+\w+=/,/\bnvm\s+use\b/,/\bpyenv\s+(shell|local|global)\b/,/\bconda\s+activate\b/,/\brbenv\s+(shell|local|global)\b/,/\bnpm\s+install\b/,/\bnpm\s+ci\b/,/\bpip\s+install\b/,/\bbun\s+install\b/,/\byarn\s+(add|install)\b/,/\bpnpm\s+(add|install)\b/,/\bcargo\s+(install|add)\b/,/\bgo\s+(install|get)\b/,/\brustup\b/,/\basdf\b/,/\bvolta\b/,/\bdeno\s+install\b/];function S(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??"");if(!m.some(r=>r.test(e)))return[];let i=e.replace(/\bexport\s+(\w+)=\S*/g,"export $1=***");return[{type:"env",category:"env",data:o(i),priority:2}]}function k(t){if(t.tool_name!=="Skill")return[];let e=String(t.tool_input.skill??"");return[{type:"skill",category:"skill",data:o(e),priority:3}]}function E(t){if(t.tool_name!=="Agent")return[];let e=o(String(t.tool_input.prompt??t.tool_input.description??"")),n=t.tool_response?o(String(t.tool_response)):"",i=n.length>0;return[{type:i?"subagent_completed":"subagent_launched",category:"subagent",data:o(i?`[completed] ${e} \u2192 ${n}`:`[launched] ${e}`),priority:i?2:3}]}function v(t){let{tool_name:e,tool_input:n,tool_response:i}=t;if(!e.startsWith("mcp__"))return[];let r=e.split("__"),s=r[r.length-1]||e,a=Object.values(n).find(c=>typeof c=="string"),p=a?`: ${o(String(a))}`:"",l=i&&i.length>0?`
2
+ response: ${o(i)}`:"";return[{type:"mcp",category:"mcp",data:o(`${s}${p}${l}`),priority:3}]}function x(t){if(t.tool_name!=="AskUserQuestion")return[];let e=t.tool_input.questions,n=Array.isArray(e)&&e.length>0?String(e[0].question??""):"",i=o(String(t.tool_response??"")),r=n?`Q: ${o(n)} \u2192 A: ${i}`:`answer: ${i}`;return[{type:"decision_question",category:"decision",data:o(r),priority:2}]}function w(t){if(t.tool_name!=="EnterWorktree")return[];let e=String(t.tool_input.name??"unnamed");return[{type:"worktree",category:"env",data:o(`entered worktree: ${e}`),priority:2}]}var T=[/\b(don'?t|do not|never|always|instead|rather|prefer)\b/i,/\b(use|switch to|go with|pick|choose)\s+\w+\s+(instead|over|not)\b/i,/\b(no,?\s+(use|do|try|make))\b/i,/\b(hayır|hayir|evet|böyle|boyle|degil|değil|yerine|kullan)\b/i];function R(t){return T.some(n=>n.test(t))?[{type:"decision",category:"decision",data:o(t),priority:2}]:[]}var A=[/\b(act as|you are|behave like|pretend|role of|persona)\b/i,/\b(senior|staff|principal|lead)\s+(engineer|developer|architect)\b/i,/\b(gibi davran|rolünde|olarak çalış)\b/i];function I(t){return A.some(n=>n.test(t))?[{type:"role",category:"role",data:o(t),priority:3}]:[]}var $=[{mode:"investigate",pattern:/\b(why|how does|explain|understand|what is|analyze|debug|look into)\b/i},{mode:"implement",pattern:/\b(create|add|build|implement|write|make|develop|fix)\b/i},{mode:"discuss",pattern:/\b(think about|consider|should we|what if|pros and cons|opinion)\b/i},{mode:"review",pattern:/\b(review|check|audit|verify|test|validate)\b/i}];function P(t){let e=$.find(({pattern:n})=>n.test(t));return e?[{type:"intent",category:"intent",data:o(e.mode),priority:4}]:[]}function H(t){return t.length<=1024?[]:[{type:"data",category:"data",data:o(t),priority:4}]}function N(t){try{let e=[];return e.push(...d(t)),e.push(...g(t)),e.push(...b(t)),e.push(...f(t)),e.push(...S(t)),e.push(...h(t)),e.push(..._(t)),e.push(...k(t)),e.push(...E(t)),e.push(...v(t)),e.push(...x(t)),e.push(...w(t)),e}catch{return[]}}function C(t){try{let e=[];return e.push(...R(t)),e.push(...I(t)),e.push(...P(t)),e.push(...H(t)),e}catch{return[]}}export{N as extractEvents,C as extractUserEvents};
@@ -46,6 +46,7 @@ function getWorktreeSuffix() {
46
46
  /** Claude Code platform options (default). */
47
47
  const CLAUDE_OPTS = {
48
48
  configDir: ".claude",
49
+ configDirEnv: "CLAUDE_CONFIG_DIR",
49
50
  projectDirEnv: "CLAUDE_PROJECT_DIR",
50
51
  sessionIdEnv: "CLAUDE_SESSION_ID",
51
52
  };
@@ -53,6 +54,7 @@ const CLAUDE_OPTS = {
53
54
  /** Gemini CLI platform options. */
54
55
  export const GEMINI_OPTS = {
55
56
  configDir: ".gemini",
57
+ configDirEnv: "GEMINI_CLI_HOME",
56
58
  projectDirEnv: "GEMINI_PROJECT_DIR",
57
59
  sessionIdEnv: undefined,
58
60
  };
@@ -60,6 +62,7 @@ export const GEMINI_OPTS = {
60
62
  /** VS Code Copilot platform options. */
61
63
  export const VSCODE_OPTS = {
62
64
  configDir: ".vscode",
65
+ configDirEnv: undefined,
63
66
  projectDirEnv: "VSCODE_CWD",
64
67
  sessionIdEnv: undefined,
65
68
  };
@@ -67,6 +70,7 @@ export const VSCODE_OPTS = {
67
70
  /** Cursor platform options. */
68
71
  export const CURSOR_OPTS = {
69
72
  configDir: ".cursor",
73
+ configDirEnv: undefined,
70
74
  projectDirEnv: "CURSOR_CWD",
71
75
  sessionIdEnv: "CURSOR_SESSION_ID",
72
76
  };
@@ -74,6 +78,7 @@ export const CURSOR_OPTS = {
74
78
  /** Codex CLI platform options. */
75
79
  export const CODEX_OPTS = {
76
80
  configDir: ".codex",
81
+ configDirEnv: "CODEX_HOME",
77
82
  projectDirEnv: undefined, // Codex passes cwd in hook stdin, no env var
78
83
  sessionIdEnv: undefined, // Uses session_id from hook stdin or ppid fallback
79
84
  };
@@ -81,10 +86,46 @@ export const CODEX_OPTS = {
81
86
  /** Kiro CLI platform options. */
82
87
  export const KIRO_OPTS = {
83
88
  configDir: ".kiro",
89
+ configDirEnv: undefined,
84
90
  projectDirEnv: undefined, // Kiro CLI provides cwd in hook stdin, no env var
85
91
  sessionIdEnv: undefined, // No session ID env var — uses ppid fallback
86
92
  };
87
93
 
94
+ /** JetBrains Copilot platform options. */
95
+ export const JETBRAINS_OPTS = {
96
+ configDir: ".config/JetBrains",
97
+ configDirEnv: undefined,
98
+ projectDirEnv: "IDEA_INITIAL_DIRECTORY",
99
+ sessionIdEnv: undefined,
100
+ };
101
+
102
+ /**
103
+ * Resolve the platform config directory, respecting env var overrides.
104
+ * Platforms like Claude Code (CLAUDE_CONFIG_DIR), Gemini CLI (GEMINI_CLI_HOME),
105
+ * and Codex CLI (CODEX_HOME) allow users to customize the config location.
106
+ * Falls back to ~/<configDir> when no env var is set.
107
+ */
108
+ export function resolveConfigDir(opts = CLAUDE_OPTS) {
109
+ if (opts.configDirEnv) {
110
+ const envVal = process.env[opts.configDirEnv];
111
+ if (envVal) {
112
+ if (envVal.startsWith("~")) return join(homedir(), envVal.replace(/^~[/\\]?/, ""));
113
+ return envVal;
114
+ }
115
+ }
116
+ return join(homedir(), opts.configDir);
117
+ }
118
+
119
+ /**
120
+ * Safely parse raw stdin string as JSON.
121
+ * Returns empty object for empty/whitespace/BOM-only input instead of throwing.
122
+ * Strips BOM prefix before parsing. Throws on genuinely malformed JSON.
123
+ */
124
+ export function parseStdin(raw) {
125
+ const cleaned = raw.replace(/^\uFEFF/, "").trim();
126
+ return cleaned ? JSON.parse(cleaned) : {};
127
+ }
128
+
88
129
  /**
89
130
  * Read all of stdin as a string (event-based, cross-platform safe).
90
131
  */
@@ -147,7 +188,7 @@ export function getSessionId(input, opts = CLAUDE_OPTS) {
147
188
  export function getSessionDBPath(opts = CLAUDE_OPTS) {
148
189
  const projectDir = getProjectDir(opts);
149
190
  const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
150
- const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
191
+ const dir = join(resolveConfigDir(opts), "context-mode", "sessions");
151
192
  mkdirSync(dir, { recursive: true });
152
193
  return join(dir, `${hash}${getWorktreeSuffix()}.db`);
153
194
  }
@@ -160,7 +201,7 @@ export function getSessionDBPath(opts = CLAUDE_OPTS) {
160
201
  export function getSessionEventsPath(opts = CLAUDE_OPTS) {
161
202
  const projectDir = getProjectDir(opts);
162
203
  const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
163
- const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
204
+ const dir = join(resolveConfigDir(opts), "context-mode", "sessions");
164
205
  mkdirSync(dir, { recursive: true });
165
206
  return join(dir, `${hash}${getWorktreeSuffix()}-events.md`);
166
207
  }
@@ -173,7 +214,7 @@ export function getSessionEventsPath(opts = CLAUDE_OPTS) {
173
214
  export function getCleanupFlagPath(opts = CLAUDE_OPTS) {
174
215
  const projectDir = getProjectDir(opts);
175
216
  const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
176
- const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
217
+ const dir = join(resolveConfigDir(opts), "context-mode", "sessions");
177
218
  mkdirSync(dir, { recursive: true });
178
219
  return join(dir, `${hash}${getWorktreeSuffix()}.cleanup`);
179
220
  }
@@ -34,6 +34,19 @@ export function createSessionLoaders(hookDir) {
34
34
  async loadSessionDB() {
35
35
  return await loadModule("session-db.bundle.mjs", "db.js");
36
36
  },
37
+ async loadProjectAttribution() {
38
+ const bundlePath = join(bundleDir, "session-attribution.bundle.mjs");
39
+ if (existsSync(bundlePath)) {
40
+ return await import(pathToFileURL(bundlePath).href);
41
+ }
42
+ const buildPath = join(buildSession, "project-attribution.js");
43
+ if (existsSync(buildPath)) {
44
+ return await import(pathToFileURL(buildPath).href);
45
+ }
46
+ // Last-resort fallback for dev environments without a fresh build.
47
+ const localPath = join(bundleDir, "project-attribution.mjs");
48
+ return await import(pathToFileURL(localPath).href);
49
+ },
37
50
  async loadExtract() {
38
51
  return await loadModule("session-extract.bundle.mjs", "extract.js");
39
52
  },
@@ -42,3 +55,27 @@ export function createSessionLoaders(hookDir) {
42
55
  },
43
56
  };
44
57
  }
58
+
59
+ /**
60
+ * Shared helper — resolves project attributions and inserts events into the DB.
61
+ * Eliminates the ~15-line attribution block duplicated across all hook files.
62
+ *
63
+ * @returns {Array} The resolved attributions array (useful when a subsequent
64
+ * attribution block needs `lastKnownProjectDir` from the first).
65
+ */
66
+ export function attributeAndInsertEvents(db, sessionId, events, input, projectDir, hookName, resolveProjectAttributions) {
67
+ const sessionStats = db.getSessionStats(sessionId);
68
+ const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
69
+ ? db.getLatestAttributedProjectDir(sessionId)
70
+ : null;
71
+ const attributions = resolveProjectAttributions(events, {
72
+ sessionOriginDir: sessionStats?.project_dir || projectDir,
73
+ inputProjectDir: projectDir,
74
+ workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
75
+ lastKnownProjectDir,
76
+ });
77
+ for (let i = 0; i < events.length; i++) {
78
+ db.insertEvent(sessionId, events[i], hookName, attributions[i]);
79
+ }
80
+ return attributions;
81
+ }
@@ -20,13 +20,12 @@ import { createToolNamer } from "./core/tool-naming.mjs";
20
20
 
21
21
  const toolNamer = createToolNamer("claude-code");
22
22
  const ROUTING_BLOCK = createRoutingBlock(toolNamer);
23
- import { readStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath } from "./session-helpers.mjs";
23
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir } from "./session-helpers.mjs";
24
24
  import { writeSessionEventsFile, buildSessionDirective, getSessionEvents, getLatestSessionEvents } from "./session-directive.mjs";
25
25
  import { createSessionLoaders } from "./session-loaders.mjs";
26
26
  import { join, dirname } from "node:path";
27
27
  import { fileURLToPath } from "node:url";
28
28
  import { readFileSync, unlinkSync, readdirSync, rmSync, statSync } from "node:fs";
29
- import { homedir } from "node:os";
30
29
 
31
30
  // Resolve absolute path for imports (fileURLToPath for Windows compat)
32
31
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
@@ -36,7 +35,7 @@ let additionalContext = ROUTING_BLOCK;
36
35
 
37
36
  try {
38
37
  const raw = await readStdin();
39
- const input = JSON.parse(raw);
38
+ const input = parseStdin(raw);
40
39
  const source = input.source ?? "startup";
41
40
 
42
41
  if (source === "compact") {
@@ -93,7 +92,7 @@ try {
93
92
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
94
93
  db.ensureSession(sessionId, projectDir);
95
94
  const claudeMdPaths = [
96
- join(homedir(), ".claude", "CLAUDE.md"),
95
+ join(resolveConfigDir(), "CLAUDE.md"),
97
96
  join(projectDir, "CLAUDE.md"),
98
97
  join(projectDir, ".claude", "CLAUDE.md"),
99
98
  ];
@@ -140,8 +139,9 @@ try {
140
139
  const { appendFileSync } = await import("node:fs");
141
140
  const { join: pjoin } = await import("node:path");
142
141
  const { homedir } = await import("node:os");
142
+ const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
143
143
  appendFileSync(
144
- pjoin(homedir(), ".claude", "context-mode", "sessionstart-debug.log"),
144
+ pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
145
145
  `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
146
146
  );
147
147
  } catch { /* ignore logging failure */ }
@@ -10,17 +10,18 @@ import "./ensure-deps.mjs";
10
10
  * Must be fast (<10ms). Just a single SQLite write.
11
11
  */
12
12
 
13
- import { readStdin, getSessionId, getSessionDBPath } from "./session-helpers.mjs";
14
- import { createSessionLoaders } from "./session-loaders.mjs";
13
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
14
+ import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
15
15
  import { dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
 
18
18
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
19
- const { loadSessionDB, loadExtract } = createSessionLoaders(HOOK_DIR);
19
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
20
20
 
21
21
  try {
22
22
  const raw = await readStdin();
23
- const input = JSON.parse(raw);
23
+ const input = parseStdin(raw);
24
+ const projectDir = getInputProjectDir(input);
24
25
 
25
26
  const prompt = input.prompt ?? input.message ?? "";
26
27
  const trimmed = (prompt || "").trim();
@@ -34,24 +35,40 @@ try {
34
35
  if (trimmed.length > 0 && !isSystemMessage) {
35
36
  const { SessionDB } = await loadSessionDB();
36
37
  const { extractUserEvents } = await loadExtract();
38
+ const { resolveProjectAttributions } = await loadProjectAttribution();
37
39
  const dbPath = getSessionDBPath();
38
40
  const db = new SessionDB({ dbPath });
39
41
  const sessionId = getSessionId(input);
40
42
 
41
- db.ensureSession(sessionId, process.env.CLAUDE_PROJECT_DIR || process.cwd());
43
+ db.ensureSession(sessionId, projectDir);
42
44
 
43
45
  // 1. Always save the raw prompt
44
- db.insertEvent(sessionId, {
46
+ const promptEvent = {
45
47
  type: "user_prompt",
46
48
  category: "prompt",
47
49
  data: prompt,
48
50
  priority: 1,
49
- }, "UserPromptSubmit");
51
+ };
52
+ const promptAttributions = attributeAndInsertEvents(
53
+ db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
54
+ );
50
55
 
51
56
  // 2. Extract decision/role/intent/data from user message
52
57
  const userEvents = extractUserEvents(trimmed);
53
- for (const ev of userEvents) {
54
- db.insertEvent(sessionId, ev, "UserPromptSubmit");
58
+ // Feed lastKnownProjectDir from the first attribution into the second batch
59
+ const savedLastKnown = promptAttributions[0]?.projectDir || null;
60
+ const sessionStats = db.getSessionStats(sessionId);
61
+ const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
62
+ ? db.getLatestAttributedProjectDir(sessionId)
63
+ : null;
64
+ const userAttributions = resolveProjectAttributions(userEvents, {
65
+ sessionOriginDir: sessionStats?.project_dir || projectDir,
66
+ inputProjectDir: projectDir,
67
+ workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
68
+ lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
69
+ });
70
+ for (let i = 0; i < userEvents.length; i++) {
71
+ db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
55
72
  }
56
73
 
57
74
  db.close();
@@ -10,32 +10,34 @@ import "../ensure-deps.mjs";
10
10
  * Must be fast (<20ms). No network, no LLM, just SQLite writes.
11
11
  */
12
12
 
13
- import { createSessionLoaders } from "../session-loaders.mjs";
14
- import { readStdin, getSessionId, getSessionDBPath, getProjectDir, VSCODE_OPTS } from "../session-helpers.mjs";
13
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
14
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, VSCODE_OPTS } from "../session-helpers.mjs";
15
15
  import { appendFileSync } from "node:fs";
16
16
  import { join, dirname } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { homedir } from "node:os";
19
19
 
20
20
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
21
- const { loadSessionDB, loadExtract } = createSessionLoaders(HOOK_DIR);
21
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
22
22
  const OPTS = VSCODE_OPTS;
23
23
  const DEBUG_LOG = join(homedir(), ".vscode", "context-mode", "posttooluse-debug.log");
24
24
 
25
25
  try {
26
26
  const raw = await readStdin();
27
- const input = JSON.parse(raw);
27
+ const input = parseStdin(raw);
28
+ const projectDir = getInputProjectDir(input, OPTS);
28
29
 
29
30
  appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] CALL: ${input.tool_name}\n`);
30
31
 
31
32
  const { extractEvents } = await loadExtract();
33
+ const { resolveProjectAttributions } = await loadProjectAttribution();
32
34
  const { SessionDB } = await loadSessionDB();
33
35
 
34
36
  const dbPath = getSessionDBPath(OPTS);
35
37
  const db = new SessionDB({ dbPath });
36
38
  const sessionId = getSessionId(input, OPTS);
37
39
 
38
- db.ensureSession(sessionId, getProjectDir(OPTS));
40
+ db.ensureSession(sessionId, projectDir);
39
41
 
40
42
  const events = extractEvents({
41
43
  tool_name: input.tool_name,
@@ -46,9 +48,7 @@ try {
46
48
  tool_output: input.tool_output,
47
49
  });
48
50
 
49
- for (const event of events) {
50
- db.insertEvent(sessionId, event, "PostToolUse");
51
- }
51
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
52
52
 
53
53
  appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] OK: ${input.tool_name} → ${events.length} events\n`);
54
54
  db.close();
@@ -10,7 +10,7 @@ import "../ensure-deps.mjs";
10
10
  */
11
11
 
12
12
  import { createSessionLoaders } from "../session-loaders.mjs";
13
- import { readStdin, getSessionId, getSessionDBPath, VSCODE_OPTS } from "../session-helpers.mjs";
13
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, VSCODE_OPTS } from "../session-helpers.mjs";
14
14
  import { appendFileSync } from "node:fs";
15
15
  import { join, dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
@@ -23,7 +23,7 @@ const DEBUG_LOG = join(homedir(), ".vscode", "context-mode", "precompact-debug.l
23
23
 
24
24
  try {
25
25
  const raw = await readStdin();
26
- const input = JSON.parse(raw);
26
+ const input = parseStdin(raw);
27
27
 
28
28
  const { buildResumeSnapshot } = await loadSnapshot();
29
29
  const { SessionDB } = await loadSessionDB();