@f5-sales-demo/xcsh 19.84.0 → 19.85.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.84.0",
4
+ "version": "19.85.2",
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",
@@ -56,13 +56,13 @@
56
56
  "dependencies": {
57
57
  "@agentclientprotocol/sdk": "0.16.1",
58
58
  "@mozilla/readability": "^0.6",
59
- "@f5-sales-demo/xcsh-stats": "19.84.0",
60
- "@f5-sales-demo/pi-agent-core": "19.84.0",
61
- "@f5-sales-demo/pi-ai": "19.84.0",
62
- "@f5-sales-demo/pi-natives": "19.84.0",
63
- "@f5-sales-demo/pi-resource-management": "19.84.0",
64
- "@f5-sales-demo/pi-tui": "19.84.0",
65
- "@f5-sales-demo/pi-utils": "19.84.0",
59
+ "@f5-sales-demo/xcsh-stats": "19.85.2",
60
+ "@f5-sales-demo/pi-agent-core": "19.85.2",
61
+ "@f5-sales-demo/pi-ai": "19.85.2",
62
+ "@f5-sales-demo/pi-natives": "19.85.2",
63
+ "@f5-sales-demo/pi-resource-management": "19.85.2",
64
+ "@f5-sales-demo/pi-tui": "19.85.2",
65
+ "@f5-sales-demo/pi-utils": "19.85.2",
66
66
  "@sinclair/typebox": "^0.34",
67
67
  "@xterm/headless": "^6.0",
68
68
  "ajv": "^8.20",
@@ -555,7 +555,19 @@ function titleFromUrl(url: string): string {
555
555
  }
556
556
  }
557
557
 
