memorix 0.2.3 → 0.3.0

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.
package/README.md CHANGED
@@ -44,8 +44,26 @@ Memorix stores and indexes project knowledge (architecture decisions, bug fixes,
44
44
  - **Rules Parser**: 4 format adapters (Cursor `.mdc`, Claude Code `CLAUDE.md`, Codex `SKILL.md`, Windsurf `.windsurfrules`)
45
45
  - **Rules Syncer**: Scan → Deduplicate → Conflict detection → Cross-format generation
46
46
  - **Workspace Sync**: MCP config migration + workflow sync across agents
47
+ - **Skills Sync**: Scan `.codex/skills/`, `.cursor/skills/`, `.windsurf/skills/`, `.claude/skills/` → copy entire skill folders across agents (no format conversion needed — SKILL.md is a universal standard)
47
48
  - **Apply with Safety**: Backup → Atomic write → Auto-rollback on failure
48
49
 
50
+ ### P3 — Auto-Memory Hooks
51
+
52
+ - **Hook Events**: `user_prompt`, `post_response`, `post_edit`, `post_command`, `post_tool`, `session_end`
53
+ - **Agent Normalizer**: Maps Windsurf/Cursor/Claude/Codex native events to unified hook events
54
+ - **Pattern Detection**: Auto-detects decisions, errors, gotchas, configurations, learnings, implementations
55
+ - **Cooldown Filtering**: Prevents duplicate storage within configurable time windows
56
+ - **Noise Filtering**: Skips trivial commands (`ls`, `cat`, `pwd`, etc.)
57
+ - **Agent Rules**: Auto-installs `.windsurf/rules/memorix.md` (or equivalent) to guide agents in proactive memory management
58
+ - **One-Command Install**: `memorix hooks install` sets up hooks + rules for your agent
59
+
60
+ ### Context Continuity
61
+
62
+ - **Session Start**: Agent rules instruct AI to search memories before responding
63
+ - **During Session**: Auto-capture decisions, bugs, gotchas via hooks + agent-driven `memorix_store`
64
+ - **Session End**: Agent stores a "handoff note" summarizing progress and next steps
65
+ - **Result**: Start a new session and your AI already knows everything — no re-explaining needed
66
+
49
67
  ### P5 — Intelligence (Competitor-Inspired)
50
68
 
51
69
  - **Access Tracking**: `accessCount` + `lastAccessedAt` on every search hit (from mcp-memory-service)
@@ -115,7 +133,7 @@ npm install memorix
115
133
  | `memorix_detail` | L3 | Full observation details | ~500-1000/result |
116
134
  | `memorix_retention` | Analytics | Memory decay & retention status | — |
117
135
  | `memorix_rules_sync` | Rules | Scan, dedup, convert rules across agents | — |
118
- | `memorix_workspace_sync` | Workspace | Scan/migrate MCP configs across agents | — |
136
+ | `memorix_workspace_sync` | Workspace | Scan/migrate MCP configs, workflows, and skills across agents | — |
119
137
 
120
138
  #### MCP Official Compatible
121
139
 
@@ -158,9 +176,16 @@ npm install memorix
158
176
  │ └─────────────────────────────────────┘ │
159
177
  │ │
160
178
  │ ┌────────────────────────────────────┐ │
161
- │ │ Rules Syncer │ │
179
+ │ │ Rules & Skills Syncer │ │
162
180
  │ │ Cursor│Claude│Codex│Windsurf │ │
163
- │ │ scan dedup conflict gen │ │
181
+ │ │ rules: scan→dedup→conflict→gen │ │
182
+ │ │ skills: scan→copy (no convert) │ │
183
+ │ └────────────────────────────────────┘ │
184
+ │ │
185
+ │ ┌────────────────────────────────────┐ │
186
+ │ │ Auto-Memory Hooks │ │
187
+ │ │ normalize→detect→filter→store │ │
188
+ │ │ + agent rules (context cont.) │ │
164
189
  │ └────────────────────────────────────┘ │
165
190
  └──────────────────────────────────────────────┘
166
191
  ```
@@ -179,7 +204,7 @@ npm install memorix
179
204
  | Entity extraction | Regex patterns | MemCP |
180
205
  | Rule parsing | `gray-matter` | — |
181
206
  | Build | `tsup` | — |
182
- | Test | `vitest` | 195 tests |
207
+ | Test | `vitest` | 219 tests |
183
208
 
184
209
  ## Optional: Enable Vector Search
185
210
 
@@ -200,7 +225,7 @@ npm install
200
225
  # Build
201
226
  npm run build
202
227
 
203
- # Run tests (195 tests)
228
+ # Run tests (219 tests)
204
229
  npm test
205
230
 
206
231
  # Type check
package/dist/cli/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
4
10
  var __esm = (fn, res) => function __init() {
5
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
12
  };
@@ -12,9 +18,13 @@ var __export = (target, all) => {
12
18
  // node_modules/tsup/assets/esm_shims.js
13
19
  import path from "path";
14
20
  import { fileURLToPath } from "url";
21
+ var getFilename, getDirname, __dirname;
15
22
  var init_esm_shims = __esm({
16
23
  "node_modules/tsup/assets/esm_shims.js"() {
17
24
  "use strict";
25
+ getFilename = () => fileURLToPath(import.meta.url);
26
+ getDirname = () => path.dirname(getFilename());
27
+ __dirname = /* @__PURE__ */ getDirname();
18
28
  }
19
29
  });
20
30
 
@@ -2236,8 +2246,9 @@ var init_applier = __esm({
2236
2246
  });
2237
2247
 
2238
2248
  // src/workspace/engine.ts
2239
- import { readFileSync, readdirSync, existsSync as existsSync3 } from "fs";
2249
+ import { readFileSync, readdirSync, existsSync as existsSync3, cpSync, mkdirSync as mkdirSync2 } from "fs";
2240
2250
  import { join as join6 } from "path";
2251
+ import { homedir as homedir5 } from "os";
2241
2252
  var WorkspaceSyncEngine;
2242
2253
  var init_engine2 = __esm({
2243
2254
  "src/workspace/engine.ts"() {
@@ -2251,7 +2262,7 @@ var init_engine2 = __esm({
2251
2262
  init_syncer();
2252
2263
  init_sanitizer();
2253
2264
  init_applier();
2254
- WorkspaceSyncEngine = class {
2265
+ WorkspaceSyncEngine = class _WorkspaceSyncEngine {
2255
2266
  constructor(projectRoot) {
2256
2267
  this.projectRoot = projectRoot;
2257
2268
  this.adapters = /* @__PURE__ */ new Map([
@@ -2300,7 +2311,8 @@ var init_engine2 = __esm({
2300
2311
  rulesCount = rules.length;
2301
2312
  } catch {
2302
2313
  }
2303
- return { mcpConfigs, workflows, rulesCount };
2314
+ const skills = this.scanSkills();
2315
+ return { mcpConfigs, workflows, rulesCount, skills };
2304
2316
  }
2305
2317
  /**
2306
2318
  * Migrate workspace configs to a target agent format.
@@ -2310,7 +2322,8 @@ var init_engine2 = __esm({
2310
2322
  const result = {
2311
2323
  mcpServers: { scanned: [], generated: [] },
2312
2324
  workflows: { scanned: [], generated: [] },
2313
- rules: { scanned: 0, generated: 0 }
2325
+ rules: { scanned: 0, generated: 0 },
2326
+ skills: { scanned: [], copied: [] }
2314
2327
  };
2315
2328
  const allServers = /* @__PURE__ */ new Map();
2316
2329
  for (const servers of Object.values(scan.mcpConfigs)) {
@@ -2347,9 +2360,85 @@ var init_engine2 = __esm({
2347
2360
  }
2348
2361
  } catch {
2349
2362
  }
2363
+ result.skills.scanned = scan.skills;
2350
2364
  return result;
2351
2365
  }
2352
2366
  // ---- Private helpers ----
2367
+ /** Skills directories per agent */
2368
+ static SKILLS_DIRS = {
2369
+ codex: [".codex/skills", ".agents/skills"],
2370
+ cursor: [".cursor/skills", ".cursor/skills-cursor"],
2371
+ windsurf: [".windsurf/skills"],
2372
+ "claude-code": [".claude/skills"]
2373
+ };
2374
+ /** Get the target skills directory for an agent */
2375
+ getTargetSkillsDir(target) {
2376
+ const dirs = _WorkspaceSyncEngine.SKILLS_DIRS[target];
2377
+ return join6(this.projectRoot, dirs[0]);
2378
+ }
2379
+ /**
2380
+ * Scan all agent skills directories and collect unique skills.
2381
+ */
2382
+ scanSkills() {
2383
+ const skills = [];
2384
+ const seen = /* @__PURE__ */ new Set();
2385
+ const home = homedir5();
2386
+ for (const [agent, dirs] of Object.entries(_WorkspaceSyncEngine.SKILLS_DIRS)) {
2387
+ for (const dir of dirs) {
2388
+ const paths = [
2389
+ join6(this.projectRoot, dir),
2390
+ join6(home, dir)
2391
+ ];
2392
+ for (const skillsRoot of paths) {
2393
+ if (!existsSync3(skillsRoot)) continue;
2394
+ try {
2395
+ const entries = readdirSync(skillsRoot, { withFileTypes: true });
2396
+ for (const entry of entries) {
2397
+ if (!entry.isDirectory()) continue;
2398
+ if (seen.has(entry.name)) continue;
2399
+ const skillMd = join6(skillsRoot, entry.name, "SKILL.md");
2400
+ if (!existsSync3(skillMd)) continue;
2401
+ let description = "";
2402
+ try {
2403
+ const content = readFileSync(skillMd, "utf-8");
2404
+ const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
2405
+ if (match) description = match[1];
2406
+ } catch {
2407
+ }
2408
+ seen.add(entry.name);
2409
+ skills.push({
2410
+ name: entry.name,
2411
+ description,
2412
+ sourcePath: join6(skillsRoot, entry.name),
2413
+ sourceAgent: agent
2414
+ });
2415
+ }
2416
+ } catch {
2417
+ }
2418
+ }
2419
+ }
2420
+ }
2421
+ return skills;
2422
+ }
2423
+ /**
2424
+ * Copy skills to a target agent's skills directory.
2425
+ * Returns list of copied skill names.
2426
+ */
2427
+ copySkills(skills, target) {
2428
+ const targetDir = this.getTargetSkillsDir(target);
2429
+ const copied = [];
2430
+ for (const skill of skills) {
2431
+ const dest = join6(targetDir, skill.name);
2432
+ if (existsSync3(dest)) continue;
2433
+ try {
2434
+ mkdirSync2(targetDir, { recursive: true });
2435
+ cpSync(skill.sourcePath, dest, { recursive: true });
2436
+ copied.push(skill.name);
2437
+ } catch {
2438
+ }
2439
+ }
2440
+ return copied;
2441
+ }
2353
2442
  scanWorkflows() {
2354
2443
  const workflows = [];
2355
2444
  const wfDir = join6(this.projectRoot, ".windsurf", "workflows");
@@ -2384,12 +2473,23 @@ var init_engine2 = __esm({
2384
2473
  ...syncResult.workflows.generated
2385
2474
  ];
2386
2475
  const applyResult = await applier.apply(filesToWrite);
2476
+ let copiedSkills = [];
2477
+ if (syncResult.skills.scanned.length > 0) {
2478
+ copiedSkills = this.copySkills(syncResult.skills.scanned, target);
2479
+ }
2387
2480
  const lines = [];
2388
2481
  if (applyResult.success) {
2389
2482
  lines.push(`\u2705 Applied ${applyResult.filesWritten.length} file(s) for ${target}`);
2390
2483
  for (const f of applyResult.filesWritten) {
2391
2484
  lines.push(` \u2192 ${f}`);
2392
2485
  }
2486
+ if (copiedSkills.length > 0) {
2487
+ lines.push(`
2488
+ \u{1F9E9} Copied ${copiedSkills.length} skill(s):`);
2489
+ for (const sk of copiedSkills) {
2490
+ lines.push(` \u2192 ${sk}`);
2491
+ }
2492
+ }
2393
2493
  if (applyResult.backups.length > 0) {
2394
2494
  lines.push(`
2395
2495
  \u{1F4E6} Backups created (${applyResult.backups.length}):`);
@@ -2438,10 +2538,33 @@ __export(installers_exports, {
2438
2538
  import * as fs3 from "fs/promises";
2439
2539
  import * as path5 from "path";
2440
2540
  import * as os2 from "os";
2541
+ import { createRequire } from "module";
2542
+ function resolveHookCommand() {
2543
+ if (process.platform === "win32") {
2544
+ try {
2545
+ const devPath = path5.resolve(import.meta.dirname ?? __dirname, "../../cli/index.js");
2546
+ try {
2547
+ const fsStat = __require("fs");
2548
+ if (fsStat.existsSync(devPath)) {
2549
+ return `node ${devPath.replace(/\\/g, "/")}`;
2550
+ }
2551
+ } catch {
2552
+ }
2553
+ const require_ = createRequire(import.meta.url);
2554
+ const pkgPath = require_.resolve("memorix/package.json");
2555
+ const cliPath = path5.join(path5.dirname(pkgPath), "dist", "cli", "index.js");
2556
+ return `node ${cliPath.replace(/\\/g, "/")}`;
2557
+ } catch {
2558
+ return "memorix";
2559
+ }
2560
+ }
2561
+ return "memorix";
2562
+ }
2441
2563
  function generateClaudeConfig() {
2564
+ const cmd = `${resolveHookCommand()} hook`;
2442
2565
  const hookEntry = {
2443
2566
  type: "command",
2444
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`,
2567
+ command: cmd,
2445
2568
  timeout: 10
2446
2569
  };
