@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.
- package/examples/extensions/plan-mode.ts +1 -1
- package/package.json +7 -7
- package/src/capability/types.ts +2 -2
- package/src/config/settings-schema.ts +17 -17
- package/src/discovery/claude-plugins.ts +15 -15
- package/src/discovery/claude.ts +3 -3
- package/src/discovery/helpers.ts +27 -27
- package/src/discovery/plugin-dir-roots.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +2 -2
- package/src/extensibility/plugins/marketplace/types.ts +5 -5
- package/src/extensibility/skills.ts +7 -7
- package/src/extensibility/slash-commands.ts +1 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +2 -2
- package/src/mcp/loader.ts +1 -1
- package/src/mcp/tool-bridge.ts +2 -2
- package/src/modes/components/gutter-block.ts +1 -1
- package/src/modes/components/plugins/plugin-dashboard.ts +4 -4
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/discovery.ts +3 -3
- package/src/tools/json-tree.ts +1 -1
- package/src/utils/shell-snapshot.ts +1 -1
- package/src/web/search/providers/anthropic.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plan Mode Extension
|
|
3
3
|
*
|
|
4
|
-
* Provides a
|
|
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": "
|
|
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": "
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "
|
|
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",
|
package/src/capability/types.ts
CHANGED
|
@@ -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., "
|
|
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., "
|
|
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
|
-
"
|
|
231
|
-
"
|
|
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.
|
|
1642
|
+
"skills.enableXcshUser": {
|
|
1643
1643
|
type: "boolean",
|
|
1644
1644
|
default: false,
|
|
1645
1645
|
ui: {
|
|
1646
1646
|
tab: "tasks",
|
|
1647
|
-
label: "
|
|
1648
|
-
description: "Load skills from ~/.xcsh/agent/skills/
|
|
1647
|
+
label: "xcsh User Skills",
|
|
1648
|
+
description: "Load skills from ~/.xcsh/agent/skills/",
|
|
1649
1649
|
},
|
|
1650
1650
|
},
|
|
1651
1651
|
|
|
1652
|
-
"skills.
|
|
1652
|
+
"skills.enableXcshProject": {
|
|
1653
1653
|
type: "boolean",
|
|
1654
1654
|
default: false,
|
|
1655
1655
|
ui: {
|
|
1656
1656
|
tab: "tasks",
|
|
1657
|
-
label: "
|
|
1658
|
-
description: "Load skills from .xcsh/skills/
|
|
1657
|
+
label: "xcsh Project Skills",
|
|
1658
|
+
description: "Load skills from .xcsh/skills/",
|
|
1659
1659
|
},
|
|
1660
1660
|
},
|
|
1661
1661
|
|
|
1662
|
-
"skills.
|
|
1662
|
+
"skills.enableXcshPlugins": {
|
|
1663
1663
|
type: "boolean",
|
|
1664
1664
|
default: false,
|
|
1665
1665
|
ui: {
|
|
1666
1666
|
tab: "tasks",
|
|
1667
|
-
label: "
|
|
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.
|
|
1699
|
+
"commands.enableXcshUser": {
|
|
1700
1700
|
type: "boolean",
|
|
1701
1701
|
default: true,
|
|
1702
|
-
ui: { tab: "tasks", label: "
|
|
1702
|
+
ui: { tab: "tasks", label: "xcsh User Commands", description: "Load commands from ~/.xcsh/agent/commands/" },
|
|
1703
1703
|
},
|
|
1704
1704
|
|
|
1705
|
-
"commands.
|
|
1705
|
+
"commands.enableXcshProject": {
|
|
1706
1706
|
type: "boolean",
|
|
1707
1707
|
default: true,
|
|
1708
|
-
ui: { tab: "tasks", label: "
|
|
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
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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 = "
|
|
28
|
-
const DISPLAY_NAME = "
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
285
|
+
description: "Load custom tools from xcsh marketplace plugins",
|
|
286
286
|
priority: PRIORITY,
|
|
287
287
|
load: loadTools,
|
|
288
288
|
});
|
package/src/discovery/claude.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 = "
|
|
32
|
-
const DISPLAY_NAME = "
|
|
31
|
+
const PROVIDER_ID = "xcsh";
|
|
32
|
+
const DISPLAY_NAME = "xcsh";
|
|
33
33
|
const PRIORITY = 80;
|
|
34
34
|
const CONFIG_DIR = ".xcsh";
|
|
35
35
|
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -583,13 +583,13 @@ export function getExtensionNameFromPath(extensionPath: string): string {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
// =============================================================================
|
|
586
|
-
//
|
|
586
|
+
// xcsh Plugin Cache Helpers
|
|
587
587
|
// =============================================================================
|
|
588
588
|
|
|
589
589
|
/**
|
|
590
|
-
* Entry for an installed
|
|
590
|
+
* Entry for an installed xcsh plugin.
|
|
591
591
|
*/
|
|
592
|
-
export interface
|
|
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
|
-
*
|
|
603
|
+
* xcsh installed_plugins.json registry format.
|
|
604
604
|
*/
|
|
605
|
-
export interface
|
|
605
|
+
export interface XcshPluginsRegistry {
|
|
606
606
|
version: number;
|
|
607
|
-
plugins: Record<string,
|
|
607
|
+
plugins: Record<string, XcshPluginEntry[]>;
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
/**
|
|
611
611
|
* Resolved plugin root for loading.
|
|
612
612
|
*/
|
|
613
|
-
export interface
|
|
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
|
|
629
|
+
* Parse xcsh installed_plugins.json content.
|
|
630
630
|
*/
|
|
631
|
-
export function
|
|
632
|
-
const data = tryParseJson<
|
|
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:
|
|
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
|
|
723
|
+
export async function listXcshPluginRoots(
|
|
724
724
|
home: string,
|
|
725
725
|
cwd?: string,
|
|
726
|
-
): Promise<{ roots:
|
|
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:
|
|
732
|
+
const roots: XcshPluginRoot[] = [];
|
|
733
733
|
const warnings: string[] = [];
|
|
734
|
-
const projectRoots:
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
855
|
-
let injectedPluginDirRoots:
|
|
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
|
|
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
|
|
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/
|
|
882
|
-
* Must be called before any
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
6
|
+
* - InstalledPluginsRegistry: which plugins are installed (data)
|
|
7
7
|
*
|
|
8
|
-
* The installed registry MUST pass `
|
|
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
|
|
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 —
|
|
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 —
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 ||
|
|
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 === "
|
|
106
|
-
if (provider === "
|
|
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 === "
|
|
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
|
|
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": "
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.1.0",
|
|
21
|
+
"commit": "a158adfdf2a6f8be6887817aa87e4d4ca80d612f",
|
|
22
|
+
"shortCommit": "a158adf",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -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., "
|
|
71
|
+
/** Provider ID (e.g., "xcsh", "mcp-json") */
|
|
72
72
|
provider?: string;
|
|
73
|
-
/** Provider display name (e.g., "
|
|
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
|
-
*
|
|
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 {
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1185
|
+
clearXcshPluginRootsCache();
|
|
1186
1186
|
await runtime.ctx.refreshSlashCommandState();
|
|
1187
1187
|
runtime.ctx.showStatus("Plugins reloaded.");
|
|
1188
1188
|
runtime.ctx.editor.setText("");
|
package/src/task/discovery.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
91
|
-
const { roots: pluginRoots } = await
|
|
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;
|
package/src/tools/json-tree.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
*/
|