@contextstream/mcp-server 0.4.65 → 0.4.67

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/dist/index.js CHANGED
@@ -1563,10 +1563,12 @@ var init_files = __esm({
1563
1563
  // src/hooks-config.ts
1564
1564
  var hooks_config_exports = {};
1565
1565
  __export(hooks_config_exports, {
1566
+ CLAUDE_ENFORCEMENT_CRITICAL_HOOKS: () => CLAUDE_ENFORCEMENT_CRITICAL_HOOKS,
1566
1567
  CLINE_POSTTOOLUSE_HOOK_SCRIPT: () => CLINE_POSTTOOLUSE_HOOK_SCRIPT,
1567
1568
  CLINE_PRETOOLUSE_HOOK_SCRIPT: () => CLINE_PRETOOLUSE_HOOK_SCRIPT,
1568
1569
  CLINE_USER_PROMPT_HOOK_SCRIPT: () => CLINE_USER_PROMPT_HOOK_SCRIPT,
1569
1570
  CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT: () => CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT,
1571
+ CURSOR_ENFORCEMENT_CRITICAL_HOOKS: () => CURSOR_ENFORCEMENT_CRITICAL_HOOKS,
1570
1572
  CURSOR_PRETOOLUSE_HOOK_SCRIPT: () => CURSOR_PRETOOLUSE_HOOK_SCRIPT,
1571
1573
  MEDIA_AWARE_HOOK_SCRIPT: () => MEDIA_AWARE_HOOK_SCRIPT,
1572
1574
  PRECOMPACT_HOOK_SCRIPT: () => PRECOMPACT_HOOK_SCRIPT,
@@ -1585,6 +1587,7 @@ __export(hooks_config_exports, {
1585
1587
  getIndexStatusPath: () => getIndexStatusPath,
1586
1588
  getKiloCodeHooksDir: () => getKiloCodeHooksDir,
1587
1589
  getRooCodeHooksDir: () => getRooCodeHooksDir,
1590
+ getWindsurfHooksConfigPath: () => getWindsurfHooksConfigPath,
1588
1591
  installAllEditorHooks: () => installAllEditorHooks,
1589
1592
  installClaudeCodeHooks: () => installClaudeCodeHooks,
1590
1593
  installClineHookScripts: () => installClineHookScripts,
@@ -1593,15 +1596,18 @@ __export(hooks_config_exports, {
1593
1596
  installHookScripts: () => installHookScripts,
1594
1597
  installKiloCodeHookScripts: () => installKiloCodeHookScripts,
1595
1598
  installRooCodeHookScripts: () => installRooCodeHookScripts,
1599
+ installWindsurfHookScripts: () => installWindsurfHookScripts,
1596
1600
  markProjectIndexed: () => markProjectIndexed,
1597
1601
  mergeHooksIntoSettings: () => mergeHooksIntoSettings,
1598
1602
  readClaudeSettings: () => readClaudeSettings,
1599
1603
  readCursorHooksConfig: () => readCursorHooksConfig,
1600
1604
  readIndexStatus: () => readIndexStatus,
1605
+ readWindsurfHooksConfig: () => readWindsurfHooksConfig,
1601
1606
  unmarkProjectIndexed: () => unmarkProjectIndexed,
1602
1607
  writeClaudeSettings: () => writeClaudeSettings,
1603
1608
  writeCursorHooksConfig: () => writeCursorHooksConfig,
1604
- writeIndexStatus: () => writeIndexStatus
1609
+ writeIndexStatus: () => writeIndexStatus,
1610
+ writeWindsurfHooksConfig: () => writeWindsurfHooksConfig
1605
1611
  });
1606
1612
  import * as fs4 from "node:fs/promises";
1607
1613
  import * as fsSync from "node:fs";
@@ -1711,6 +1717,18 @@ function buildHooksConfig(options) {
1711
1717
  ]
1712
1718
  }
1713
1719
  ];
1720
+ config.PostCompact = [
1721
+ {
1722
+ matcher: "*",
1723
+ hooks: [
1724
+ {
1725
+ type: "command",
1726
+ command: getHookCommand("post-compact"),
1727
+ timeout: 15
1728
+ }
1729
+ ]
1730
+ }
1731
+ ];
1714
1732
  }
1715
1733
  if (options?.includeSessionInit !== false) {
1716
1734
  config.SessionStart = [
@@ -1725,6 +1743,102 @@ function buildHooksConfig(options) {
1725
1743
  ]
1726
1744
  }
1727
1745
  ];
1746
+ config.InstructionsLoaded = [
1747
+ {
1748
+ matcher: "*",
1749
+ hooks: [
1750
+ {
1751
+ type: "command",
1752
+ command: getHookCommand("instructions-loaded"),
1753
+ timeout: 10
1754
+ }
1755
+ ]
1756
+ }
1757
+ ];
1758
+ config.ConfigChange = [
1759
+ {
1760
+ matcher: "*",
1761
+ hooks: [
1762
+ {
1763
+ type: "command",
1764
+ command: getHookCommand("config-change"),
1765
+ timeout: 10
1766
+ }
1767
+ ]
1768
+ }
1769
+ ];
1770
+ config.CwdChanged = [
1771
+ {
1772
+ matcher: "*",
1773
+ hooks: [
1774
+ {
1775
+ type: "command",
1776
+ command: getHookCommand("cwd-changed"),
1777
+ timeout: 10
1778
+ }
1779
+ ]
1780
+ }
1781
+ ];
1782
+ config.FileChanged = [
1783
+ {
1784
+ matcher: ".*",
1785
+ hooks: [
1786
+ {
1787
+ type: "command",
1788
+ command: getHookCommand("file-changed"),
1789
+ timeout: 10
1790
+ }
1791
+ ]
1792
+ }
1793
+ ];
1794
+ config.WorktreeCreate = [
1795
+ {
1796
+ matcher: "*",
1797
+ hooks: [
1798
+ {
1799
+ type: "command",
1800
+ command: getHookCommand("worktree-create"),
1801
+ timeout: 15
1802
+ }
1803
+ ]
1804
+ }
1805
+ ];
1806
+ config.WorktreeRemove = [
1807
+ {
1808
+ matcher: "*",
1809
+ hooks: [
1810
+ {
1811
+ type: "command",
1812
+ command: getHookCommand("worktree-remove"),
1813
+ timeout: 15
1814
+ }
1815
+ ]
1816
+ }
1817
+ ];
1818
+ config.Elicitation = [
1819
+ {
1820
+ matcher: ".*",
1821
+ hooks: [
1822
+ {
1823
+ type: "command",
1824
+ command: getHookCommand("elicitation"),
1825
+ timeout: 10
1826
+ }
1827
+ ]
1828
+ }
1829
+ ];
1830
+ config.ElicitationResult = [
1831
+ {
1832
+ matcher: ".*",
1833
+ hooks: [
1834
+ {
1835
+ type: "command",
1836
+ command: getHookCommand("elicitation-result"),
1837
+ timeout: 10
1838
+ }
1839
+ ]
1840
+ }
1841
+ ];
1728
1842
  }
1729
1843
  if (options?.includeSessionEnd !== false) {
1730
1844
  config.Stop = [
@@ -1751,6 +1865,18 @@ function buildHooksConfig(options) {
1751
1865
  ]
1752
1866
  }
1753
1867
  ];
1868
+ config.StopFailure = [
1869
+ {
1870
+ matcher: "*",
1871
+ hooks: [
1872
+ {
1873
+ type: "command",
1874
+ command: getHookCommand("stop-failure"),
1875
+ timeout: 10
1876
+ }
1877
+ ]
1878
+ }
1879
+ ];
1754
1880
  }
1755
1881
  const postToolUseHooks = [];
1756
1882
  if (options?.includePostWrite !== false) {
@@ -1846,6 +1972,12 @@ function buildHooksConfig(options) {
1846
1972
  hooks: [{ type: "command", command: getHookCommand("subagent-stop"), timeout: 15 }]
1847
1973
  }
1848
1974
  ];
1975
+ config.TaskCreated = [
1976
+ {
1977
+ matcher: "*",
1978
+ hooks: [{ type: "command", command: getHookCommand("task-created"), timeout: 10 }]
1979
+ }
1980
+ ];
1849
1981
  config.TaskCompleted = [
1850
1982
  {
1851
1983
  matcher: "*",
@@ -1926,11 +2058,21 @@ async function installClaudeCodeHooks(options) {
1926
2058
  getHookCommand("user-prompt-submit"),
1927
2059
  getHookCommand("on-save-intent"),
1928
2060
  getHookCommand("session-start"),
2061
+ getHookCommand("instructions-loaded"),
2062
+ getHookCommand("config-change"),
2063
+ getHookCommand("cwd-changed"),
2064
+ getHookCommand("file-changed"),
2065
+ getHookCommand("worktree-create"),
2066
+ getHookCommand("worktree-remove"),
2067
+ getHookCommand("elicitation"),
2068
+ getHookCommand("elicitation-result"),
1929
2069
  getHookCommand("stop"),
2070
+ getHookCommand("stop-failure"),
1930
2071
  getHookCommand("session-end"),
1931
2072
  getHookCommand("post-tool-use-failure"),
1932
2073
  getHookCommand("subagent-start"),
1933
2074
  getHookCommand("subagent-stop"),
2075
+ getHookCommand("task-created"),
1934
2076
  getHookCommand("task-completed"),
1935
2077
  getHookCommand("teammate-idle"),
1936
2078
  getHookCommand("notification"),
@@ -1938,6 +2080,7 @@ async function installClaudeCodeHooks(options) {
1938
2080
  );
1939
2081
  if (options.includePreCompact !== false) {
1940
2082
  result.scripts.push(getHookCommand("pre-compact"));
2083
+ result.scripts.push(getHookCommand("post-compact"));
1941
2084
  }
1942
2085
  if (options.includeMediaAware === true) {
1943
2086
  result.scripts.push(getHookCommand("media-aware"));
@@ -2232,6 +2375,15 @@ function getCursorHooksDir(scope, projectPath) {
2232
2375
  }
2233
2376
  return path4.join(projectPath, ".cursor", "hooks");
2234
2377
  }
2378
+ function getWindsurfHooksConfigPath(scope, projectPath) {
2379
+ if (scope === "global") {
2380
+ return path4.join(homedir3(), ".codeium", "windsurf", "hooks.json");
2381
+ }
2382
+ if (!projectPath) {
2383
+ throw new Error("projectPath required for project scope");
2384
+ }
2385
+ return path4.join(projectPath, ".windsurf", "hooks.json");
2386
+ }
2235
2387
  async function readCursorHooksConfig(scope, projectPath) {
2236
2388
  const configPath = getCursorHooksConfigPath(scope, projectPath);
2237
2389
  try {
@@ -2258,17 +2410,31 @@ async function installCursorHookScripts(options) {
2258
2410
  return !hook.command?.includes("contextstream");
2259
2411
  });
2260
2412
  };
2261
- const filteredPreToolUse = filterContextStreamHooks(existingConfig.hooks.preToolUse);
2262
- const filteredBeforeSubmit = filterContextStreamHooks(existingConfig.hooks.beforeSubmitPrompt);
2413
+ const cleanedHooks = {};
2414
+ for (const [eventName, entries] of Object.entries(existingConfig.hooks || {})) {
2415
+ cleanedHooks[eventName] = filterContextStreamHooks(entries);
2416
+ }
2263
2417
  const preToolUseCommand = getHookCommand("pre-tool-use");
2264
2418
  const userPromptCommand = getHookCommand("user-prompt-submit");
2265
2419
  const saveIntentCommand = getHookCommand("on-save-intent");
2420
+ const postToolUseCommand = getHookCommand("post-tool-use");
2421
+ const postToolUseFailureCommand = getHookCommand("post-tool-use-failure");
2422
+ const preCompactCommand = getHookCommand("pre-compact");
2423
+ const sessionStartCommand = getHookCommand("session-start");
2424
+ const sessionEndCommand = getHookCommand("session-end");
2425
+ const stopCommand = getHookCommand("stop");
2426
+ const notificationCommand = getHookCommand("notification");
2427
+ const permissionRequestCommand = getHookCommand("permission-request");
2428
+ const subagentStartCommand = getHookCommand("subagent-start");
2429
+ const subagentStopCommand = getHookCommand("subagent-stop");
2430
+ const taskCompletedCommand = getHookCommand("task-completed");
2431
+ const teammateIdleCommand = getHookCommand("teammate-idle");
2266
2432
  const config = {
2267
2433
  version: 1,
2268
2434
  hooks: {
2269
- ...existingConfig.hooks,
2435
+ ...cleanedHooks,
2270
2436
  preToolUse: [
2271
- ...filteredPreToolUse,
2437
+ ...cleanedHooks.preToolUse || [],
2272
2438
  {
2273
2439
  command: preToolUseCommand,
2274
2440
  type: "command",
@@ -2276,7 +2442,7 @@ async function installCursorHookScripts(options) {
2276
2442
  }
2277
2443
  ],
2278
2444
  beforeSubmitPrompt: [
2279
- ...filteredBeforeSubmit,
2445
+ ...cleanedHooks.beforeSubmitPrompt || [],
2280
2446
  {
2281
2447
  command: userPromptCommand,
2282
2448
  type: "command",
@@ -2287,6 +2453,70 @@ async function installCursorHookScripts(options) {
2287
2453
  type: "command",
2288
2454
  timeout: 5
2289
2455
  }
2456
+ ],
2457
+ beforeMCPExecution: [
2458
+ ...cleanedHooks.beforeMCPExecution || [],
2459
+ { command: preToolUseCommand, type: "command", timeout: 5 }
2460
+ ],
2461
+ afterMCPExecution: [
2462
+ ...cleanedHooks.afterMCPExecution || [],
2463
+ { command: postToolUseCommand, type: "command", timeout: 10 }
2464
+ ],
2465
+ beforeShellExecution: [
2466
+ ...cleanedHooks.beforeShellExecution || [],
2467
+ { command: preToolUseCommand, type: "command", timeout: 5 }
2468
+ ],
2469
+ afterShellExecution: [
2470
+ ...cleanedHooks.afterShellExecution || [],
2471
+ { command: postToolUseFailureCommand, type: "command", timeout: 10 }
2472
+ ],
2473
+ beforeReadFile: [
2474
+ ...cleanedHooks.beforeReadFile || [],
2475
+ { command: preToolUseCommand, type: "command", timeout: 5 }
2476
+ ],
2477
+ afterFileEdit: [
2478
+ ...cleanedHooks.afterFileEdit || [],
2479
+ { command: postToolUseCommand, type: "command", timeout: 10 }
2480
+ ],
2481
+ preCompact: [
2482
+ ...cleanedHooks.preCompact || [],
2483
+ { command: preCompactCommand, type: "command", timeout: 15 }
2484
+ ],
2485
+ sessionStart: [
2486
+ ...cleanedHooks.sessionStart || [],
2487
+ { command: sessionStartCommand, type: "command", timeout: 15 }
2488
+ ],
2489
+ sessionEnd: [
2490
+ ...cleanedHooks.sessionEnd || [],
2491
+ { command: sessionEndCommand, type: "command", timeout: 10 }
2492
+ ],
2493
+ stop: [
2494
+ ...cleanedHooks.stop || [],
2495
+ { command: stopCommand, type: "command", timeout: 15 }
2496
+ ],
2497
+ subagentStart: [
2498
+ ...cleanedHooks.subagentStart || [],
2499
+ { command: subagentStartCommand, type: "command", timeout: 10 }
2500
+ ],
2501
+ subagentStop: [
2502
+ ...cleanedHooks.subagentStop || [],
2503
+ { command: subagentStopCommand, type: "command", timeout: 10 }
2504
+ ],
2505
+ taskCompleted: [
2506
+ ...cleanedHooks.taskCompleted || [],
2507
+ { command: taskCompletedCommand, type: "command", timeout: 10 }
2508
+ ],
2509
+ teammateIdle: [
2510
+ ...cleanedHooks.teammateIdle || [],
2511
+ { command: teammateIdleCommand, type: "command", timeout: 10 }
2512
+ ],
2513
+ notification: [
2514
+ ...cleanedHooks.notification || [],
2515
+ { command: notificationCommand, type: "command", timeout: 10 }
2516
+ ],
2517
+ permissionRequest: [
2518
+ ...cleanedHooks.permissionRequest || [],
2519
+ { command: permissionRequestCommand, type: "command", timeout: 10 }
2290
2520
  ]
2291
2521
  }
2292
2522
  };
@@ -2298,6 +2528,83 @@ async function installCursorHookScripts(options) {
2298
2528
  config: configPath
2299
2529
  };
2300
2530
  }
2531
+ async function readWindsurfHooksConfig(scope, projectPath) {
2532
+ const configPath = getWindsurfHooksConfigPath(scope, projectPath);
2533
+ try {
2534
+ const content = await fs4.readFile(configPath, "utf-8");
2535
+ return JSON.parse(content);
2536
+ } catch {
2537
+ return { hooks: {} };
2538
+ }
2539
+ }
2540
+ async function writeWindsurfHooksConfig(config, scope, projectPath) {
2541
+ const configPath = getWindsurfHooksConfigPath(scope, projectPath);
2542
+ const dir = path4.dirname(configPath);
2543
+ await fs4.mkdir(dir, { recursive: true });
2544
+ await fs4.writeFile(configPath, JSON.stringify(config, null, 2));
2545
+ }
2546
+ async function installWindsurfHookScripts(options) {
2547
+ const existingConfig = await readWindsurfHooksConfig(options.scope, options.projectPath);
2548
+ const filterContextStreamHooks = (hooks) => {
2549
+ if (!hooks) return [];
2550
+ return hooks.filter((h) => {
2551
+ const hook = h;
2552
+ return !hook.command?.toLowerCase().includes("contextstream");
2553
+ });
2554
+ };
2555
+ const cleanedHooks = {};
2556
+ for (const [eventName, entries] of Object.entries(existingConfig.hooks || {})) {
2557
+ cleanedHooks[eventName] = filterContextStreamHooks(entries);
2558
+ }
2559
+ const preToolUseCommand = getHookCommand("pre-tool-use");
2560
+ const userPromptCommand = getHookCommand("user-prompt-submit");
2561
+ const postToolUseCommand = getHookCommand("post-tool-use");
2562
+ const sessionEndCommand = getHookCommand("session-end");
2563
+ const config = {
2564
+ hooks: {
2565
+ ...cleanedHooks,
2566
+ pre_mcp_tool_use: [
2567
+ ...cleanedHooks.pre_mcp_tool_use || [],
2568
+ { command: preToolUseCommand, show_output: true }
2569
+ ],
2570
+ pre_user_prompt: [
2571
+ ...cleanedHooks.pre_user_prompt || [],
2572
+ { command: userPromptCommand }
2573
+ ],
2574
+ pre_read_code: [
2575
+ ...cleanedHooks.pre_read_code || [],
2576
+ { command: preToolUseCommand, show_output: true }
2577
+ ],
2578
+ pre_write_code: [
2579
+ ...cleanedHooks.pre_write_code || [],
2580
+ { command: preToolUseCommand, show_output: true }
2581
+ ],
2582
+ pre_run_command: [
2583
+ ...cleanedHooks.pre_run_command || [],
2584
+ { command: preToolUseCommand, show_output: true }
2585
+ ],
2586
+ post_write_code: [
2587
+ ...cleanedHooks.post_write_code || [],
2588
+ { command: postToolUseCommand, show_output: false }
2589
+ ],
2590
+ post_mcp_tool_use: [
2591
+ ...cleanedHooks.post_mcp_tool_use || [],
2592
+ { command: postToolUseCommand, show_output: false }
2593
+ ],
2594
+ post_cascade_response_with_transcript: [
2595
+ ...cleanedHooks.post_cascade_response_with_transcript || [],
2596
+ { command: sessionEndCommand }
2597
+ ]
2598
+ }
2599
+ };
2600
+ await writeWindsurfHooksConfig(config, options.scope, options.projectPath);
2601
+ const configPath = getWindsurfHooksConfigPath(options.scope, options.projectPath);
2602
+ return {
2603
+ preMcpToolUse: preToolUseCommand,
2604
+ preUserPrompt: userPromptCommand,
2605
+ config: configPath
2606
+ };
2607
+ }
2301
2608
  async function installEditorHooks(options) {
2302
2609
  const { editor, scope, projectPath, includePreCompact, includePostWrite } = options;
2303
2610
  switch (editor) {
@@ -2357,12 +2664,20 @@ async function installEditorHooks(options) {
2357
2664
  hooksDir: getCursorHooksDir(scope, projectPath)
2358
2665
  };
2359
2666
  }
2667
+ case "windsurf": {
2668
+ const scripts = await installWindsurfHookScripts({ scope, projectPath });
2669
+ return {
2670
+ editor: "windsurf",
2671
+ installed: [scripts.preMcpToolUse, scripts.preUserPrompt, scripts.config],
2672
+ hooksDir: path4.dirname(scripts.config)
2673
+ };
2674
+ }
2360
2675
  default:
2361
2676
  throw new Error(`Unsupported editor: ${editor}`);
2362
2677
  }
2363
2678
  }
2364
2679
  async function installAllEditorHooks(options) {
2365
- const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor"];
2680
+ const editors = options.editors || ["claude", "cline", "roo", "kilo", "cursor", "windsurf"];
2366
2681
  const results = [];
2367
2682
  for (const editor of editors) {
2368
2683
  try {
@@ -2390,8 +2705,9 @@ ContextStream can install hooks for multiple AI code editors to enforce ContextS
2390
2705
 
2391
2706
  | Editor | Hooks Location | Hook Types |
2392
2707
  |--------|---------------|------------|
2393
- | **Claude Code** | \`~/.claude/hooks/\` | PreToolUse, UserPromptSubmit, PreCompact |
2394
- | **Cursor** | \`~/.cursor/hooks/\` | preToolUse, beforeSubmit |
2708
+ | **Claude Code** | \`~/.claude/hooks/\` | PreToolUse, UserPromptSubmit, SessionStart, Pre/PostCompact, PostToolUse |
2709
+ | **Cursor** | \`~/.cursor/hooks/\` | preToolUse, beforeSubmitPrompt, before/after MCP+Shell, beforeReadFile, afterFileEdit |
2710
+ | **Windsurf** | \`~/.codeium/windsurf/hooks.json\` | pre_mcp_tool_use, pre_user_prompt, pre/post code + command hooks |
2395
2711
  | **Cline** | \`~/Documents/Cline/Rules/Hooks/\` | PreToolUse, UserPromptSubmit |
2396
2712
  | **Roo Code** | \`~/.roo/hooks/\` | PreToolUse, UserPromptSubmit |
2397
2713
  | **Kilo Code** | \`~/.kilocode/hooks/\` | PreToolUse, UserPromptSubmit |
@@ -2402,9 +2718,19 @@ ${generateHooksDocumentation()}
2402
2718
 
2403
2719
  ### Cursor Hooks
2404
2720
 
2405
- Cursor uses a \`hooks.json\` configuration file:
2406
- - **preToolUse**: Blocks discovery tools before execution
2407
- - **beforeSubmitPrompt**: Injects ContextStream rules reminder
2721
+ Cursor uses a \`hooks.json\` configuration file with enforcement + lifecycle hooks:
2722
+ - **preToolUse / beforeMCPExecution / beforeShellExecution / beforeReadFile**: blocks or redirects non-ContextStream-first flows
2723
+ - **beforeSubmitPrompt**: injects ContextStream rules reminder + save-intent capture
2724
+ - **afterMCPExecution / afterShellExecution / afterFileEdit**: post-action bookkeeping/index updates
2725
+ - **sessionStart / preCompact / stop / sessionEnd**: lifecycle persistence and reliability
2726
+
2727
+ ### Windsurf Hooks
2728
+
2729
+ Windsurf uses a \`hooks.json\` configuration file:
2730
+ - **pre_mcp_tool_use / pre_read_code / pre_write_code / pre_run_command**: pre-action gating and enforcement
2731
+ - **pre_user_prompt**: injects ContextStream-first guidance each prompt
2732
+ - **post_write_code / post_mcp_tool_use**: post-action indexing bookkeeping
2733
+ - **post_cascade_response_with_transcript**: end-of-response/session capture
2408
2734
 
2409
2735
  #### Output Format
2410
2736
  \`\`\`json
@@ -2434,7 +2760,7 @@ Hooks are executable scripts named after the hook type (no extension).
2434
2760
 
2435
2761
  ### Installation
2436
2762
 
2437
- Use \`generate_rules(install_hooks=true, editors=["claude", "cursor", "cline", "roo", "kilo"])\` to install hooks for specific editors, or omit \`editors\` to install for all.
2763
+ Use \`generate_rules(install_hooks=true, editors=["claude", "cursor", "windsurf", "cline", "roo", "kilo"])\` to install hooks for specific editors, or omit \`editors\` to install for all.
2438
2764
 
2439
2765
  ### Disabling Hooks
2440
2766
 
@@ -2443,7 +2769,7 @@ Set environment variables:
2443
2769
  - \`CONTEXTSTREAM_REMINDER_ENABLED=false\` - Disable UserPromptSubmit reminders
2444
2770
  `.trim();
2445
2771
  }
2446
- var PRETOOLUSE_HOOK_SCRIPT, USER_PROMPT_HOOK_SCRIPT, MEDIA_AWARE_HOOK_SCRIPT, PRECOMPACT_HOOK_SCRIPT, CLINE_PRETOOLUSE_HOOK_SCRIPT, CLINE_USER_PROMPT_HOOK_SCRIPT, CLINE_POSTTOOLUSE_HOOK_SCRIPT, CURSOR_PRETOOLUSE_HOOK_SCRIPT, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT;
2772
+ var PRETOOLUSE_HOOK_SCRIPT, USER_PROMPT_HOOK_SCRIPT, MEDIA_AWARE_HOOK_SCRIPT, PRECOMPACT_HOOK_SCRIPT, CLAUDE_ENFORCEMENT_CRITICAL_HOOKS, CURSOR_ENFORCEMENT_CRITICAL_HOOKS, CLINE_PRETOOLUSE_HOOK_SCRIPT, CLINE_USER_PROMPT_HOOK_SCRIPT, CLINE_POSTTOOLUSE_HOOK_SCRIPT, CURSOR_PRETOOLUSE_HOOK_SCRIPT, CURSOR_BEFORE_SUBMIT_HOOK_SCRIPT;
2447
2773
  var init_hooks_config = __esm({
2448
2774
  "src/hooks-config.ts"() {
2449
2775
  "use strict";
@@ -2924,6 +3250,23 @@ After compaction, call session_init(is_post_compact=true) to restore context.
2924
3250
  if __name__ == "__main__":
2925
3251
  main()
2926
3252
  `;
3253
+ CLAUDE_ENFORCEMENT_CRITICAL_HOOKS = [
3254
+ "PreToolUse",
3255
+ "UserPromptSubmit",
3256
+ "SessionStart",
3257
+ "PreCompact",
3258
+ "PostToolUse"
3259
+ ];
3260
+ CURSOR_ENFORCEMENT_CRITICAL_HOOKS = [
3261
+ "preToolUse",
3262
+ "beforeSubmitPrompt",
3263
+ "beforeMCPExecution",
3264
+ "beforeShellExecution",
3265
+ "beforeReadFile",
3266
+ "afterFileEdit",
3267
+ "sessionStart",
3268
+ "preCompact"
3269
+ ];
2927
3270
  CLINE_PRETOOLUSE_HOOK_SCRIPT = `#!/usr/bin/env python3
2928
3271
  """
2929
3272
  ContextStream PreToolUse Hook for Cline
@@ -3427,6 +3770,7 @@ memory(
3427
3770
  | Persistent plan | \`session(action="capture_plan")\` |
3428
3771
  | Task status | \`memory(action="update_task")\` |
3429
3772
  | Decision capture | \`session(action="capture", event_type="decision")\` |
3773
+ | Update skill by name | \`skill(action="update", name="...", instruction_body="...", change_summary="...")\` |
3430
3774
  | Past context | \`session(action="recall", query="...")\` |
3431
3775
  | Lessons | \`session(action="get_lessons", query="...")\` |
3432
3776
 
@@ -3462,6 +3806,9 @@ ${options.workspaceId ? `# Workspace ID: ${options.workspaceId}` : ""}
3462
3806
  if (NO_HOOKS_EDITORS.includes(editor.toLowerCase())) {
3463
3807
  content += NO_HOOKS_SUPPLEMENT;
3464
3808
  }
3809
+ if (editor.toLowerCase() === "antigravity") {
3810
+ content += ANTIGRAVITY_SUPPLEMENT;
3811
+ }
3465
3812
  if (options?.additionalRules) {
3466
3813
  content += "\n\n## Project-Specific Rules\n\n" + options.additionalRules;
3467
3814
  }
@@ -3497,7 +3844,7 @@ function generateAllRuleFiles(options) {
3497
3844
  }))
3498
3845
  ).filter((r) => r !== null);
3499
3846
  }
3500
- var DEFAULT_CLAUDE_MCP_SERVER_NAME, RULES_VERSION, CONTEXTSTREAM_TOOL_NAMES, CONTEXTSTREAM_RULES_BOOTSTRAP, CONTEXTSTREAM_RULES_DYNAMIC, CONTEXTSTREAM_RULES_FULL, CONTEXTSTREAM_RULES_MINIMAL, NO_HOOKS_SUPPLEMENT, NO_HOOKS_EDITORS, TEMPLATES;
3847
+ var DEFAULT_CLAUDE_MCP_SERVER_NAME, RULES_VERSION, CONTEXTSTREAM_TOOL_NAMES, CONTEXTSTREAM_RULES_BOOTSTRAP, CONTEXTSTREAM_RULES_DYNAMIC, CONTEXTSTREAM_RULES_FULL, CONTEXTSTREAM_RULES_MINIMAL, NO_HOOKS_SUPPLEMENT, ANTIGRAVITY_SUPPLEMENT, NO_HOOKS_EDITORS, TEMPLATES;
3501
3848
  var init_rules_templates = __esm({
3502
3849
  "src/rules-templates.ts"() {
3503
3850
  "use strict";
@@ -4429,13 +4776,32 @@ After updating, user should restart their AI tool.
4429
4776
 
4430
4777
  ---
4431
4778
  `;
4432
- NO_HOOKS_EDITORS = ["codex", "aider", "antigravity"];
4779
+ ANTIGRAVITY_SUPPLEMENT = `
4780
+ ---
4781
+ ## Antigravity-Specific Reliability Notes
4782
+
4783
+ - Antigravity currently has no documented lifecycle hooks for ContextStream enforcement.
4784
+ - Treat ContextStream-first behavior as mandatory policy: run \`context(...)\` first, then \`search(mode="auto", ...)\` before local discovery.
4785
+ - Keep \`mcp_config.json\` valid and minimal: preserve non-ContextStream servers and only update the \`contextstream\` server block.
4786
+ - If ContextStream appears skipped, verify:
4787
+ 1. MCP server status is healthy in Antigravity settings
4788
+ 2. Project is indexed and \`search(mode="auto", ...)\` is retried before local fallbacks
4789
+ 3. Instructions file contains the current ContextStream managed block
4790
+ `;
4791
+ NO_HOOKS_EDITORS = ["copilot", "codex", "opencode", "aider", "antigravity"];
4433
4792
  TEMPLATES = {
4434
4793
  codex: {
4435
4794
  filename: "AGENTS.md",
4436
4795
  description: "Codex CLI agent instructions",
4437
4796
  build: (rules) => `# Codex CLI Instructions
4438
4797
  ${rules}
4798
+ `
4799
+ },
4800
+ opencode: {
4801
+ filename: "AGENTS.md",
4802
+ description: "OpenCode CLI agent instructions",
4803
+ build: (rules) => `# OpenCode CLI Instructions
4804
+ ${rules}
4439
4805
  `
4440
4806
  },
4441
4807
  cursor: {
@@ -4443,6 +4809,17 @@ ${rules}
4443
4809
  description: "Cursor AI rules",
4444
4810
  build: (rules) => `# Cursor Rules
4445
4811
  ${rules}
4812
+ `
4813
+ },
4814
+ windsurf: {
4815
+ filename: ".windsurf/rules/contextstream.md",
4816
+ description: "Windsurf AI rules",
4817
+ build: (rules) => `---
4818
+ trigger: always_on
4819
+ ---
4820
+
4821
+ # Windsurf Rules
4822
+ ${rules}
4446
4823
  `
4447
4824
  },
4448
4825
  cline: {
@@ -12919,6 +13296,37 @@ function normalizeTags(tags) {
12919
13296
  );
12920
13297
  return normalized.length > 0 ? normalized : void 0;
12921
13298
  }
13299
+ function extractEventTags(item) {
13300
+ const tags = [];
13301
+ if (Array.isArray(item.tags)) {
13302
+ tags.push(...item.tags.filter((t) => typeof t === "string"));
13303
+ }
13304
+ const metaTags = item.metadata?.tags;
13305
+ if (Array.isArray(metaTags)) {
13306
+ tags.push(...metaTags.filter((t) => typeof t === "string"));
13307
+ }
13308
+ return tags;
13309
+ }
13310
+ function extractEffectiveEventType(item) {
13311
+ for (const field of ["event_type", "node_type", "type"]) {
13312
+ const val = item[field];
13313
+ if (typeof val === "string" && val.trim()) return val.trim();
13314
+ }
13315
+ for (const field of ["original_type", "node_type", "event_type", "type"]) {
13316
+ const val = item.metadata?.[field];
13317
+ if (typeof val === "string" && val.trim()) return val.trim();
13318
+ }
13319
+ return "unknown";
13320
+ }
13321
+ function isLessonResult(item) {
13322
+ const effectiveType = extractEffectiveEventType(item);
13323
+ if (effectiveType === "lesson") return true;
13324
+ const tags = extractEventTags(item);
13325
+ if (tags.some((t) => t === "lesson" || t === "lesson_system")) return true;
13326
+ const content = typeof item.content === "string" ? item.content : "";
13327
+ if (content.includes("### Prevention") && content.includes("### Trigger")) return true;
13328
+ return false;
13329
+ }
12922
13330
  function pickString(value) {
12923
13331
  if (typeof value !== "string") return null;
12924
13332
  const trimmed = value.trim();
@@ -13472,6 +13880,8 @@ var ContextStreamClient = class _ContextStreamClient {
13472
13880
  const query = new URLSearchParams();
13473
13881
  if (params?.limit) query.set("limit", String(params.limit));
13474
13882
  if (withDefaults.project_id) query.set("project_id", withDefaults.project_id);
13883
+ if (params?.event_type) query.set("event_type", params.event_type);
13884
+ if (params?.tags && params.tags.length > 0) query.set("tags", params.tags.join(","));
13475
13885
  const suffix = query.toString() ? `?${query.toString()}` : "";
13476
13886
  return request(this.config, `/memory/events/workspace/${withDefaults.workspace_id}${suffix}`, {
13477
13887
  method: "GET"
@@ -15835,14 +16245,13 @@ ${context}`;
15835
16245
  });
15836
16246
  if (!searchResult?.results) return [];
15837
16247
  const lessons = searchResult.results.filter((item) => {
15838
- const tags = item.metadata?.tags || [];
15839
- const isLesson = tags.includes("lesson") || tags.includes("lesson_system");
15840
- if (!isLesson) return false;
16248
+ if (!isLessonResult(item)) return false;
16249
+ const tags = extractEventTags(item);
15841
16250
  const severityTag = tags.find((t) => t.startsWith("severity:"));
15842
16251
  const severity = severityTag?.split(":")[1] || item.metadata?.importance || "medium";
15843
16252
  return severity === "critical" || severity === "high";
15844
16253
  }).slice(0, limit).map((item) => {
15845
- const tags = item.metadata?.tags || [];
16254
+ const tags = extractEventTags(item);
15846
16255
  const severityTag = tags.find((t) => t.startsWith("severity:"));
15847
16256
  const severity = severityTag?.split(":")[1] || item.metadata?.importance || "medium";
15848
16257
  const category = tags.find(
@@ -15856,7 +16265,7 @@ ${context}`;
15856
16265
  ) || "unknown";
15857
16266
  const content = item.content || "";
15858
16267
  const preventionMatch = content.match(/### Prevention\n([\s\S]*?)(?:\n\n|\n\*\*|$)/);
15859
- const prevention = preventionMatch?.[1]?.trim() || content.slice(0, 200);
16268
+ const prevention = preventionMatch?.[1]?.trim() || content.slice(0, 1e3);
15860
16269
  return {
15861
16270
  title: item.title || "Lesson",
15862
16271
  severity,
@@ -17111,6 +17520,55 @@ ${context}`;
17111
17520
  return request(this.config, `/docs/${params.doc_id}`, { method: "DELETE" });
17112
17521
  }
17113
17522
  // -------------------------------------------------------------------------
17523
+ // Skill methods (portable instruction + action bundles)
17524
+ // -------------------------------------------------------------------------
17525
+ async listSkills(params) {
17526
+ const withDefaults = this.withDefaults(params || {});
17527
+ const query = new URLSearchParams();
17528
+ if (withDefaults.workspace_id) query.set("workspace_id", withDefaults.workspace_id);
17529
+ if (params?.project_id) query.set("project_id", params.project_id);
17530
+ if (params?.scope) query.set("scope", params.scope);
17531
+ if (params?.status) query.set("status", params.status);
17532
+ if (params?.category) query.set("category", params.category);
17533
+ if (params?.query) query.set("query", params.query);
17534
+ if (params?.is_personal !== void 0) query.set("is_personal", String(params.is_personal));
17535
+ if (params?.limit) query.set("limit", String(params.limit));
17536
+ const suffix = query.toString() ? `?${query.toString()}` : "";
17537
+ return request(this.config, `/skills${suffix}`, { method: "GET" });
17538
+ }
17539
+ async getSkill(skillId) {
17540
+ return request(this.config, `/skills/${skillId}`, { method: "GET" });
17541
+ }
17542
+ async createSkill(params) {
17543
+ return request(this.config, "/skills", { body: this.withDefaults(params) });
17544
+ }
17545
+ async updateSkill(skillId, params) {
17546
+ return request(this.config, `/skills/${skillId}`, {
17547
+ method: "PATCH",
17548
+ body: params
17549
+ });
17550
+ }
17551
+ async runSkill(skillId, params) {
17552
+ return request(this.config, `/skills/${skillId}/run`, {
17553
+ body: params || {}
17554
+ });
17555
+ }
17556
+ async deleteSkill(skillId) {
17557
+ return request(this.config, `/skills/${skillId}`, { method: "DELETE" });
17558
+ }
17559
+ async importSkills(params) {
17560
+ return request(this.config, "/skills/import", { body: this.withDefaults(params) });
17561
+ }
17562
+ async exportSkills(params) {
17563
+ const withDefaults = this.withDefaults(params || {});
17564
+ return request(this.config, "/skills/export", { body: withDefaults });
17565
+ }
17566
+ async shareSkill(skillId, scope) {
17567
+ return request(this.config, `/skills/${skillId}/share`, {
17568
+ body: { scope }
17569
+ });
17570
+ }
17571
+ // -------------------------------------------------------------------------
17114
17572
  // Transcript methods (conversation session storage)
17115
17573
  // -------------------------------------------------------------------------
17116
17574
  /**
@@ -17202,14 +17660,14 @@ ${context}`;
17202
17660
  };
17203
17661
 
17204
17662
  // src/tools.ts
17205
- init_files();
17206
- init_rules_templates();
17207
- init_version();
17208
17663
  import * as fs5 from "node:fs";
17209
17664
  import * as path6 from "node:path";
17210
17665
  import { execFile } from "node:child_process";
17211
17666
  import { homedir as homedir4 } from "node:os";
17212
17667
  import { promisify as promisify2 } from "node:util";
17668
+ init_files();
17669
+ init_rules_templates();
17670
+ init_version();
17213
17671
 
17214
17672
  // src/tool-catalog.ts
17215
17673
  var TOOL_CATALOG = [
@@ -19313,6 +19771,8 @@ var CONSOLIDATED_TOOLS = /* @__PURE__ */ new Set([
19313
19771
  // Consolidates slack_*, github_*, notion_*, integrations_*
19314
19772
  "media",
19315
19773
  // Consolidates media indexing, search, and clip retrieval for Remotion/FFmpeg
19774
+ "skill",
19775
+ // Skill management: list, get, create, update, run, delete, import, export, share
19316
19776
  "help"
19317
19777
  // Consolidates session_tools, auth_me, mcp_server_version, etc.
19318
19778
  ]);
@@ -19332,6 +19792,7 @@ function mapToolToConsolidatedDomain(toolName) {
19332
19792
  return "integration";
19333
19793
  }
19334
19794
  if (toolName.startsWith("media_")) return "media";
19795
+ if (toolName.startsWith("skill_")) return "skill";
19335
19796
  if (toolName === "session_tools" || toolName === "auth_me" || toolName === "mcp_server_version" || toolName === "tools_enable_bundle") {
19336
19797
  return "help";
19337
19798
  }
@@ -19427,6 +19888,93 @@ function toStructured(data) {
19427
19888
  }
19428
19889
  return void 0;
19429
19890
  }
19891
+ function formatSkillDetail(skill) {
19892
+ const lines = [
19893
+ `**${skill.title || skill.name || "?"}** (${skill.name || "?"})`,
19894
+ `- ID: ${skill.id || "?"}`,
19895
+ `- Scope: ${skill.scope || "personal"} | Status: ${skill.status || "active"}`
19896
+ ];
19897
+ if (skill.description) lines.push(`- Description: ${skill.description}`);
19898
+ if (skill.trigger_patterns?.length) lines.push(`- Triggers: ${skill.trigger_patterns.join(", ")}`);
19899
+ if (skill.categories?.length) lines.push(`- Categories: ${skill.categories.join(", ")}`);
19900
+ if (skill.priority != null) lines.push(`- Priority: ${skill.priority}`);
19901
+ if (skill.version) lines.push(`- Version: ${skill.version}`);
19902
+ if (skill.instruction_body) {
19903
+ const body = String(skill.instruction_body);
19904
+ lines.push(`
19905
+ ### Instruction
19906
+ ${body.length > 2e3 ? body.slice(0, 2e3) + "\u2026" : body}`);
19907
+ }
19908
+ return lines.join("\n");
19909
+ }
19910
+ function formatRunResult(result) {
19911
+ if (result.instruction) return result.instruction;
19912
+ if (result.output) return `Skill output:
19913
+ ${result.output}`;
19914
+ return JSON.stringify(result, null, 2);
19915
+ }
19916
+ function computeSkillMatchScore(query, name, title) {
19917
+ const normalizedQuery = query.trim().toLowerCase();
19918
+ if (!normalizedQuery) return null;
19919
+ const normalizedName = String(name || "").trim().toLowerCase();
19920
+ const normalizedTitle = String(title || "").trim().toLowerCase();
19921
+ if (normalizedName === normalizedQuery) return 0;
19922
+ if (normalizedTitle === normalizedQuery) return 1;
19923
+ if (normalizedName.includes(normalizedQuery)) return 2;
19924
+ if (normalizedTitle.includes(normalizedQuery)) return 3;
19925
+ return null;
19926
+ }
19927
+ function rankSkillCandidates(query, items) {
19928
+ return items.map((item) => {
19929
+ const id = String(item?.id || "");
19930
+ const name = String(item?.name || "?");
19931
+ const title = String(item?.title || name);
19932
+ const score = computeSkillMatchScore(query, name, title);
19933
+ if (!id || score == null) return null;
19934
+ return {
19935
+ id,
19936
+ name,
19937
+ title,
19938
+ scope: String(item?.scope || "?"),
19939
+ status: String(item?.status || "?"),
19940
+ score
19941
+ };
19942
+ }).filter((item) => item !== null).sort((a, b) => {
19943
+ if (a.score !== b.score) return a.score - b.score;
19944
+ if (a.name !== b.name) return a.name.localeCompare(b.name);
19945
+ if (a.title !== b.title) return a.title.localeCompare(b.title);
19946
+ return a.id.localeCompare(b.id);
19947
+ });
19948
+ }
19949
+ async function resolveSkillIdForAction(client, input, action) {
19950
+ if (input.skill_id) return input.skill_id;
19951
+ const name = typeof input.name === "string" ? input.name.trim() : "";
19952
+ if (!name) {
19953
+ throw new Error(`Either skill_id or name is required for '${action}'`);
19954
+ }
19955
+ const result = await client.listSkills({
19956
+ workspace_id: input.workspace_id,
19957
+ project_id: input.project_id,
19958
+ query: name,
19959
+ limit: 25
19960
+ });
19961
+ const ranked = rankSkillCandidates(name, result.items || []);
19962
+ if (ranked.length === 0) {
19963
+ throw new Error(`Skill '${name}' not found. Provide a different name or skill_id.`);
19964
+ }
19965
+ const bestScore = ranked[0].score;
19966
+ const bestCandidates = ranked.filter((candidate) => candidate.score === bestScore);
19967
+ if (bestCandidates.length > 1) {
19968
+ const candidates = bestCandidates.slice(0, 5).map(
19969
+ (candidate) => `- ${candidate.title} (${candidate.name}) [${candidate.scope}|${candidate.status}] id=${candidate.id}`
19970
+ ).join("\n");
19971
+ throw new Error(
19972
+ `Skill name '${name}' is ambiguous. Provide skill_id or use a more specific name. Candidates:
19973
+ ${candidates}`
19974
+ );
19975
+ }
19976
+ return ranked[0].id;
19977
+ }
19430
19978
  function readStatNumber(payload, key) {
19431
19979
  if (!payload || typeof payload !== "object") return void 0;
19432
19980
  const direct = payload[key];
@@ -21750,6 +22298,178 @@ Access: Free`,
21750
22298
  };
21751
22299
  }
21752
22300
  );
22301
+ registerTool(
22302
+ "skill",
22303
+ {
22304
+ title: "Manage reusable skills",
22305
+ description: `Manage and execute reusable skills (instruction + action bundles). Skills are portable across projects, sessions, and tools.
22306
+
22307
+ Actions:
22308
+ - list: Browse skills (filter by scope, status, category)
22309
+ - get: Get skill details by ID or name
22310
+ - create: Define a new skill with name, instruction, and triggers
22311
+ - update: Modify an existing skill
22312
+ - run: Execute a skill (by ID or name)
22313
+ - delete: Remove a skill
22314
+ - import: Import skills from file or content (supports markdown, JSON, cursorrules, claude_md)
22315
+ - export: Export skills in various formats
22316
+ - share: Change skill visibility scope`,
22317
+ inputSchema: external_exports.object({
22318
+ action: external_exports.enum(["list", "get", "create", "update", "run", "delete", "import", "export", "share"]).describe("The action to perform"),
22319
+ skill_id: external_exports.string().optional().describe("Skill ID (UUID)"),
22320
+ name: external_exports.string().optional().describe("Skill name (slug, e.g. 'deploy-checker')"),
22321
+ title: external_exports.string().optional().describe("Skill display title"),
22322
+ description: external_exports.string().optional().describe("Skill description"),
22323
+ instruction_body: external_exports.string().optional().describe("Markdown instruction text (the prompt)"),
22324
+ trigger_patterns: external_exports.array(external_exports.string()).optional().describe("Keywords/phrases for auto-activation"),
22325
+ trigger_regex: external_exports.string().optional().describe("Optional regex for advanced trigger matching"),
22326
+ categories: external_exports.array(external_exports.string()).optional().describe("Tags for discovery/filtering"),
22327
+ actions: external_exports.any().optional().describe("Action steps array [{type, tool, params, ...}]"),
22328
+ params: external_exports.any().optional().describe("Parameters passed to skill execution"),
22329
+ dry_run: external_exports.boolean().optional().describe("Preview execution without running"),
22330
+ scope: external_exports.enum(["personal", "team", "public", "all"]).optional().describe("Visibility scope"),
22331
+ status: external_exports.enum(["active", "draft", "archived"]).optional().describe("Skill status"),
22332
+ is_personal: external_exports.boolean().optional().describe("Whether skill is personal"),
22333
+ priority: external_exports.number().optional().describe("Skill priority 0-100 (higher = matched first)"),
22334
+ content: external_exports.string().optional().describe("Content string for import"),
22335
+ file_path: external_exports.string().optional().describe("Local file path for import"),
22336
+ format: external_exports.enum(["auto", "json", "markdown", "skills_md", "cursorrules", "claude_md", "aider", "zip"]).optional().describe("Import/export format"),
22337
+ source_tool: external_exports.string().optional().describe("Source tool name (for import provenance)"),
22338
+ source_file: external_exports.string().optional().describe("Source filename (for import provenance)"),
22339
+ skill_ids: external_exports.array(external_exports.string()).optional().describe("Skill IDs for export"),
22340
+ change_summary: external_exports.string().optional().describe("Summary of changes (for version history)"),
22341
+ workspace_id: external_exports.string().optional().describe("Workspace ID (UUID)"),
22342
+ project_id: external_exports.string().optional().describe("Project ID (UUID)"),
22343
+ query: external_exports.string().optional().describe("Search query"),
22344
+ category: external_exports.string().optional().describe("Filter by category tag"),
22345
+ limit: external_exports.number().optional().describe("Max results to return")
22346
+ })
22347
+ },
22348
+ async (input) => {
22349
+ const action = input.action;
22350
+ switch (action) {
22351
+ case "list": {
22352
+ const result = await client.listSkills({
22353
+ workspace_id: input.workspace_id,
22354
+ project_id: input.project_id,
22355
+ scope: input.scope,
22356
+ status: input.status,
22357
+ category: input.category,
22358
+ query: input.query,
22359
+ is_personal: input.is_personal,
22360
+ limit: input.limit
22361
+ });
22362
+ const items = result.items || [];
22363
+ let text = `Found ${items.length} skill(s).
22364
+ `;
22365
+ for (const item of items) {
22366
+ const name = item.name || "?";
22367
+ const title = item.title || "?";
22368
+ const scope = item.scope || "?";
22369
+ const status = item.status || "?";
22370
+ const id = item.id || "?";
22371
+ text += `- ${title} (${name}) [${scope}|${status}] id=${id}
22372
+ `;
22373
+ }
22374
+ return { content: [{ type: "text", text }] };
22375
+ }
22376
+ case "get": {
22377
+ const resolvedId = await resolveSkillIdForAction(client, input, "get");
22378
+ const skillData = await client.getSkill(resolvedId);
22379
+ const detail = formatSkillDetail(skillData);
22380
+ return { content: [{ type: "text", text: detail }] };
22381
+ }
22382
+ case "create": {
22383
+ if (!input.name) throw new Error("'name' is required for create");
22384
+ if (!input.instruction_body) throw new Error("'instruction_body' is required for create");
22385
+ const result = await client.createSkill({
22386
+ name: input.name,
22387
+ title: input.title || input.name,
22388
+ instruction_body: input.instruction_body,
22389
+ description: input.description,
22390
+ trigger_patterns: input.trigger_patterns,
22391
+ trigger_regex: input.trigger_regex,
22392
+ categories: input.categories,
22393
+ actions: input.actions,
22394
+ scope: input.scope,
22395
+ is_personal: input.is_personal,
22396
+ priority: input.priority,
22397
+ workspace_id: input.scope === "team" ? input.workspace_id : void 0,
22398
+ project_id: void 0,
22399
+ // Skills are account-level by default
22400
+ source_tool: input.source_tool,
22401
+ source_file: input.source_file
22402
+ });
22403
+ return { content: [{ type: "text", text: `Skill '${input.name}' created (id=${result.id || "?"}).` }] };
22404
+ }
22405
+ case "update": {
22406
+ const resolvedId = await resolveSkillIdForAction(client, input, "update");
22407
+ const result = await client.updateSkill(resolvedId, {
22408
+ title: input.title,
22409
+ description: input.description,
22410
+ instruction_body: input.instruction_body,
22411
+ trigger_patterns: input.trigger_patterns,
22412
+ trigger_regex: input.trigger_regex,
22413
+ categories: input.categories,
22414
+ actions: input.actions,
22415
+ scope: input.scope,
22416
+ status: input.status,
22417
+ is_personal: input.is_personal,
22418
+ priority: input.priority,
22419
+ change_summary: input.change_summary
22420
+ });
22421
+ return { content: [{ type: "text", text: `Skill ${resolvedId} updated (version=${result.version || 0}).` }] };
22422
+ }
22423
+ case "run": {
22424
+ const resolvedId = await resolveSkillIdForAction(client, input, "run");
22425
+ const result = await client.runSkill(resolvedId, {
22426
+ params: input.params,
22427
+ dry_run: input.dry_run
22428
+ });
22429
+ return { content: [{ type: "text", text: formatRunResult(result) }] };
22430
+ }
22431
+ case "delete": {
22432
+ const resolvedId = await resolveSkillIdForAction(client, input, "delete");
22433
+ await client.deleteSkill(resolvedId);
22434
+ return { content: [{ type: "text", text: `Skill ${resolvedId} deleted.` }] };
22435
+ }
22436
+ case "import": {
22437
+ let importContent = input.content;
22438
+ if (!importContent && input.file_path) {
22439
+ const { readFile: readFile4 } = await import("fs/promises");
22440
+ importContent = await readFile4(input.file_path, "utf-8");
22441
+ }
22442
+ if (!importContent) throw new Error("Either 'content' or 'file_path' is required for import");
22443
+ const result = await client.importSkills({
22444
+ content: importContent,
22445
+ format: input.format,
22446
+ source_tool: input.source_tool,
22447
+ source_file: input.source_file || input.file_path,
22448
+ scope: input.scope,
22449
+ workspace_id: input.workspace_id
22450
+ });
22451
+ return { content: [{ type: "text", text: `Import complete: ${result.imported || 0} imported, ${result.skipped || 0} skipped (duplicates).` }] };
22452
+ }
22453
+ case "export": {
22454
+ const result = await client.exportSkills({
22455
+ skill_ids: input.skill_ids,
22456
+ format: input.format,
22457
+ scope: input.scope,
22458
+ workspace_id: input.workspace_id
22459
+ });
22460
+ return { content: [{ type: "text", text: result.content || JSON.stringify(result, null, 2) }] };
22461
+ }
22462
+ case "share": {
22463
+ if (!input.scope) throw new Error("'scope' is required for share");
22464
+ const resolvedId = await resolveSkillIdForAction(client, input, "share");
22465
+ const result = await client.shareSkill(resolvedId, input.scope);
22466
+ return { content: [{ type: "text", text: `Skill ${resolvedId} shared with scope=${result.scope || input.scope}.` }] };
22467
+ }
22468
+ default:
22469
+ throw new Error(`Invalid skill action: '${action}'. Valid: list, get, create, update, run, delete, import, export, share`);
22470
+ }
22471
+ }
22472
+ );
21753
22473
  registerTool(
21754
22474
  "memory_bulk_ingest",
21755
22475
  {
@@ -21772,15 +22492,24 @@ Access: Free`,
21772
22492
  "memory_list_events",
21773
22493
  {
21774
22494
  title: "List memory events",
21775
- description: "List memory events (optionally scoped)",
22495
+ description: "List memory events (optionally scoped). Supports tag-based and event_type filtering for precise provenance tracking.",
21776
22496
  inputSchema: external_exports.object({
21777
22497
  workspace_id: external_exports.string().uuid().optional(),
21778
22498
  project_id: external_exports.string().uuid().optional(),
21779
- limit: external_exports.number().optional()
22499
+ limit: external_exports.number().optional(),
22500
+ tags: external_exports.array(external_exports.string()).optional().describe("Filter events that contain ALL of these tags"),
22501
+ event_type: external_exports.string().optional().describe("Filter by event type (e.g. decision, lesson, manual_note)")
21780
22502
  })
21781
22503
  },
21782
22504
  async (input) => {
21783
22505
  const result = await client.listMemoryEvents(input);
22506
+ if (input.tags && input.tags.length > 0 && result.items) {
22507
+ const requiredTags = input.tags;
22508
+ result.items = result.items.filter((item) => {
22509
+ const itemTags = extractEventTags(item);
22510
+ return requiredTags.every((tag) => itemTags.includes(tag));
22511
+ });
22512
+ }
21784
22513
  return {
21785
22514
  content: [{ type: "text", text: formatContent(result) }]
21786
22515
  };
@@ -21834,16 +22563,24 @@ Access: Free`,
21834
22563
  "memory_search",
21835
22564
  {
21836
22565
  title: "Memory-aware search",
21837
- description: "Search memory events/notes",
22566
+ description: "Search memory events/notes. Supports optional tag-based pre-filtering.",
21838
22567
  inputSchema: external_exports.object({
21839
22568
  query: external_exports.string(),
21840
22569
  workspace_id: external_exports.string().uuid().optional(),
21841
22570
  project_id: external_exports.string().uuid().optional(),
21842
- limit: external_exports.number().optional()
22571
+ limit: external_exports.number().optional(),
22572
+ tags: external_exports.array(external_exports.string()).optional().describe("Filter results that contain ALL of these tags")
21843
22573
  })
21844
22574
  },
21845
22575
  async (input) => {
21846
22576
  const result = await client.memorySearch(input);
22577
+ if (input.tags && input.tags.length > 0 && result.results) {
22578
+ const requiredTags = input.tags;
22579
+ result.results = result.results.filter((item) => {
22580
+ const itemTags = extractEventTags(item);
22581
+ return requiredTags.every((tag) => itemTags.includes(tag));
22582
+ });
22583
+ }
21847
22584
  return {
21848
22585
  content: [{ type: "text", text: formatContent(result) }]
21849
22586
  };
@@ -23631,9 +24368,8 @@ Returns lessons filtered by:
23631
24368
  // Fetch more to filter
23632
24369
  });
23633
24370
  const lessons = (searchResult.results || []).filter((item) => {
23634
- const tags = item.metadata?.tags || [];
23635
- const isLesson = tags.includes("lesson");
23636
- if (!isLesson) return false;
24371
+ if (!isLessonResult(item)) return false;
24372
+ const tags = extractEventTags(item);
23637
24373
  if (input.category && !tags.includes(input.category)) {
23638
24374
  return false;
23639
24375
  }
@@ -23652,7 +24388,7 @@ Returns lessons filtered by:
23652
24388
  };
23653
24389
  }
23654
24390
  const formattedLessons = lessons.map((lesson, i) => {
23655
- const tags = lesson.metadata?.tags || [];
24391
+ const tags = extractEventTags(lesson);
23656
24392
  const severity = tags.find((t) => t.startsWith("severity:"))?.split(":")[1] || "medium";
23657
24393
  const category = tags.find(
23658
24394
  (t) => [
@@ -23671,7 +24407,7 @@ Returns lessons filtered by:
23671
24407
  }[severity] || "\u26AA";
23672
24408
  return `${i + 1}. ${severityEmoji} **${lesson.title}**
23673
24409
  Category: ${category} | Severity: ${severity}
23674
- ${lesson.content?.slice(0, 200)}...`;
24410
+ ${lesson.content?.slice(0, 500)}...`;
23675
24411
  }).join("\n\n");
23676
24412
  return {
23677
24413
  content: [
@@ -23903,6 +24639,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
23903
24639
  ).length;
23904
24640
  const skippedCount = results.filter((r) => r.status.startsWith("skipped")).length;
23905
24641
  const baseMessage = input.dry_run ? "Dry run complete. Use dry_run: false to write files." : skippedCount > 0 ? `Generated ${createdCount} rule files. ${skippedCount} skipped (existing files). Re-run with overwrite_existing: true to replace ContextStream blocks.` : `Generated ${createdCount} rule files.`;
24642
+ const copilotSetupNote = editors.includes("copilot") ? "Select Copilot in setup; setup handles both ~/.copilot/mcp-config.json and project .vscode/mcp.json automatically." : void 0;
23906
24643
  const globalPrompt = input.apply_global ? "Global rule update complete." : globalTargets.length > 0 ? "Apply rules globally too? Re-run with apply_global: true." : "No global rule locations are known for these editors.";
23907
24644
  let hooksResults;
23908
24645
  let hooksPrompt;
@@ -23911,7 +24648,8 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
23911
24648
  cline: "cline",
23912
24649
  roo: "roo",
23913
24650
  kilo: "kilo",
23914
- cursor: "cursor"
24651
+ cursor: "cursor",
24652
+ windsurf: "windsurf"
23915
24653
  };
23916
24654
  const hookSupportedEditors = editors.filter((e) => e in editorHookMap);
23917
24655
  const shouldInstallHooks = hookSupportedEditors.length > 0 && input.install_hooks !== false;
@@ -23950,6 +24688,10 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
23950
24688
  { editor, file: "~/.cursor/hooks/contextstream-beforesubmit.py", status: "dry run - would create" },
23951
24689
  { editor, file: "~/.cursor/hooks.json", status: "dry run - would update" }
23952
24690
  );
24691
+ } else if (editor === "windsurf") {
24692
+ hooksResults.push(
24693
+ { editor, file: "~/.codeium/windsurf/hooks.json", status: "dry run - would update" }
24694
+ );
23953
24695
  }
23954
24696
  }
23955
24697
  } else {
@@ -23978,6 +24720,7 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
23978
24720
  ...globalTargets.length > 0 ? { global_targets: globalTargets } : {},
23979
24721
  ...hooksResults ? { hooks_results: hooksResults } : {},
23980
24722
  message: baseMessage,
24723
+ ...copilotSetupNote ? { copilot_setup_note: copilotSetupNote } : {},
23981
24724
  global_prompt: globalPrompt,
23982
24725
  ...hooksPrompt ? { hooks_prompt: hooksPrompt } : {}
23983
24726
  };
@@ -24068,7 +24811,10 @@ Supported editors: ${getAvailableEditors().join(", ")}`,
24068
24811
  const summary = {
24069
24812
  folder: folderPath,
24070
24813
  results,
24071
- message: input.dry_run ? "Dry run complete. Use dry_run: false to write files." : skippedCount > 0 ? `Generated ${createdCount} rule files. ${skippedCount} skipped (existing files). Re-run with overwrite_existing: true to replace ContextStream blocks.` : `Generated ${createdCount} rule files.`
24814
+ message: input.dry_run ? "Dry run complete. Use dry_run: false to write files." : skippedCount > 0 ? `Generated ${createdCount} rule files. ${skippedCount} skipped (existing files). Re-run with overwrite_existing: true to replace ContextStream blocks.` : `Generated ${createdCount} rule files.`,
24815
+ ...editors.includes("copilot") ? {
24816
+ copilot_setup_note: "Select Copilot in setup; setup handles both ~/.copilot/mcp-config.json and project .vscode/mcp.json automatically."
24817
+ } : {}
24072
24818
  };
24073
24819
  return {
24074
24820
  content: [{ type: "text", text: formatContent(summary) }]
@@ -30226,6 +30972,7 @@ This will:
30226
30972
  - Start a 5-day Pro trial
30227
30973
  - Auto-configure your editor's MCP settings
30228
30974
  - Write rules files for better AI assistance
30975
+ - If you select Copilot, setup automatically writes both ~/.copilot/mcp-config.json and project .vscode/mcp.json
30229
30976
 
30230
30977
  Preview first:
30231
30978
  npx --prefer-online -y @contextstream/mcp-server@latest setup --dry-run
@@ -32067,9 +32814,11 @@ init_hooks_config();
32067
32814
  init_files();
32068
32815
  var EDITOR_LABELS = {
32069
32816
  codex: "Codex CLI",
32817
+ copilot: "GitHub Copilot (VS Code)",
32070
32818
  opencode: "OpenCode",
32071
32819
  claude: "Claude Code",
32072
32820
  cursor: "Cursor / VS Code",
32821
+ windsurf: "Windsurf",
32073
32822
  cline: "Cline",
32074
32823
  kilo: "Kilo Code",
32075
32824
  roo: "Roo Code",
@@ -32077,7 +32826,7 @@ var EDITOR_LABELS = {
32077
32826
  antigravity: "Antigravity (Google)"
32078
32827
  };
32079
32828
  function supportsProjectMcpConfig(editor) {
32080
- return editor === "opencode" || editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo" || editor === "antigravity";
32829
+ return editor === "opencode" || editor === "copilot" || editor === "cursor" || editor === "claude" || editor === "kilo" || editor === "roo";
32081
32830
  }
32082
32831
  function normalizeInput(value) {
32083
32832
  return value.trim();
@@ -32148,6 +32897,7 @@ var CONTEXTSTREAM_PREAMBLE_PATTERNS2 = [
32148
32897
  /^#\s+codex cli instructions$/i,
32149
32898
  /^#\s+claude code instructions$/i,
32150
32899
  /^#\s+cursor rules$/i,
32900
+ /^#\s+windsurf rules$/i,
32151
32901
  /^#\s+cline rules$/i,
32152
32902
  /^#\s+kilo code rules$/i,
32153
32903
  /^#\s+roo code rules$/i,
@@ -32288,6 +33038,10 @@ function globalRulesPathForEditor(editor) {
32288
33038
  switch (editor) {
32289
33039
  case "codex":
32290
33040
  return path8.join(home, ".codex", "AGENTS.md");
33041
+ case "copilot":
33042
+ return null;
33043
+ case "opencode":
33044
+ return path8.join(home, ".opencode", "AGENTS.md");
32291
33045
  case "claude":
32292
33046
  return path8.join(home, ".claude", "CLAUDE.md");
32293
33047
  case "cline":
@@ -32302,6 +33056,8 @@ function globalRulesPathForEditor(editor) {
32302
33056
  return path8.join(home, ".gemini", "GEMINI.md");
32303
33057
  case "cursor":
32304
33058
  return null;
33059
+ case "windsurf":
33060
+ return path8.join(home, ".codeium", "windsurf", "memories", "global_rules.md");
32305
33061
  default:
32306
33062
  return null;
32307
33063
  }
@@ -32367,6 +33123,16 @@ async function isClineInstalled() {
32367
33123
  ];
32368
33124
  return anyPathExists(candidates);
32369
33125
  }
33126
+ async function isCopilotInstalled() {
33127
+ const home = homedir6();
33128
+ const candidates = [
33129
+ path8.join(home, ".copilot"),
33130
+ path8.join(home, ".config", "github-copilot"),
33131
+ path8.join(home, ".vscode", "extensions", "github.copilot-chat"),
33132
+ path8.join(home, ".vscode", "extensions", "github.copilot")
33133
+ ];
33134
+ return anyPathExists(candidates);
33135
+ }
32370
33136
  async function isKiloInstalled() {
32371
33137
  const home = homedir6();
32372
33138
  const candidates = [path8.join(home, ".kilocode"), path8.join(home, ".config", "kilocode")];
@@ -32405,6 +33171,29 @@ async function isCursorInstalled() {
32405
33171
  }
32406
33172
  return anyPathExists(candidates);
32407
33173
  }
33174
+ async function isWindsurfInstalled() {
33175
+ const home = homedir6();
33176
+ const candidates = [path8.join(home, ".codeium", "windsurf")];
33177
+ if (process.platform === "darwin") {
33178
+ candidates.push("/Applications/Windsurf.app");
33179
+ candidates.push(path8.join(home, "Applications", "Windsurf.app"));
33180
+ candidates.push(path8.join(home, "Library", "Application Support", "Windsurf"));
33181
+ } else if (process.platform === "win32") {
33182
+ const localApp = process.env.LOCALAPPDATA;
33183
+ const programFiles = process.env.ProgramFiles;
33184
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
33185
+ if (localApp) candidates.push(path8.join(localApp, "Programs", "Windsurf", "Windsurf.exe"));
33186
+ if (localApp) candidates.push(path8.join(localApp, "Windsurf", "Windsurf.exe"));
33187
+ if (programFiles) candidates.push(path8.join(programFiles, "Windsurf", "Windsurf.exe"));
33188
+ if (programFilesX86) candidates.push(path8.join(programFilesX86, "Windsurf", "Windsurf.exe"));
33189
+ } else {
33190
+ candidates.push("/usr/bin/windsurf");
33191
+ candidates.push("/usr/local/bin/windsurf");
33192
+ candidates.push("/opt/Windsurf");
33193
+ candidates.push("/opt/windsurf");
33194
+ }
33195
+ return anyPathExists(candidates);
33196
+ }
32408
33197
  async function isAntigravityInstalled() {
32409
33198
  const home = homedir6();
32410
33199
  const candidates = [path8.join(home, ".gemini")];
@@ -32434,12 +33223,16 @@ async function isEditorInstalled(editor) {
32434
33223
  switch (editor) {
32435
33224
  case "codex":
32436
33225
  return isCodexInstalled();
33226
+ case "copilot":
33227
+ return isCopilotInstalled();
32437
33228
  case "opencode":
32438
33229
  return isOpenCodeInstalled();
32439
33230
  case "claude":
32440
33231
  return isClaudeInstalled();
32441
33232
  case "cursor":
32442
33233
  return isCursorInstalled();
33234
+ case "windsurf":
33235
+ return isWindsurfInstalled();
32443
33236
  case "cline":
32444
33237
  return isClineInstalled();
32445
33238
  case "kilo":
@@ -33239,7 +34032,7 @@ Code: ${device.user_code}`);
33239
34032
  );
33240
34033
  }
33241
34034
  }
33242
- const NO_HOOKS_EDITORS2 = ["codex", "opencode", "aider", "antigravity"];
34035
+ const NO_HOOKS_EDITORS2 = ["codex", "copilot", "opencode", "aider", "antigravity"];
33243
34036
  const getModeForEditor = (editor) => NO_HOOKS_EDITORS2.includes(editor) ? "full" : "bootstrap";
33244
34037
  const detectedPlanName = await client.getPlanName();
33245
34038
  const detectedGraphTier = await client.getGraphTier();
@@ -33266,9 +34059,11 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33266
34059
  }
33267
34060
  const editors = [
33268
34061
  "codex",
34062
+ "copilot",
33269
34063
  "opencode",
33270
34064
  "claude",
33271
34065
  "cursor",
34066
+ "windsurf",
33272
34067
  "cline",
33273
34068
  "kilo",
33274
34069
  "roo",
@@ -33338,6 +34133,28 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33338
34133
  )
33339
34134
  ) || mcpChoiceDefault;
33340
34135
  const mcpScope = mcpChoice === "2" && hasCodex && !hasProjectMcpEditors ? "skip" : mcpChoice === "4" ? "skip" : mcpChoice === "1" ? "global" : mcpChoice === "2" ? "project" : "both";
34136
+ const copilotSelected = configuredEditors.includes("copilot");
34137
+ let enforceCopilotCanonicalPair = copilotSelected && mcpScope !== "skip";
34138
+ if (enforceCopilotCanonicalPair) {
34139
+ const confirmCopilotPair = normalizeInput(
34140
+ await rl.question(
34141
+ "Configure Copilot canonical MCP files (~/.copilot/mcp-config.json + .vscode/mcp.json)? [Y/n]: "
34142
+ )
34143
+ ).toLowerCase();
34144
+ enforceCopilotCanonicalPair = confirmCopilotPair !== "n" && confirmCopilotPair !== "no";
34145
+ }
34146
+ if (enforceCopilotCanonicalPair) {
34147
+ console.log(
34148
+ "\nCopilot automation enabled: setup will write both ~/.copilot/mcp-config.json and project .vscode/mcp.json (no separate Copilot command needed)."
34149
+ );
34150
+ console.log(
34151
+ "If you installed the Rust runtime, replace the generated command with `contextstream-mcp` in both files."
34152
+ );
34153
+ } else if (copilotSelected && mcpScope !== "skip") {
34154
+ console.log(
34155
+ "\nCopilot canonical pair disabled for this run. You can rerun setup anytime to generate both files together."
34156
+ );
34157
+ }
33341
34158
  const mcpServer = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
33342
34159
  const mcpServerClaude = buildContextStreamMcpServer({ apiUrl, apiKey, contextPackEnabled });
33343
34160
  const mcpServerOpenCode = buildContextStreamOpenCodeLocalServer({
@@ -33354,7 +34171,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33354
34171
  " OpenCode reads CONTEXTSTREAM_API_KEY from your environment. Export it before launching OpenCode."
33355
34172
  );
33356
34173
  };
33357
- const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex;
34174
+ const needsGlobalMcpConfig = mcpScope === "global" || mcpScope === "both" || mcpScope === "project" && hasCodex || enforceCopilotCanonicalPair;
33358
34175
  if (needsGlobalMcpConfig) {
33359
34176
  console.log("\nInstalling global MCP config...");
33360
34177
  for (const editor of configuredEditors) {
@@ -33376,6 +34193,18 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33376
34193
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
33377
34194
  continue;
33378
34195
  }
34196
+ if (editor === "copilot") {
34197
+ const filePath = path8.join(homedir6(), ".copilot", "mcp-config.json");
34198
+ if (dryRun) {
34199
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
34200
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
34201
+ continue;
34202
+ }
34203
+ const status = await upsertJsonMcpConfig(filePath, mcpServer);
34204
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
34205
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
34206
+ continue;
34207
+ }
33379
34208
  if (editor === "claude") {
33380
34209
  const desktopPath = claudeDesktopConfigPath();
33381
34210
  if (desktopPath) {
@@ -33431,6 +34260,18 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33431
34260
  console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
33432
34261
  continue;
33433
34262
  }
34263
+ if (editor === "windsurf") {
34264
+ const filePath = path8.join(homedir6(), ".codeium", "windsurf", "mcp_config.json");
34265
+ if (dryRun) {
34266
+ writeActions.push({ kind: "mcp-config", target: filePath, status: "dry-run" });
34267
+ console.log(`- ${EDITOR_LABELS[editor]}: would update ${filePath}`);
34268
+ continue;
34269
+ }
34270
+ const status = await upsertJsonMcpConfig(filePath, mcpServer);
34271
+ writeActions.push({ kind: "mcp-config", target: filePath, status });
34272
+ console.log(`- ${EDITOR_LABELS[editor]}: ${status} ${filePath}`);
34273
+ continue;
34274
+ }
33434
34275
  if (editor === "cline") {
33435
34276
  console.log(
33436
34277
  `- ${EDITOR_LABELS[editor]}: MCP config is managed via the extension UI (skipping global).`
@@ -33456,11 +34297,14 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33456
34297
  const HOOKS_SUPPORTED_EDITORS = {
33457
34298
  claude: "claude",
33458
34299
  cursor: "cursor",
34300
+ windsurf: "windsurf",
33459
34301
  cline: "cline",
33460
34302
  roo: "roo",
33461
34303
  kilo: "kilo",
33462
34304
  codex: null,
33463
34305
  // No hooks API
34306
+ copilot: null,
34307
+ // No hooks API
33464
34308
  opencode: null,
33465
34309
  // No hooks API
33466
34310
  aider: null,
@@ -33489,6 +34333,16 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33489
34333
  writeActions.push({ kind: "hooks", target: script, status: "created" });
33490
34334
  console.log(`- ${EDITOR_LABELS[editor]}: installed ${path8.basename(script)}`);
33491
34335
  }
34336
+ if (editor === "cursor") {
34337
+ console.log(
34338
+ " Cursor hooks enabled: preToolUse, beforeSubmitPrompt, before/after MCP+shell, beforeReadFile, afterFileEdit, session lifecycle"
34339
+ );
34340
+ }
34341
+ if (editor === "windsurf") {
34342
+ console.log(
34343
+ " Windsurf hooks enabled: pre_mcp_tool_use, pre_user_prompt, pre/post code+command hooks, post_cascade_response_with_transcript"
34344
+ );
34345
+ }
33492
34346
  } catch (err) {
33493
34347
  const message = err instanceof Error ? err.message : String(err);
33494
34348
  console.log(`- ${EDITOR_LABELS[editor]}: failed to install hooks: ${message}`);
@@ -33496,6 +34350,19 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33496
34350
  }
33497
34351
  console.log(" Disable hooks anytime with CONTEXTSTREAM_HOOK_ENABLED=false");
33498
34352
  }
34353
+ const noHookEditors = configuredEditors.filter((e) => HOOKS_SUPPORTED_EDITORS[e] === null);
34354
+ if (noHookEditors.length > 0) {
34355
+ console.log("\nEditors without lifecycle hooks (rules-only enforcement):");
34356
+ for (const editor of noHookEditors) {
34357
+ if (editor === "antigravity") {
34358
+ console.log(
34359
+ `- ${EDITOR_LABELS[editor]}: no hooks API available; relying on strict ContextStream rules + index/search discipline.`
34360
+ );
34361
+ } else {
34362
+ console.log(`- ${EDITOR_LABELS[editor]}: no hooks API available; using rules-only enforcement.`);
34363
+ }
34364
+ }
34365
+ }
33499
34366
  console.log("\nCode Privacy:");
33500
34367
  console.log(" \u2713 Encrypted in transit (TLS 1.3) and at rest (AES-256)");
33501
34368
  console.log(" \u2713 Isolated per workspace \u2014 no cross-tenant access");
@@ -33503,12 +34370,6 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33503
34370
  if (scope === "global" || scope === "both") {
33504
34371
  console.log("\nInstalling global rules...");
33505
34372
  for (const editor of configuredEditors) {
33506
- if (editor === "opencode") {
33507
- console.log(
33508
- `- ${EDITOR_LABELS[editor]}: rules are not auto-generated yet (MCP config only).`
33509
- );
33510
- continue;
33511
- }
33512
34373
  const filePath = globalRulesPathForEditor(editor);
33513
34374
  if (!filePath) {
33514
34375
  console.log(
@@ -33539,7 +34400,7 @@ Detected plan: ${planLabel} (graph: ${graphTierLabel})`);
33539
34400
  }
33540
34401
  }
33541
34402
  const projectPaths = /* @__PURE__ */ new Set();
33542
- const needsProjects = scope === "project" || scope === "both" || (mcpScope === "project" || mcpScope === "both") && hasProjectMcpEditors;
34403
+ const needsProjects = scope === "project" || scope === "both" || (mcpScope === "project" || mcpScope === "both") && hasProjectMcpEditors || enforceCopilotCanonicalPair;
33543
34404
  if (needsProjects) {
33544
34405
  console.log("\nProject setup...");
33545
34406
  const addCwd = normalizeInput(
@@ -33622,8 +34483,10 @@ Applying to ${projects.length} project(s)...`);
33622
34483
  status: "dry-run"
33623
34484
  });
33624
34485
  }
33625
- if (mcpScope === "project" || mcpScope === "both") {
34486
+ if (mcpScope === "project" || mcpScope === "both" || enforceCopilotCanonicalPair) {
33626
34487
  for (const editor of configuredEditors) {
34488
+ const shouldWriteProjectMcp = mcpScope === "project" || mcpScope === "both" || enforceCopilotCanonicalPair && editor === "copilot";
34489
+ if (!shouldWriteProjectMcp) continue;
33627
34490
  try {
33628
34491
  if (editor === "cursor") {
33629
34492
  const cursorPath = path8.join(projectPath, ".cursor", "mcp.json");
@@ -33639,6 +34502,12 @@ Applying to ${projects.length} project(s)...`);
33639
34502
  }
33640
34503
  continue;
33641
34504
  }
34505
+ if (editor === "windsurf") {
34506
+ console.log(
34507
+ `- ${EDITOR_LABELS[editor]}: uses global MCP config only (${path8.join(homedir6(), ".codeium", "windsurf", "mcp_config.json")}).`
34508
+ );
34509
+ continue;
34510
+ }
33642
34511
  if (editor === "claude") {
33643
34512
  const mcpPath = path8.join(projectPath, ".mcp.json");
33644
34513
  if (dryRun) {
@@ -33664,6 +34533,16 @@ Applying to ${projects.length} project(s)...`);
33664
34533
  printOpenCodeEnvNote();
33665
34534
  continue;
33666
34535
  }
34536
+ if (editor === "copilot") {
34537
+ const vsCodePath = path8.join(projectPath, ".vscode", "mcp.json");
34538
+ if (dryRun) {
34539
+ writeActions.push({ kind: "mcp-config", target: vsCodePath, status: "dry-run" });
34540
+ } else {
34541
+ const status = await upsertJsonVsCodeMcpConfig(vsCodePath, vsCodeServer);
34542
+ writeActions.push({ kind: "mcp-config", target: vsCodePath, status });
34543
+ }
34544
+ continue;
34545
+ }
33667
34546
  if (editor === "kilo") {
33668
34547
  const kiloPath = path8.join(projectPath, ".kilocode", "mcp.json");
33669
34548
  if (dryRun) {
@@ -33695,7 +34574,6 @@ Applying to ${projects.length} project(s)...`);
33695
34574
  for (const editor of selectedEditors) {
33696
34575
  if (scope !== "project" && scope !== "both") continue;
33697
34576
  if (!configuredEditors.includes(editor)) continue;
33698
- if (editor === "opencode") continue;
33699
34577
  const rule = generateRuleContent(editor, {
33700
34578
  workspaceName,
33701
34579
  workspaceId: workspaceId && workspaceId !== "dry-run" ? workspaceId : void 0,
@@ -33819,6 +34697,16 @@ Applying to ${projects.length} project(s)...`);
33819
34697
  console.log(
33820
34698
  "- For UI-based MCP setup (Cline/Kilo/Roo global), see https://contextstream.io/docs/mcp"
33821
34699
  );
34700
+ if (copilotSelected) {
34701
+ const nodeCommand = IS_WINDOWS ? "cmd /c npx --prefer-online -y @contextstream/mcp-server@latest" : "npx --prefer-online -y @contextstream/mcp-server@latest";
34702
+ console.log("\nGitHub Copilot + VS Code checklist:");
34703
+ console.log("- Confirm ~/.copilot/mcp-config.json has a contextstream server entry.");
34704
+ console.log("- Confirm .vscode/mcp.json has a contextstream server entry.");
34705
+ console.log(`- Node runtime command: ${nodeCommand}`);
34706
+ console.log(
34707
+ "- If Rust MCP is installed, replace the command in both files with `contextstream-mcp` and set args to []."
34708
+ );
34709
+ }
33822
34710
  console.log("");
33823
34711
  console.log(
33824
34712
  "You're all set! ContextStream gives your AI persistent memory, semantic code search, and cross-session context."