2447
2570
  return {
@@ -2455,9 +2578,10 @@ function generateClaudeConfig() {
2455
2578
  };
2456
2579
  }
2457
2580
  function generateWindsurfConfig() {
2581
+ const cmd = `${resolveHookCommand()} hook`;
2458
2582
  const hookEntry = {
2459
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`,
2460
- timeout: 10
2583
+ command: cmd,
2584
+ show_output: false
2461
2585
  };
2462
2586
  return {
2463
2587
  hooks: {
@@ -2470,16 +2594,17 @@ function generateWindsurfConfig() {
2470
2594
  };
2471
2595
  }
2472
2596
  function generateCursorConfig() {
2597
+ const cmd = `${resolveHookCommand()} hook`;
2473
2598
  return {
2474
2599
  hooks: {
2475
2600
  beforeSubmitPrompt: {
2476
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2601
+ command: cmd
2477
2602
  },
2478
2603
  afterFileEdit: {
2479
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2604
+ command: cmd
2480
2605
  },
2481
2606
  stop: {
2482
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2607
+ command: cmd
2483
2608
  }
2484
2609
  }
2485
2610
  };
@@ -2495,7 +2620,7 @@ filePattern: "**/*"
2495
2620
  Run the memorix hook command to analyze changes and store relevant memories:
2496
2621
 
2497
2622
  \`\`\`bash
2498
- memorix hook
2623
+ ${resolveHookCommand()} hook
2499
2624
  \`\`\`
2500
2625
  `;
2501
2626
  }
@@ -2619,6 +2744,7 @@ async function installHooks(agent, projectRoot, global = false) {
2619
2744
  events.push("post_edit");
2620
2745
  break;
2621
2746
  }
2747
+ await installAgentRules(agent, projectRoot);
2622
2748
  return {
2623
2749
  agent,
2624
2750
  configPath,
@@ -2626,6 +2752,79 @@ async function installHooks(agent, projectRoot, global = false) {
2626
2752
  generated: typeof generated === "string" ? { content: generated } : generated
2627
2753
  };
2628
2754
  }
2755
+ async function installAgentRules(agent, projectRoot) {
2756
+ const rulesContent = getAgentRulesContent();
2757
+ let rulesPath;
2758
+ switch (agent) {
2759
+ case "windsurf":
2760
+ rulesPath = path5.join(projectRoot, ".windsurf", "rules", "memorix.md");
2761
+ break;
2762
+ case "cursor":
2763
+ rulesPath = path5.join(projectRoot, ".cursor", "rules", "memorix.mdc");
2764
+ break;
2765
+ case "claude":
2766
+ case "copilot":
2767
+ rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
2768
+ break;
2769
+ default:
2770
+ return;
2771
+ }
2772
+ try {
2773
+ await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
2774
+ try {
2775
+ await fs3.access(rulesPath);
2776
+ } catch {
2777
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
2778
+ }
2779
+ } catch {
2780
+ }
2781
+ }
2782
+ function getAgentRulesContent() {
2783
+ return `# Memorix \u2014 Automatic Memory Rules
2784
+
2785
+ You have access to Memorix memory tools. Follow these rules to maintain persistent context across sessions.
2786
+
2787
+ ## Session Start \u2014 Load Context
2788
+
2789
+ At the **beginning of every conversation**, before responding to the user:
2790
+
2791
+ 1. Call \`memorix_search\` with query related to the user's first message or the current project
2792
+ 2. If results are found, use them to understand the current project state, recent decisions, and pending tasks
2793
+ 3. Reference relevant memories naturally in your response
2794
+
2795
+ This ensures you already know the project context without the user re-explaining.
2796
+
2797
+ ## During Session \u2014 Capture Important Context
2798
+
2799
+ Proactively call \`memorix_store\` when any of the following happen:
2800
+
2801
+ - **Architecture decision**: You or the user decide on a technology, pattern, or approach
2802
+ - **Bug fix**: A bug is identified and resolved \u2014 store the root cause and fix
2803
+ - **Gotcha/pitfall**: Something unexpected or tricky is discovered
2804
+ - **Configuration change**: Environment, port, path, or tooling changes
2805
+
2806
+ Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`.
2807
+
2808
+ ## Session End \u2014 Store Summary
2809
+
2810
+ When the conversation is ending or the user says goodbye:
2811
+
2812
+ 1. Call \`memorix_store\` with type \`session-request\` to record:
2813
+ - What was accomplished in this session
2814
+ - Current project state
2815
+ - Pending tasks or next steps
2816
+ - Any unresolved issues
2817
+
2818
+ This creates a "handoff note" for the next session.
2819
+
2820
+ ## Guidelines
2821
+
2822
+ - **Don't store trivial information** (greetings, acknowledgments, simple file reads)
2823
+ - **Do store anything you'd want to know if you lost all context**
2824
+ - **Use concise titles** and structured facts
2825
+ - **Include file paths** in filesModified when relevant
2826
+ `;
2827
+ }
2629
2828
  async function uninstallHooks(agent, projectRoot, global = false) {
2630
2829
  const configPath = global ? getGlobalConfigPath(agent) : getProjectConfigPath(agent, projectRoot);
2631
2830
  try {
@@ -2669,13 +2868,10 @@ async function getHookStatus(projectRoot) {
2669
2868
  }
2670
2869
  return results;
2671
2870
  }
2672
- var HOOK_COMMAND, HOOK_ARGS;
2673
2871
  var init_installers = __esm({
2674
2872
  "src/hooks/installers/index.ts"() {
2675
2873
  "use strict";
2676
2874
  init_esm_shims();
2677
- HOOK_COMMAND = "memorix";
2678
- HOOK_ARGS = ["hook"];
2679
2875
  }
2680
2876
  });
2681
2877
 
@@ -3271,7 +3467,7 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3271
3467
  "memorix_workspace_sync",
3272
3468
  {
3273
3469
  title: "Workspace Sync",
3274
- description: 'Migrate your entire workspace environment between AI agents. Syncs MCP server configs, workflows, and rules. Action "scan": detect all workspace configs. Action "migrate": generate configs for target agent (preview only). Action "apply": migrate AND write configs to disk with backup/rollback.',
3470
+ description: 'Migrate your entire workspace environment between AI agents. Syncs MCP server configs, workflows, rules, and skills. Action "scan": detect all workspace configs. Action "migrate": generate configs for target agent (preview only). Action "apply": migrate AND write configs to disk with backup/rollback.',
3275
3471
  inputSchema: {
3276
3472
  action: z.enum(["scan", "migrate", "apply"]).describe('Action: "scan" to detect configs, "migrate" to preview, "apply" to write to disk'),
3277
3473
  target: z.enum(AGENT_TARGETS).optional().describe("Target agent for migration (required for migrate)")
@@ -3301,6 +3497,14 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3301
3497
  }
3302
3498
  lines2.push("", `### Rules`);
3303
3499
  lines2.push(`- ${scan.rulesCount} rule(s) detected across all agents`);
3500
+ lines2.push("", `### Skills`);
3501
+ if (scan.skills.length > 0) {
3502
+ for (const sk of scan.skills) {
3503
+ lines2.push(`- **${sk.name}** (${sk.sourceAgent}): ${sk.description || "(no description)"}`);
3504
+ }
3505
+ } else {
3506
+ lines2.push("- No skills found");
3507
+ }
3304
3508
  return {
3305
3509
  content: [{ type: "text", text: lines2.join("\n") }]
3306
3510
  };
@@ -3338,6 +3542,12 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3338
3542
  if (result.rules.generated > 0) {
3339
3543
  lines.push(`### Rules`, `- ${result.rules.generated} rule file(s) generated`);
3340
3544
  }
3545
+ if (result.skills.scanned.length > 0) {
3546
+ lines.push("### Skills", `- ${result.skills.scanned.length} skill(s) found, ready to copy:`);
3547
+ for (const sk of result.skills.scanned) {
3548
+ lines.push(` - **${sk.name}** (from ${sk.sourceAgent})`);
3549
+ }
3550
+ }
3341
3551
  lines.push("", '> Review the generated configs above. Use action "apply" to write them to disk.');
3342
3552
  return {
3343
3553
  content: [{ type: "text", text: lines.join("\n") }]
@@ -3844,9 +4054,10 @@ var init_pattern_detector = __esm({
3844
4054
  {
3845
4055
  type: "configuration",
3846
4056
  keywords: [
3847
- /\b(config(ured?|uration)?|setting|environment|env\b|\.env|path|port)\b/i,
3848
- /(配置|环境|路径|端口|设置|安装)/,
3849
- /\b(gradle|webpack|vite|tsconfig|package\.json|docker)\b/i
4057
+ /\b(config(ured?|uration)?|setting|environment|\.env)\b/i,
4058
+ /(配置|环境变量|端口配置|设置项|安装配置)/,
4059
+ /\b(gradle|webpack|vite|tsconfig|package\.json|docker|nginx)\b/i,
4060
+ /\b(port\s*[:=]|listen\s+\d|bind\s+\d|DATABASE_URL|API_KEY)\b/i
3850
4061
  ],
3851
4062
  minLength: 80,
3852
4063
  baseConfidence: 0.7
@@ -3974,24 +4185,75 @@ async function handleHookEvent(input) {
3974
4185
  observation: buildObservation(input, extractContent(input)),
3975
4186
  output: defaultOutput
3976
4187
  };
3977
- case "post_edit":
3978
- case "post_command":
3979
- case "post_tool":
4188
+ case "post_edit": {
4189
+ const editKey = `post_edit:${input.filePath ?? "general"}`;
4190
+ if (isInCooldown(editKey)) {
4191
+ return { observation: null, output: defaultOutput };
4192
+ }
4193
+ const editContent = extractContent(input);
4194
+ if (editContent.length < MIN_EDIT_LENGTH) {
4195
+ return { observation: null, output: defaultOutput };
4196
+ }
4197
+ const editPattern = detectBestPattern(editContent, 0.6);
4198
+ if (!editPattern) {
4199
+ return { observation: null, output: defaultOutput };
4200
+ }
4201
+ markTriggered(editKey);
4202
+ return {
4203
+ observation: buildObservation(input, editContent),
4204
+ output: defaultOutput
4205
+ };
4206
+ }
4207
+ case "post_command": {
4208
+ if (input.command && NOISE_COMMANDS.some((r) => r.test(input.command))) {
4209
+ return { observation: null, output: defaultOutput };
4210
+ }
4211
+ const cmdKey = `post_command:${input.command ?? "general"}`;
4212
+ if (isInCooldown(cmdKey)) {
4213
+ return { observation: null, output: defaultOutput };
4214
+ }
4215
+ const cmdContent = input.commandOutput || extractContent(input);
4216
+ if (cmdContent.length < MIN_STORE_LENGTH) {
4217
+ return { observation: null, output: defaultOutput };
4218
+ }
4219
+ detectBestPattern(cmdContent);
4220
+ markTriggered(cmdKey);
4221
+ return {
4222
+ observation: buildObservation(input, cmdContent),
4223
+ output: defaultOutput
4224
+ };
4225
+ }
4226
+ case "post_tool": {
4227
+ const toolKey = `post_tool:${input.toolName ?? "general"}`;
4228
+ if (isInCooldown(toolKey)) {
4229
+ return { observation: null, output: defaultOutput };
4230
+ }
4231
+ const toolContent = extractContent(input);
4232
+ if (toolContent.length < MIN_STORE_LENGTH) {
4233
+ return { observation: null, output: defaultOutput };
4234
+ }
4235
+ const toolPattern = detectBestPattern(toolContent);
4236
+ if (!toolPattern) {
4237
+ return { observation: null, output: defaultOutput };
4238
+ }
4239
+ markTriggered(toolKey);
4240
+ return {
4241
+ observation: buildObservation(input, toolContent),
4242
+ output: defaultOutput
4243
+ };
4244
+ }
3980
4245
  case "post_response":
3981
4246
  case "user_prompt": {
3982
- const cooldownKey = `${input.event}:${input.filePath ?? input.command ?? "general"}`;
3983
- if (isInCooldown(cooldownKey)) {
4247
+ const promptKey = `${input.event}:${input.sessionId ?? "general"}`;
4248
+ if (isInCooldown(promptKey)) {
3984
4249
  return { observation: null, output: defaultOutput };
3985
4250
  }
3986
4251
  const content = extractContent(input);
3987
4252
  if (content.length < MIN_STORE_LENGTH) {
3988
4253
  return { observation: null, output: defaultOutput };
3989
4254
  }
3990
- const pattern = detectBestPattern(content);
3991
- if (!pattern) {
3992
- return { observation: null, output: defaultOutput };
3993
- }
3994
- markTriggered(cooldownKey);
4255
+ detectBestPattern(content);
4256
+ markTriggered(promptKey);
3995
4257
  return {
3996
4258
  observation: buildObservation(input, content),
3997
4259
  output: defaultOutput
@@ -4034,7 +4296,7 @@ async function runHook() {
4034
4296
  }
4035
4297
  process.stdout.write(JSON.stringify(output));
4036
4298
  }
4037
- var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MAX_CONTENT_LENGTH;
4299
+ var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MIN_EDIT_LENGTH, NOISE_COMMANDS, MAX_CONTENT_LENGTH;
4038
4300
  var init_handler = __esm({
4039
4301
  "src/hooks/handler.ts"() {
4040
4302
  "use strict";
@@ -4044,6 +4306,14 @@ var init_handler = __esm({
4044
4306
  cooldowns = /* @__PURE__ */ new Map();
4045
4307
  COOLDOWN_MS = 3e4;
4046
4308
  MIN_STORE_LENGTH = 100;
4309
+ MIN_EDIT_LENGTH = 30;
4310
+ NOISE_COMMANDS = [
4311
+ /^(ls|dir|cd|pwd|echo|cat|type|head|tail|wc|find|which|where|whoami)\b/i,
4312
+ /^(Get-Content|Test-Path|Get-Item|Get-ChildItem|Set-Location|Write-Host)\b/i,
4313
+ /^(Start-Sleep|Select-String|Select-Object|Format-Table|Measure-Object)\b/i,
4314
+ /^(mkdir|rm|cp|mv|touch|chmod|chown)\b/i,
4315
+ /^(node -[ep]|python -c)\b/i
4316
+ ];
4047
4317
  MAX_CONTENT_LENGTH = 4e3;
4048
4318
  }
4049
4319
  });