@f5-sales-demo/xcsh 21.40.0 → 21.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +8 -8
  2. package/src/capability/fs.ts +20 -10
  3. package/src/capability/index.ts +5 -2
  4. package/src/capability/types.ts +4 -0
  5. package/src/deprecations.ts +4 -4
  6. package/src/discovery/agents-md.ts +2 -1
  7. package/src/discovery/agents.ts +4 -4
  8. package/src/discovery/builtin.ts +6 -4
  9. package/src/discovery/claude-plugins.ts +2 -1
  10. package/src/discovery/claude.ts +5 -4
  11. package/src/discovery/codex.ts +3 -1
  12. package/src/discovery/gemini.ts +4 -4
  13. package/src/discovery/github.ts +1 -1
  14. package/src/discovery/helpers.ts +110 -17
  15. package/src/discovery/opencode.ts +3 -1
  16. package/src/extensibility/plugins/marketplace/cache.ts +78 -1
  17. package/src/extensibility/plugins/marketplace/manager.ts +10 -3
  18. package/src/extensibility/skills.ts +12 -3
  19. package/src/integrations/index.ts +8 -1
  20. package/src/integrations/registry.ts +2 -1
  21. package/src/integrations/setup.ts +35 -15
  22. package/src/internal-urls/api-catalog-index.generated.ts +1525 -1525
  23. package/src/internal-urls/api-catalog-preflight.ts +210 -0
  24. package/src/internal-urls/api-spec-index.generated.ts +1 -1
  25. package/src/internal-urls/api-spec-resolve.ts +156 -0
  26. package/src/internal-urls/branding-index.generated.ts +1 -1
  27. package/src/internal-urls/build-info.generated.ts +8 -8
  28. package/src/internal-urls/docs-index.generated.ts +1 -1
  29. package/src/modes/components/plugins/plugin-dashboard.ts +28 -3
  30. package/src/modes/components/plugins/state-manager.ts +16 -10
  31. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  32. package/src/modes/controllers/selector-controller.ts +86 -1
  33. package/src/modes/print-mode.ts +8 -4
  34. package/src/prompts/rules/llms-search.md +2 -1
  35. package/src/prompts/system/system-prompt.md +12 -3
  36. package/src/sdk.ts +73 -43
  37. package/src/session/agent-session.ts +54 -1
  38. package/src/slash-commands/builtin-registry.ts +18 -2
  39. package/src/system-prompt.ts +469 -162
  40. package/src/task/executor.ts +5 -7
  41. package/src/task/index.ts +1 -0
  42. package/src/tools/index.ts +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "21.40.0",
4
+ "version": "21.41.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -64,13 +64,13 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@agentclientprotocol/sdk": "1.4.0",
67
- "@f5-sales-demo/pi-agent-core": "21.40.0",
68
- "@f5-sales-demo/pi-ai": "21.40.0",
69
- "@f5-sales-demo/pi-natives": "21.40.0",
70
- "@f5-sales-demo/pi-resource-management": "21.40.0",
71
- "@f5-sales-demo/pi-tui": "21.40.0",
72
- "@f5-sales-demo/pi-utils": "21.40.0",
73
- "@f5-sales-demo/xcsh-stats": "21.40.0",
67
+ "@f5-sales-demo/pi-agent-core": "21.41.1",
68
+ "@f5-sales-demo/pi-ai": "21.41.1",
69
+ "@f5-sales-demo/pi-natives": "21.41.1",
70
+ "@f5-sales-demo/pi-resource-management": "21.41.1",
71
+ "@f5-sales-demo/pi-tui": "21.41.1",
72
+ "@f5-sales-demo/pi-utils": "21.41.1",
73
+ "@f5-sales-demo/xcsh-stats": "21.41.1",
74
74
  "@mozilla/readability": "^0.6",
75
75
  "@sinclair/typebox": "0.34.52",
76
76
  "@tobilu/qmd": "2.8.3",
@@ -8,40 +8,47 @@ function resolvePath(filePath: string): string {
8
8
  return path.resolve(filePath);
9
9
  }
10
10
 
