memorix 0.2.3 → 0.3.1

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, conflicts: skillConflicts } = this.scanSkills();
2315
+ return { mcpConfigs, workflows, rulesCount, skills, skillConflicts };
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: [], conflicts: [], copied: [], skipped: [] }
2314
2327
  };
2315
2328
  const allServers = /* @__PURE__ */ new Map();
2316
2329
  for (const servers of Object.values(scan.mcpConfigs)) {
@@ -2347,9 +2360,103 @@ var init_engine2 = __esm({
2347
2360
  }
2348
2361
  } catch {
2349
2362
  }
2363
+ result.skills.scanned = scan.skills;
2364
+ result.skills.conflicts = scan.skillConflicts;
2350
2365
  return result;
2351
2366
  }
2352
2367
  // ---- Private helpers ----
2368
+ /** Skills directories per agent */
2369
+ static SKILLS_DIRS = {
2370
+ codex: [".codex/skills", ".agents/skills"],
2371
+ cursor: [".cursor/skills", ".cursor/skills-cursor"],
2372
+ windsurf: [".windsurf/skills"],
2373
+ "claude-code": [".claude/skills"]
2374
+ };
2375
+ /** Get the target skills directory for an agent */
2376
+ getTargetSkillsDir(target) {
2377
+ const dirs = _WorkspaceSyncEngine.SKILLS_DIRS[target];
2378
+ return join6(this.projectRoot, dirs[0]);
2379
+ }
2380
+ /**
2381
+ * Scan all agent skills directories and collect unique skills.
2382
+ */
2383
+ scanSkills() {
2384
+ const skills = [];
2385
+ const conflicts = [];
2386
+ const seen = /* @__PURE__ */ new Map();
2387
+ const home = homedir5();
2388
+ for (const [agent, dirs] of Object.entries(_WorkspaceSyncEngine.SKILLS_DIRS)) {
2389
+ for (const dir of dirs) {
2390
+ const paths = [
2391
+ join6(this.projectRoot, dir),
2392
+ join6(home, dir)
2393
+ ];
2394
+ for (const skillsRoot of paths) {
2395
+ if (!existsSync3(skillsRoot)) continue;
2396
+ try {
2397
+ const entries = readdirSync(skillsRoot, { withFileTypes: true });
2398
+ for (const entry of entries) {
2399
+ if (!entry.isDirectory()) continue;
2400
+ const skillMd = join6(skillsRoot, entry.name, "SKILL.md");
2401
+ if (!existsSync3(skillMd)) continue;
2402
+ let description = "";
2403
+ try {
2404
+ const content = readFileSync(skillMd, "utf-8");
2405
+ const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
2406
+ if (match) description = match[1];
2407
+ } catch {
2408
+ }
2409
+ const newEntry = {
2410
+ name: entry.name,
2411
+ description,
2412
+ sourcePath: join6(skillsRoot, entry.name),
2413
+ sourceAgent: agent
2414
+ };
2415
+ const existing = seen.get(entry.name);
2416
+ if (existing) {
2417
+ if (existing.sourceAgent !== agent) {
2418
+ conflicts.push({
2419
+ name: entry.name,
2420
+ kept: existing,
2421
+ skipped: newEntry
2422
+ });
2423
+ }
2424
+ continue;
2425
+ }
2426
+ seen.set(entry.name, newEntry);
2427
+ skills.push(newEntry);
2428
+ }
2429
+ } catch {
2430
+ }
2431
+ }
2432
+ }
2433
+ }
2434
+ return { skills, conflicts };
2435
+ }
2436
+ /**
2437
+ * Copy skills to a target agent's skills directory.
2438
+ * Returns list of copied skill names.
2439
+ */
2440
+ copySkills(skills, target) {
2441
+ const targetDir = this.getTargetSkillsDir(target);
2442
+ const copied = [];
2443
+ const skipped = [];
2444
+ for (const skill of skills) {
2445
+ if (skill.sourceAgent === target) continue;
2446
+ const dest = join6(targetDir, skill.name);
2447
+ if (existsSync3(dest)) {
2448
+ skipped.push(`${skill.name} (already exists in ${target})`);
2449
+ continue;
2450
+ }
2451
+ try {
2452
+ mkdirSync2(targetDir, { recursive: true });
2453
+ cpSync(skill.sourcePath, dest, { recursive: true });
2454
+ copied.push(skill.name);
2455
+ } catch {
2456
+ }
2457
+ }
2458
+ return { copied, skipped };
2459
+ }
2353
2460
  scanWorkflows() {
2354
2461
  const workflows = [];
2355
2462
  const wfDir = join6(this.projectRoot, ".windsurf", "workflows");
@@ -2384,12 +2491,37 @@ var init_engine2 = __esm({
2384
2491
  ...syncResult.workflows.generated
2385
2492
  ];
2386
2493
  const applyResult = await applier.apply(filesToWrite);
2494
+ let skillResult = { copied: [], skipped: [] };
2495
+ if (syncResult.skills.scanned.length > 0) {
2496
+ skillResult = this.copySkills(syncResult.skills.scanned, target);
2497
+ }
2387
2498
  const lines = [];
2388
2499
  if (applyResult.success) {
2389
2500
  lines.push(`\u2705 Applied ${applyResult.filesWritten.length} file(s) for ${target}`);
2390
2501
  for (const f of applyResult.filesWritten) {
2391
2502
  lines.push(` \u2192 ${f}`);
2392
2503
  }
2504
+ if (skillResult.copied.length > 0) {
2505
+ lines.push(`
2506
+ \u{1F9E9} Copied ${skillResult.copied.length} skill(s):`);
2507
+ for (const sk of skillResult.copied) {
2508
+ lines.push(` \u2192 ${sk}`);
2509
+ }
2510
+ }
2511
+ if (skillResult.skipped.length > 0) {
2512
+ lines.push(`
2513
+ \u23ED\uFE0F Skipped ${skillResult.skipped.length} skill(s):`);
2514
+ for (const sk of skillResult.skipped) {
2515
+ lines.push(` \u2192 ${sk}`);
2516
+ }
2517
+ }
2518
+ if (syncResult.skills.conflicts.length > 0) {
2519
+ lines.push(`
2520
+ \u26A0\uFE0F Name conflicts (${syncResult.skills.conflicts.length}):`);
2521
+ for (const c of syncResult.skills.conflicts) {
2522
+ lines.push(` \u2192 "${c.name}": kept ${c.kept.sourceAgent}, skipped ${c.skipped.sourceAgent}`);
2523
+ }
2524
+ }
2393
2525
  if (applyResult.backups.length > 0) {
2394
2526
  lines.push(`
2395
2527
  \u{1F4E6} Backups created (${applyResult.backups.length}):`);
@@ -2438,10 +2570,33 @@ __export(installers_exports, {
2438
2570
  import * as fs3 from "fs/promises";
2439
2571
  import * as path5 from "path";
2440
2572
  import * as os2 from "os";
2573
+ import { createRequire } from "module";
2574
+ function resolveHookCommand() {
2575
+ if (process.platform === "win32") {
2576
+ try {
2577
+ const devPath = path5.resolve(import.meta.dirname ?? __dirname, "../../cli/index.js");
2578
+ try {
2579
+ const fsStat = __require("fs");
2580
+ if (fsStat.existsSync(devPath)) {
2581
+ return `node ${devPath.replace(/\\/g, "/")}`;
2582
+ }
2583
+ } catch {
2584
+ }
2585
+ const require_ = createRequire(import.meta.url);
2586
+ const pkgPath = require_.resolve("memorix/package.json");
2587
+ const cliPath = path5.join(path5.dirname(pkgPath), "dist", "cli", "index.js");
2588
+ return `node ${cliPath.replace(/\\/g, "/")}`;
2589
+ } catch {
2590
+ return "memorix";
2591
+ }
2592
+ }
2593
+ return "memorix";
2594
+ }
2441
2595
  function generateClaudeConfig() {
2596
+ const cmd = `${resolveHookCommand()} hook`;
2442
2597
  const hookEntry = {
2443
2598
  type: "command",
2444
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`,
2599
+ command: cmd,
2445
2600
  timeout: 10
2446
2601
  };
2447
2602
  return {
@@ -2455,9 +2610,10 @@ function generateClaudeConfig() {
2455
2610
  };
2456
2611
  }
2457
2612
  function generateWindsurfConfig() {
2613
+ const cmd = `${resolveHookCommand()} hook`;
2458
2614
  const hookEntry = {
2459
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`,
2460
- timeout: 10
2615
+ command: cmd,
2616
+ show_output: false
2461
2617
  };
2462
2618
  return {
2463
2619
  hooks: {
@@ -2470,16 +2626,17 @@ function generateWindsurfConfig() {
2470
2626
  };
2471
2627
  }
2472
2628
  function generateCursorConfig() {
2629
+ const cmd = `${resolveHookCommand()} hook`;
2473
2630
  return {
2474
2631
  hooks: {
2475
2632
  beforeSubmitPrompt: {
2476
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2633
+ command: cmd
2477
2634
  },
2478
2635
  afterFileEdit: {
2479
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2636
+ command: cmd
2480
2637
  },
2481
2638
  stop: {
2482
- command: `${HOOK_COMMAND} ${HOOK_ARGS.join(" ")}`
2639
+ command: cmd
2483
2640
  }
2484
2641
  }
2485
2642
  };
@@ -2495,7 +2652,7 @@ filePattern: "**/*"
2495
2652
  Run the memorix hook command to analyze changes and store relevant memories:
2496
2653
 
2497
2654
  \`\`\`bash
2498
- memorix hook
2655
+ ${resolveHookCommand()} hook
2499
2656
  \`\`\`
2500
2657
  `;
2501
2658
  }
@@ -2619,6 +2776,7 @@ async function installHooks(agent, projectRoot, global = false) {
2619
2776
  events.push("post_edit");
2620
2777
  break;
2621
2778
  }
2779
+ await installAgentRules(agent, projectRoot);
2622
2780
  return {
2623
2781
  agent,
2624
2782
  configPath,
@@ -2626,6 +2784,79 @@ async function installHooks(agent, projectRoot, global = false) {
2626
2784
  generated: typeof generated === "string" ? { content: generated } : generated
2627
2785
  };
2628
2786
  }
2787
+ async function installAgentRules(agent, projectRoot) {
2788
+ const rulesContent = getAgentRulesContent();
2789
+ let rulesPath;
2790
+ switch (agent) {
2791
+ case "windsurf":
2792
+ rulesPath = path5.join(projectRoot, ".windsurf", "rules", "memorix.md");
2793
+ break;
2794
+ case "cursor":
2795
+ rulesPath = path5.join(projectRoot, ".cursor", "rules", "memorix.mdc");
2796
+ break;
2797
+ case "claude":
2798
+ case "copilot":
2799
+ rulesPath = path5.join(projectRoot, ".github", "copilot-instructions.md");
2800
+ break;
2801
+ default:
2802
+ return;
2803
+ }
2804
+ try {
2805
+ await fs3.mkdir(path5.dirname(rulesPath), { recursive: true });
2806
+ try {
2807
+ await fs3.access(rulesPath);
2808
+ } catch {
2809
+ await fs3.writeFile(rulesPath, rulesContent, "utf-8");
2810
+ }
2811
+ } catch {
2812
+ }
2813
+ }
2814
+ function getAgentRulesContent() {
2815
+ return `# Memorix \u2014 Automatic Memory Rules
2816
+
2817
+ You have access to Memorix memory tools. Follow these rules to maintain persistent context across sessions.
2818
+
2819
+ ## Session Start \u2014 Load Context
2820
+
2821
+ At the **beginning of every conversation**, before responding to the user:
2822
+
2823
+ 1. Call \`memorix_search\` with query related to the user's first message or the current project
2824
+ 2. If results are found, use them to understand the current project state, recent decisions, and pending tasks
2825
+ 3. Reference relevant memories naturally in your response
2826
+
2827
+ This ensures you already know the project context without the user re-explaining.
2828
+
2829
+ ## During Session \u2014 Capture Important Context
2830
+
2831
+ Proactively call \`memorix_store\` when any of the following happen:
2832
+
2833
+ - **Architecture decision**: You or the user decide on a technology, pattern, or approach
2834
+ - **Bug fix**: A bug is identified and resolved \u2014 store the root cause and fix
2835
+ - **Gotcha/pitfall**: Something unexpected or tricky is discovered
2836
+ - **Configuration change**: Environment, port, path, or tooling changes
2837
+
2838
+ Use appropriate types: \`decision\`, \`problem-solution\`, \`gotcha\`, \`what-changed\`, \`discovery\`.
2839
+
2840
+ ## Session End \u2014 Store Summary
2841
+
2842
+ When the conversation is ending or the user says goodbye:
2843
+
2844
+ 1. Call \`memorix_store\` with type \`session-request\` to record:
2845
+ - What was accomplished in this session
2846
+ - Current project state
2847
+ - Pending tasks or next steps
2848
+ - Any unresolved issues
2849
+
2850
+ This creates a "handoff note" for the next session.
2851
+
2852
+ ## Guidelines
2853
+
2854
+ - **Don't store trivial information** (greetings, acknowledgments, simple file reads)
2855
+ - **Do store anything you'd want to know if you lost all context**
2856
+ - **Use concise titles** and structured facts
2857
+ - **Include file paths** in filesModified when relevant
2858
+ `;
2859
+ }
2629
2860
  async function uninstallHooks(agent, projectRoot, global = false) {
2630
2861
  const configPath = global ? getGlobalConfigPath(agent) : getProjectConfigPath(agent, projectRoot);
2631
2862
  try {
@@ -2669,13 +2900,10 @@ async function getHookStatus(projectRoot) {
2669
2900
  }
2670
2901
  return results;
2671
2902
  }
2672
- var HOOK_COMMAND, HOOK_ARGS;
2673
2903
  var init_installers = __esm({
2674
2904
  "src/hooks/installers/index.ts"() {
2675
2905
  "use strict";
2676
2906
  init_esm_shims();
2677
- HOOK_COMMAND = "memorix";
2678
- HOOK_ARGS = ["hook"];
2679
2907
  }
2680
2908
  });
2681
2909
 
@@ -3271,7 +3499,7 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3271
3499
  "memorix_workspace_sync",
3272
3500
  {
3273
3501
  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.',
3502
+ 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
3503
  inputSchema: {
3276
3504
  action: z.enum(["scan", "migrate", "apply"]).describe('Action: "scan" to detect configs, "migrate" to preview, "apply" to write to disk'),
3277
3505
  target: z.enum(AGENT_TARGETS).optional().describe("Target agent for migration (required for migrate)")
@@ -3301,6 +3529,20 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3301
3529
  }
3302
3530
  lines2.push("", `### Rules`);
3303
3531
  lines2.push(`- ${scan.rulesCount} rule(s) detected across all agents`);
3532
+ lines2.push("", `### Skills`);
3533
+ if (scan.skills.length > 0) {
3534
+ for (const sk of scan.skills) {
3535
+ lines2.push(`- **${sk.name}** (${sk.sourceAgent}): ${sk.description || "(no description)"}`);
3536
+ }
3537
+ } else {
3538
+ lines2.push("- No skills found");
3539
+ }
3540
+ if (scan.skillConflicts.length > 0) {
3541
+ lines2.push("", `### \u26A0\uFE0F Skill Name Conflicts`);
3542
+ for (const c of scan.skillConflicts) {
3543
+ lines2.push(`- **${c.name}**: kept from ${c.kept.sourceAgent}, duplicate in ${c.skipped.sourceAgent}`);
3544
+ }
3545
+ }
3304
3546
  return {
3305
3547
  content: [{ type: "text", text: lines2.join("\n") }]
3306
3548
  };
@@ -3338,6 +3580,12 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
3338
3580
  if (result.rules.generated > 0) {
3339
3581
  lines.push(`### Rules`, `- ${result.rules.generated} rule file(s) generated`);
3340
3582
  }
3583
+ if (result.skills.scanned.length > 0) {
3584
+ lines.push("### Skills", `- ${result.skills.scanned.length} skill(s) found, ready to copy:`);
3585
+ for (const sk of result.skills.scanned) {
3586
+ lines.push(` - **${sk.name}** (from ${sk.sourceAgent})`);
3587
+ }
3588
+ }
3341
3589
  lines.push("", '> Review the generated configs above. Use action "apply" to write them to disk.');
3342
3590
  return {
3343
3591
  content: [{ type: "text", text: lines.join("\n") }]
@@ -3844,9 +4092,10 @@ var init_pattern_detector = __esm({
3844
4092
  {
3845
4093
  type: "configuration",
3846
4094
  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
4095
+ /\b(config(ured?|uration)?|setting|environment|\.env)\b/i,
4096
+ /(配置|环境变量|端口配置|设置项|安装配置)/,
4097
+ /\b(gradle|webpack|vite|tsconfig|package\.json|docker|nginx)\b/i,
4098
+ /\b(port\s*[:=]|listen\s+\d|bind\s+\d|DATABASE_URL|API_KEY)\b/i
3850
4099
  ],
3851
4100
  minLength: 80,
3852
4101
  baseConfidence: 0.7
@@ -3974,24 +4223,75 @@ async function handleHookEvent(input) {
3974
4223
  observation: buildObservation(input, extractContent(input)),
3975
4224
  output: defaultOutput
3976
4225
  };
3977
- case "post_edit":
3978
- case "post_command":
3979
- case "post_tool":
4226
+ case "post_edit": {
4227
+ const editKey = `post_edit:${input.filePath ?? "general"}`;
4228
+ if (isInCooldown(editKey)) {
4229
+ return { observation: null, output: defaultOutput };
4230
+ }
4231
+ const editContent = extractContent(input);
4232
+ if (editContent.length < MIN_EDIT_LENGTH) {
4233
+ return { observation: null, output: defaultOutput };
4234
+ }
4235
+ const editPattern = detectBestPattern(editContent, 0.6);
4236
+ if (!editPattern) {
4237
+ return { observation: null, output: defaultOutput };
4238
+ }
4239
+ markTriggered(editKey);
4240
+ return {
4241
+ observation: buildObservation(input, editContent),
4242
+ output: defaultOutput
4243
+ };
4244
+ }
4245
+ case "post_command": {
4246
+ if (input.command && NOISE_COMMANDS.some((r) => r.test(input.command))) {
4247
+ return { observation: null, output: defaultOutput };
4248
+ }
4249
+ const cmdKey = `post_command:${input.command ?? "general"}`;
4250
+ if (isInCooldown(cmdKey)) {
4251
+ return { observation: null, output: defaultOutput };
4252
+ }
4253
+ const cmdContent = input.commandOutput || extractContent(input);
4254
+ if (cmdContent.length < MIN_STORE_LENGTH) {
4255
+ return { observation: null, output: defaultOutput };
4256
+ }
4257
+ detectBestPattern(cmdContent);
4258
+ markTriggered(cmdKey);
4259
+ return {
4260
+ observation: buildObservation(input, cmdContent),
4261
+ output: defaultOutput
4262
+ };
4263
+ }
4264
+ case "post_tool": {
4265
+ const toolKey = `post_tool:${input.toolName ?? "general"}`;
4266
+ if (isInCooldown(toolKey)) {
4267
+ return { observation: null, output: defaultOutput };
4268
+ }
4269
+ const toolContent = extractContent(input);
4270
+ if (toolContent.length < MIN_STORE_LENGTH) {
4271
+ return { observation: null, output: defaultOutput };
4272
+ }
4273
+ const toolPattern = detectBestPattern(toolContent);
4274
+ if (!toolPattern) {
4275
+ return { observation: null, output: defaultOutput };
4276
+ }
4277
+ markTriggered(toolKey);
4278
+ return {
4279
+ observation: buildObservation(input, toolContent),
4280
+ output: defaultOutput
4281
+ };
4282
+ }
3980
4283
  case "post_response":
3981
4284
  case "user_prompt": {
3982
- const cooldownKey = `${input.event}:${input.filePath ?? input.command ?? "general"}`;
3983
- if (isInCooldown(cooldownKey)) {
4285
+ const promptKey = `${input.event}:${input.sessionId ?? "general"}`;
4286
+ if (isInCooldown(promptKey)) {
3984
4287
  return { observation: null, output: defaultOutput };
3985
4288
  }
3986
4289
  const content = extractContent(input);
3987
4290
  if (content.length < MIN_STORE_LENGTH) {
3988
4291
  return { observation: null, output: defaultOutput };
3989
4292
  }
3990
- const pattern = detectBestPattern(content);
3991
- if (!pattern) {
3992
- return { observation: null, output: defaultOutput };
3993
- }
3994
- markTriggered(cooldownKey);
4293
+ detectBestPattern(content);
4294
+ markTriggered(promptKey);
3995
4295
  return {
3996
4296
  observation: buildObservation(input, content),
3997
4297
  output: defaultOutput
@@ -4034,7 +4334,7 @@ async function runHook() {
4034
4334
  }
4035
4335
  process.stdout.write(JSON.stringify(output));
4036
4336
  }
4037
- var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MAX_CONTENT_LENGTH;
4337
+ var cooldowns, COOLDOWN_MS, MIN_STORE_LENGTH, MIN_EDIT_LENGTH, NOISE_COMMANDS, MAX_CONTENT_LENGTH;
4038
4338
  var init_handler = __esm({
4039
4339
  "src/hooks/handler.ts"() {
4040
4340
  "use strict";
@@ -4044,6 +4344,14 @@ var init_handler = __esm({
4044
4344
  cooldowns = /* @__PURE__ */ new Map();
4045
4345
  COOLDOWN_MS = 3e4;
4046
4346
  MIN_STORE_LENGTH = 100;
4347
+ MIN_EDIT_LENGTH = 30;
4348
+ NOISE_COMMANDS = [
4349
+ /^(ls|dir|cd|pwd|echo|cat|type|head|tail|wc|find|which|where|whoami)\b/i,
4350
+ /^(Get-Content|Test-Path|Get-Item|Get-ChildItem|Set-Location|Write-Host)\b/i,
4351
+ /^(Start-Sleep|Select-String|Select-Object|Format-Table|Measure-Object)\b/i,
4352
+ /^(mkdir|rm|cp|mv|touch|chmod|chown)\b/i,
4353
+ /^(node -[ep]|python -c)\b/i
4354
+ ];
4047
4355
  MAX_CONTENT_LENGTH = 4e3;
4048
4356
  }
4049
4357
  });