@f5xc-salesdemos/xcsh 18.92.0 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Plan Mode Extension
3
3
  *
4
- * Provides a Claude Code-style "plan mode" for safe code exploration.
4
+ * Provides a xcsh-style "plan mode" for safe code exploration.
5
5
  * When enabled, the agent can only use read-only tools and cannot modify files.
6
6
  *
7
7
  * Features:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.92.0",
4
+ "version": "19.1.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -50,12 +50,12 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "18.92.0",
54
- "@f5xc-salesdemos/pi-agent-core": "18.92.0",
55
- "@f5xc-salesdemos/pi-ai": "18.92.0",
56
- "@f5xc-salesdemos/pi-natives": "18.92.0",
57
- "@f5xc-salesdemos/pi-tui": "18.92.0",
58
- "@f5xc-salesdemos/pi-utils": "18.92.0",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.1.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.1.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.1.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.1.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.1.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.1.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -31,10 +31,10 @@ export interface LoadResult<T> {
31
31
  * A provider that can load items for a capability.
32
32
  */
33
33
  export interface Provider<T> {
34
- /** Unique provider ID (e.g., "claude", "xcsh", "mcp-json", "agents-md") */
34
+ /** Unique provider ID (e.g., "xcsh", "mcp-json", "agents-md") */
35
35
  id: string;
36
36
 
37
- /** Human-readable name for UI display (e.g., "Claude Code", "OpenAI Codex") */
37
+ /** Human-readable name for UI display (e.g., "xcsh", "OpenAI Codex") */
38
38
  displayName: string;
39
39
 
40
40
  /** Short description for settings UI (e.g., "Load config from ~/.xcsh/ and .xcsh/") */
@@ -227,8 +227,8 @@ export const SETTINGS_SCHEMA = {
227
227
  disabledProviders: {
228
228
  type: "array",
229
229
  default: [
230
- "claude",
231
- "claude-plugins",
230
+ "xcsh",
231
+ "xcsh-plugins",
232
232
  "codex",
233
233
  "agents",
234
234
  "agents-md",
@@ -1639,32 +1639,32 @@ export const SETTINGS_SCHEMA = {
1639
1639
  },
1640
1640
  },
1641
1641
 
1642
- "skills.enableClaudeUser": {
1642
+ "skills.enableXcshUser": {
1643
1643
  type: "boolean",
1644
1644
  default: false,
1645
1645
  ui: {
1646
1646
  tab: "tasks",
1647
- label: "Claude User Skills",
1648
- description: "Load skills from ~/.xcsh/agent/skills/ (legacy Claude Code compatibility)",
1647
+ label: "xcsh User Skills",
1648
+ description: "Load skills from ~/.xcsh/agent/skills/",
1649
1649
  },
1650
1650
  },
1651
1651
 
1652
- "skills.enableClaudeProject": {
1652
+ "skills.enableXcshProject": {
1653
1653
  type: "boolean",
1654
1654
  default: false,
1655
1655
  ui: {
1656
1656
  tab: "tasks",
1657
- label: "Claude Project Skills",
1658
- description: "Load skills from .xcsh/skills/ (legacy Claude Code compatibility)",
1657
+ label: "xcsh Project Skills",
1658
+ description: "Load skills from .xcsh/skills/",
1659
1659
  },
1660
1660
  },
1661
1661
 
1662
- "skills.enableClaudePlugins": {
1662
+ "skills.enableXcshPlugins": {
1663
1663
  type: "boolean",
1664
1664
  default: false,
1665
1665
  ui: {
1666
1666
  tab: "tasks",
1667
- label: "Claude Marketplace Skills",
1667
+ label: "xcsh Marketplace Skills",
1668
1668
  description: "Load skills from marketplace plugins (~/.xcsh/plugins/cache/)",
1669
1669
  },
1670
1670
  },
@@ -1696,16 +1696,16 @@ export const SETTINGS_SCHEMA = {
1696
1696
  "skills.includeSkills": { type: "array", default: [] as string[] },
1697
1697
 
1698
1698
  // Commands
1699
- "commands.enableClaudeUser": {
1699
+ "commands.enableXcshUser": {
1700
1700
  type: "boolean",
1701
1701
  default: true,
1702
- ui: { tab: "tasks", label: "Claude User Commands", description: "Load commands from ~/.xcsh/agent/commands/" },
1702
+ ui: { tab: "tasks", label: "xcsh User Commands", description: "Load commands from ~/.xcsh/agent/commands/" },
1703
1703
  },
1704
1704
 
1705
- "commands.enableClaudeProject": {
1705
+ "commands.enableXcshProject": {
1706
1706
  type: "boolean",
1707
1707
  default: true,
1708
- ui: { tab: "tasks", label: "Claude Project Commands", description: "Load commands from .xcsh/commands/" },
1708
+ ui: { tab: "tasks", label: "xcsh Project Commands", description: "Load commands from .xcsh/commands/" },
1709
1709
  },
1710
1710
 
1711
1711
  // ────────────────────────────────────────────────────────────────────────
@@ -2011,9 +2011,9 @@ export interface SkillsSettings {
2011
2011
  enabled?: boolean;
2012
2012
  enableSkillCommands?: boolean;
2013
2013
  enableCodexUser?: boolean;
2014
- enableClaudeUser?: boolean;
2015
- enableClaudeProject?: boolean;
2016
- enableClaudePlugins?: boolean;
2014
+ enableXcshUser?: boolean;
2015
+ enableXcshProject?: boolean;
2016
+ enableXcshPlugins?: boolean;
2017
2017
  enablePiUser?: boolean;
2018
2018
  enablePiProject?: boolean;
2019
2019
  customDirectories?: string[];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Claude Code Marketplace Plugin Provider
2
+ * xcsh Marketplace Plugin Provider
3
3
  *
4
4
  * Loads configuration from ~/.xcsh/plugins/cache/ based on installed_plugins.json registry.
5
5
  * Priority: 70 (below claude.ts at 80, so user overrides in .xcsh/ take precedence)
@@ -15,17 +15,17 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
15
15
  import { type CustomTool, toolCapability } from "../capability/tool";
16
16
  import type { LoadContext, LoadResult } from "../capability/types";
17
17
  import {
18
- type ClaudePluginRoot,
19
18
  createSourceMeta,
20
- listClaudePluginRoots,
19
+ listXcshPluginRoots,
21
20
  loadFilesFromDir,
22
21
  scanSkillsFromDir,
22
+ type XcshPluginRoot,
23
23
  } from "./helpers";
24
24
 
25
25
  import { substitutePluginRoot } from "./substitute-plugin-root";
26
26
 
27
- const PROVIDER_ID = "claude-plugins";
28
- const DISPLAY_NAME = "Claude Code Marketplace";
27
+ const PROVIDER_ID = "xcsh-plugins";
28
+ const DISPLAY_NAME = "xcsh Marketplace";
29
29
  const PRIORITY = 70; // Below claude.ts (80) so user .xcsh/ overrides win
30
30
 
31
31
  // =============================================================================
@@ -36,7 +36,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
36
36
  const items: Skill[] = [];
37
37
  const warnings: string[] = [];
38
38
 
39
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
39
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
40
40
  warnings.push(...rootWarnings);
41
41
 
42
42
  const results = await Promise.all(
@@ -70,7 +70,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
70
70
  const items: SlashCommand[] = [];
71
71
  const warnings: string[] = [];
72
72
 
73
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
73
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
74
74
  warnings.push(...rootWarnings);
75
75
 
76
76
  const results = await Promise.all(
@@ -108,12 +108,12 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
108
108
  const items: Hook[] = [];
109
109
  const warnings: string[] = [];
110
110
 
111
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
111
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
112
112
  warnings.push(...rootWarnings);
113
113
 
114
114
  const hookTypes = ["pre", "post"] as const;
115
115
 
116
- const loadTasks: { root: ClaudePluginRoot; hookType: "pre" | "post" }[] = [];
116
+ const loadTasks: { root: XcshPluginRoot; hookType: "pre" | "post" }[] = [];
117
117
  for (const root of roots) {
118
118
  for (const hookType of hookTypes) {
119
119
  loadTasks.push({ root, hookType });
@@ -155,7 +155,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
155
155
  const items: CustomTool[] = [];
156
156
  const warnings: string[] = [];
157
157
 
158
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
158
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
159
159
  warnings.push(...rootWarnings);
160
160
 
161
161
  const results = await Promise.all(
@@ -192,7 +192,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
192
192
  const items: MCPServer[] = [];
193
193
  const warnings: string[] = [];
194
194
 
195
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
195
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
196
196
  warnings.push(...rootWarnings);
197
197
 
198
198
  for (const root of roots) {
@@ -258,7 +258,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
258
258
  registerProvider<Skill>(skillCapability.id, {
259
259
  id: PROVIDER_ID,
260
260
  displayName: DISPLAY_NAME,
261
- description: "Load skills from Claude Code marketplace plugins (~/.xcsh/plugins/cache/)",
261
+ description: "Load skills from xcsh marketplace plugins (~/.xcsh/plugins/cache/)",
262
262
  priority: PRIORITY,
263
263
  load: loadSkills,
264
264
  });
@@ -266,7 +266,7 @@ registerProvider<Skill>(skillCapability.id, {
266
266
  registerProvider<SlashCommand>(slashCommandCapability.id, {
267
267
  id: PROVIDER_ID,
268
268
  displayName: DISPLAY_NAME,
269
- description: "Load slash commands from Claude Code marketplace plugins",
269
+ description: "Load slash commands from xcsh marketplace plugins",
270
270
  priority: PRIORITY,
271
271
  load: loadSlashCommands,
272
272
  });
@@ -274,7 +274,7 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
274
274
  registerProvider<Hook>(hookCapability.id, {
275
275
  id: PROVIDER_ID,
276
276
  displayName: DISPLAY_NAME,
277
- description: "Load hooks from Claude Code marketplace plugins",
277
+ description: "Load hooks from xcsh marketplace plugins",
278
278
  priority: PRIORITY,
279
279
  load: loadHooks,
280
280
  });
@@ -282,7 +282,7 @@ registerProvider<Hook>(hookCapability.id, {
282
282
  registerProvider<CustomTool>(toolCapability.id, {
283
283
  id: PROVIDER_ID,
284
284
  displayName: DISPLAY_NAME,
285
- description: "Load custom tools from Claude Code marketplace plugins",
285
+ description: "Load custom tools from xcsh marketplace plugins",
286
286
  priority: PRIORITY,
287
287
  load: loadTools,
288
288
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Claude Code Provider
2
+ * xcsh Provider
3
3
  *
4
4
  * Loads configuration from .xcsh directories.
5
5
  * Priority: 80 (tool-specific, below builtin but above shared standards)
@@ -28,8 +28,8 @@ import {
28
28
  scanSkillsFromDir,
29
29
  } from "./helpers";
30
30
 
31
- const PROVIDER_ID = "claude";
32
- const DISPLAY_NAME = "Claude Code";
31
+ const PROVIDER_ID = "xcsh";
32
+ const DISPLAY_NAME = "xcsh";
33
33
  const PRIORITY = 80;
34
34
  const CONFIG_DIR = ".xcsh";
35
35
 
@@ -583,13 +583,13 @@ export function getExtensionNameFromPath(extensionPath: string): string {
583
583
  }
584
584
 
585
585
  // =============================================================================
586
- // Claude Code Plugin Cache Helpers
586
+ // xcsh Plugin Cache Helpers
587
587
  // =============================================================================
588
588
 
589
589
  /**
590
- * Entry for an installed Claude Code plugin.
590
+ * Entry for an installed xcsh plugin.
591
591
  */
592
- export interface ClaudePluginEntry {
592
+ export interface XcshPluginEntry {
593
593
  scope: "user" | "project";
594
594
  installPath: string;
595
595
  version: string;
@@ -600,17 +600,17 @@ export interface ClaudePluginEntry {
600
600
  }
601
601
 
602
602
  /**
603
- * Claude Code installed_plugins.json registry format.
603
+ * xcsh installed_plugins.json registry format.
604
604
  */
605
- export interface ClaudePluginsRegistry {
605
+ export interface XcshPluginsRegistry {
606
606
  version: number;
607
- plugins: Record<string, ClaudePluginEntry[]>;
607
+ plugins: Record<string, XcshPluginEntry[]>;
608
608
  }
609
609
 
610
610
  /**
611
611
  * Resolved plugin root for loading.
612
612
  */
613
- export interface ClaudePluginRoot {
613
+ export interface XcshPluginRoot {
614
614
  /** Plugin ID (e.g., "simpleclaude-core@simpleclaude") */
615
615
  id: string;
616
616
  /** Marketplace name */
@@ -626,10 +626,10 @@ export interface ClaudePluginRoot {
626
626
  }
627
627
 
628
628
  /**
629
- * Parse Claude Code installed_plugins.json content.
629
+ * Parse xcsh installed_plugins.json content.
630
630
  */
631
- export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegistry | null {
632
- const data = tryParseJson<ClaudePluginsRegistry>(content);
631
+ export function parseXcshPluginsRegistry(content: string): XcshPluginsRegistry | null {
632
+ const data = tryParseJson<XcshPluginsRegistry>(content);
633
633
  if (!data || typeof data !== "object") return null;
634
634
  if (
635
635
  typeof data.version !== "number" ||
@@ -711,7 +711,7 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
711
711
  return path.join(cwd, getConfigDirName(), "plugins", "installed_plugins.json");
712
712
  }
713
713
 
714
- const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
714
+ const pluginRootsCache = new Map<string, { roots: XcshPluginRoot[]; warnings: string[] }>();
715
715
 
716
716
  /**
717
717
  * List all installed marketplace plugin roots from the plugin cache.
@@ -720,25 +720,25 @@ const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings:
720
720
  *
721
721
  * Results are cached per `home:resolvedProjectPath` key to avoid repeated parsing.
722
722
  */
723
- export async function listClaudePluginRoots(
723
+ export async function listXcshPluginRoots(
724
724
  home: string,
725
725
  cwd?: string,
726
- ): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
726
+ ): Promise<{ roots: XcshPluginRoot[]; warnings: string[] }> {
727
727
  const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
728
728
  const cacheKey = `${home}:${resolvedProjectPath ?? ""}`;
729
729
  const cached = pluginRootsCache.get(cacheKey);
730
730
  if (cached) return cached;
731
731
 
732
- const roots: ClaudePluginRoot[] = [];
732
+ const roots: XcshPluginRoot[] = [];
733
733
  const warnings: string[] = [];
734
- const projectRoots: ClaudePluginRoot[] = [];
734
+ const projectRoots: XcshPluginRoot[] = [];
735
735
 
736
736
  // ── Installed plugins registry ───────────────────────────────────────────
737
737
  // Path derived from `home` (not os.homedir()) so test isolation works when home is overridden.
738
738
  const registryPath = path.join(home, getConfigDirName(), "plugins", "installed_plugins.json");
739
739
  const content = await readFile(registryPath);
740
740
  if (content) {
741
- const registry = parseClaudePluginsRegistry(content);
741
+ const registry = parseXcshPluginsRegistry(content);
742
742
  if (registry) {
743
743
  for (const [pluginId, entries] of Object.entries(registry.plugins)) {
744
744
  if (!Array.isArray(entries) || entries.length === 0) continue;
@@ -781,7 +781,7 @@ export async function listClaudePluginRoots(
781
781
  if (resolvedProjectPath && resolvedProjectPath !== registryPath) {
782
782
  const projectContent = await readFile(resolvedProjectPath);
783
783
  if (projectContent) {
784
- const projectRegistry = parseClaudePluginsRegistry(projectContent);
784
+ const projectRegistry = parseXcshPluginsRegistry(projectContent);
785
785
  if (projectRegistry) {
786
786
  for (const [pluginId, entries] of Object.entries(projectRegistry.plugins)) {
787
787
  if (!Array.isArray(entries) || entries.length === 0) continue;
@@ -838,7 +838,7 @@ export async function listClaudePluginRoots(
838
838
  /**
839
839
  * Clear the plugin roots cache (useful for testing or when plugins change).
840
840
  */
841
- export function clearClaudePluginRootsCache(): void {
841
+ export function clearXcshPluginRootsCache(): void {
842
842
  pluginRootsCache.clear();
843
843
  preloadedPluginRoots = [...injectedPluginDirRoots];
844
844
  // Re-warm preloaded roots asynchronously so sync LSP config reads stay valid
@@ -851,8 +851,8 @@ export function clearClaudePluginRootsCache(): void {
851
851
  // Populated at startup by preloadPluginRoots(). Read synchronously by
852
852
  // getPreloadedPluginRoots(). Safe degradation: empty array if not warmed.
853
853
 
854
- let preloadedPluginRoots: ClaudePluginRoot[] = [];
855
- let injectedPluginDirRoots: ClaudePluginRoot[] = [];
854
+ let preloadedPluginRoots: XcshPluginRoot[] = [];
855
+ let injectedPluginDirRoots: XcshPluginRoot[] = [];
856
856
  let lastPreloadHome: string | undefined;
857
857
 
858
858
  /**
@@ -862,7 +862,7 @@ let lastPreloadHome: string | undefined;
862
862
  */
863
863
  export async function preloadPluginRoots(home: string, cwd?: string): Promise<void> {
864
864
  lastPreloadHome = home;
865
- const { roots } = await listClaudePluginRoots(home, cwd);
865
+ const { roots } = await listXcshPluginRoots(home, cwd);
866
866
  preloadedPluginRoots = roots;
867
867
  }
868
868
 
@@ -870,7 +870,7 @@ export async function preloadPluginRoots(home: string, cwd?: string): Promise<vo
870
870
  * Get pre-loaded plugin roots synchronously.
871
871
  * Returns empty array if preloadPluginRoots() hasn't been called.
872
872
  */
873
- export function getPreloadedPluginRoots(): readonly ClaudePluginRoot[] {
873
+ export function getPreloadedPluginRoots(): readonly XcshPluginRoot[] {
874
874
  return preloadedPluginRoots;
875
875
  }
876
876
 
@@ -878,11 +878,11 @@ export function getPreloadedPluginRoots(): readonly ClaudePluginRoot[] {
878
878
 
879
879
  /**
880
880
  * Inject synthetic plugin roots from --plugin-dir paths.
881
- * These are prepended to the cache with highest precedence (before OMP/Claude entries).
882
- * Must be called before any listClaudePluginRoots() access.
881
+ * These are prepended to the cache with highest precedence (before OMP/xcsh entries).
882
+ * Must be called before any listXcshPluginRoots() access.
883
883
  */
884
884
  export async function injectPluginDirRoots(home: string, dirs: string[], cwd?: string): Promise<void> {
885
- const injected: ClaudePluginRoot[] = [];
885
+ const injected: XcshPluginRoot[] = [];
886
886
  for (const dir of dirs) {
887
887
  const resolved = path.resolve(dir);
888
888
  // Read plugin name from manifest
@@ -901,13 +901,13 @@ export async function injectPluginDirRoots(home: string, dirs: string[], cwd?: s
901
901
  injected.push(buildPluginDirRoot(resolved, pluginName));
902
902
  }
903
903
 
904
- // Set injected roots BEFORE populating cache so listClaudePluginRoots merges them.
904
+ // Set injected roots BEFORE populating cache so listXcshPluginRoots merges them.
905
905
  injectedPluginDirRoots = injected;
906
906
  lastPreloadHome = home; // ensure cache-clear re-warm fires even when injectPluginDirRoots was the startup path
907
907
  // Clear any stale cache entries (populated before injected roots were set).
908
908
  pluginRootsCache.clear();
909
909
  // Rebuild — cache miss triggers fresh load that includes both user+project registries
910
910
  // and prepends injectedPluginDirRoots at highest precedence.
911
- const { roots } = await listClaudePluginRoots(home, cwd);
911
+ const { roots } = await listXcshPluginRoots(home, cwd);
912
912
  preloadedPluginRoots = roots;
913
913
  }
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
 
3
- /** Synthetic plugin root for a --plugin-dir path. Shape-compatible with ClaudePluginRoot. */
3
+ /** Synthetic plugin root for a --plugin-dir path. Shape-compatible with XcshPluginRoot. */
4
4
  export interface PluginDirRoot {
5
5
  id: string;
6
6
  marketplace: string;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Constructor takes explicit paths for testability (same pattern as registry.ts).
5
5
  * The `clearPluginRootsCache` dependency is injected so callers can provide
6
- * the real `clearClaudePluginRootsCache` while tests supply a counter stub.
6
+ * the real `clearXcshPluginRootsCache` while tests supply a counter stub.
7
7
  */
8
8
 
9
9
  import * as fs from "node:fs/promises";
@@ -51,7 +51,7 @@ export interface MarketplaceManagerOptions {
51
51
  projectInstalledRegistryPath?: string;
52
52
  marketplacesCacheDir: string;
53
53
  pluginsCacheDir: string;
54
- /** Injected for testing; production callers pass clearClaudePluginRootsCache.
54
+ /** Injected for testing; production callers pass clearXcshPluginRootsCache.
55
55
  * Receives any additional file paths that should also be invalidated from the fs cache.
56
56
  */
57
57
  clearPluginRootsCache?: (extraPaths?: readonly string[]) => void;
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Two registries:
5
5
  * - MarketplacesRegistry: which marketplace catalogs the user has added (config)
6
- * - InstalledPluginsRegistry: which plugins are installed (data, Claude Code-compatible)
6
+ * - InstalledPluginsRegistry: which plugins are installed (data)
7
7
  *
8
- * The installed registry MUST pass `parseClaudePluginsRegistry()` validation —
8
+ * The installed registry MUST pass `parseXcshPluginsRegistry()` validation —
9
9
  * it uses `version: 2` (numeric) and `plugins: Record<string, ...[]>`.
10
10
  */
11
11
 
@@ -151,11 +151,11 @@ export interface MarketplaceRegistryEntry {
151
151
  }
152
152
 
153
153
  // ── Installed plugins registry ───────────────────────────────────────
154
- // MUST match ClaudePluginsRegistry shape for parseClaudePluginsRegistry()
154
+ // MUST match XcshPluginsRegistry shape for parseXcshPluginsRegistry()
155
155
  // compatibility: `version: number`, `plugins: Record<string, entry[]>`.
156
156
 
157
157
  export interface InstalledPluginsRegistry {
158
- /** MUST be 2 — parseClaudePluginsRegistry rejects non-numeric version. */
158
+ /** MUST be 2 — parseXcshPluginsRegistry rejects non-numeric version. */
159
159
  version: 2;
160
160
  plugins: Record<string, InstalledPluginEntry[]>;
161
161
  }
@@ -171,7 +171,7 @@ export interface InstalledPluginEntry {
171
171
  lastUpdated: string;
172
172
  /** For git-sourced plugins. */
173
173
  gitCommitSha?: string;
174
- /** OMP extension — not in Claude Code's type. CLI/UI concern only in v1. */
174
+ /** OMP extension — CLI/UI concern only in v1. */
175
175
  enabled?: boolean;
176
176
  }
177
177
 
@@ -80,9 +80,9 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
80
80
  cwd = getProjectDir(),
81
81
  enabled = true,
82
82
  enableCodexUser = true,
83
- enableClaudeUser = true,
84
- enableClaudeProject = true,
85
- enableClaudePlugins = false,
83
+ enableXcshUser = true,
84
+ enableXcshProject = true,
85
+ enableXcshPlugins = false,
86
86
  enablePiUser = true,
87
87
  enablePiProject = true,
88
88
  customDirectories = [],
@@ -97,16 +97,16 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
97
97
  }
98
98
 
99
99
  const anyBuiltInSkillSourceEnabled =
100
- enableCodexUser || enableClaudeUser || enableClaudeProject || enablePiUser || enablePiProject;
100
+ enableCodexUser || enableXcshUser || enableXcshProject || enablePiUser || enablePiProject;
101
101
  // Helper to check if a source is enabled
102
102
  function isSourceEnabled(source: SourceMeta): boolean {
103
103
  const { provider, level } = source;
104
104
  if (provider === "codex" && level === "user") return enableCodexUser;
105
- if (provider === "claude" && level === "user") return enableClaudeUser;
106
- if (provider === "claude" && level === "project") return enableClaudeProject;
105
+ if (provider === "xcsh" && level === "user") return enableXcshUser;
106
+ if (provider === "xcsh" && level === "project") return enableXcshProject;
107
107
  if (provider === "native" && level === "user") return enablePiUser;
108
108
  if (provider === "native" && level === "project") return enablePiProject;
109
- if (provider === "claude-plugins") return enableClaudePlugins;
109
+ if (provider === "xcsh-plugins") return enableXcshPlugins;
110
110
  return anyBuiltInSkillSourceEnabled;
111
111
  }
112
112
 
@@ -142,7 +142,7 @@ export interface FileSlashCommand {
142
142
  name: string;
143
143
  description: string;
144
144
  content: string;
145
- source: string; // e.g., "via Claude Code (User)"
145
+ source: string; // e.g., "via xcsh (User)"
146
146
  /** Source metadata for display */
147
147
  _source?: { providerName: string; level: "user" | "project" | "native" };
148
148
  }
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.92.0",
21
- "commit": "2f4efd2a77846d164e2c0bdfbf3c6b204f3c6561",
22
- "shortCommit": "2f4efd2",
20
+ "version": "19.1.0",
21
+ "commit": "a158adfdf2a6f8be6887817aa87e4d4ca80d612f",
22
+ "shortCommit": "a158adf",
23
23
  "branch": "main",
24
- "tag": "v18.92.0",
25
- "commitDate": "2026-06-03T01:35:38Z",
26
- "buildDate": "2026-06-03T01:53:31.494Z",
24
+ "tag": "v19.1.0",
25
+ "commitDate": "2026-06-03T12:52:34Z",
26
+ "buildDate": "2026-06-03T13:13:05.554Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/2f4efd2a77846d164e2c0bdfbf3c6b204f3c6561",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.92.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/a158adfdf2a6f8be6887817aa87e4d4ca80d612f",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.1.0"
33
33
  };
@@ -15,7 +15,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
15
15
  "extensions/extension-loading.md": "---\ntitle: Extension Loading (TypeScript/JavaScript Modules)\ndescription: TypeScript and JavaScript module loading pipeline for extensions with resolution, validation, and caching.\nsidebar:\n order: 2\n label: Extension loading\n---\n\n# Extension Loading (TypeScript/JavaScript Modules)\n\nThis document covers how the coding agent discovers and loads **extension modules** (`.ts`/`.js`) at startup.\n\nIt does **not** cover `gemini-extension.json` manifest extensions (documented separately).\n\n## What this subsystem does\n\nExtension loading builds a list of module entry files, imports each module with Bun, executes its factory, and returns:\n\n- loaded extension definitions\n- per-path load errors (without aborting the whole load)\n- a shared extension runtime object used later by `ExtensionRunner`\n\n## Primary implementation files\n\n- `src/extensibility/extensions/loader.ts` — path discovery + import/execution\n- `src/extensibility/extensions/index.ts` — public exports\n- `src/extensibility/extensions/runner.ts` — runtime/event execution after load\n- `src/discovery/builtin.ts` — native auto-discovery provider for extension modules\n- `src/config/settings.ts` — loads merged `extensions` / `disabledExtensions` settings\n\n---\n\n## Inputs to extension loading\n\n### 1) Auto-discovered native extension modules\n\n`discoverAndLoadExtensions()` first asks discovery providers for `extension-module` capability items, then keeps only provider `native` items.\n\nEffective native locations:\n\n- Project: `<cwd>/.xcsh/extensions`\n- User: `~/.xcsh/agent/extensions`\n\nPath roots come from the native provider (`SOURCE_PATHS.native`).\n\nNotes:\n\n- Native auto-discovery is currently `.xcsh` based.\n- Legacy `.pi` is still accepted in `package.json` manifest keys (`pi.extensions`), but not as a native root here.\n\n### 2) Explicitly configured paths\n\nAfter auto-discovery, configured paths are appended and resolved.\n\nConfigured path sources in the main session startup path (`sdk.ts`):\n\n1. CLI-provided paths (`--extension/-e`, and `--hook` is also treated as an extension path)\n2. Settings `extensions` array (merged global + project settings)\n\nGlobal settings file:\n\n- `~/.xcsh/agent/config.yml` (or custom agent dir via `PI_CODING_AGENT_DIR`)\n\nProject settings file:\n\n- `<cwd>/.xcsh/settings.json`\n\nExamples:\n\n```yaml\n# ~/.xcsh/agent/config.yml\nextensions:\n - ~/my-exts/safety.ts\n - ./local/ext-pack\n```\n\n```json\n{\n \"extensions\": [\"./.xcsh/extensions/my-extra\"]\n}\n```\n\n---\n\n## Enable/disable controls\n\n### Disable discovery\n\n- CLI: `--no-extensions`\n- SDK option: `disableExtensionDiscovery`\n\nBehavior split:\n\n- SDK: when `disableExtensionDiscovery=true`, it still loads `additionalExtensionPaths` via `loadExtensions()`.\n- CLI path building (`main.ts`) currently clears CLI extension paths when `--no-extensions` is set, so explicit `-e/--hook` are not forwarded in that mode.\n\n### Disable specific extension modules\n\n`disabledExtensions` setting filters by extension id format:\n\n- `extension-module:<derivedName>`\n\n`derivedName` is based on entry path (`getExtensionNameFromPath`), for example:\n\n- `/x/foo.ts` -> `foo`\n- `/x/bar/index.ts` -> `bar`\n\nExample:\n\n```yaml\ndisabledExtensions:\n - extension-module:foo\n```\n\n---\n\n## Path and entry resolution\n\n### Path normalization\n\nFor configured paths:\n\n1. Normalize unicode spaces\n2. Expand `~`\n3. If relative, resolve against current `cwd`\n\n### If configured path is a file\n\nIt is used directly as a module entry candidate.\n\n### If configured path is a directory\n\nResolution order:\n\n1. `package.json` in that directory with `xcsh.extensions` (or legacy `pi.extensions`) -> use declared entries\n2. `index.ts`\n3. `index.js`\n4. Otherwise scan one level for extension entries:\n - direct `*.ts` / `*.js`\n - subdir `index.ts` / `index.js`\n - subdir `package.json` with `xcsh.extensions` / `pi.extensions`\n\nRules and constraints:\n\n- no recursive discovery beyond one subdirectory level\n- declared `extensions` manifest entries are resolved relative to that package directory\n- declared entries are included only if file exists/access is allowed\n- in `*/index.{ts,js}` pairs, TypeScript is preferred over JavaScript\n- symlinks are treated as eligible files/directories\n\n### Ignore behavior differs by source\n\n- Native auto-discovery (`discoverExtensionModulePaths` in discovery helpers) uses native glob with `gitignore: true` and `hidden: false`.\n- Explicit configured directory scanning in `loader.ts` uses `readdir` rules and does **not** apply gitignore filtering.\n\n---\n\n## Load order and precedence\n\n`discoverAndLoadExtensions()` builds one ordered list and then calls `loadExtensions()`.\n\nOrder:\n\n1. Native auto-discovered modules\n2. Explicit configured paths (in provided order)\n\nIn `sdk.ts`, configured order is:\n\n1. CLI additional paths\n2. Settings `extensions`\n\nDe-duplication:\n\n- absolute path based\n- first seen path wins\n- later duplicates are ignored\n\nImplication: if the same module path is both auto-discovered and explicitly configured, it is loaded once at the first position (auto-discovered stage).\n\n---\n\n## Module import and factory contract\n\nEach candidate path is loaded with dynamic import:\n\n- `await import(resolvedPath)`\n- factory is `module.default ?? module`\n- factory must be a function (`ExtensionFactory`)\n\nIf export is not a function, that path fails with a structured error and loading continues.\n\n---\n\n## Failure handling and isolation\n\n### During loading\n\nPer extension path, failures are captured as `{ path, error }` and do not stop other paths from loading.\n\nCommon cases:\n\n- import failure / missing file\n- invalid factory export (non-function)\n- exception thrown while executing factory\n\n### Runtime isolation model\n\n- Extensions are **not sandboxed** (same process/runtime).\n- They share one `EventBus` and one `ExtensionRuntime` instance.\n- During load, runtime action methods intentionally throw `ExtensionRuntimeNotInitializedError`; action wiring happens later in `ExtensionRunner.initialize()`.\n\n### After loading\n\nWhen events run through `ExtensionRunner`, handler exceptions are caught and emitted as extension errors instead of crashing the runner loop.\n\n---\n\n## Minimal user/project layout examples\n\n### User-level\n\n```text\n~/.xcsh/agent/\n config.yml\n extensions/\n guardrails.ts\n audit/\n index.ts\n```\n\n### Project-level\n\n```text\n<repo>/\n .xcsh/\n settings.json\n extensions/\n checks/\n package.json\n lint-gates.ts\n```\n\n`checks/package.json`:\n\n```json\n{\n \"xcsh\": {\n \"extensions\": [\"./src/check-a.ts\", \"./src/check-b.js\"]\n }\n}\n```\n\nLegacy manifest key still accepted:\n\n```json\n{\n \"pi\": {\n \"extensions\": [\"./index.ts\"]\n }\n}\n```\n",
16
16
  "extensions/extensions.md": "---\ntitle: Extensions\ndescription: Extension runtime overview covering types, runner lifecycle, registration, and discovery.\nsidebar:\n order: 1\n label: Overview\n---\n\n# Extensions\n\nPrimary guide for authoring runtime extensions in `packages/coding-agent`.\n\nThis document covers the current extension runtime in:\n\n- `src/extensibility/extensions/types.ts`\n- `src/extensibility/extensions/runner.ts`\n- `src/extensibility/extensions/wrapper.ts`\n- `src/extensibility/extensions/index.ts`\n- `src/modes/controllers/extension-ui-controller.ts`\n\nFor discovery paths and filesystem loading rules, see `docs/extension-loading.md`.\n\n## What an extension is\n\nAn extension is a TS/JS module exporting a default factory:\n\n```ts\nimport type { ExtensionAPI } from \"@f5xc-salesdemos/xcsh\";\n\nexport default function myExtension(pi: ExtensionAPI) {\n // register handlers/tools/commands/renderers\n}\n```\n\nExtensions can combine all of the following in one module:\n\n- event handlers (`pi.on(...)`)\n- LLM-callable tools (`pi.registerTool(...)`)\n- slash commands (`pi.registerCommand(...)`)\n- keyboard shortcuts and flags\n- custom message rendering\n- session/message injection APIs (`sendMessage`, `sendUserMessage`, `appendEntry`)\n\n## Runtime model\n\n1. Extensions are imported and their factory functions run.\n2. During that load phase, registration methods are valid; runtime action methods are not yet initialized.\n3. `ExtensionRunner.initialize(...)` wires live actions/contexts for the active mode.\n4. Session/agent/tool lifecycle events are emitted to handlers.\n5. Every tool execution is wrapped with extension interception (`tool_call` / `tool_result`).\n\n```text\nExtension lifecycle (simplified)\n\nload paths\n │\n ▼\nimport module + run factory (registration only)\n │\n ▼\nExtensionRunner.initialize(mode/session/tool registry)\n │\n ├─ emit session/agent events to handlers\n ├─ wrap tool execution (tool_call/tool_result)\n └─ expose runtime actions (sendMessage, setActiveTools, ...)\n```\n\nImportant constraint from `loader.ts`:\n\n- calling action methods like `pi.sendMessage()` during extension load throws `ExtensionRuntimeNotInitializedError`\n- register first; perform runtime behavior from events/commands/tools\n\n## Quick start\n\n```ts\nimport type { ExtensionAPI } from \"@f5xc-salesdemos/xcsh\";\nimport { Type } from \"@sinclair/typebox\";\n\nexport default function (pi: ExtensionAPI) {\n pi.setLabel(\"Safety + Utilities\");\n\n pi.on(\"session_start\", async (_event, ctx) => {\n ctx.ui.notify(`Extension loaded in ${ctx.cwd}`, \"info\");\n });\n\n pi.on(\"tool_call\", async (event) => {\n if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n return { block: true, reason: \"Blocked by extension policy\" };\n }\n });\n\n pi.registerTool({\n name: \"hello_extension\",\n label: \"Hello Extension\",\n description: \"Return a greeting\",\n parameters: Type.Object({ name: Type.String() }),\n async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {\n return {\n content: [{ type: \"text\", text: `Hello, ${params.name}` }],\n details: { greeted: params.name },\n };\n },\n });\n\n pi.registerCommand(\"hello-ext\", {\n description: \"Show queue state\",\n handler: async (_args, ctx) => {\n ctx.ui.notify(`pending=${ctx.hasPendingMessages()}`, \"info\");\n },\n });\n}\n```\n\n## Extension API surfaces\n\n## 1) Registration and actions (`ExtensionAPI`)\n\nCore methods:\n\n- `on(event, handler)`\n- `registerTool`, `registerCommand`, `registerShortcut`, `registerFlag`\n- `registerMessageRenderer`\n- `sendMessage`, `sendUserMessage`, `appendEntry`\n- `getActiveTools`, `getAllTools`, `setActiveTools`\n- `getSessionName`, `setSessionName`\n- `setModel`, `getThinkingLevel`, `setThinkingLevel`\n- `registerProvider`\n- `events` (shared event bus)\n\nIn interactive mode, `input` handlers run before the built-in first-message auto-title check. Extensions that call `await pi.setSessionName(...)` from `input` can set the persisted session name and prevent the default auto-generated title from running for that session.\n\nAlso exposed:\n\n- `pi.logger`\n- `pi.typebox`\n- `pi.pi` (package exports)\n\n### Message delivery semantics\n\n`pi.sendMessage(message, options)` supports:\n\n- `deliverAs: \"steer\"` (default) — interrupts current run\n- `deliverAs: \"followUp\"` — queued to run after current run\n- `deliverAs: \"nextTurn\"` — stored and injected on the next user prompt\n- `triggerTurn: true` — starts a turn when idle (`nextTurn` ignores this)\n\n`pi.sendUserMessage(content, { deliverAs })` always goes through prompt flow; while streaming it queues as steer/follow-up.\n\n## 2) Handler context (`ExtensionContext`)\n\nHandlers and tool `execute` receive `ctx` with:\n\n- `ui`\n- `hasUI`\n- `cwd`\n- `sessionManager` (read-only)\n- `modelRegistry`, `model`\n- `getContextUsage()`\n- `compact(...)`\n- `isIdle()`, `hasPendingMessages()`, `abort()`\n- `shutdown()`\n- `getSystemPrompt()`\n\n## 3) Command context (`ExtensionCommandContext`)\n\nCommand handlers additionally get:\n\n- `waitForIdle()`\n- `newSession(...)`\n- `switchSession(...)`\n- `branch(entryId)`\n- `navigateTree(targetId, { summarize })`\n- `reload()`\n\nUse command context for session-control flows; these methods are intentionally separated from general event handlers.\n\n## Event surface (current names and behavior)\n\nCanonical event unions and payload types are in `types.ts`.\n\n### Session lifecycle\n\n- `session_start`\n- `session_before_switch` / `session_switch`\n- `session_before_branch` / `session_branch`\n- `session_before_compact` / `session.compacting` / `session_compact`\n- `session_before_tree` / `session_tree`\n- `session_shutdown`\n\nCancelable pre-events:\n\n- `session_before_switch` → `{ cancel?: boolean }`\n- `session_before_branch` → `{ cancel?: boolean; skipConversationRestore?: boolean }`\n- `session_before_compact` → `{ cancel?: boolean; compaction?: CompactionResult }`\n- `session_before_tree` → `{ cancel?: boolean; summary?: { summary: string; details?: unknown } }`\n\n### Prompt and turn lifecycle\n\n- `input`\n- `before_agent_start`\n- `context`\n- `agent_start` / `agent_end`\n- `turn_start` / `turn_end`\n- `message_start` / `message_update` / `message_end`\n\n### Tool lifecycle\n\n- `tool_call` (pre-exec, may block)\n- `tool_result` (post-exec, may patch content/details/isError)\n- `tool_execution_start` / `tool_execution_update` / `tool_execution_end` (observability)\n\n`tool_result` is middleware-style: handlers run in extension order and each sees prior modifications.\n\n### Reliability/runtime signals\n\n- `auto_compaction_start` / `auto_compaction_end`\n- `auto_retry_start` / `auto_retry_end`\n- `ttsr_triggered`\n- `todo_reminder`\n\n### User command interception\n\n- `user_bash` (override with `{ result }`)\n- `user_python` (override with `{ result }`)\n\n### `resources_discover`\n\n`resources_discover` exists in extension types and `ExtensionRunner`.\nCurrent runtime note: `ExtensionRunner.emitResourcesDiscover(...)` is implemented, but there are no `AgentSession` callsites invoking it in the current codebase.\n\n## Tool authoring details\n\n`registerTool` uses `ToolDefinition` from `types.ts`.\n\nCurrent `execute` signature:\n\n```ts\nexecute(\n toolCallId,\n params,\n signal,\n onUpdate,\n ctx,\n): Promise<AgentToolResult>\n```\n\nTemplate:\n\n```ts\npi.registerTool({\n name: \"my_tool\",\n label: \"My Tool\",\n description: \"...\",\n parameters: Type.Object({}),\n async execute(_id, _params, signal, onUpdate, ctx) {\n if (signal?.aborted) {\n return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n }\n onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n return { content: [{ type: \"text\", text: \"Done\" }], details: {} };\n },\n onSession(event, ctx) {\n // reason: start|switch|branch|tree|shutdown\n },\n renderCall(args, theme) {\n // optional TUI render\n },\n renderResult(result, options, theme, args) {\n // optional TUI render\n },\n});\n```\n\n`tool_call`/`tool_result` intercept all tools once the registry is wrapped in `sdk.ts`, including built-ins and extension/custom tools.\n\n## UI integration points\n\n`ctx.ui` implements the `ExtensionUIContext` interface. Support differs by mode.\n\n### Interactive mode (`extension-ui-controller.ts`)\n\nSupported:\n\n- dialogs: `select`, `confirm`, `input`, `editor`\n- notifications/status/editor text/terminal input/custom overlays\n- theme listing/loading by name (`setTheme` supports string names)\n- tools expanded toggle\n\nCurrent no-op methods in this controller:\n\n- `setFooter`\n- `setHeader`\n- `setEditorComponent`\n\nAlso note: `setWidget` currently routes to status-line text via `setHookWidget(...)`.\n\n### RPC mode (`rpc-mode.ts`)\n\n`ctx.ui` is backed by RPC `extension_ui_request` events:\n\n- dialog methods (`select`, `confirm`, `input`, `editor`) round-trip to client responses\n- fire-and-forget methods emit requests (`notify`, `setStatus`, `setWidget` for string arrays, `setTitle`, `setEditorText`)\n\nUnsupported/no-op in RPC implementation:\n\n- `onTerminalInput`\n- `custom`\n- `setFooter`, `setHeader`, `setEditorComponent`\n- `setWorkingMessage`\n- theme switching/loading (`setTheme` returns failure)\n- tool expansion controls are inert\n\n### Print/headless/subagent paths\n\nWhen no UI context is supplied to runner init, `ctx.hasUI` is `false` and methods are no-op/default-returning.\n\n### Background interactive mode\n\nBackground mode installs a non-interactive UI context object. In current implementation, `ctx.hasUI` may still be `true` while interactive dialogs return defaults/no-op behavior.\n\n## Session and state patterns\n\nFor durable extension state:\n\n1. Persist with `pi.appendEntry(customType, data)`.\n2. Rebuild state from `ctx.sessionManager.getBranch()` on `session_start`, `session_branch`, `session_tree`.\n3. Keep tool result `details` structured when state should be visible/reconstructible from tool result history.\n\nExample reconstruction pattern:\n\n```ts\npi.on(\"session_start\", async (_event, ctx) => {\n let latest;\n for (const entry of ctx.sessionManager.getBranch()) {\n if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n latest = entry.data;\n }\n }\n // restore from latest\n});\n```\n\n## Rendering extension points\n\n## Custom message renderer\n\n```ts\npi.registerMessageRenderer(\"my-type\", (message, { expanded }, theme) => {\n // return pi-tui Component\n});\n```\n\nUsed by interactive rendering when custom messages are displayed.\n\n## Tool call/result renderer\n\nProvide `renderCall` / `renderResult` on `registerTool` definitions for custom tool visualization in TUI.\n\n## Constraints and pitfalls\n\n- Runtime actions are unavailable during extension load.\n- `tool_call` errors block execution (fail-closed).\n- Command name conflicts with built-ins are skipped with diagnostics.\n- Reserved shortcuts are ignored (`ctrl+c`, `ctrl+d`, `ctrl+z`, `ctrl+k`, `ctrl+p`, `ctrl+l`, `ctrl+o`, `ctrl+t`, `ctrl+g`, `shift+tab`, `shift+ctrl+p`, `alt+enter`, `escape`, `enter`).\n- Treat `ctx.reload()` as terminal for the current command handler frame.\n\n## Extensions vs hooks vs custom-tools\n\nUse the right surface:\n\n- **Extensions** (`src/extensibility/extensions/*`): unified system (events + tools + commands + renderers + provider registration).\n- **Hooks** (`src/extensibility/hooks/*`): separate legacy event API.\n- **Custom-tools** (`src/extensibility/custom-tools/*`): tool-focused modules; when loaded alongside extensions they are adapted and still pass through extension interception wrappers.\n\nIf you need one package that owns policy, tools, command UX, and rendering together, use extensions.\n",
17
17
  "extensions/gemini-manifest-extensions.md": "---\ntitle: Gemini Manifest Extensions\ndescription: Gemini manifest extension format for cross-platform skill and agent compatibility.\nsidebar:\n order: 7\n label: Gemini manifest\n---\n\n# Gemini Manifest Extensions (`gemini-extension.json`)\n\nThis document covers how the coding-agent discovers and parses Gemini-style manifest extensions (`gemini-extension.json`) into the `extensions` capability.\n\nIt does **not** cover TypeScript/JavaScript extension module loading (`extensions/*.ts`, `index.ts`, `package.json xcsh.extensions`), which is documented in `extension-loading.md`.\n\n## Implementation files\n\n- [`../src/discovery/gemini.ts`](../../packages/coding-agent/src/discovery/gemini.ts)\n- [`../src/discovery/builtin.ts`](../../packages/coding-agent/src/discovery/builtin.ts)\n- [`../src/discovery/helpers.ts`](../../packages/coding-agent/src/discovery/helpers.ts)\n- [`../src/capability/extension.ts`](../../packages/coding-agent/src/capability/extension.ts)\n- [`../src/capability/index.ts`](../../packages/coding-agent/src/capability/index.ts)\n- [`../src/extensibility/extensions/loader.ts`](../../packages/coding-agent/src/extensibility/extensions/loader.ts)\n\n---\n\n## What gets discovered\n\nThe Gemini provider (`id: gemini`, priority `60`) registers an `extensions` loader that scans two fixed roots:\n\n- User: `~/.gemini/extensions`\n- Project: `<cwd>/.gemini/extensions`\n\nPath resolution is direct from `ctx.home` and `ctx.cwd` via `getUserPath()` / `getProjectPath()`.\n\nImportant scope rule: project lookup is **cwd-only**. It does not walk parent directories.\n\n---\n\n## Directory scan rules\n\nFor each root (`~/.gemini/extensions` and `<cwd>/.gemini/extensions`), discovery does:\n\n1. `readDirEntries(root)`\n2. keep only direct child directories (`entry.isDirectory()`)\n3. for each child `<name>`, attempt to read exactly:\n - `<root>/<name>/gemini-extension.json`\n\nThere is no recursive scan beyond one directory level.\n\n### Hidden directories\n\nGemini manifest discovery does **not** filter out dot-prefixed directory names. If a hidden child directory exists and contains `gemini-extension.json`, it is considered.\n\n### Missing/unreadable files\n\nIf `gemini-extension.json` is missing or unreadable, that directory is skipped silently (no warning).\n\n---\n\n## Manifest shape (as implemented)\n\nThe capability type defines this manifest shape:\n\n```ts\ninterface ExtensionManifest {\n name?: string;\n description?: string;\n mcpServers?: Record<string, Omit<MCPServer, \"name\" | \"_source\">>;\n tools?: unknown[];\n context?: unknown;\n}\n```\n\nDiscovery-time behavior is intentionally loose:\n\n- JSON parse success is required.\n- There is no runtime schema validation for field types/content beyond JSON syntax.\n- The parsed object is stored as `manifest` on the capability item.\n\n### Name normalization\n\n`Extension.name` is set to:\n\n1. `manifest.name` if it is not `null`/`undefined`\n2. otherwise the extension directory name\n\nNo string-type enforcement is applied here.\n\n---\n\n## Materialization into capability items\n\nA valid parsed manifest creates one `Extension` capability item:\n\n```ts\n{\n name: manifest.name ?? <directory-name>,\n path: <extension-directory>,\n manifest: <parsed-json>,\n level: \"user\" | \"project\",\n _source: {\n provider: \"gemini\",\n providerName: \"Gemini CLI\" // attached by capability registry\n path: <absolute-manifest-path>,\n level: \"user\" | \"project\"\n }\n}\n```\n\nNotes:\n\n- `_source.path` is normalized to an absolute path by `createSourceMeta()`.\n- Registry-level capability validation for `extensions` only checks presence of `name` and `path`.\n- Manifest internals (`mcpServers`, `tools`, `context`) are not validated during discovery.\n\n---\n\n## Error handling and warning semantics\n\n### Warned\n\n- Invalid JSON in a manifest file:\n - warning format: `Invalid JSON in <manifestPath>`\n\n### Not warned (silent skip)\n\n- `extensions` directory missing\n- child directory has no `gemini-extension.json`\n- unreadable manifest file\n- manifest JSON is syntactically valid but semantically odd/incomplete\n\nThis means partial validity is accepted: only syntactic JSON failure emits a warning.\n\n---\n\n## Precedence and deduplication with other sources\n\n`extensions` capability is aggregated across providers by the capability registry.\n\nCurrent providers for this capability:\n\n- `native` (`packages/coding-agent/src/discovery/builtin.ts`) priority `100`\n- `gemini` (`packages/coding-agent/src/discovery/gemini.ts`) priority `60`\n\nDedup key is `ext.name` (`extensionCapability.key = ext => ext.name`).\n\n### Cross-provider precedence\n\nHigher-priority provider wins on duplicate extension names.\n\n- If `native` and `gemini` both emit extension name `foo`, the native item is kept.\n- Lower-priority duplicate is retained only in `result.all` with `_shadowed = true`.\n\n### Intra-provider order effects\n\nBecause dedup is “first seen wins”, provider-local item order matters.\n\n- Gemini loader appends **user first**, then **project**.\n- Therefore, duplicate names between `~/.gemini/extensions` and `<cwd>/.gemini/extensions` keep the user entry and shadow the project entry.\n\nBy contrast, native provider builds config dir order differently (`project` then `user` in `getConfigDirs()`), so native intra-provider shadowing is the opposite direction.\n\n---\n\n## User vs project behavior summary\n\nFor Gemini manifests specifically:\n\n- Both user and project roots are scanned every load.\n- Project root is fixed to `<cwd>/.gemini/extensions` (no ancestor walk).\n- Duplicate names inside Gemini source resolve to user-first.\n- Duplicate names against higher-priority providers (notably native) lose by priority.\n\n---\n\n## Boundary: discovery metadata vs runtime extension loading\n\n`gemini-extension.json` discovery currently feeds capability metadata (`Extension` items). It does **not** directly load runnable TS/JS extension modules.\n\nRuntime module loading (`discoverAndLoadExtensions()` / `loadExtensions()`) uses `extension-modules` and explicit paths, and currently filters auto-discovered modules to provider `native` only.\n\nPractical implication:\n\n- Gemini manifest extensions are discoverable as capability records.\n- They are not, by themselves, executed as runtime extension modules by the extension loader pipeline.\n\nThis boundary is intentional in current implementation and explains why manifest discovery and executable module loading can diverge.\n",
18
- "extensions/marketplace.md": "---\ntitle: Marketplace Plugin System\ndescription: Marketplace plugin system for discovering, installing, and managing curated plugin collections.\nsidebar:\n order: 4\n label: Marketplace\n---\n\n# Marketplace plugin system\n\nThe marketplace system lets you discover, install, and manage plugins from Git-hosted catalogs. It is compatible with the Claude Code plugin registry format.\n\n## Quick start\n\n```\n/marketplace add anthropics/claude-plugins-official\n/marketplace install wordpress.com@claude-plugins-official\n```\n\nOr just type `/marketplace` with no arguments to open the interactive plugin browser.\n\n## Concepts\n\nA **marketplace** is a Git repository (or local directory) containing a catalog file at `.claude-plugin/marketplace.json`. The catalog lists available plugins with their sources, descriptions, and metadata.\n\nA **plugin** is a directory containing skills, commands, hooks, MCP servers, or LSP servers. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`).\n\n**Scopes**: plugins can be installed at two scopes:\n\n- **user** (default) -- available in all projects, stored in `~/.xcsh/plugins/installed_plugins.json`\n- **project** -- available only in the current project, stored in `.xcsh/installed_plugins.json`\n\nProject-scoped installs shadow user-scoped installs of the same plugin.\n\n## Commands\n\n### Interactive mode\n\n| Command | Effect |\n|---|---|\n| `/marketplace` | Open interactive plugin browser (install) |\n\n### Marketplace management\n\n| Command | Effect |\n|---|---|\n| `/marketplace add <source>` | Add a marketplace source |\n| `/marketplace remove <name>` | Remove a marketplace |\n| `/marketplace update [name]` | Re-fetch catalog(s); omit name to update all |\n| `/marketplace list` | List configured marketplaces |\n\n### Plugin operations\n\n| Command | Effect |\n|---|---|\n| `/marketplace discover [marketplace]` | Browse available plugins |\n| `/marketplace install [--force] [--scope user\\|project] name@marketplace` | Install a plugin |\n| `/marketplace uninstall [--scope user\\|project] name@marketplace` | Uninstall a plugin |\n| `/marketplace installed` | List installed marketplace plugins |\n| `/marketplace upgrade [--scope user\\|project] [name@marketplace]` | Upgrade one or all plugins |\n\n### CLI equivalents\n\nThe same operations are available from the command line:\n\n```\nxcsh plugin marketplace add <source>\nxcsh plugin marketplace remove <name>\nxcsh plugin marketplace update [name]\nxcsh plugin marketplace list\nxcsh plugin discover [marketplace]\nxcsh plugin install --scope project name@marketplace\n```\n\n## Marketplace sources\n\nWhen you run `/marketplace add <source>`, the system classifies the source:\n\n| Source format | Type | Example |\n|---|---|---|\n| `owner/repo` | GitHub shorthand | `anthropics/claude-plugins-official` |\n| `https://...*.json` | Direct catalog URL | `https://example.com/marketplace.json` |\n| `https://...*.git` or `git@...` | Git repository | `https://github.com/org/repo.git` |\n| `./path` or `~/path` or `/path` | Local directory | `./my-marketplace` |\n\nThe system clones the repository (or reads the local directory), locates `.claude-plugin/marketplace.json`, validates it, and caches the catalog locally.\n\n## Catalog format (marketplace.json)\n\nA marketplace catalog lives at `.claude-plugin/marketplace.json` in the repository root:\n\n```json\n{\n \"$schema\": \"https://anthropic.com/claude-code/marketplace.schema.json\",\n \"name\": \"my-marketplace\",\n \"owner\": {\n \"name\": \"Your Name\",\n \"email\": \"you@example.com\"\n },\n \"description\": \"A collection of plugins\",\n \"plugins\": [\n {\n \"name\": \"my-plugin\",\n \"description\": \"What this plugin does\",\n \"source\": \"./plugins/my-plugin\",\n \"category\": \"development\",\n \"homepage\": \"https://github.com/you/my-plugin\"\n }\n ]\n}\n```\n\n### Required fields\n\n| Field | Description |\n|---|---|\n| `name` | Marketplace name. Lowercase alphanumeric, hyphens, and dots. Must start and end with alphanumeric. Max 64 chars. |\n| `owner.name` | Marketplace owner name |\n| `plugins` | Array of plugin entries |\n\n### Plugin entry fields\n\n| Field | Required | Description |\n|---|---|---|\n| `name` | yes | Plugin name (same rules as marketplace name) |\n| `source` | yes | Where to find the plugin (see below) |\n| `description` | no | Short description |\n| `version` | no | Version string |\n| `author` | no | `{ name, email? }` |\n| `homepage` | no | URL |\n| `category` | no | Category string (e.g. `development`, `productivity`, `security`) |\n| `tags` | no | Array of string tags |\n| `strict` | no | Boolean |\n| `commands` | no | Slash commands provided |\n| `agents` | no | Agents provided |\n| `hooks` | no | Hook definitions |\n| `mcpServers` | no | MCP server definitions |\n| `lspServers` | no | LSP server definitions |\n\n### Plugin source formats\n\nThe `source` field supports several formats:\n\n**Relative path** (within the marketplace repo):\n\n```json\n\"source\": \"./plugins/my-plugin\"\n```\n\n**Git repository URL**:\n\n```json\n\"source\": {\n \"source\": \"url\",\n \"url\": \"https://github.com/org/repo.git\",\n \"sha\": \"abc123...\"\n}\n```\n\n**GitHub shorthand**:\n\n```json\n\"source\": {\n \"source\": \"github\",\n \"repo\": \"org/repo\",\n \"ref\": \"main\",\n \"sha\": \"abc123...\"\n}\n```\n\n**Git subdirectory** (monorepo):\n\n```json\n\"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/org/monorepo.git\",\n \"path\": \"plugins/my-plugin\",\n \"ref\": \"main\",\n \"sha\": \"abc123...\"\n}\n```\n\n**npm package**:\n\n```json\n\"source\": {\n \"source\": \"npm\",\n \"package\": \"@scope/my-plugin\",\n \"version\": \"1.0.0\"\n}\n```\n\n## On-disk layout\n\n```\n~/.xcsh/\n config/\n marketplaces.json # Registry of added marketplaces\n plugins/\n installed_plugins.json # User-scoped installed plugins\n cache/\n marketplaces/ # Cached marketplace catalogs\n plugins/ # Cached plugin directories\n\n<project>/.xcsh/\n installed_plugins.json # Project-scoped installed plugins\n```\n\n## Naming rules\n\nMarketplace and plugin names must:\n\n- Start and end with a lowercase letter or digit\n- Contain only lowercase letters, digits, hyphens, and dots\n- Be at most 64 characters\n\nPlugin IDs (`name@marketplace`) must be at most 128 characters total.\n\nValid examples: `my-plugin`, `code-review`, `wordpress.com`, `ai-firstify`\nInvalid examples: `-bad`, `bad-`, `.bad`, `Bad`, `under_score`\n",
18
+ "extensions/marketplace.md": "---\ntitle: Marketplace Plugin System\ndescription: Marketplace plugin system for discovering, installing, and managing curated plugin collections.\nsidebar:\n order: 4\n label: Marketplace\n---\n\n# Marketplace plugin system\n\nThe marketplace system lets you discover, install, and manage plugins from Git-hosted catalogs. It is compatible with the Claude Code plugin registry format.\n\n## Quick start\n\n```\n/marketplace add anthropics/f5xc-salesdemos-marketplace\n/marketplace install wordpress.com@f5xc-salesdemos-marketplace\n```\n\nOr just type `/marketplace` with no arguments to open the interactive plugin browser.\n\n## Concepts\n\nA **marketplace** is a Git repository (or local directory) containing a catalog file at `.xcsh-plugin/marketplace.json`. The catalog lists available plugins with their sources, descriptions, and metadata.\n\nA **plugin** is a directory containing skills, commands, hooks, MCP servers, or LSP servers. Plugins are identified by `name@marketplace` (e.g. `code-review@f5xc-salesdemos-marketplace`).\n\n**Scopes**: plugins can be installed at two scopes:\n\n- **user** (default) -- available in all projects, stored in `~/.xcsh/plugins/installed_plugins.json`\n- **project** -- available only in the current project, stored in `.xcsh/installed_plugins.json`\n\nProject-scoped installs shadow user-scoped installs of the same plugin.\n\n## Commands\n\n### Interactive mode\n\n| Command | Effect |\n|---|---|\n| `/marketplace` | Open interactive plugin browser (install) |\n\n### Marketplace management\n\n| Command | Effect |\n|---|---|\n| `/marketplace add <source>` | Add a marketplace source |\n| `/marketplace remove <name>` | Remove a marketplace |\n| `/marketplace update [name]` | Re-fetch catalog(s); omit name to update all |\n| `/marketplace list` | List configured marketplaces |\n\n### Plugin operations\n\n| Command | Effect |\n|---|---|\n| `/marketplace discover [marketplace]` | Browse available plugins |\n| `/marketplace install [--force] [--scope user\\|project] name@marketplace` | Install a plugin |\n| `/marketplace uninstall [--scope user\\|project] name@marketplace` | Uninstall a plugin |\n| `/marketplace installed` | List installed marketplace plugins |\n| `/marketplace upgrade [--scope user\\|project] [name@marketplace]` | Upgrade one or all plugins |\n\n### CLI equivalents\n\nThe same operations are available from the command line:\n\n```\nxcsh plugin marketplace add <source>\nxcsh plugin marketplace remove <name>\nxcsh plugin marketplace update [name]\nxcsh plugin marketplace list\nxcsh plugin discover [marketplace]\nxcsh plugin install --scope project name@marketplace\n```\n\n## Marketplace sources\n\nWhen you run `/marketplace add <source>`, the system classifies the source:\n\n| Source format | Type | Example |\n|---|---|---|\n| `owner/repo` | GitHub shorthand | `anthropics/f5xc-salesdemos-marketplace` |\n| `https://...*.json` | Direct catalog URL | `https://example.com/marketplace.json` |\n| `https://...*.git` or `git@...` | Git repository | `https://github.com/org/repo.git` |\n| `./path` or `~/path` or `/path` | Local directory | `./my-marketplace` |\n\nThe system clones the repository (or reads the local directory), locates `.xcsh-plugin/marketplace.json`, validates it, and caches the catalog locally.\n\n## Catalog format (marketplace.json)\n\nA marketplace catalog lives at `.xcsh-plugin/marketplace.json` in the repository root:\n\n```json\n{\n \"$schema\": \"https://anthropic.com/claude-code/marketplace.schema.json\",\n \"name\": \"my-marketplace\",\n \"owner\": {\n \"name\": \"Your Name\",\n \"email\": \"you@example.com\"\n },\n \"description\": \"A collection of plugins\",\n \"plugins\": [\n {\n \"name\": \"my-plugin\",\n \"description\": \"What this plugin does\",\n \"source\": \"./plugins/my-plugin\",\n \"category\": \"development\",\n \"homepage\": \"https://github.com/you/my-plugin\"\n }\n ]\n}\n```\n\n### Required fields\n\n| Field | Description |\n|---|---|\n| `name` | Marketplace name. Lowercase alphanumeric, hyphens, and dots. Must start and end with alphanumeric. Max 64 chars. |\n| `owner.name` | Marketplace owner name |\n| `plugins` | Array of plugin entries |\n\n### Plugin entry fields\n\n| Field | Required | Description |\n|---|---|---|\n| `name` | yes | Plugin name (same rules as marketplace name) |\n| `source` | yes | Where to find the plugin (see below) |\n| `description` | no | Short description |\n| `version` | no | Version string |\n| `author` | no | `{ name, email? }` |\n| `homepage` | no | URL |\n| `category` | no | Category string (e.g. `development`, `productivity`, `security`) |\n| `tags` | no | Array of string tags |\n| `strict` | no | Boolean |\n| `commands` | no | Slash commands provided |\n| `agents` | no | Agents provided |\n| `hooks` | no | Hook definitions |\n| `mcpServers` | no | MCP server definitions |\n| `lspServers` | no | LSP server definitions |\n\n### Plugin source formats\n\nThe `source` field supports several formats:\n\n**Relative path** (within the marketplace repo):\n\n```json\n\"source\": \"./plugins/my-plugin\"\n```\n\n**Git repository URL**:\n\n```json\n\"source\": {\n \"source\": \"url\",\n \"url\": \"https://github.com/org/repo.git\",\n \"sha\": \"abc123...\"\n}\n```\n\n**GitHub shorthand**:\n\n```json\n\"source\": {\n \"source\": \"github\",\n \"repo\": \"org/repo\",\n \"ref\": \"main\",\n \"sha\": \"abc123...\"\n}\n```\n\n**Git subdirectory** (monorepo):\n\n```json\n\"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/org/monorepo.git\",\n \"path\": \"plugins/my-plugin\",\n \"ref\": \"main\",\n \"sha\": \"abc123...\"\n}\n```\n\n**npm package**:\n\n```json\n\"source\": {\n \"source\": \"npm\",\n \"package\": \"@scope/my-plugin\",\n \"version\": \"1.0.0\"\n}\n```\n\n## On-disk layout\n\n```\n~/.xcsh/\n config/\n marketplaces.json # Registry of added marketplaces\n plugins/\n installed_plugins.json # User-scoped installed plugins\n cache/\n marketplaces/ # Cached marketplace catalogs\n plugins/ # Cached plugin directories\n\n<project>/.xcsh/\n installed_plugins.json # Project-scoped installed plugins\n```\n\n## Naming rules\n\nMarketplace and plugin names must:\n\n- Start and end with a lowercase letter or digit\n- Contain only lowercase letters, digits, hyphens, and dots\n- Be at most 64 characters\n\nPlugin IDs (`name@marketplace`) must be at most 128 characters total.\n\nValid examples: `my-plugin`, `code-review`, `wordpress.com`, `ai-firstify`\nInvalid examples: `-bad`, `bad-`, `.bad`, `Bad`, `under_score`\n",
19
19
  "extensions/plugin-manager-installer-plumbing.md": "---\ntitle: Plugin Manager and Installer Plumbing\ndescription: Plugin manager internals covering installation, validation, dependency resolution, and lifecycle management.\nsidebar:\n order: 5\n label: Plugin manager\n---\n\n# Plugin manager and installer plumbing\n\nThis document describes how `xcsh plugin` operations mutate plugin state on disk and how installed plugins become runtime capabilities (tools today, hooks/commands path resolution available).\n\n## Scope and architecture\n\nThere are two plugin-management implementations in the codebase:\n\n1. **Active path used by CLI commands**: `PluginManager` (`src/extensibility/plugins/manager.ts`)\n2. **Legacy helper module**: installer functions (`src/extensibility/plugins/installer.ts`)\n\n`xcsh plugin ...` command execution goes through `PluginManager`.\n\n`installer.ts` still documents important safety checks and filesystem behavior, but it is not the path used by `src/commands/plugin.ts` + `src/cli/plugin-cli.ts`.\n\n## Lifecycle: from CLI invocation to runtime availability\n\n```text\nxcsh plugin <action> ...\n -> src/commands/plugin.ts\n -> runPluginCommand(...) in src/cli/plugin-cli.ts\n -> PluginManager method (install/list/uninstall/link/...) \n -> mutate ~/.xcsh/plugins/{package.json,node_modules,xcsh-plugins.lock.json}\n -> runtime discovery: discoverAndLoadCustomTools(...)\n -> getAllPluginToolPaths(cwd)\n -> custom tool loader imports tool modules\n```\n\n### Command entrypoints\n\n- `src/commands/plugin.ts` defines command/flags and forwards to `runPluginCommand`.\n- `src/cli/plugin-cli.ts` maps subcommands to `PluginManager` methods:\n - `install`, `uninstall`, `list`, `link`, `doctor`, `features`, `config`, `enable`, `disable`\n- No explicit `update` action exists; update is done by re-running `install` with a new package/version spec.\n\n## On-disk model\n\nGlobal plugin state lives under `~/.xcsh/plugins`:\n\n- `package.json` — dependency manifest used by `bun install`/`bun uninstall`\n- `node_modules/` — installed plugin packages or symlinks\n- `xcsh-plugins.lock.json` — runtime state:\n - enabled/disabled per plugin\n - selected feature set per plugin\n - persisted plugin settings\n\nProject-local overrides live at:\n\n- `<cwd>/.xcsh/plugin-overrides.json`\n\nOverrides are read-only from manager/loader perspective (no write path here) and can disable plugins or override features/settings for this project.\n\n## Plugin spec parsing and metadata interpretation\n\n## Install spec grammar\n\n`parsePluginSpec` (`parser.ts`) supports:\n\n- `pkg` -> `features: null` (defaults behavior)\n- `pkg[*]` -> enable all manifest features\n- `pkg[]` -> enable no optional features\n- `pkg[a,b]` -> enable named features\n- `@scope/pkg@1.2.3[feat]` -> scoped + versioned package with explicit feature selection\n\n`extractPackageName` strips version suffix for on-disk path lookup after install.\n\n## Manifest source and required fields\n\nManifest is resolved as:\n\n1. `package.json.xcsh`\n2. fallback `package.json.pi`\n3. fallback `{ version: package.version }`\n\nImplications:\n\n- There is no strict schema validation in manager/loader.\n- A package missing `xcsh`/`pi` is still installable and listable.\n- Runtime plugin loading (`getEnabledPlugins`) skips packages without `xcsh`/`pi` manifest.\n- `manifest.version` is always overwritten from package `version`.\n\nMalformed `package.json` JSON is a hard failure at read time; malformed manifest shape may fail later only when specific fields are consumed.\n\n## Install/update flow (`PluginManager.install`)\n\n1. Parse feature bracket syntax from install spec.\n2. Validate package name against regex + shell-metacharacter denylist.\n3. Ensure plugin `package.json` exists (`xcsh-plugins`, private dependencies map).\n4. Run `bun install <packageSpec>` in `~/.xcsh/plugins`.\n5. Read installed package `node_modules/<name>/package.json`.\n6. Resolve manifest and compute `enabledFeatures`:\n - `[*]`: all declared features (or `null` if no feature map)\n - `[a,b]`: validates each feature exists in manifest features map\n - `[]`: empty feature list\n - bare spec: `null` (use defaults policy later in loader)\n7. Upsert lockfile runtime state: `{ version, enabledFeatures, enabled: true }`.\n\n### Update semantics\n\nBecause update is install-driven:\n\n- `xcsh plugin install pkg@newVersion` updates dependency and lockfile version.\n- Existing settings are preserved; state entry is overwritten for version/features/enabled.\n- No separate “check updates” or transactional migration logic exists.\n\n## Remove flow (`PluginManager.uninstall`)\n\n1. Validate package name.\n2. Run `bun uninstall <name>` in plugin dir.\n3. Remove plugin runtime state from lockfile:\n - `config.plugins[name]`\n - `config.settings[name]`\n\nIf uninstall command fails, runtime state is not changed.\n\n## List flow (`PluginManager.list`)\n\n1. Read plugin dependency map from `~/.xcsh/plugins/package.json`.\n2. Load lockfile runtime config (missing file -> empty defaults).\n3. Load project overrides (`<cwd>/.xcsh/plugin-overrides.json`, parse/read errors -> empty object with warning).\n4. For each dependency with a resolvable package.json:\n - build `InstalledPlugin` record\n - merge feature/enable state:\n - base from lockfile (or defaults)\n - project overrides can replace feature selection\n - project `disabled` list masks plugin as disabled\n\nThis is the effective state used by CLI status output and settings/features operations.\n\n## Link flow (`PluginManager.link`)\n\n`link` supports local plugin development by symlinking a local package into `~/.xcsh/plugins/node_modules/<pkg.name>`.\n\nBehavior:\n\n1. Resolve `localPath` against manager cwd.\n2. Require local `package.json` and `name` field.\n3. Ensure plugin dirs exist.\n4. For scoped names, create scope directory.\n5. Remove existing path at target link location.\n6. Create symlink.\n7. Add runtime lockfile entry enabled with default features (`null`).\n\nCaveat: current `PluginManager.link` does not enforce the `cwd` path-boundary check present in legacy `installer.ts` (`normalizedPath.startsWith(normalizedCwd)`), so trust is the caller’s responsibility.\n\n## Runtime loading: from installed plugin to callable capabilities\n\n## Discovery gate\n\n`getEnabledPlugins(cwd)` (`plugins/loader.ts`) reads:\n\n- plugin dependency manifest (`package.json`)\n- lockfile runtime state\n- project overrides via `getConfigDirPaths(\"plugin-overrides.json\", { user: false, cwd })`\n\nFiltering:\n\n- skip if no plugin package.json\n- skip if manifest (`xcsh`/`pi`) absent\n- skip if globally disabled in lockfile\n- skip if project-disabled\n\n## Capability path resolution\n\nFor each enabled plugin:\n\n- `resolvePluginToolPaths(plugin)`\n- `resolvePluginHookPaths(plugin)`\n- `resolvePluginCommandPaths(plugin)`\n\nEach resolver includes base entries plus feature entries:\n\n- explicit feature list -> only selected features\n- `enabledFeatures === null` -> enable features marked `default: true`\n\nMissing files are silently skipped (`existsSync` guard).\n\n## Current runtime wiring differences\n\n- **Tools are wired into runtime today** via `discoverAndLoadCustomTools` (`custom-tools/loader.ts`), which calls `getAllPluginToolPaths(cwd)`.\n- Paths are de-duplicated by resolved absolute path in custom tool discovery (`seen` set, first path wins).\n- **Hooks/commands resolvers exist** and are exported, but this code path does not currently wire them into a runtime registry in the same way tools are wired.\n\n## Lock/state management details\n\n`PluginManager` caches runtime config in memory per instance (`#runtimeConfig`) and lazily loads once.\n\nLoad behavior:\n\n- lockfile missing -> `{ plugins: {}, settings: {} }`\n- lockfile read/parse failure -> warning + same empty defaults\n\nSave behavior:\n\n- writes full lockfile JSON pretty-printed each mutation\n\nNo cross-process locking or merge strategy exists; concurrent writers can overwrite each other.\n\n## Safety checks and trust boundaries\n\n## Input/package validation\n\nActive manager path enforces package-name validation:\n\n- regex for scoped/unscoped package specs (optionally with version)\n- explicit shell metacharacter denylist (`[;&|`$(){}[]<>\\\\]`)\n\nThis limits command-injection risk when invoking `bun install/uninstall`.\n\n## Filesystem trust boundary\n\n- Plugin code executes in-process when custom tool modules are imported; no sandboxing.\n- Manifest relative paths are joined against plugin package directory and only existence-checked.\n- The plugin package itself is trusted code once installed.\n\n## Legacy installer-only checks\n\n`installer.ts` includes additional link-time checks not mirrored in `PluginManager.link`:\n\n- local path must resolve inside project cwd\n- extra package name/path traversal guards for symlink target naming\n\nBecause CLI uses `PluginManager`, these stricter link guards are not currently on the main path.\n\n## Failure, partial success, and rollback behavior\n\nThe plugin manager is not transactional.\n\n| Operation stage | Failure behavior | Rollback |\n| --- | --- | --- |\n| `bun install` fails | install aborts with stderr | N/A (no state writes yet) |\n| Install succeeds, then manifest/feature validation fails | command fails | No uninstall rollback; dependency may remain in `node_modules`/`package.json` |\n| Install succeeds, then lockfile write fails | command fails | No rollback of installed package |\n| `bun uninstall` succeeds, lockfile write fails | command fails | Package removed, stale runtime state may remain |\n| `link` removes old target then symlink creation fails | command fails | No restoration of previous link/dir |\n\nOperationally, `doctor --fix` can repair some drift (`bun install`, orphaned config cleanup, invalid-feature cleanup), but it is best-effort.\n\n## Malformed/missing manifest behavior summary\n\n- Missing `xcsh`/`pi` field:\n - install/list: tolerated (minimal manifest)\n - runtime enabled-plugin discovery: skipped as non-plugin\n- Missing feature referenced by install spec or `features --set/--enable`: hard error with available feature list\n- Invalid `plugin-overrides.json`: ignored with fallback to `{}` in both manager and loader paths\n- Missing tool/hook/command file paths referenced by manifest: silently ignored during resolver expansion; flagged as errors only by `doctor`\n\n## Mode differences and precedence\n\n- `--dry-run` (install): returns synthetic install result, no filesystem/network/state writes.\n- `--json`: output formatting only, no behavior change.\n- Project overrides always take precedence over global lockfile for feature/settings view.\n- Effective enablement is `runtimeEnabled && !projectDisabled`.\n\n## Implementation files\n\n- [`src/commands/plugin.ts`](../../packages/coding-agent/src/commands/plugin.ts) — CLI command declaration and flag mapping\n- [`src/cli/plugin-cli.ts`](../../packages/coding-agent/src/cli/plugin-cli.ts) — action dispatch, user-facing command handlers\n- [`src/extensibility/plugins/manager.ts`](../../packages/coding-agent/src/extensibility/plugins/manager.ts) — active install/remove/list/link/state/doctor implementation\n- [`src/extensibility/plugins/installer.ts`](../../packages/coding-agent/src/extensibility/plugins/installer.ts) — legacy installer helpers and additional link safety checks\n- [`src/extensibility/plugins/loader.ts`](../../packages/coding-agent/src/extensibility/plugins/loader.ts) — enabled-plugin discovery and tool/hook/command path resolution\n- [`src/extensibility/plugins/parser.ts`](../../packages/coding-agent/src/extensibility/plugins/parser.ts) — install spec and package-name parsing helpers\n- [`src/extensibility/plugins/types.ts`](../../packages/coding-agent/src/extensibility/plugins/types.ts) — manifest/runtime/override type contracts\n- [`src/extensibility/custom-tools/loader.ts`](../../packages/coding-agent/src/extensibility/custom-tools/loader.ts) — runtime wiring for plugin-provided tool modules\n",
20
20
  "extensions/rulebook-matching-pipeline.md": "---\ntitle: Rulebook Matching Pipeline\ndescription: Rulebook matching pipeline for selecting and applying context-specific instruction sets to agent sessions.\nsidebar:\n order: 6\n label: Rulebook matching\n---\n\n# Rulebook Matching Pipeline\n\nThis document describes how coding-agent discovers rules from supported config formats, normalizes them into a single `Rule` shape, resolves precedence conflicts, and splits the result into:\n\n- **Rulebook rules** (available to the model via system prompt + `rule://` URLs)\n- **TTSR rules** (time-travel stream interruption rules)\n\nIt reflects the current implementation, including partial semantics and metadata that is parsed but not enforced.\n\n## Implementation files\n\n- [`../src/capability/rule.ts`](../../packages/coding-agent/src/capability/rule.ts)\n- [`../src/capability/index.ts`](../../packages/coding-agent/src/capability/index.ts)\n- [`../src/discovery/index.ts`](../../packages/coding-agent/src/discovery/index.ts)\n- [`../src/discovery/helpers.ts`](../../packages/coding-agent/src/discovery/helpers.ts)\n- [`../src/discovery/builtin.ts`](../../packages/coding-agent/src/discovery/builtin.ts)\n- [`../src/discovery/cursor.ts`](../../packages/coding-agent/src/discovery/cursor.ts)\n- [`../src/discovery/windsurf.ts`](../../packages/coding-agent/src/discovery/windsurf.ts)\n- [`../src/discovery/cline.ts`](../../packages/coding-agent/src/discovery/cline.ts)\n- [`../src/sdk.ts`](../../packages/coding-agent/src/sdk.ts)\n- [`../src/system-prompt.ts`](../../packages/coding-agent/src/system-prompt.ts)\n- [`../src/internal-urls/rule-protocol.ts`](../../packages/coding-agent/src/internal-urls/rule-protocol.ts)\n- [`../src/utils/frontmatter.ts`](../../packages/coding-agent/src/utils/frontmatter.ts)\n\n## 1. Canonical rule shape\n\nAll providers normalize source files into `Rule`:\n\n```ts\ninterface Rule {\n name: string;\n path: string;\n content: string;\n globs?: string[];\n alwaysApply?: boolean;\n description?: string;\n ttsrTrigger?: string;\n _source: SourceMeta;\n}\n```\n\nCapability identity is `rule.name` (`ruleCapability.key = rule => rule.name`).\n\nConsequence: precedence and deduplication are **name-based only**. Two different files with the same `name` are considered the same logical rule.\n\n## 2. Discovery sources and normalization\n\n`src/discovery/index.ts` auto-registers providers. For `rules`, current providers are:\n\n- `native` (priority `100`)\n- `cursor` (priority `50`)\n- `windsurf` (priority `50`)\n- `cline` (priority `40`)\n\n### Native provider (`builtin.ts`)\n\nLoads `.xcsh` rules from:\n\n- project: `<cwd>/.xcsh/rules/*.{md,mdc}`\n- user: `~/.xcsh/agent/rules/*.{md,mdc}`\n\nNormalization:\n\n- `name` = filename without `.md`/`.mdc`\n- frontmatter parsed via `parseFrontmatter`\n- `content` = body (frontmatter stripped)\n- `globs`, `alwaysApply`, `description`, `ttsr_trigger` mapped directly\n\nImportant caveat: `globs` is cast as `string[] | undefined` with no element filtering in this provider.\n\n### Cursor provider (`cursor.ts`)\n\nLoads from:\n\n- user: `~/.cursor/rules/*.{mdc,md}`\n- project: `<cwd>/.cursor/rules/*.{mdc,md}`\n\nNormalization (`transformMDCRule`):\n\n- `description`: kept only if string\n- `alwaysApply`: only `true` is preserved (`false` becomes `undefined`)\n- `globs`: accepts array (string elements only) or single string\n- `ttsr_trigger`: string only\n- `name` from filename without extension\n\n### Windsurf provider (`windsurf.ts`)\n\nLoads from:\n\n- user: `~/.codeium/windsurf/memories/global_rules.md` (fixed rule name `global_rules`)\n- project: `<cwd>/.windsurf/rules/*.md`\n\nNormalization:\n\n- `globs`: array-of-string or single string\n- `alwaysApply`, `description` cast from frontmatter\n- `ttsr_trigger`: string only\n- `name` from filename for project rules\n\n### Cline provider (`cline.ts`)\n\nSearches upward from `cwd` for nearest `.clinerules`:\n\n- if directory: loads `*.md` inside it\n- if file: loads single file as rule named `clinerules`\n\nNormalization:\n\n- `globs`: array-of-string or single string\n- `alwaysApply`: only if boolean\n- `description`: string only\n- `ttsr_trigger`: string only\n\n## 3. Frontmatter parsing behavior and ambiguity\n\nAll providers use `parseFrontmatter` (`utils/frontmatter.ts`) with these semantics:\n\n1. Frontmatter is parsed only when content starts with `---` and has a closing `\\n---`.\n2. Body is trimmed after frontmatter extraction.\n3. If YAML parse fails:\n - warning is logged,\n - parser falls back to simple `key: value` line parsing (`^(\\w+):\\s*(.*)$`).\n\nAmbiguity consequences:\n\n- Fallback parser does not support arrays, nested objects, quoting rules, or hyphenated keys.\n- Fallback values become strings (for example `alwaysApply: true` becomes string `\"true\"`), so providers requiring boolean/string types may drop metadata.\n- `ttsr_trigger` works in fallback (underscore key); keys like `thinking-level` would not.\n- Files without valid frontmatter still load as rules with empty metadata and full content body.\n\n## 4. Provider precedence and deduplication\n\n`loadCapability(\"rules\")` (`capability/index.ts`) merges provider outputs and then deduplicates by `rule.name`.\n\n### Precedence model\n\n- Providers are ordered by priority descending.\n- Equal priority keeps registration order (`cursor` before `windsurf` from `discovery/index.ts`).\n- Dedup is first-wins: first encountered rule name is kept; later same-name items are marked `_shadowed` in `all` and excluded from `items`.\n\nEffective rule provider order is currently:\n\n1. `native` (100)\n2. `cursor` (50)\n3. `windsurf` (50)\n4. `cline` (40)\n\n### Intra-provider ordering caveat\n\nWithin a provider, item order comes from `loadFilesFromDir` glob result ordering plus explicit push order. This is deterministic enough for normal use but not explicitly sorted in code.\n\nNotable source-order differences:\n\n- `native` appends project then user config dirs.\n- `cursor` appends user then project results.\n- `windsurf` appends user `global_rules` first, then project rules.\n- `cline` loads only nearest `.clinerules` source.\n\n## 5. Split into Rulebook, Always-Apply, and TTSR buckets\n\nAfter rule discovery in `createAgentSession` (`sdk.ts`):\n\n1. All discovered rules are scanned.\n2. Rules with `condition` (frontmatter key; `ttsr_trigger` / `ttsrTrigger` accepted as fallback) are registered into `TtsrManager`.\n3. A separate `rulebookRules` list is built with this predicate:\n\n```ts\n!registeredTtsrRuleNames.has(rule.name) && !rule.alwaysApply && !!rule.description\n```\n\n4. An `alwaysApplyRules` list is built:\n\n```ts\n!registeredTtsrRuleNames.has(rule.name) && rule.alwaysApply === true\n```\n\n### Bucket behavior\n\n- **TTSR bucket**: any rule with `condition` (description not required). Takes priority over other buckets.\n- **Always-apply bucket**: `alwaysApply === true`, not TTSR. Full content injected into system prompt. Resolvable via `rule://`.\n- **Rulebook bucket**: must have description, must not be TTSR, must not be `alwaysApply`. Listed in system prompt by name+description; content read on demand via `rule://`.\n- A rule with both `condition` and `alwaysApply` goes to TTSR only (TTSR takes priority).\n- A rule with both `alwaysApply` and `description` goes to always-apply only (not rulebook).\n\n## 6. How metadata affects runtime surfaces\n\n### `description`\n\n- Required for inclusion in rulebook.\n- Rendered in system prompt `<rules>` block.\n- Missing description means rule is not available via `rule://` and not listed in system prompt rules.\n\n### `globs`\n\n- Carried through on `Rule`.\n- Rendered as `<glob>...</glob>` entries in the system prompt rules block.\n- Exposed in rules UI state (`extensions` mode list).\n- **Not enforced for automatic matching in this pipeline.** There is no runtime glob matcher selecting rules by current file/tool target.\n\n### `alwaysApply`\n\n- Parsed and preserved by providers.\n- Used in UI display (`\"always\"` trigger label in extensions state manager).\n- Used as an exclusion condition from `rulebookRules`.\n- **Full rule content is auto-injected into the system prompt** (before the rulebook rules section).\n- Rule is also addressable via `rule://<name>` for re-reading.\n\n### `ttsr_trigger`\n\n- Mapped to `rule.ttsrTrigger`.\n- If present, rule is routed to TTSR manager, not rulebook.\n\n## 7. System prompt inclusion path\n\n`buildSystemPromptInternal` receives both `rules` (rulebook) and `alwaysApplyRules`.\n\nAlways-apply rules are rendered first, injecting their raw content directly into the prompt.\n\nRulebook rules are rendered in a `# Rules` section with:\n\n- `Read rule://<name> when working in matching domain`\n- Each rule's `name`, `description`, and optional `<glob>` list\n\nThis is advisory/contextual: prompt text asks the model to read applicable rules, but code does not enforce glob applicability.\n\n## 8. `rule://` internal URL behavior\n\n`RuleProtocolHandler` is registered with:\n\n```ts\nnew RuleProtocolHandler({ getRules: () => [...rulebookRules, ...alwaysApplyRules] })\n```\n\nImplications:\n\n- `rule://<name>` resolves against both **rulebookRules** and **alwaysApplyRules**.\n- TTSR-only rules and rules with no description and no `alwaysApply` are not addressable via `rule://`.\n- Resolution is exact name match.\n- Unknown names return error listing available rule names.\n- Returned content is raw `rule.content` (frontmatter stripped), content type `text/markdown`.\n\n## 9. Known partial / non-enforced semantics\n\n1. Provider descriptions mention legacy files (`.cursorrules`, `.windsurfrules`), but current loader code paths do not actually read those files.\n2. `globs` metadata is surfaced to prompt/UI but not enforced by rule selection logic.\n3. Rule selection for `rule://` includes rulebook and always-apply rules, but not TTSR-only rules.\n4. Discovery warnings (`loadCapability(\"rules\").warnings`) are produced but `createAgentSession` does not currently surface/log them in this path.\n",
21
21
  "extensions/skills.md": "---\ntitle: Skills\ndescription: Skills system for registering, discovering, and invoking specialized capabilities in the coding agent.\nsidebar:\n order: 3\n label: Skills\n---\n\n# Skills\n\nSkills are file-backed capability packs discovered at startup and exposed to the model as:\n\n- lightweight metadata in the system prompt (name + description)\n- on-demand content via `read skill://...`\n- optional interactive `/skill:<name>` commands\n\nThis document covers current runtime behavior in `src/extensibility/skills.ts`, `src/discovery/builtin.ts`, `src/internal-urls/skill-protocol.ts`, and `src/discovery/agents-md.ts`.\n\n## What a skill is in this codebase\n\nA discovered skill is represented as:\n\n- `name`\n- `description`\n- `filePath` (the `SKILL.md` path)\n- `baseDir` (skill directory)\n- source metadata (`provider`, `level`, path)\n\nThe runtime only requires `name` and `path` for validity. In practice, matching quality depends on `description` being meaningful.\n\n## Required layout and SKILL.md expectations\n\n### Directory layout\n\nFor provider-based discovery (native/Claude/Codex/Agents/plugin providers), skills are discovered as **one level under `skills/`**:\n\n- `<skills-root>/<skill-name>/SKILL.md`\n\nNested patterns like `<skills-root>/group/<skill>/SKILL.md` are not discovered by provider loaders.\n\nFor `skills.customDirectories`, scanning uses the same non-recursive layout (`*/SKILL.md`).\n\n```text\nProvider-discovered layout (non-recursive under skills/):\n\n<root>/skills/\n ├─ postgres/\n │ └─ SKILL.md ✅ discovered\n ├─ pdf/\n │ └─ SKILL.md ✅ discovered\n └─ team/\n └─ internal/\n └─ SKILL.md ❌ not discovered by provider loaders\n\nCustom-directory scanning is also non-recursive, so nested paths are ignored unless you point `customDirectories` at that nested parent.\n```\n\n### `SKILL.md` frontmatter\n\nSupported frontmatter fields on the skill type:\n\n- `name?: string`\n- `description?: string`\n- `globs?: string[]`\n- `alwaysApply?: boolean`\n- additional keys are preserved as unknown metadata\n\nCurrent runtime behavior:\n\n- `name` defaults to the skill directory name\n- `description` is required for:\n - native `.xcsh` provider skill discovery (`requireDescription: true`)\n - `skills.customDirectories` scans via `scanSkillsFromDir` in `src/discovery/helpers.ts` (non-recursive)\n- non-native providers can load skills without description\n\n## Discovery pipeline\n\n`discoverSkills()` in `src/extensibility/skills.ts` does two passes:\n\n1. **Capability providers** via `loadCapability(\"skills\")`\n2. **Custom directories** via `scanSkillsFromDir(..., { requireDescription: true })` (one-level directory enumeration)\n\nIf `skills.enabled` is `false`, discovery returns no skills.\n\n### Built-in skill providers and precedence\n\nProvider ordering is priority-first (higher wins), then registration order for ties.\n\nCurrent registered skill providers:\n\n1. `native` (priority 100) — `.xcsh` user/project skills via `src/discovery/builtin.ts`\n2. `claude` (priority 80)\n3. priority 70 group (in registration order):\n - `claude-plugins`\n - `agents`\n - `codex`\n\nDedup key is skill name. First item with a given name wins.\n\n### Source toggles and filtering\n\n`discoverSkills()` applies these controls:\n\n- source toggles: `enableCodexUser`, `enableClaudeUser`, `enableClaudeProject`, `enablePiUser`, `enablePiProject`\n- glob filters on skill name:\n - `ignoredSkills` (exclude)\n - `includeSkills` (include allowlist; empty means include all)\n\nFilter order is:\n\n1. source enabled\n2. not ignored\n3. included (if include list present)\n\nFor providers other than codex/claude/native (for example `agents`, `claude-plugins`), enablement currently falls back to: enabled if **any** built-in source toggle is enabled.\n\n### Collision and duplicate handling\n\n- Capability dedup already keeps first skill per name (highest-precedence provider)\n- `extensibility/skills.ts` additionally:\n - de-duplicates identical files by `realpath` (symlink-safe)\n - emits collision warnings when a later skill name conflicts\n - keeps the convenience `discoverSkillsFromDir({ dir, source })` API as a thin adapter over `scanSkillsFromDir`\n- Custom-directory skills are merged after provider skills and follow the same collision behavior\n\n## Runtime usage behavior\n\n### System prompt exposure\n\nSystem prompt construction (`src/system-prompt.ts`) uses discovered skills as follows:\n\n- if `read` tool is available:\n - include discovered skills list in prompt\n- otherwise:\n - omit discovered list\n\nTask tool subagents receive the session's discovered/provided skills list via normal session creation; there is no per-task skill pinning override.\n\n### Interactive `/skill:<name>` commands\n\nIf `skills.enableSkillCommands` is true, interactive mode registers one slash command per discovered skill.\n\n`/skill:<name> [args]` behavior:\n\n- reads the skill file directly from `filePath`\n- strips frontmatter\n- injects skill body as a follow-up custom message\n- appends metadata (`Skill: <path>`, optional `User: <args>`)\n\n## `skill://` URL behavior\n\n`src/internal-urls/skill-protocol.ts` supports:\n\n- `skill://<name>` → resolves to that skill's `SKILL.md`\n- `skill://<name>/<relative-path>` → resolves inside that skill directory\n\n```text\nskill:// URL resolution\n\nskill://pdf\n -> <pdf-base>/SKILL.md\n\nskill://pdf/references/tables.md\n -> <pdf-base>/references/tables.md\n\nGuards:\n- reject absolute paths\n- reject `..` traversal\n- reject any resolved path escaping <pdf-base>\n```\n\nResolution details:\n\n- skill name must match exactly\n- relative paths are URL-decoded\n- absolute paths are rejected\n- path traversal (`..`) is rejected\n- resolved path must remain within `baseDir`\n- missing files return an explicit `File not found` error\n\nContent type:\n\n- `.md` => `text/markdown`\n- everything else => `text/plain`\n\nNo fallback search is performed for missing assets.\n\n## Skills vs AGENTS.md, commands, tools, hooks\n\n### Skills vs AGENTS.md\n\n- **Skills**: named, optional capability packs selected by task context or explicitly requested\n- **AGENTS.md/context files**: persistent instruction files loaded as context-file capability and merged by level/depth rules\n\n`src/discovery/agents-md.ts` specifically walks ancestor directories from `cwd` to discover standalone `AGENTS.md` files (up to depth 20), excluding hidden-directory segments.\n\n### Skills vs slash commands\n\n- **Skills**: model-readable knowledge/workflow content\n- **Slash commands**: user-invoked command entry points\n- `/skill:<name>` is a convenience wrapper that injects skill text; it does not change skill discovery semantics\n\n### Skills vs custom tools\n\n- **Skills**: documentation/workflow content loaded through prompt context and `read`\n- **Custom tools**: executable tool APIs callable by the model with schemas and runtime side effects\n\n### Skills vs hooks\n\n- **Skills**: passive content\n- **Hooks**: event-driven runtime interceptors that can block/modify behavior during execution\n\n## Practical authoring guidance tied to discovery logic\n\n- Put each skill in its own directory: `<skills-root>/<skill-name>/SKILL.md`\n- Always include explicit `name` and `description` frontmatter\n- Keep referenced assets under the same skill directory and access with `skill://<name>/...`\n- For nested taxonomy (`team/domain/skill`), point `skills.customDirectories` to the nested parent directory; scanning itself remains non-recursive\n- Avoid duplicate skill names across sources; first match wins by provider precedence\n",
package/src/main.ts CHANGED
@@ -33,7 +33,7 @@ import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedM
33
33
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
34
34
  import { initializeWithSettings } from "./discovery";
35
35
  import {
36
- clearClaudePluginRootsCache,
36
+ clearXcshPluginRootsCache,
37
37
  injectPluginDirRoots,
38
38
  preloadPluginRoots,
39
39
  resolveActiveProjectRegistryPath,
@@ -739,7 +739,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
739
739
  const h = os.homedir();
740
740
  invalidateFsCache(path.join(h, getConfigDirName(), "plugins", "installed_plugins.json"));
741
741
  for (const p of extraPaths ?? []) invalidateFsCache(p);
742
- clearClaudePluginRootsCache();
742
+ clearXcshPluginRootsCache();
743
743
  },
744
744
  });
745
745
  await mgr.refreshStaleMarketplaces();
package/src/mcp/loader.ts CHANGED
@@ -98,7 +98,7 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
98
98
  connection?._source?.providerName ?? source?.providerName ?? connection?._source?.provider ?? source?.provider;
99
99
 
100
100
  // Format path with provider info if available
101
- // Format: "mcp:serverName via providerName" (e.g., "mcp:agentx via Claude Code")
101
+ // Format: "mcp:serverName via providerName" (e.g., "mcp:agentx via xcsh")
102
102
  const path = serverName && providerName ? `mcp:${serverName} via ${providerName}` : `mcp:${tool.name}`;
103
103
 
104
104
  return {
@@ -68,9 +68,9 @@ export interface MCPToolDetails {
68
68
  isError?: boolean;
69
69
  /** Raw content from MCP response */
70
70
  rawContent?: MCPContent[];
71
- /** Provider ID (e.g., "claude", "mcp-json") */
71
+ /** Provider ID (e.g., "xcsh", "mcp-json") */
72
72
  provider?: string;
73
- /** Provider display name (e.g., "Claude Code", "MCP Config") */
73
+ /** Provider display name (e.g., "xcsh", "MCP Config") */
74
74
  providerName?: string;
75
75
  }
76
76
  /**
@@ -230,7 +230,7 @@ export class DisposableContainer extends Container {
230
230
  * Animated ● gutter for tool calls and slash-command executions.
231
231
  * Active: pulsing dot — alternates ● / blank in `muted` color to
232
232
  * differentiate from the braille ✻ thinking spinner. Matches the
233
- * Claude Code tool-initialization aesthetic.
233
+ * xcsh tool-initialization aesthetic.
234
234
  * Done (unknown outcome): `dim` — neutral "completed" color when the call
235
235
  * site does not have success/error information.
236
236
  * Done (success): `gutterSuccess` (falls back to `success` when the theme
@@ -13,7 +13,7 @@ import {
13
13
  } from "@f5xc-salesdemos/pi-tui";
14
14
  import { getConfigDirName } from "@f5xc-salesdemos/pi-utils";
15
15
  import { invalidate as invalidateFsCache } from "../../../capability/fs";
16
- import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../../discovery/helpers";
16
+ import { clearXcshPluginRootsCache, resolveActiveProjectRegistryPath } from "../../../discovery/helpers";
17
17
  import { PluginManager } from "../../../extensibility/plugins";
18
18
  import {
19
19
  getInstalledPluginsRegistryPath,
@@ -30,7 +30,7 @@ import { PluginListPane } from "./plugin-list-pane";
30
30
  import { applySearch, buildTabs, createInitialState, filterByTab, loadAllPlugins } from "./state-manager";
31
31
  import type { DashboardPlugin, PluginDashboardState, PluginTabId } from "./types";
32
32
 
33
- const DEFAULT_MARKETPLACE = "anthropics/claude-plugins-official";
33
+ const DEFAULT_MARKETPLACE = "f5xc-salesdemos/marketplace";
34
34
 
35
35
  class TwoColumnBody implements Component {
36
36
  constructor(
@@ -98,7 +98,7 @@ export class PluginDashboard extends Container {
98
98
  const home = os.homedir();
99
99
  invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
100
100
  for (const p of extraPaths ?? []) invalidateFsCache(p);
101
- clearClaudePluginRootsCache();
101
+ clearXcshPluginRootsCache();
102
102
  },
103
103
  });
104
104
 
@@ -331,7 +331,7 @@ export class PluginDashboard extends Container {
331
331
  #buildLayout(): void {
332
332
  this.clear();
333
333
  this.addChild(new DynamicBorder());
334
- this.addChild(new Text(theme.bold(theme.fg("contentAccent", " Plugin Control Center")), 0, 0));
334
+ this.addChild(new Text(theme.bold(theme.fg("contentAccent", " xcsh Plugin Center")), 0, 0));
335
335
  this.addChild(new Text(this.#renderTabBar(), 0, 0));
336
336
  this.addChild(new Spacer(1));
337
337
 
@@ -13,7 +13,7 @@ import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@f5xc-sal
13
13
  import { formatDuration, Snowflake, setProjectDir, setShellPwd } from "@f5xc-salesdemos/pi-utils";
14
14
  import { $ } from "bun";
15
15
  import { reset as resetCapabilities } from "../../capability";
16
- import { clearClaudePluginRootsCache } from "../../discovery/helpers";
16
+ import { clearXcshPluginRootsCache } from "../../discovery/helpers";
17
17
  import { loadCustomShare } from "../../export/custom-share";
18
18
  import type { CompactOptions } from "../../extensibility/extensions/types";
19
19
  import { getGatewayStatus } from "../../ipy/gateway-coordinator";
@@ -677,7 +677,7 @@ export class CommandController {
677
677
  await this.ctx.sessionManager.flush();
678
678
  await this.ctx.sessionManager.moveTo(resolvedPath);
679
679
  setProjectDir(resolvedPath);
680
- clearClaudePluginRootsCache(); // re-warms preloadedPluginRoots with new project dir (async)
680
+ clearXcshPluginRootsCache(); // re-warms preloadedPluginRoots with new project dir (async)
681
681
  resetCapabilities();
682
682
  await this.ctx.refreshSlashCommandState(resolvedPath);
683
683
 
@@ -19,7 +19,7 @@ import { formatModelSelectorValue } from "../../config/model-resolver";
19
19
  import { settings } from "../../config/settings";
20
20
  import { DebugSelectorComponent } from "../../debug";
21
21
  import { disableProvider, enableProvider } from "../../discovery";
22
- import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
22
+ import { clearXcshPluginRootsCache, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
23
23
  import {
24
24
  getInstalledPluginsRegistryPath,
25
25
  getMarketplacesCacheDir,
@@ -482,7 +482,7 @@ export class SelectorController {
482
482
  const home = os.homedir();
483
483
  invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
484
484
  for (const p of extraPaths ?? []) invalidateFsCache(p);
485
- clearClaudePluginRootsCache();
485
+ clearXcshPluginRootsCache();
486
486
  },
487
487
  });
488
488
 
@@ -8,7 +8,7 @@ import { invalidate as invalidateFsCache } from "../capability/fs";
8
8
  import type { SettingPath, SettingValue } from "../config/settings";
9
9
  import { settings } from "../config/settings";
10
10
  import {
11
- clearClaudePluginRootsCache,
11
+ clearXcshPluginRootsCache,
12
12
  resolveActiveProjectRegistryPath,
13
13
  resolveOrDefaultProjectRegistryPath,
14
14
  } from "../discovery/helpers.js";
@@ -905,7 +905,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
905
905
  const home = os.homedir();
906
906
  invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
907
907
  for (const p of extraPaths ?? []) invalidateFsCache(p);
908
- clearClaudePluginRootsCache();
908
+ clearXcshPluginRootsCache();
909
909
  },
910
910
  });
911
911
 
@@ -946,7 +946,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
946
946
  const marketplaces = await mgr.listMarketplaces();
947
947
  if (marketplaces.length === 0) {
948
948
  runtime.ctx.showStatus(
949
- "No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official",
949
+ "No marketplaces configured. Try:\n /marketplace add f5xc-salesdemos/marketplace",
950
950
  );
951
951
  } else {
952
952
  runtime.ctx.showStatus("No plugins available in configured marketplaces");
@@ -1043,7 +1043,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1043
1043
  " /marketplace upgrade [name@marketplace] Upgrade plugin(s)",
1044
1044
  "",
1045
1045
  "Quick start:",
1046
- " /marketplace add anthropics/claude-plugins-official",
1046
+ " /marketplace add f5xc-salesdemos/marketplace",
1047
1047
  " /marketplace (opens interactive browser)",
1048
1048
  ].join("\n"),
1049
1049
  );
@@ -1053,7 +1053,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1053
1053
  const marketplaces = await mgr.listMarketplaces();
1054
1054
  if (marketplaces.length === 0) {
1055
1055
  runtime.ctx.showStatus(
1056
- "No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse plugins with /marketplace or /marketplace discover",
1056
+ "No marketplaces configured.\n\nGet started:\n /marketplace add f5xc-salesdemos/marketplace\n\nThen browse plugins with /marketplace or /marketplace discover",
1057
1057
  );
1058
1058
  } else {
1059
1059
  const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
@@ -1104,7 +1104,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1104
1104
  const home = os.homedir();
1105
1105
  invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
1106
1106
  for (const p of extraPaths ?? []) invalidateFsCache(p);
1107
- clearClaudePluginRootsCache();
1107
+ clearXcshPluginRootsCache();
1108
1108
  },
1109
1109
  });
1110
1110
 
@@ -1177,12 +1177,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
1177
1177
  description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
1178
1178
  handle: async (_command, runtime) => {
1179
1179
  // Invalidate the fs content cache for all registry files so
1180
- // listClaudePluginRoots re-reads from disk on next access.
1180
+ // listXcshPluginRoots re-reads from disk on next access.
1181
1181
  const home = os.homedir();
1182
1182
  invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
1183
1183
  const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
1184
1184
  if (projectPath) invalidateFsCache(projectPath);
1185
- clearClaudePluginRootsCache();
1185
+ clearXcshPluginRootsCache();
1186
1186
  await runtime.ctx.refreshSlashCommandState();
1187
1187
  runtime.ctx.showStatus("Plugins reloaded.");
1188
1188
  runtime.ctx.editor.setText("");
@@ -16,7 +16,7 @@ import * as os from "node:os";
16
16
  import * as path from "node:path";
17
17
  import { logger } from "@f5xc-salesdemos/pi-utils";
18
18
  import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config";
19
- import { listClaudePluginRoots } from "../discovery/helpers";
19
+ import { listXcshPluginRoots } from "../discovery/helpers";
20
20
  import { loadBundledAgents, parseAgent } from "./agents";
21
21
  import type { AgentDefinition, AgentSource } from "./types";
22
22
 
@@ -87,8 +87,8 @@ export async function discoverAgents(cwd: string, home: string = os.homedir()):
87
87
  if (user) orderedDirs.push({ dir: user.path, source: "user" });
88
88
  }
89
89
 
90
- // Load agents from Claude Code marketplace plugins
91
- const { roots: pluginRoots } = await listClaudePluginRoots(home, resolvedCwd);
90
+ // Load agents from xcsh marketplace plugins
91
+ const { roots: pluginRoots } = await listXcshPluginRoots(home, resolvedCwd);
92
92
  const sortedPluginRoots = [...pluginRoots].sort((a, b) => {
93
93
  if (a.scope === b.scope) return 0;
94
94
  return a.scope === "project" ? -1 : 1;
@@ -47,7 +47,7 @@ export function formatScalar(value: unknown, maxLen: number): string {
47
47
 
48
48
  /**
49
49
  * Color a formatted scalar value based on its JS type using syntax highlighting colors.
50
- * Matches Claude Code / VS Code Dark+ JSON highlighting semantics.
50
+ * Matches xcsh / VS Code Dark+ JSON highlighting semantics.
51
51
  */
52
52
  function colorScalar(value: unknown, formatted: string, theme: Theme): string {
53
53
  if (value === null || value === undefined) return theme.fg("syntaxKeyword", formatted);
@@ -33,7 +33,7 @@ function getShellConfigFile(shell: string): string {
33
33
  /**
34
34
  * Generate the snapshot creation script.
35
35
  * This script sources the user's rc file and extracts functions, aliases, and options.
36
- * Matches Claude Code's snapshot generation logic.
36
+ * Matches xcsh snapshot generation logic.
37
37
  */
38
38
  function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
39
39
  const hasRcFile = fs.existsSync(rcFile);
@@ -85,7 +85,7 @@ function getModel(): string {
85
85
  /**
86
86
  * Builds system instruction blocks for the Anthropic API request.
87
87
  * @param auth - Authentication configuration
88
- * @param model - Model identifier (affects whether Claude Code instruction is included)
88
+ * @param model - Model identifier (affects whether xcsh instruction is included)
89
89
  * @param systemPrompt - Optional system prompt for guiding response style
90
90
  * @returns Array of system blocks for the API request
91
91
  */