11
- export async function readFile(filePath: string): Promise<string | null> {
11
+ export async function readFile(filePath: string, signal?: AbortSignal): Promise<string | null> {
12
12
  const abs = resolvePath(filePath);
13
13
  if (contentCache.has(abs)) {
14
14
  return contentCache.get(abs) ?? null;
15
15
  }
16
16
 
17
17
  try {
18
- const content = await Bun.file(abs).text();
18
+ signal?.throwIfAborted();
19
+ const stat = await fs.promises.stat(abs);
20
+ if (!stat.isFile()) return null;
21
+ const content = await fs.promises.readFile(abs, { encoding: "utf8", signal });
19
22
  contentCache.set(abs, content);
20
23
  return content;
21
- } catch {
24
+ } catch (error) {
25
+ if (signal?.aborted) throw error;
22
26
  contentCache.set(abs, null);
23
27
  return null;
24
28
  }
25
29
  }
26
30
 
27
- export async function readDirEntries(dirPath: string): Promise<fs.Dirent[]> {
31
+ export async function readDirEntries(dirPath: string, signal?: AbortSignal): Promise<fs.Dirent[]> {
28
32
  const abs = resolvePath(dirPath);
29
33
  if (dirCache.has(abs)) {
30
34
  return dirCache.get(abs) ?? [];
31
35
  }
32
36
 
33
37
  try {
38
+ signal?.throwIfAborted();
34
39
  const entries = await fs.promises.readdir(abs, { withFileTypes: true });
40
+ signal?.throwIfAborted();
35
41
  dirCache.set(abs, entries);
36
42
  return entries;
37
- } catch {
43
+ } catch (error) {
44
+ if (signal?.aborted) throw error;
38
45
  dirCache.set(abs, []);
39
46
  return [];
40
47
  }
41
48
  }
42
49
 
43
- export async function readDir(dirPath: string): Promise<string[]> {
44
- const entries = await readDirEntries(dirPath);
50
+ export async function readDir(dirPath: string, signal?: AbortSignal): Promise<string[]> {
51
+ const entries = await readDirEntries(dirPath, signal);
45
52
  return entries.map(entry => entry.name);
46
53
  }
47
54
 
@@ -49,12 +56,14 @@ export async function walkUp(
49
56
  startDir: string,
50
57
  name: string,
51
58
  opts: { file?: boolean; dir?: boolean } = {},
59
+ signal?: AbortSignal,
52
60
  ): Promise<string | null> {
53
61
  const { file = true, dir = true } = opts;
54
62
  let current = resolvePath(startDir);
55
63
 
56
64
  while (true) {
57
- const entries = await readDirEntries(current);
65
+ signal?.throwIfAborted();
66
+ const entries = await readDirEntries(current, signal);
58
67
  const entry = entries.find(e => e.name === name);
59
68
  if (entry) {
60
69
  if (file && entry.isFile()) return path.join(current, name);
@@ -71,10 +80,11 @@ export async function walkUp(
71
80
  * Returns the directory containing `.git` (the repo root), or null if not in a git repo.
72
81
  * Results are based on the cached readDirEntries, so repeated calls are cheap.
73
82
  */
74
- export async function findRepoRoot(startDir: string): Promise<string | null> {
83
+ export async function findRepoRoot(startDir: string, signal?: AbortSignal): Promise<string | null> {
75
84
  let current = resolvePath(startDir);
76
85
  while (true) {
77
- const entries = await readDirEntries(current);
86
+ signal?.throwIfAborted();
87
+ const entries = await readDirEntries(current, signal);
78
88
  if (entries.some(e => e.name === ".git")) {
79
89
  return current;
80
90
  }
@@ -114,6 +114,7 @@ async function loadImpl<T>(
114
114
  const results = await Promise.all(
115
115
  providers.map(async provider => {
116
116
  try {
117
+ ctx.signal?.throwIfAborted();
117
118
  const result = await logger.time(
118
119
  `capability:${capability.id}:${provider.id}`,
119
120
  provider.load.bind(provider),
@@ -121,6 +122,7 @@ async function loadImpl<T>(
121
122
  );
122
123
  return { provider, result };
123
124
  } catch (error) {
125
+ if (ctx.signal?.aborted) throw error;
124
126
  logger.debug(`capability:${capability.id}:${provider.id}:error`);
125
127
  return { provider, error };
126
128
  }
@@ -235,8 +237,9 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
235
237
 
236
238
  const cwd = options.cwd ?? getProjectDir();
237
239
  const home = os.homedir();
238
- const repoRoot = await findRepoRoot(cwd);
239
- const ctx: LoadContext = { cwd, home, repoRoot };
240
+ options.signal?.throwIfAborted();
241
+ const repoRoot = await findRepoRoot(cwd, options.signal);
242
+ const ctx: LoadContext = { cwd, home, repoRoot, signal: options.signal };
240
243
  const providers = filterProviders(capability, options);
241
244
 
242
245
  return await loadImpl(capability, providers, ctx, options);
@@ -16,6 +16,8 @@ export interface LoadContext {
16
16
  home: string;
17
17
  /** Git repository root (directory containing .git), or null if not in a repo */
18
18
  repoRoot: string | null;
19
+ /** Cancellation for bounded discovery. */
20
+ signal?: AbortSignal;
19
21
  }
20
22
 
21
23
  /**
@@ -72,6 +74,8 @@ export interface LoadOptions {
72
74
  includeDisabled?: boolean;
73
75
  /** Explicit disabled extension IDs to apply instead of settings. */
74
76
  disabledExtensions?: string[];
77
+ /** Cancellation for bounded discovery. */
78
+ signal?: AbortSignal;
75
79
  }
76
80
 
77
81
  /**
@@ -17,9 +17,9 @@ interface DeprecationEntry {
17
17
  const DEPRECATIONS = BRANDING_DEPRECATIONS as unknown as Record<string, DeprecationEntry>;
18
18
 
19
19
  /** Markers that identify a command as targeting the F5 XC API. */
20
- const F5XC_API_MARKERS = [
21
- "f5xc_api_url",
22
- "f5xc_api_token",
20
+ const XCSH_API_MARKERS = [
21
+ "xcsh_api_url",
22
+ "xcsh_api_token",
23
23
  "apitoken",
24
24
  ".volterra.io",
25
25
  ".volterra.us",
@@ -57,7 +57,7 @@ export function isDisallowedCliCommand(command: string): boolean {
57
57
  if (!lower) return false;
58
58
  const leadingToken = lower.split(/\s+/)[0] ?? "";
59
59
  if (getDeprecatedClis().includes(leadingToken)) return true;
60
- if (leadingToken === "curl" && F5XC_API_MARKERS.some(marker => lower.includes(marker))) return true;
60
+ if (leadingToken === "curl" && XCSH_API_MARKERS.some(marker => lower.includes(marker))) return true;
61
61
  return false;
62
62
  }
63
63
 
@@ -26,8 +26,9 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
26
26
  let current = ctx.cwd;
27
27
 
28
28
  while (true) {
29
+ ctx.signal?.throwIfAborted();
29
30
  const candidate = path.join(current, "XCSH.md");
30
- const content = await readFile(candidate);
31
+ const content = await readFile(candidate, ctx.signal);
31
32
 
32
33
  if (content !== null) {
33
34
  const parent = path.dirname(candidate);
@@ -52,10 +52,10 @@ function getProjectPathCandidates(ctx: LoadContext, ...segments: string[]): stri
52
52
  // Skills
53
53
  async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
54
54
  const projectScans = getProjectPathCandidates(ctx, "skills").map(dir =>
55
- scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "project" }),
55
+ scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "project", signal: ctx.signal }),
56
56
  );
57
57
  const userScans = getUserPathCandidates(ctx, "skills").map(dir =>
58
- scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "user" }),
58
+ scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "user", signal: ctx.signal }),
59
59
  );
60
60
 
61
61
  const results = await Promise.all([...projectScans, ...userScans]);
@@ -170,7 +170,7 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
170
170
  // Context Files (AGENTS.md)
171
171
  async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
172
172
  const load = async (filePath: string, level: "user" | "project"): Promise<ContextFile | null> => {
173
- const content = await readFile(filePath);
173
+ const content = await readFile(filePath, ctx.signal);
174
174
  if (!content) return null;
175
175
  // filePath is <ancestor>/.agent(s)/AGENTS.md — go up past the config dir to the ancestor
176
176
  const ancestorDir = path.dirname(path.dirname(filePath));
@@ -197,7 +197,7 @@ registerProvider<ContextFile>(contextFileCapability.id, {
197
197
  // System Prompt (SYSTEM.md)
198
198
  async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
199
199
  const load = async (filePath: string, level: "user" | "project"): Promise<SystemPrompt | null> => {
200
- const content = await readFile(filePath);
200
+ const content = await readFile(filePath, ctx.signal);
201
201
  if (!content) return null;
202
202
  return { path: filePath, content, level, _source: createSourceMeta(PROVIDER_ID, filePath, level) };
203
203
  };
@@ -225,7 +225,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
225
225
  const items: SystemPrompt[] = [];
226
226
 
227
227
  const userPath = path.join(ctx.home, PATHS.userAgent, "SYSTEM.md");
228
- const userContent = await readFile(userPath);
228
+ const userContent = await readFile(userPath, ctx.signal);
229
229
  if (userContent) {
230
230
  items.push({
231
231
  path: userPath,
@@ -238,7 +238,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
238
238
  const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
239
239
  if (nearestProjectConfigDir) {
240
240
  const projectPath = path.join(nearestProjectConfigDir.dir, "SYSTEM.md");
241
- const projectContent = await readFile(projectPath);
241
+ const projectContent = await readFile(projectPath, ctx.signal);
242
242
  if (projectContent) {
243
243
  items.push({
244
244
  path: projectPath,
@@ -270,6 +270,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
270
270
  providerId: PROVIDER_ID,
271
271
  level: "project",
272
272
  requireDescription: true,
273
+ signal: ctx.signal,
273
274
  }),
274
275
  );
275
276
 
@@ -279,6 +280,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
279
280
  providerId: PROVIDER_ID,
280
281
  level: "user",
281
282
  requireDescription: true,
283
+ signal: ctx.signal,
282
284
  });
283
285
 
284
286
  const results = await Promise.all([...projectScans, userScan]);
@@ -814,7 +816,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
814
816
  const warnings: string[] = [];
815
817
 
816
818
  const userPath = path.join(ctx.home, PATHS.userAgent, "XCSH.md");
817
- const userContent = await readFile(userPath);
819
+ const userContent = await readFile(userPath, ctx.signal);
818
820
  if (userContent) {
819
821
  items.push({
820
822
  path: userPath,
@@ -827,7 +829,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
827
829
  const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
828
830
  if (nearestProjectConfigDir) {
829
831
  const projectPath = path.join(nearestProjectConfigDir.dir, "XCSH.md");
830
- const projectContent = await readFile(projectPath);
832
+ const projectContent = await readFile(projectPath, ctx.signal);
831
833
  if (projectContent) {
832
834
  items.push({
833
835
  path: projectPath,
@@ -37,7 +37,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
37
37
  const items: Skill[] = [];
38
38
  const warnings: string[] = [];
39
39
 
40
- const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd);
40
+ const { roots, warnings: rootWarnings } = await listXcshPluginRoots(ctx.home, ctx.cwd, ctx.signal);
41
41
  warnings.push(...rootWarnings);
42
42
 
43
43
  const results = await Promise.all(
@@ -47,6 +47,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
47
47
  dir: skillsDir,
48
48
  providerId: PROVIDER_ID,
49
49
  level: scopeToLevel(root.scope),
50
+ signal: ctx.signal,
50
51
  });
51
52
  return { root, result };
52
53
  }),
@@ -132,7 +132,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
132
132
  const userBase = getUserClaude(ctx);
133
133
  const userClaudeMd = path.join(userBase, "CLAUDE.md");
134
134
 
135
- const userContent = await readFile(userClaudeMd);
135
+ const userContent = await readFile(userClaudeMd, ctx.signal);
136
136
  if (userContent !== null) {
137
137
  items.push({
138
138
  path: userClaudeMd,
@@ -144,7 +144,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
144
144
 
145
145
  const projectBase = getProjectClaude(ctx);
146
146
  const projectClaudeMd = path.join(projectBase, "CLAUDE.md");
147
- const projectContent = await readFile(projectClaudeMd);
147
+ const projectContent = await readFile(projectClaudeMd, ctx.signal);
148
148
  if (projectContent !== null) {
149
149
  const depth = calculateDepth(ctx.cwd, path.dirname(projectBase), path.sep);
150
150
  items.push({
@@ -175,6 +175,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
175
175
  dir: path.join(current, CONFIG_DIR, "skills"),
176
176
  providerId: PROVIDER_ID,
177
177
  level: "project",
178
+ signal: ctx.signal,
178
179
  }),
179
180
  );
180
181
  if (current === (ctx.repoRoot ?? ctx.home)) break;
@@ -184,7 +185,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
184
185
  }
185
186
 
186
187
  const [userResult, ...projectResults] = await Promise.allSettled([
187
- scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
188
+ scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user", signal: ctx.signal }),
188
189
  ...projectScans,
189
190
  ]);
190
191
 
@@ -407,7 +408,7 @@ async function loadSystemPrompts(ctx: LoadContext): Promise<LoadResult<SystemPro
407
408
  const userBase = getUserClaude(ctx);
408
409
  const userSystemMd = path.join(userBase, "SYSTEM.md");
409
410
 
410
- const content = await readFile(userSystemMd);
411
+ const content = await readFile(userSystemMd, ctx.signal);
411
412
  if (content !== null) {
412
413
  items.push({
413
414
  path: userSystemMd,
@@ -56,7 +56,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
56
56
 
57
57
  // User level only: ~/.codex/AGENTS.md
58
58
  const agentsMd = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "AGENTS.md");
59
- const agentsContent = await readFile(agentsMd);
59
+ const agentsContent = await readFile(agentsMd, ctx.signal);
60
60
  if (agentsContent) {
61
61
  items.push({
62
62
  path: agentsMd,
@@ -221,11 +221,13 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
221
221
  dir: userSkillsDir,
222
222
  providerId: PROVIDER_ID,
223
223
  level: "user",
224
+ signal: ctx.signal,
224
225
  }),
225
226
  scanSkillsFromDir(ctx, {
226
227
  dir: projectSkillsDir,
227
228
  providerId: PROVIDER_ID,
228
229
  level: "project",
230
+ signal: ctx.signal,
229
231
  }),
230
232
  ]);
231
233
 
@@ -129,7 +129,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
129
129
  // User-level: ~/.gemini/GEMINI.md
130
130
  const userGeminiMd = getUserPath(ctx, "gemini", "GEMINI.md");
131
131
  if (userGeminiMd) {
132
- const content = await readFile(userGeminiMd);
132
+ const content = await readFile(userGeminiMd, ctx.signal);
133
133
  if (content) {
134
134
  items.push({
135
135
  path: userGeminiMd,
@@ -143,7 +143,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
143
143
  // Project-level: .gemini/GEMINI.md
144
144
  const projectGeminiMd = getProjectPath(ctx, "gemini", "GEMINI.md");
145
145
  if (projectGeminiMd) {
146
- const content = await readFile(projectGeminiMd);
146
+ const content = await readFile(projectGeminiMd, ctx.signal);
147
147
  if (content) {
148
148
  const projectBase = getProjectPath(ctx, "gemini", "");
149
149
  const depth = projectBase ? calculateDepth(ctx.cwd, projectBase, path.sep) : 0;
@@ -335,7 +335,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
335
335
  // User-level: ~/.gemini/system.md
336
336
  const userSystemMd = getUserPath(ctx, "gemini", "system.md");
337
337
  if (userSystemMd) {
338
- const content = await readFile(userSystemMd);
338
+ const content = await readFile(userSystemMd, ctx.signal);
339
339
  if (content) {
340
340
  items.push({
341
341
  path: userSystemMd,
@@ -349,7 +349,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
349
349
  // Project-level: .gemini/system.md
350
350
  const projectSystemMd = getProjectPath(ctx, "gemini", "system.md");
351
351
  if (projectSystemMd) {
352
- const content = await readFile(projectSystemMd);
352
+ const content = await readFile(projectSystemMd, ctx.signal);
353
353
  if (content) {
354
354
  items.push({
355
355
  path: projectSystemMd,
@@ -35,7 +35,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
35
35
 
36
36
  const copilotInstructionsPath = getProjectPath(ctx, "github", "copilot-instructions.md");
37
37
  if (copilotInstructionsPath) {
38
- const content = await readFile(copilotInstructionsPath);
38
+ const content = await readFile(copilotInstructionsPath, ctx.signal);
39
39
  if (content) {
40
40
  const fileDir = path.dirname(copilotInstructionsPath);
41
41
  const depth = calculateDepth(ctx.cwd, fileDir, path.sep);