@f5xc-salesdemos/xcsh 18.33.3 → 18.35.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/package.json +7 -7
- package/scripts/generate-api-spec-index.ts +17 -15
- package/src/cli.ts +11 -0
- package/src/config/settings-schema.ts +11 -0
- package/src/internal-urls/api-catalog-resolve.ts +14 -21
- package/src/internal-urls/api-spec-resolve.ts +11 -28
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/xcsh-protocol.ts +14 -14
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +8 -2
- package/src/prompts/tools/xcsh-api.md +9 -0
- package/src/tools/bash.ts +44 -7
- package/src/tools/index.ts +2 -0
- package/src/tools/xcsh-api.ts +106 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.35.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",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
50
50
|
"@mozilla/readability": "^0.6",
|
|
51
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.35.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.35.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.35.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.35.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.35.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.35.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { gzipSync } from "node:zlib";
|
|
7
6
|
import { $ } from "bun";
|
|
8
7
|
|
|
9
8
|
interface SpecPathOperation {
|
|
@@ -81,9 +80,16 @@ const REPO = "f5xc-salesdemos/api-specs-enriched";
|
|
|
81
80
|
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-spec-index.generated.ts");
|
|
82
81
|
const catalogOutputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-catalog-index.generated.ts");
|
|
83
82
|
|
|
83
|
+
function githubHeaders(): Record<string, string> {
|
|
84
|
+
const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
|
|
85
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
86
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
87
|
+
return headers;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
async function resolveLatestTag(): Promise<string> {
|
|
85
91
|
const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
|
|
86
|
-
headers:
|
|
92
|
+
headers: githubHeaders(),
|
|
87
93
|
});
|
|
88
94
|
if (!response.ok) {
|
|
89
95
|
throw new Error(`Failed to fetch latest release from ${REPO}: ${response.status} ${response.statusText}`);
|
|
@@ -203,7 +209,7 @@ if (catalog) {
|
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
const domainEntries: string[] = [];
|
|
206
|
-
const
|
|
212
|
+
const specDataEntries: string[] = [];
|
|
207
213
|
let processedCount = 0;
|
|
208
214
|
let skippedCount = 0;
|
|
209
215
|
|
|
@@ -220,8 +226,6 @@ for (const entry of rawIndex.specifications) {
|
|
|
220
226
|
paths?: Record<string, Record<string, SpecPathOperation>>;
|
|
221
227
|
[k: string]: unknown;
|
|
222
228
|
};
|
|
223
|
-
const compressed = gzipSync(Buffer.from(specContent));
|
|
224
|
-
const b64 = compressed.toString("base64");
|
|
225
229
|
|
|
226
230
|
const resources = (entry["x-f5xc-primary-resources"] ?? []).map(r => {
|
|
227
231
|
const upstreamSc = r.schema_components ?? [];
|
|
@@ -282,7 +286,7 @@ for (const entry of rawIndex.specifications) {
|
|
|
282
286
|
.join("\n"),
|
|
283
287
|
);
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
specDataEntries.push(`\t${JSON.stringify(entry.domain)}: ${JSON.stringify(specJson)},`);
|
|
286
290
|
processedCount++;
|
|
287
291
|
}
|
|
288
292
|
|
|
@@ -310,8 +314,8 @@ const output = [
|
|
|
310
314
|
serializeEnrichment("acronyms", acronyms),
|
|
311
315
|
`};`,
|
|
312
316
|
"",
|
|
313
|
-
`export const
|
|
314
|
-
...
|
|
317
|
+
`export const API_SPEC_DATA: Readonly<Record<string, unknown>> = {`,
|
|
318
|
+
...specDataEntries,
|
|
315
319
|
`};`,
|
|
316
320
|
"",
|
|
317
321
|
]
|
|
@@ -328,13 +332,11 @@ console.log(
|
|
|
328
332
|
// Generate API catalog index
|
|
329
333
|
if (catalog) {
|
|
330
334
|
const categories = (catalog.categories ?? []) as Array<{ name: string; displayName: string; operations: unknown[] }>;
|
|
331
|
-
const catalogBlobEntries: string[] = [];
|
|
332
335
|
const catalogIndexEntries: string[] = [];
|
|
333
336
|
|
|
337
|
+
const catalogDataEntries: string[] = [];
|
|
334
338
|
for (const cat of categories) {
|
|
335
|
-
|
|
336
|
-
const catCompressed = gzipSync(Buffer.from(catJson));
|
|
337
|
-
catalogBlobEntries.push(`\t${JSON.stringify(cat.name)}: ${JSON.stringify(catCompressed.toString("base64"))},`);
|
|
339
|
+
catalogDataEntries.push(`\t${JSON.stringify(cat.name)}: ${JSON.stringify(cat)},`);
|
|
338
340
|
catalogIndexEntries.push(
|
|
339
341
|
`\t\t{ name: ${JSON.stringify(cat.name)}, displayName: ${JSON.stringify(cat.displayName)}, operationCount: ${cat.operations?.length ?? 0} },`,
|
|
340
342
|
);
|
|
@@ -343,7 +345,7 @@ if (catalog) {
|
|
|
343
345
|
const catalogOutput = [
|
|
344
346
|
"// Auto-generated by scripts/generate-api-spec-index.ts - DO NOT EDIT",
|
|
345
347
|
"",
|
|
346
|
-
`import type { ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";`,
|
|
348
|
+
`import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";`,
|
|
347
349
|
"",
|
|
348
350
|
`export const API_CATALOG_INDEX: ApiCatalogIndex = {`,
|
|
349
351
|
`\tversion: ${JSON.stringify(catalog.version ?? "unknown")},`,
|
|
@@ -358,8 +360,8 @@ if (catalog) {
|
|
|
358
360
|
...catalogIndexEntries,
|
|
359
361
|
`];`,
|
|
360
362
|
"",
|
|
361
|
-
`export const
|
|
362
|
-
...
|
|
363
|
+
`export const API_CATALOG_DATA: Readonly<Record<string, ApiCatalogCategory>> = {`,
|
|
364
|
+
...catalogDataEntries,
|
|
363
365
|
`};`,
|
|
364
366
|
"",
|
|
365
367
|
].join("\n");
|
package/src/cli.ts
CHANGED
|
@@ -93,4 +93,15 @@ export function runCli(argv: string[]): Promise<void> {
|
|
|
93
93
|
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
if (process.env.XCSH_SMOKE_TEST_SPECS === "1") {
|
|
97
|
+
const specMod = require("./internal-urls/api-spec-index.generated") as { API_SPEC_INDEX?: { domains?: unknown[] } };
|
|
98
|
+
const catalogMod = require("./internal-urls/api-catalog-index.generated") as {
|
|
99
|
+
API_CATALOG_CATEGORY_SUMMARIES?: unknown[];
|
|
100
|
+
};
|
|
101
|
+
const domainCount = specMod.API_SPEC_INDEX?.domains?.length ?? 0;
|
|
102
|
+
const categoryCount = catalogMod.API_CATALOG_CATEGORY_SUMMARIES?.length ?? 0;
|
|
103
|
+
console.log(`api-specs: ${domainCount} domains, ${categoryCount} categories`);
|
|
104
|
+
process.exit(domainCount > 0 && categoryCount > 0 ? 0 : 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
await runCli(process.argv.slice(2));
|
|
@@ -1438,6 +1438,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1438
1438
|
},
|
|
1439
1439
|
},
|
|
1440
1440
|
|
|
1441
|
+
"bash.verbose": {
|
|
1442
|
+
type: "boolean",
|
|
1443
|
+
default: false,
|
|
1444
|
+
ui: {
|
|
1445
|
+
tab: "tools",
|
|
1446
|
+
label: "Bash Verbose",
|
|
1447
|
+
description:
|
|
1448
|
+
"Show standard command output panel. When disabled, shows a compact single-line summary with status indicator.",
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
|
|
1441
1452
|
// MCP
|
|
1442
1453
|
"mcp.enableProjectConfig": {
|
|
1443
1454
|
type: "boolean",
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { LRUCache } from "lru-cache";
|
|
3
1
|
import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
|
|
4
2
|
import type { ApiSpecIndex } from "./api-spec-types";
|
|
5
3
|
import type { InternalResource, InternalUrl } from "./types";
|
|
6
4
|
|
|
7
|
-
const LRU_CAPACITY = 5;
|
|
8
|
-
|
|
9
5
|
function normalizeSearchTerm(s: string): string {
|
|
10
6
|
return s.toLowerCase().replace(/_/g, "-");
|
|
11
7
|
}
|
|
@@ -17,22 +13,12 @@ export interface ApiCatalogResolver {
|
|
|
17
13
|
export function createApiCatalogResolver(
|
|
18
14
|
index: ApiCatalogIndex,
|
|
19
15
|
categorySummaries: readonly ApiCatalogCategorySummary[],
|
|
20
|
-
|
|
16
|
+
data: Readonly<Record<string, ApiCatalogCategory>>,
|
|
21
17
|
specIndex?: ApiSpecIndex,
|
|
22
18
|
): ApiCatalogResolver {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const cached = cache.get(category);
|
|
27
|
-
if (cached) return cached;
|
|
28
|
-
|
|
29
|
-
const blob = blobs[category];
|
|
30
|
-
if (!blob) throw new Error(`No catalog blob for category: ${category}`);
|
|
31
|
-
|
|
32
|
-
const buffer = Buffer.from(blob, "base64");
|
|
33
|
-
const decompressed = gunzipSync(buffer);
|
|
34
|
-
const cat = JSON.parse(decompressed.toString("utf-8")) as ApiCatalogCategory;
|
|
35
|
-
cache.set(category, cat);
|
|
19
|
+
function lookup(category: string): ApiCatalogCategory {
|
|
20
|
+
const cat = data[category];
|
|
21
|
+
if (!cat) throw new Error(`No catalog data for category: ${category}`);
|
|
36
22
|
return cat;
|
|
37
23
|
}
|
|
38
24
|
|
|
@@ -52,7 +38,7 @@ export function createApiCatalogResolver(
|
|
|
52
38
|
const catName = res.catalogCategories[0];
|
|
53
39
|
if (categorySummaries.some(c => c.name === catName)) {
|
|
54
40
|
try {
|
|
55
|
-
const cat =
|
|
41
|
+
const cat = lookup(catName);
|
|
56
42
|
return makeResource(url, renderCatalogDetail(cat, index, { compact }));
|
|
57
43
|
} catch {
|
|
58
44
|
break;
|
|
@@ -75,7 +61,7 @@ export function createApiCatalogResolver(
|
|
|
75
61
|
}
|
|
76
62
|
|
|
77
63
|
try {
|
|
78
|
-
const cat =
|
|
64
|
+
const cat = lookup(category);
|
|
79
65
|
return makeResource(url, renderCatalogDetail(cat, index, { compact }));
|
|
80
66
|
} catch (err) {
|
|
81
67
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -174,6 +160,13 @@ function formatRequiredFor(
|
|
|
174
160
|
return ops.length > 0 ? ops.join(", ") : "--";
|
|
175
161
|
}
|
|
176
162
|
|
|
163
|
+
function fieldMetadataFingerprint(metadata: Record<string, unknown>): string {
|
|
164
|
+
return JSON.stringify(metadata, (key, value) => {
|
|
165
|
+
if (key === "validatedAt" || key === "confidence" || key === "source") return undefined;
|
|
166
|
+
return value;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
177
170
|
function formatDefault(defaultVal: unknown, serverDefault: boolean | undefined): string {
|
|
178
171
|
if (defaultVal == null && !serverDefault) return "--";
|
|
179
172
|
let val = "--";
|
|
@@ -235,7 +228,7 @@ function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex, op
|
|
|
235
228
|
|
|
236
229
|
// Field Constraints (Tier 2) — skipped in compact mode, deduped when identical across operations
|
|
237
230
|
if (op.fieldMetadata && Object.keys(op.fieldMetadata).length > 0 && !options?.compact) {
|
|
238
|
-
const currentFingerprint =
|
|
231
|
+
const currentFingerprint = fieldMetadataFingerprint(op.fieldMetadata as Record<string, unknown>);
|
|
239
232
|
if (fieldConstraintsRenderedForOp && currentFingerprint === fieldConstraintsFingerprint) {
|
|
240
233
|
sections.push("", "### Field Constraints");
|
|
241
234
|
sections.push(`Same as ${fieldConstraintsRenderedForOp} — see above.`, "");
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { LRUCache } from "lru-cache";
|
|
3
1
|
import type { ApiSpecDomainEntry, ApiSpecIndex, OpenAPIPathOperation, OpenAPISpec } from "./api-spec-types";
|
|
4
2
|
import type { InternalResource, InternalUrl } from "./types";
|
|
5
3
|
|
|
6
|
-
const LRU_CAPACITY = 5;
|
|
7
4
|
const SCHEMA_RENDER_MAX_DEPTH = 3;
|
|
8
5
|
const CRUD_OPERATION_SUFFIXES = [".API.Create", ".API.Replace", ".API.Get", ".API.List", ".API.Delete"];
|
|
9
6
|
|
|
@@ -21,28 +18,14 @@ export interface ApiSpecResolver {
|
|
|
21
18
|
resolve(url: InternalUrl): Promise<InternalResource>;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
export function createApiSpecResolver(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!blob) {
|
|
33
|
-
throw new Error(`No spec blob for domain: ${domain}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const buffer = Buffer.from(blob, "base64");
|
|
38
|
-
const decompressed = gunzipSync(buffer);
|
|
39
|
-
const spec = JSON.parse(decompressed.toString("utf-8")) as OpenAPISpec;
|
|
40
|
-
cache.set(domain, spec);
|
|
41
|
-
return spec;
|
|
42
|
-
} catch (err) {
|
|
43
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
-
throw new Error(`Failed to decompress spec for domain '${domain}': ${message}`);
|
|
45
|
-
}
|
|
21
|
+
export function createApiSpecResolver(
|
|
22
|
+
index: ApiSpecIndex,
|
|
23
|
+
data: Readonly<Record<string, OpenAPISpec>>,
|
|
24
|
+
): ApiSpecResolver {
|
|
25
|
+
function lookup(domain: string): OpenAPISpec {
|
|
26
|
+
const spec = data[domain];
|
|
27
|
+
if (!spec) throw new Error(`No spec data for domain: ${domain}`);
|
|
28
|
+
return spec;
|
|
46
29
|
}
|
|
47
30
|
|
|
48
31
|
return {
|
|
@@ -80,7 +63,7 @@ export function createApiSpecResolver(index: ApiSpecIndex, blobs: Record<string,
|
|
|
80
63
|
|
|
81
64
|
if (resource) {
|
|
82
65
|
const crud = url.searchParams.get("crud") === "true";
|
|
83
|
-
const spec =
|
|
66
|
+
const spec = lookup(domain);
|
|
84
67
|
const matchingPaths = filterPathsByResource(spec, resource, entry);
|
|
85
68
|
if (Object.keys(matchingPaths).length === 0) {
|
|
86
69
|
return makeResource(url, renderUnknownResource(resource, entry, spec));
|
|
@@ -89,11 +72,11 @@ export function createApiSpecResolver(index: ApiSpecIndex, blobs: Record<string,
|
|
|
89
72
|
}
|
|
90
73
|
|
|
91
74
|
if (pathFilter) {
|
|
92
|
-
const spec =
|
|
75
|
+
const spec = lookup(domain);
|
|
93
76
|
return makeResource(url, renderPathSpec(domain, pathFilter, spec));
|
|
94
77
|
}
|
|
95
78
|
|
|
96
|
-
const spec =
|
|
79
|
+
const spec = lookup(domain);
|
|
97
80
|
return makeResource(url, renderDomainDetail(domain, entry, spec));
|
|
98
81
|
} catch (err) {
|
|
99
82
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.35.0",
|
|
21
|
+
"commit": "ec582a736845e44f023ae6975c86e5377b6ccbdf",
|
|
22
|
+
"shortCommit": "ec582a7",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.35.0",
|
|
25
|
+
"commitDate": "2026-05-03T21:42:09Z",
|
|
26
|
+
"buildDate": "2026-05-03T22:04:53.312Z",
|
|
27
27
|
"dirty": false,
|
|
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/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/ec582a736845e44f023ae6975c86e5377b6ccbdf",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.35.0"
|
|
33
33
|
};
|
|
@@ -21,9 +21,9 @@ import * as path from "node:path";
|
|
|
21
21
|
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
22
22
|
import type { ContextStatus } from "../services/f5xc-context";
|
|
23
23
|
import { type ApiCatalogResolver, createApiCatalogResolver } from "./api-catalog-resolve";
|
|
24
|
-
import type { ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
|
|
24
|
+
import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
|
|
25
25
|
import { type ApiSpecResolver, createApiSpecResolver } from "./api-spec-resolve";
|
|
26
|
-
import type { ApiSpecIndex } from "./api-spec-types";
|
|
26
|
+
import type { ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
|
|
27
27
|
import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
|
|
28
28
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
29
29
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
@@ -43,14 +43,14 @@ const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
|
|
|
43
43
|
defaults: {},
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
let _apiSpecCache: { index: ApiSpecIndex;
|
|
46
|
+
let _apiSpecCache: { index: ApiSpecIndex; data: Readonly<Record<string, OpenAPISpec>>; version: string } | null = null;
|
|
47
47
|
|
|
48
|
-
function loadApiSpecs(): { index: ApiSpecIndex;
|
|
48
|
+
function loadApiSpecs(): { index: ApiSpecIndex; data: Readonly<Record<string, OpenAPISpec>>; version: string } {
|
|
49
49
|
if (_apiSpecCache) return _apiSpecCache;
|
|
50
50
|
try {
|
|
51
51
|
const mod = require("./api-spec-index.generated") as {
|
|
52
52
|
API_SPEC_INDEX?: ApiSpecIndex;
|
|
53
|
-
|
|
53
|
+
API_SPEC_DATA?: Readonly<Record<string, unknown>>;
|
|
54
54
|
API_SPEC_VERSION?: string;
|
|
55
55
|
};
|
|
56
56
|
const index = mod.API_SPEC_INDEX ?? EMPTY_INDEX;
|
|
@@ -60,14 +60,14 @@ function loadApiSpecs(): { index: ApiSpecIndex; blobs: Record<string, string>; v
|
|
|
60
60
|
}
|
|
61
61
|
_apiSpecCache = {
|
|
62
62
|
index,
|
|
63
|
-
|
|
63
|
+
data: (mod.API_SPEC_DATA ?? {}) as Readonly<Record<string, OpenAPISpec>>,
|
|
64
64
|
version,
|
|
65
65
|
};
|
|
66
66
|
} catch (err) {
|
|
67
67
|
logger.warn("api-spec index unavailable, embedded specs disabled", {
|
|
68
68
|
error: err instanceof Error ? err.message : String(err),
|
|
69
69
|
});
|
|
70
|
-
_apiSpecCache = { index: EMPTY_INDEX,
|
|
70
|
+
_apiSpecCache = { index: EMPTY_INDEX, data: {}, version: "unavailable" };
|
|
71
71
|
}
|
|
72
72
|
return _apiSpecCache;
|
|
73
73
|
}
|
|
@@ -75,20 +75,20 @@ function loadApiSpecs(): { index: ApiSpecIndex; blobs: Record<string, string>; v
|
|
|
75
75
|
let _apiCatalogCache: {
|
|
76
76
|
index: ApiCatalogIndex;
|
|
77
77
|
summaries: readonly ApiCatalogCategorySummary[];
|
|
78
|
-
|
|
78
|
+
data: Readonly<Record<string, ApiCatalogCategory>>;
|
|
79
79
|
} | null = null;
|
|
80
80
|
|
|
81
81
|
function loadApiCatalog(): {
|
|
82
82
|
index: ApiCatalogIndex;
|
|
83
83
|
summaries: readonly ApiCatalogCategorySummary[];
|
|
84
|
-
|
|
84
|
+
data: Readonly<Record<string, ApiCatalogCategory>>;
|
|
85
85
|
} {
|
|
86
86
|
if (_apiCatalogCache) return _apiCatalogCache;
|
|
87
87
|
try {
|
|
88
88
|
const mod = require("./api-catalog-index.generated") as {
|
|
89
89
|
API_CATALOG_INDEX?: ApiCatalogIndex;
|
|
90
90
|
API_CATALOG_CATEGORY_SUMMARIES?: readonly ApiCatalogCategorySummary[];
|
|
91
|
-
|
|
91
|
+
API_CATALOG_DATA?: Readonly<Record<string, ApiCatalogCategory>>;
|
|
92
92
|
};
|
|
93
93
|
const index = mod.API_CATALOG_INDEX ?? EMPTY_CATALOG_INDEX;
|
|
94
94
|
const summaries = mod.API_CATALOG_CATEGORY_SUMMARIES ?? [];
|
|
@@ -98,13 +98,13 @@ function loadApiCatalog(): {
|
|
|
98
98
|
_apiCatalogCache = {
|
|
99
99
|
index,
|
|
100
100
|
summaries,
|
|
101
|
-
|
|
101
|
+
data: mod.API_CATALOG_DATA ?? {},
|
|
102
102
|
};
|
|
103
103
|
} catch (err) {
|
|
104
104
|
logger.warn("api-catalog index unavailable, catalog disabled", {
|
|
105
105
|
error: err instanceof Error ? err.message : String(err),
|
|
106
106
|
});
|
|
107
|
-
_apiCatalogCache = { index: EMPTY_CATALOG_INDEX, summaries: [],
|
|
107
|
+
_apiCatalogCache = { index: EMPTY_CATALOG_INDEX, summaries: [], data: {} };
|
|
108
108
|
}
|
|
109
109
|
return _apiCatalogCache;
|
|
110
110
|
}
|
|
@@ -133,7 +133,7 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
133
133
|
#getApiSpecResolver(): ApiSpecResolver {
|
|
134
134
|
if (!this.#apiSpecResolver) {
|
|
135
135
|
const specs = loadApiSpecs();
|
|
136
|
-
this.#apiSpecResolver = createApiSpecResolver(specs.index, specs.
|
|
136
|
+
this.#apiSpecResolver = createApiSpecResolver(specs.index, specs.data);
|
|
137
137
|
}
|
|
138
138
|
return this.#apiSpecResolver;
|
|
139
139
|
}
|
|
@@ -145,7 +145,7 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
145
145
|
this.#apiCatalogResolver = createApiCatalogResolver(
|
|
146
146
|
catalog.index,
|
|
147
147
|
catalog.summaries,
|
|
148
|
-
catalog.
|
|
148
|
+
catalog.data,
|
|
149
149
|
specs.index,
|
|
150
150
|
);
|
|
151
151
|
}
|
|
@@ -192,18 +192,19 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
192
192
|
This document contains the authoritative repository URL, issues URL, and source location.
|
|
193
193
|
For identity questions (source code, repo, version, who built this) — answer from `xcsh://about` alone. Do not call external GitHub tools.
|
|
194
194
|
- `xcsh://api-spec/` — F5 XC API specifications (schema introspection, field types, validation).
|
|
195
|
-
- `xcsh://api-catalog/` — F5 XC API operations
|
|
195
|
+
- `xcsh://api-catalog/` — F5 XC API operations catalog (CRUD execution).
|
|
196
196
|
|
|
197
197
|
When the user needs to **make an API call** (create, read, update, delete):
|
|
198
198
|
|
|
199
|
-
1. `xcsh://api-catalog/?
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
1. `xcsh://api-catalog/?resource={resource_name}` → get endpoint path, method, minimum
|
|
200
|
+
payload JSON, required fields, and response summary
|
|
201
|
+
2. Call `xcsh_api` tool with `method`, `path`, `params` (all `{placeholder}` substitutions), and `payload`
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
The `xcsh_api` tool handles authentication, URL construction, and HTTP execution.
|
|
204
|
+
Never construct curl commands for F5 XC API calls — use `xcsh_api` instead.
|
|
205
|
+
|
|
206
|
+
If the resource name is unknown, search first:
|
|
207
|
+
`xcsh://api-catalog/?search={term}` → find the matching category, then read it.
|
|
207
208
|
|
|
208
209
|
When the user needs **field-level validation rules** (constraints, patterns, enums):
|
|
209
210
|
|
|
@@ -215,8 +216,7 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
215
216
|
If the domain is unknown, read `xcsh://api-spec/` first to identify it.
|
|
216
217
|
|
|
217
218
|
**MUST NOT** read proactively.
|
|
218
|
-
Never start at `xcsh://api-spec/` for CRUD operations — the catalog
|
|
219
|
-
the same endpoint with curl template at a fraction of the token cost.
|
|
219
|
+
Never start at `xcsh://api-spec/` for CRUD operations — the catalog is faster.
|
|
220
220
|
Never guess API paths or request schemas.
|
|
221
221
|
Also available: `xcsh://api-spec/workflows/` (step-by-step guides),
|
|
222
222
|
`xcsh://api-spec/errors/{code}` (error resolution), `xcsh://api-spec/glossary/` (acronym reference).
|
|
@@ -2,13 +2,14 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- You **MUST** use `cwd` parameter to set working directory instead of `cd dir && …`
|
|
5
|
+
- Always provide a `description` parameter: a short, human-readable summary of what the command does (e.g. "Install dependencies", "Run test suite"). This is displayed in compact mode and during execution.
|
|
5
6
|
- Prefer `env: { NAME: "…" }` for multiline, quote-heavy, or untrusted values instead of inlining them into shell syntax; reference them from the command as `$NAME`
|
|
6
7
|
- Quote variable expansions like `"$NAME"` to preserve exact content and avoid shell parsing bugs
|
|
7
8
|
- PTY mode is opt-in: set `pty: true` only when command expects a real terminal (for example `sudo`, `ssh` where you need input from the user); default is `false`
|
|
8
9
|
- You **MUST** use `;` only when later commands should run regardless of earlier failures
|
|
9
10
|
- `skill://` URIs are auto-resolved to filesystem paths before execution
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
- `python skill://my-skill/scripts/init.py` runs the script from the skill directory
|
|
12
|
+
- `skill://<name>/<relative-path>` resolves within the skill's base directory
|
|
12
13
|
- Internal URLs are also auto-resolved to filesystem paths before execution.
|
|
13
14
|
{{#if asyncEnabled}}
|
|
14
15
|
- Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
|
|
@@ -45,17 +46,22 @@ You **MUST** use specialized tools instead of bash for ALL file operations:
|
|
|
45
46
|
|---|---|
|
|
46
47
|
|`cat file`, `head -n N file`|`read(path="file", limit=N)`|
|
|
47
48
|
|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
|
|
49
|
+
<!-- markdownlint-disable MD055 MD056 -->
|
|
48
50
|
{{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`|
|
|
49
51
|
|`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`|
|
|
50
52
|
|`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}}
|
|
51
53
|
{{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
|
|
54
|
+
<!-- markdownlint-enable MD055 MD056 -->
|
|
52
55
|
|`ls dir/`|`read(path="dir/")`|
|
|
53
56
|
|`cat <<'EOF' > file`|`write(path="file", content="…")`|
|
|
54
57
|
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
|
|
55
58
|
|
|
59
|
+
<!-- markdownlint-disable MD055 MD056 -->
|
|
56
60
|
{{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}}
|
|
61
|
+
<!-- markdownlint-enable MD055 MD056 -->
|
|
57
62
|
{{#if hasAstGrep}}- You **MUST** use `ast_grep` for structural code search instead of bash `grep`/`awk`/`perl` pipelines{{/if}}
|
|
58
63
|
{{#if hasAstEdit}}- You **MUST** use `ast_edit` for structural rewrites instead of bash `sed`/`awk`/`perl` pipelines{{/if}}
|
|
64
|
+
|
|
59
65
|
- You **MUST NOT** use Bash for these operations like read, grep, find, edit, write, where specialized tools exist.
|
|
60
66
|
- You **MUST NOT** use `2>&1` | `2>/dev/null` pattern, stdout and stderr are already merged.
|
|
61
67
|
- You **MUST NOT** use `| head -n 50` or `| tail -n 100` pattern, use `head` and `tail` parameters instead.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Execute an F5 Distributed Cloud API call directly.
|
|
2
|
+
|
|
3
|
+
Handles authentication, URL construction, and HTTP execution.
|
|
4
|
+
Requires `F5XC_API_URL` and `F5XC_API_TOKEN` environment variables.
|
|
5
|
+
|
|
6
|
+
Pass all path `{placeholder}` values via `params`, e.g. `{ namespace: "default", name: "example-lb", vh_name: "example-vh" }`.
|
|
7
|
+
Body is sent for all methods except GET when `payload` is provided — including DELETE operations that require a body.
|
|
8
|
+
|
|
9
|
+
Use this tool after reading the API catalog to get the endpoint path and payload structure.
|
package/src/tools/bash.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Component } from "@f5xc-salesdemos/pi-tui";
|
|
|
9
9
|
import { ImageProtocol, TERMINAL, Text } from "@f5xc-salesdemos/pi-tui";
|
|
10
10
|
import { $env, getProjectDir, isEnoent, prompt, setShellPwd } from "@f5xc-salesdemos/pi-utils";
|
|
11
11
|
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { Settings } from "../config/settings";
|
|
12
13
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
15
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
@@ -26,7 +27,7 @@ import { applyHeadTail } from "./bash-normalize";
|
|
|
26
27
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
27
28
|
import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
|
28
29
|
import { resolveToCwd } from "./path-utils";
|
|
29
|
-
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
30
|
+
import { formatToolWorkingDirectory, replaceTabs, truncateToWidth } from "./render-utils";
|
|
30
31
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
31
32
|
import { toolResult } from "./tool-result";
|
|
32
33
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -41,6 +42,12 @@ const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
|
41
42
|
|
|
42
43
|
const bashSchemaBase = Type.Object({
|
|
43
44
|
command: Type.String({ description: "Command to execute" }),
|
|
45
|
+
description: Type.Optional(
|
|
46
|
+
Type.String({
|
|
47
|
+
description:
|
|
48
|
+
"Human-readable description of what this command does (e.g. 'Install dependencies', 'Run test suite')",
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
44
51
|
env: Type.Optional(
|
|
45
52
|
Type.Record(Type.String({ pattern: BASH_ENV_NAME_PATTERN.source }), Type.String(), {
|
|
46
53
|
description:
|
|
@@ -71,6 +78,7 @@ type BashToolSchema = typeof bashSchemaBase | typeof bashSchemaWithAsync;
|
|
|
71
78
|
|
|
72
79
|
export interface BashToolInput {
|
|
73
80
|
command: string;
|
|
81
|
+
description?: string;
|
|
74
82
|
env?: Record<string, string>;
|
|
75
83
|
timeout?: number;
|
|
76
84
|
cwd?: string;
|
|
@@ -680,6 +688,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
680
688
|
|
|
681
689
|
interface BashRenderArgs {
|
|
682
690
|
command?: string;
|
|
691
|
+
description?: string;
|
|
683
692
|
env?: Record<string, string>;
|
|
684
693
|
timeout?: number;
|
|
685
694
|
cwd?: string;
|
|
@@ -711,10 +720,18 @@ function formatBashCommand(args: BashRenderArgs): string {
|
|
|
711
720
|
return displayWorkdir ? `${prompt} cd ${displayWorkdir} && ${renderedCommand}` : `${prompt} ${renderedCommand}`;
|
|
712
721
|
}
|
|
713
722
|
|
|
723
|
+
function getBashVerboseSetting(): boolean {
|
|
724
|
+
try {
|
|
725
|
+
return Settings.instance.get("bash.verbose");
|
|
726
|
+
} catch {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
714
731
|
export const bashToolRenderer = {
|
|
715
732
|
renderCall(args: BashRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
716
|
-
const
|
|
717
|
-
const text = renderStatusLine({ icon: "pending", title: "Bash", description:
|
|
733
|
+
const summaryText = args.description ?? formatBashCommand(args);
|
|
734
|
+
const text = renderStatusLine({ icon: "pending", title: "Bash", description: summaryText }, uiTheme);
|
|
718
735
|
return new Text(text, 0, 0);
|
|
719
736
|
},
|
|
720
737
|
|
|
@@ -746,6 +763,30 @@ export const bashToolRenderer = {
|
|
|
746
763
|
const displayOutput = output.trimEnd();
|
|
747
764
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
748
765
|
|
|
766
|
+
const rawOutputLines = displayOutput.split("\n");
|
|
767
|
+
const sixelLineMask =
|
|
768
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(rawOutputLines) : undefined;
|
|
769
|
+
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
770
|
+
|
|
771
|
+
// Collapsed mode: single status line when bash.verbose=false
|
|
772
|
+
const verbose = getBashVerboseSetting();
|
|
773
|
+
const hasAsyncDetails = details?.async != null;
|
|
774
|
+
const forceExpand = isError || hasAsyncDetails || hasSixelOutput;
|
|
775
|
+
if (!verbose && !expanded && !forceExpand && !options.isPartial) {
|
|
776
|
+
const rawCmd = args?.command;
|
|
777
|
+
const summaryText =
|
|
778
|
+
args?.description ?? (rawCmd && rawCmd.length > 60 ? `${rawCmd.slice(0, 60)}…` : rawCmd) ?? "…";
|
|
779
|
+
const line = renderStatusLine(
|
|
780
|
+
{
|
|
781
|
+
title: "Bash",
|
|
782
|
+
description: summaryText,
|
|
783
|
+
badge: { label: "ok", color: "success" },
|
|
784
|
+
},
|
|
785
|
+
uiTheme,
|
|
786
|
+
);
|
|
787
|
+
return [truncateToWidth(line, width)];
|
|
788
|
+
}
|
|
789
|
+
|
|
749
790
|
// Build truncation warning
|
|
750
791
|
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
751
792
|
const timeoutLine =
|
|
@@ -762,10 +803,6 @@ export const bashToolRenderer = {
|
|
|
762
803
|
|
|
763
804
|
const outputLines: string[] = [];
|
|
764
805
|
const hasOutput = displayOutput.trim().length > 0;
|
|
765
|
-
const rawOutputLines = displayOutput.split("\n");
|
|
766
|
-
const sixelLineMask =
|
|
767
|
-
TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(rawOutputLines) : undefined;
|
|
768
|
-
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
769
806
|
if (hasOutput) {
|
|
770
807
|
if (hasSixelOutput) {
|
|
771
808
|
outputLines.push(
|
package/src/tools/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { loadSshTool } from "./ssh";
|
|
|
56
56
|
import { SubmitResultTool } from "./submit-result";
|
|
57
57
|
import { type TodoPhase, TodoWriteTool } from "./todo-write";
|
|
58
58
|
import { WriteTool } from "./write";
|
|
59
|
+
import { XcshApiTool } from "./xcsh-api";
|
|
59
60
|
|
|
60
61
|
// Exa MCP tools (22 tools)
|
|
61
62
|
|
|
@@ -244,6 +245,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
244
245
|
web_search: s => new SearchTool(s),
|
|
245
246
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
246
247
|
write: s => new WriteTool(s),
|
|
248
|
+
xcsh_api: s => new XcshApiTool(s),
|
|
247
249
|
};
|
|
248
250
|
|
|
249
251
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@f5xc-salesdemos/pi-agent-core";
|
|
2
|
+
import { prompt } from "@f5xc-salesdemos/pi-utils";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import xcshApiDescription from "../prompts/tools/xcsh-api.md" with { type: "text" };
|
|
5
|
+
import type { ToolSession } from ".";
|
|
6
|
+
|
|
7
|
+
const xcshApiSchema = Type.Object({
|
|
8
|
+
method: Type.Union(
|
|
9
|
+
[Type.Literal("GET"), Type.Literal("POST"), Type.Literal("PUT"), Type.Literal("PATCH"), Type.Literal("DELETE")],
|
|
10
|
+
{ description: "HTTP method" },
|
|
11
|
+
),
|
|
12
|
+
path: Type.String({ description: "API path, e.g. /api/config/namespaces/{namespace}/http_loadbalancers" }),
|
|
13
|
+
params: Type.Optional(
|
|
14
|
+
Type.Record(Type.String(), Type.String(), {
|
|
15
|
+
description:
|
|
16
|
+
"Path parameter substitutions, e.g. { namespace: 'default', name: 'example-lb', vh_name: 'example-vh' }",
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
payload: Type.Optional(Type.Unknown({ description: "JSON body for POST/PUT/PATCH/DELETE requests" })),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type XcshApiParams = Static<typeof xcshApiSchema>;
|
|
23
|
+
|
|
24
|
+
export interface XcshApiToolDetails {
|
|
25
|
+
status: number;
|
|
26
|
+
url: string;
|
|
27
|
+
method: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type XcshApiResult = AgentToolResult<XcshApiToolDetails> & { isError?: boolean };
|
|
31
|
+
|
|
32
|
+
export class XcshApiTool implements AgentTool<typeof xcshApiSchema, XcshApiToolDetails> {
|
|
33
|
+
readonly name = "xcsh_api";
|
|
34
|
+
readonly label = "API";
|
|
35
|
+
readonly description: string;
|
|
36
|
+
readonly parameters = xcshApiSchema;
|
|
37
|
+
|
|
38
|
+
constructor(_session: ToolSession) {
|
|
39
|
+
this.description = prompt.render(xcshApiDescription);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async execute(_toolCallId: string, params: XcshApiParams): Promise<XcshApiResult> {
|
|
43
|
+
const apiUrl = process.env.F5XC_API_URL;
|
|
44
|
+
if (!apiUrl) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: "Error: F5XC_API_URL environment variable is not set." }],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const apiToken = process.env.F5XC_API_TOKEN;
|
|
52
|
+
if (!apiToken) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: "Error: F5XC_API_TOKEN environment variable is not set." }],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let resolvedPath = params.path;
|
|
60
|
+
if (params.params) {
|
|
61
|
+
for (const [key, value] of Object.entries(params.params)) {
|
|
62
|
+
resolvedPath = resolvedPath.replaceAll(`{${key}}`, value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const url = `${apiUrl.replace(/\/+$/, "")}${resolvedPath}`;
|
|
67
|
+
const headers: Record<string, string> = {
|
|
68
|
+
Authorization: `APIToken ${apiToken}`,
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const init: RequestInit = { method: params.method, headers };
|
|
73
|
+
|
|
74
|
+
if (params.payload && params.method !== "GET") {
|
|
75
|
+
headers["Content-Type"] = "application/json";
|
|
76
|
+
init.body = JSON.stringify(params.payload);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(url, init);
|
|
81
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
82
|
+
let bodyText: string;
|
|
83
|
+
|
|
84
|
+
if (contentType.includes("application/json")) {
|
|
85
|
+
const json = await response.json();
|
|
86
|
+
bodyText = JSON.stringify(json, null, 2);
|
|
87
|
+
} else {
|
|
88
|
+
bodyText = await response.text();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const statusLine = `${response.status} ${response.statusText}`;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `${statusLine}\n\n${bodyText}` }],
|
|
95
|
+
details: { status: response.status, url, method: params.method },
|
|
96
|
+
...(response.status >= 400 ? { isError: true } : {}),
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: `Request failed: ${message}` }],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|