558
- function extractReferences(msg: AssistantMessage): ChatReference[] {
558
+ /**
559
+ * Trailing characters a bare-URL match may greedily swallow at a markdown/prose
560
+ * boundary — markdown emphasis + code (`*_~` and backtick) and sentence/wrap
561
+ * punctuation. A real URL effectively never ends in these, so trimming them yields
562
+ * the intended link (e.g. `**https://…/llms.txt**` or a code-wrapped
563
+ * `` `https://…/llms.txt` `` → `https://…/llms.txt`). The markdown-link branch is
564
+ * bounded by its closing `)` and needs no trimming.
565
+ */
566
+ function trimTrailingMarkup(url: string): string {
567
+ return url.replace(/[*_~`,.;:!?'")\]}>]+$/, "");
568
+ }
569
+
570
+ export function extractReferences(msg: AssistantMessage): ChatReference[] {
559
571
  const refs: ChatReference[] = [];
560
572
  const seen = new Set<string>();
561
573
 
@@ -572,7 +584,7 @@ function extractReferences(msg: AssistantMessage): ChatReference[] {
572
584
 
573
585
  const bareUrlRegex = /(?<!\()(https?:\/\/[^\s)>\]]+)/g;
574
586
  for (let match = bareUrlRegex.exec(block.text); match !== null; match = bareUrlRegex.exec(block.text)) {
575
- const url = match[1];
587
+ const url = trimTrailingMarkup(match[1]);
576
588
  if (seen.has(url)) continue;
577
589
  seen.add(url);
578
590
  refs.push({ kind: classifyReferenceKind(url), title: titleFromUrl(url), url });
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.84.0",
21
- "commit": "61adb144f98de4a4989e874a9c8b6641effc3b52",
22
- "shortCommit": "61adb14",
20
+ "version": "19.85.2",
21
+ "commit": "a068b469d7c985c518847bf2862acabc1605f490",
22
+ "shortCommit": "a068b46",
23
23
  "branch": "main",
24
- "tag": "v19.84.0",
25
- "commitDate": "2026-07-23T14:56:34Z",
26
- "buildDate": "2026-07-23T15:21:30.427Z",
24
+ "tag": "v19.85.2",
25
+ "commitDate": "2026-07-23T18:30:49Z",
26
+ "buildDate": "2026-07-23T18:54:23.656Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/61adb144f98de4a4989e874a9c8b6641effc3b52",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.84.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/a068b469d7c985c518847bf2862acabc1605f490",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.85.2"
33
33
  };
package/src/sdk.ts CHANGED
@@ -112,6 +112,7 @@ import { SessionManager } from "./session/session-manager";
112
112
  import { closeAllConnections } from "./ssh/connection-manager";
113
113
  import { unmountAll } from "./ssh/sshfs-mount";
114
114
  import {
115
+ buildAgentsMdSearch,
115
116
  buildSystemPrompt as buildSystemPromptInternal,
116
117
  buildSystemPromptToolMetadata,
117
118
  loadProjectContextFiles as loadContextFilesInternal,
@@ -965,6 +966,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
965
966
  async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
966
967
  );
967
968
 
969
+ // Walk the CWD for nested XCSH.md ONCE per session — the walk is bounded but not
970
+ // free, so hoisting it here keeps every tool-refresh prompt rebuild off the tree
971
+ // (a large cwd like $HOME must never re-stall on set_host_tools). #2245.
972
+ const agentsMdSearch = await logger.time("buildAgentsMdSearch", buildAgentsMdSearch, cwd);
973
+
968
974
  let agent: Agent;
969
975
  let session!: AgentSession;
970
976
  let hasSession = false;
@@ -1596,6 +1602,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1596
1602
  cwd,
1597
1603
  skills,
1598
1604
  contextFiles,
1605
+ agentsMdSearch,
1599
1606
  tools: promptTools,
1600
1607
  toolNames,
1601
1608
  rules: rulebookRules,
@@ -1626,6 +1633,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1626
1633
  cwd,
1627
1634
  skills,
1628
1635
  contextFiles,
1636
+ agentsMdSearch,
1629
1637
  tools: promptTools,
1630
1638
  toolNames,
1631
1639
  rules: rulebookRules,
@@ -108,15 +108,30 @@ const AGENTS_MD_MIN_DEPTH = 1;
108
108
  const AGENTS_MD_MAX_DEPTH = 4;
109
109
  const AGENTS_MD_LIMIT = 200;
110
110
  const SYSTEM_PROMPT_PREP_TIMEOUT_MS = 5000;
111
+ // The walk's `limit` caps DISCOVERED files, not directories VISITED — so a dir
112
+ // with ~no XCSH.md (e.g. serving from $HOME) would otherwise traverse the entire
113
+ // tree to AGENTS_MD_MAX_DEPTH and stall prep. These bound the traversal itself:
114
+ // a directory-visit budget AND a wall-clock deadline, both well inside the 5s
115
+ // prep timeout, so discovery stays fast regardless of how few matches exist.
116
+ const AGENTS_MD_MAX_DIRS = 4000;
117
+ const AGENTS_MD_WALK_BUDGET_MS = 1500;
111
118
  const AGENTS_MD_EXCLUDED_DIRS = new Set(["node_modules", ".git"]);
112
119
 
113
- interface AgentsMdSearch {
120
+ /** A cited context-file search result (the nested XCSH.md files under the cwd). */
121
+ export interface AgentsMdSearch {
114
122
  scopePath: string;
115
123
  limit: number;
116
124
  pattern: string;
117
125
  files: string[];
118
126
  }
119
127
 
128
+ /** Mutable traversal budget shared across the recursive walk. */
129
+ interface WalkBudget {
130
+ readonly maxDirs: number;
131
+ readonly deadline: number;
132
+ dirsVisited: number;
133
+ }
134
+
120
135
  function normalizePath(value: string): string {
121
136
  return value.replace(/\\/g, "/");
122
137
  }
@@ -131,11 +146,24 @@ async function collectAgentsMdFiles(
131
146
  dir: string,
132
147
  depth: number,
133
148
  limit: number,
149
+ maxDepth: number,
134
150
  discovered: Set<string>,
151
+ budget: WalkBudget,
135
152
  ): Promise<void> {
136
- if (depth > AGENTS_MD_MAX_DEPTH || discovered.size >= limit) {
153
+ // Stop on any bound: depth, enough matches, the directory-visit budget, or the
154
+ // wall-clock deadline. The last two keep a match-poor tree (e.g. $HOME) bounded.
155
+ if (
156
+ depth > maxDepth ||
157
+ discovered.size >= limit ||
158
+ budget.dirsVisited >= budget.maxDirs ||
159
+ Date.now() >= budget.deadline
160
+ ) {
137
161
  return;
138
162
  }
163
+ // Reserve this directory's slot SYNCHRONOUSLY (before the readdir await) so a
164
+ // concurrent Promise.all fan-out can't blow past the budget: every scheduled
165
+ // sibling sees the updated count before it yields, making the cap effectively hard.
166
+ budget.dirsVisited++;
139
167
 
140
168
  let entries: fs.Dirent[];
141
169
  try {
@@ -157,7 +185,7 @@ async function collectAgentsMdFiles(
157
185
  }
158
186
  }
159
187
 
160
- if (depth === AGENTS_MD_MAX_DEPTH) {
188
+ if (depth === maxDepth) {
161
189
  return;
162
190
  }
163
191
 
@@ -168,24 +196,47 @@ async function collectAgentsMdFiles(
168
196
 
169
197
  await Promise.all(
170
198
  childDirs.map(async child => {
171
- if (discovered.size >= limit) return;
172
- await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, discovered);
199
+ if (discovered.size >= limit || budget.dirsVisited >= budget.maxDirs || Date.now() >= budget.deadline) return;
200
+ await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, maxDepth, discovered, budget);
173
201
  }),
174
202
  );
175
203
  }
176
204
 
177
- async function listAgentsMdFiles(root: string, limit: number): Promise<string[]> {
205
+ /** Options for {@link discoverAgentsMdFiles} (all bounded by defaults). */
206
+ export interface DiscoverAgentsMdOptions {
207
+ limit?: number;
208
+ maxDepth?: number;
209
+ maxDirs?: number;
210
+ budgetMs?: number;
211
+ }
212
+
213
+ /**
214
+ * Walk `root` (bounded by depth, match-limit, directory budget, and a wall-clock
215
+ * deadline) collecting nested `XCSH.md` paths. Returns the sorted matches plus the
216
+ * number of directories visited (for observability/tests). Never throws.
217
+ */
218
+ export async function discoverAgentsMdFiles(
219
+ root: string,
220
+ opts: DiscoverAgentsMdOptions = {},
221
+ ): Promise<{ files: string[]; dirsVisited: number }> {
222
+ const limit = opts.limit ?? AGENTS_MD_LIMIT;
223
+ const maxDepth = opts.maxDepth ?? AGENTS_MD_MAX_DEPTH;
224
+ const budget: WalkBudget = {
225
+ maxDirs: opts.maxDirs ?? AGENTS_MD_MAX_DIRS,
226
+ deadline: Date.now() + (opts.budgetMs ?? AGENTS_MD_WALK_BUDGET_MS),
227
+ dirsVisited: 0,
228
+ };
178
229
  try {
179
230
  const discovered = new Set<string>();
180
- await collectAgentsMdFiles(root, root, 0, limit, discovered);
181
- return Array.from(discovered).sort().slice(0, limit);
231
+ await collectAgentsMdFiles(root, root, 0, limit, maxDepth, discovered, budget);
232
+ return { files: Array.from(discovered).sort().slice(0, limit), dirsVisited: budget.dirsVisited };
182
233
  } catch {
183
- return [];
234
+ return { files: [], dirsVisited: budget.dirsVisited };
184
235
  }
185
236
  }
186
237
 
187
- async function buildAgentsMdSearch(cwd: string): Promise<AgentsMdSearch> {
188
- const files = await listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
238
+ export async function buildAgentsMdSearch(cwd: string): Promise<AgentsMdSearch> {
239
+ const { files } = await discoverAgentsMdFiles(cwd);
189
240
  return {
190
241
  scopePath: ".",
191
242
  limit: AGENTS_MD_LIMIT,
@@ -449,6 +500,9 @@ export interface BuildSystemPromptOptions {
449
500
  cwd?: string;
450
501
  /** Pre-loaded context files (skips discovery if provided). */
451
502
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
503
+ /** Pre-computed nested-XCSH.md search (skips the CWD walk if provided). Hoisted
504
+ * once per session so tool-refresh rebuilds never re-walk the tree. */
505
+ agentsMdSearch?: AgentsMdSearch;
452
506
  /**
453
507
  * Explicit disabled extension IDs applied to context-file discovery instead of the
454
508
  * global settings default. Pass `[]` to discover independent of process-wide settings.
@@ -519,6 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
519
573
  toolNames: providedToolNames,
520
574
  cwd,
521
575
  contextFiles: providedContextFiles,
576
+ agentsMdSearch: providedAgentsMdSearch,
522
577
  skills: providedSkills,
523
578
  rules,
524
579
  alwaysApplyRules,
@@ -541,7 +596,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
541
596
  cwd: resolvedCwd,
542
597
  disabledExtensions: options.disabledExtensions,
543
598
  });
544
- const agentsMdSearchPromise = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
599
+ const agentsMdSearchPromise = providedAgentsMdSearch
600
+ ? Promise.resolve(providedAgentsMdSearch)
601
+ : logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
545
602
  const mergedSkillsSettings = {
546
603
  ...skillsSettings,
547
604
  customDirectories: [...(skillsSettings?.customDirectories ?? []), ...(options.contextSkillDirs ?? [])],