@f5-sales-demo/xcsh 22.1.2 → 22.2.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.
- package/CHANGELOG.md +4 -0
- package/package.json +8 -8
- package/src/cli/update-cli.ts +85 -21
- package/src/cli/update-recommendation.ts +30 -4
- package/src/internal-urls/api-catalog-discovery.ts +56 -1
- package/src/internal-urls/api-catalog-preflight.ts +35 -63
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/knowledge-classifier.ts +209 -0
- package/src/internal-urls/knowledge-search.ts +90 -0
- package/src/prompts/internal-urls/about.md +1 -1
- package/src/prompts/rules/llms-search.md +10 -1
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/prompts/tools/display-media.md +4 -0
- package/src/services/xcsh-knowledge.ts +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
|
|
43
43
|
### Changed
|
|
44
44
|
|
|
45
|
+
- Made standalone executable and schema-v2 receipt updates transactional, canonicalized receipt paths,
|
|
46
|
+
removed unused adjacent native payloads from standalone installs, and added published installer
|
|
47
|
+
qualification on Linux, macOS, and Windows ([#4510](https://github.com/f5-sales-demo/xcsh/issues/4510)).
|
|
48
|
+
|
|
45
49
|
- Standardized executable updates on `xcsh self-update`; `xcsh update` is now reserved for strict manifest resource updates ([#4487](https://github.com/f5-sales-demo/xcsh/issues/4487)).
|
|
46
50
|
|
|
47
51
|
- Added `power.sleepPrevention` with an `idle` default and cumulative `off`, `idle`, `display`, and
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "22.1
|
|
4
|
+
"version": "22.2.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",
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
70
|
-
"@f5-sales-demo/pi-agent-core": "22.1
|
|
71
|
-
"@f5-sales-demo/pi-ai": "22.1
|
|
72
|
-
"@f5-sales-demo/pi-natives": "22.1
|
|
73
|
-
"@f5-sales-demo/pi-resource-management": "22.1
|
|
74
|
-
"@f5-sales-demo/pi-tui": "22.1
|
|
75
|
-
"@f5-sales-demo/pi-utils": "22.1
|
|
76
|
-
"@f5-sales-demo/xcsh-stats": "22.1
|
|
70
|
+
"@f5-sales-demo/pi-agent-core": "22.2.1",
|
|
71
|
+
"@f5-sales-demo/pi-ai": "22.2.1",
|
|
72
|
+
"@f5-sales-demo/pi-natives": "22.2.1",
|
|
73
|
+
"@f5-sales-demo/pi-resource-management": "22.2.1",
|
|
74
|
+
"@f5-sales-demo/pi-tui": "22.2.1",
|
|
75
|
+
"@f5-sales-demo/pi-utils": "22.2.1",
|
|
76
|
+
"@f5-sales-demo/xcsh-stats": "22.2.1",
|
|
77
77
|
"@mozilla/readability": "^0.6",
|
|
78
78
|
"@resvg/resvg-wasm": "2.6.2",
|
|
79
79
|
"@sinclair/typebox": "0.34.52",
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Context-aware executable update policy and standalone replacement. */
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { pipeline } from "node:stream/promises";
|
|
5
6
|
import { APP_NAME, isEnoent, VERSION } from "@f5-sales-demo/pi-utils";
|
|
6
7
|
import chalk from "chalk";
|
|
7
8
|
import {
|
|
9
|
+
createInstallReceipt,
|
|
8
10
|
defaultInstallChannelDependencies,
|
|
9
11
|
formatUpdateRecommendation,
|
|
10
12
|
resolveInstallChannel,
|
|
@@ -70,23 +72,49 @@ export interface StandaloneReplacementOptions {
|
|
|
70
72
|
arch?: NodeJS.Architecture;
|
|
71
73
|
fetchImpl?(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
72
74
|
rename?(source: string, destination: string): Promise<void>;
|
|
75
|
+
readFile?(file: string): Promise<string>;
|
|
76
|
+
writeFile?(file: string, contents: string): Promise<void>;
|
|
77
|
+
unlink?(file: string): Promise<void>;
|
|
73
78
|
validate?(executable: string, expectedVersion: string): Promise<void>;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
async function ignoreMissing(unlink: (file: string) => Promise<void>, file: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
await unlink(file);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!isEnoent(error)) throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Replace a proven standalone executable and receipt as one rollback-safe transaction. */
|
|
77
90
|
export async function replaceStandaloneExecutable(
|
|
78
91
|
expectedVersion: string,
|
|
79
92
|
options: StandaloneReplacementOptions,
|
|
80
93
|
): Promise<void> {
|
|
81
94
|
const targetPath = options.targetPath;
|
|
82
|
-
const
|
|
95
|
+
const platform = options.platform ?? process.platform;
|
|
96
|
+
const arch = options.arch ?? process.arch;
|
|
97
|
+
const binaryName = getBinaryName(platform, arch);
|
|
83
98
|
const url = `https://github.com/${REPO}/releases/download/v${expectedVersion}/${binaryName}`;
|
|
84
99
|
const tempPath = `${targetPath}.new-${process.pid}`;
|
|
85
|
-
const
|
|
100
|
+
const receiptPath = path.join(path.dirname(targetPath), "xcsh-install.json");
|
|
101
|
+
const tempReceiptPath = `${receiptPath}.new-${process.pid}`;
|
|
102
|
+
const backupPath = `${targetPath}.bak-${process.pid}`;
|
|
103
|
+
const backupReceiptPath = `${receiptPath}.bak-${process.pid}`;
|
|
86
104
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
87
105
|
const rename = options.rename ?? fs.promises.rename;
|
|
106
|
+
const readFile = options.readFile ?? (file => fs.promises.readFile(file, "utf8"));
|
|
107
|
+
const writeFile =
|
|
108
|
+
options.writeFile ??
|
|
109
|
+
(async (file, contents) => fs.promises.writeFile(file, contents, { flag: "wx", mode: 0o644 }));
|
|
110
|
+
const unlink = options.unlink ?? fs.promises.unlink;
|
|
88
111
|
const validate = options.validate ?? validateExecutableVersion;
|
|
89
|
-
|
|
112
|
+
const receiptContents = `${JSON.stringify(createInstallReceipt("standalone", expectedVersion, targetPath, platform, arch))}\n`;
|
|
113
|
+
let originalBinaryMoved = false;
|
|
114
|
+
let originalReceiptMoved = false;
|
|
115
|
+
let replacementBinaryInstalled = false;
|
|
116
|
+
let replacementReceiptInstalled = false;
|
|
117
|
+
let committed = false;
|
|
90
118
|
|
|
91
119
|
try {
|
|
92
120
|
const response = await fetchImpl(url, { redirect: "follow" });
|
|
@@ -94,33 +122,69 @@ export async function replaceStandaloneExecutable(
|
|
|
94
122
|
await pipeline(response.body, fs.createWriteStream(tempPath, { mode: 0o755, flags: "wx" }));
|
|
95
123
|
await fs.promises.chmod(tempPath, 0o755);
|
|
96
124
|
await validate(tempPath, expectedVersion);
|
|
125
|
+
await writeFile(tempReceiptPath, receiptContents);
|
|
126
|
+
if ((await readFile(tempReceiptPath)) !== receiptContents)
|
|
127
|
+
throw new Error("Staged install receipt validation failed");
|
|
97
128
|
|
|
98
|
-
try {
|
|
99
|
-
await fs.promises.unlink(backupPath);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (!isEnoent(error)) throw error;
|
|
102
|
-
}
|
|
103
129
|
await rename(targetPath, backupPath);
|
|
104
|
-
|
|
130
|
+
originalBinaryMoved = true;
|
|
131
|
+
await rename(receiptPath, backupReceiptPath);
|
|
132
|
+
originalReceiptMoved = true;
|
|
105
133
|
await rename(tempPath, targetPath);
|
|
134
|
+
replacementBinaryInstalled = true;
|
|
135
|
+
await rename(tempReceiptPath, receiptPath);
|
|
136
|
+
replacementReceiptInstalled = true;
|
|
106
137
|
await validate(targetPath, expectedVersion);
|
|
107
|
-
await
|
|
108
|
-
|
|
138
|
+
if ((await readFile(receiptPath)) !== receiptContents) throw new Error("Installed receipt validation failed");
|
|
139
|
+
committed = true;
|
|
140
|
+
await ignoreMissing(unlink, backupPath).catch(() => {});
|
|
141
|
+
originalBinaryMoved = false;
|
|
142
|
+
await ignoreMissing(unlink, backupReceiptPath).catch(() => {});
|
|
143
|
+
originalReceiptMoved = false;
|
|
109
144
|
} catch (error) {
|
|
110
|
-
|
|
145
|
+
const rollbackErrors: unknown[] = [];
|
|
146
|
+
if (replacementReceiptInstalled) {
|
|
147
|
+
try {
|
|
148
|
+
await ignoreMissing(unlink, receiptPath);
|
|
149
|
+
replacementReceiptInstalled = false;
|
|
150
|
+
} catch (rollbackError) {
|
|
151
|
+
rollbackErrors.push(rollbackError);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (originalReceiptMoved) {
|
|
155
|
+
try {
|
|
156
|
+
await rename(backupReceiptPath, receiptPath);
|
|
157
|
+
originalReceiptMoved = false;
|
|
158
|
+
} catch (rollbackError) {
|
|
159
|
+
rollbackErrors.push(rollbackError);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (replacementBinaryInstalled) {
|
|
111
163
|
try {
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
await ignoreMissing(unlink, targetPath);
|
|
165
|
+
replacementBinaryInstalled = false;
|
|
166
|
+
} catch (rollbackError) {
|
|
167
|
+
rollbackErrors.push(rollbackError);
|
|
115
168
|
}
|
|
116
|
-
await rename(backupPath, targetPath);
|
|
117
|
-
originalMoved = false;
|
|
118
169
|
}
|
|
170
|
+
if (originalBinaryMoved) {
|
|
171
|
+
try {
|
|
172
|
+
await rename(backupPath, targetPath);
|
|
173
|
+
originalBinaryMoved = false;
|
|
174
|
+
} catch (rollbackError) {
|
|
175
|
+
rollbackErrors.push(rollbackError);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (rollbackErrors.length > 0)
|
|
179
|
+
throw new AggregateError([error, ...rollbackErrors], "Self-update rollback failed");
|
|
119
180
|
throw error;
|
|
120
181
|
} finally {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
182
|
+
await ignoreMissing(unlink, tempPath).catch(() => {});
|
|
183
|
+
await ignoreMissing(unlink, tempReceiptPath).catch(() => {});
|
|
184
|
+
if (committed) {
|
|
185
|
+
await ignoreMissing(unlink, backupPath).catch(() => {});
|
|
186
|
+
await ignoreMissing(unlink, backupReceiptPath).catch(() => {});
|
|
187
|
+
}
|
|
124
188
|
}
|
|
125
189
|
}
|
|
126
190
|
|
|
@@ -37,8 +37,10 @@ export interface InstallChannelDependencies {
|
|
|
37
37
|
run(command: string, args: string[]): Promise<CommandResult>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
export const INSTALL_RECEIPT_SCHEMA_VERSION = 2;
|
|
41
|
+
|
|
42
|
+
export interface InstallReceipt {
|
|
43
|
+
schemaVersion: typeof INSTALL_RECEIPT_SCHEMA_VERSION;
|
|
42
44
|
channel: "standalone" | "windows-installer";
|
|
43
45
|
version: string;
|
|
44
46
|
executablePath: string;
|
|
@@ -46,6 +48,8 @@ interface InstallReceipt {
|
|
|
46
48
|
arch: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const INSTALL_RECEIPT_KEYS = ["arch", "channel", "executablePath", "platform", "schemaVersion", "version"];
|
|
52
|
+
|
|
49
53
|
function platformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 {
|
|
50
54
|
return platform === "win32" ? path.win32 : path.posix;
|
|
51
55
|
}
|
|
@@ -55,6 +59,25 @@ function comparablePath(value: string, platform: NodeJS.Platform): string {
|
|
|
55
59
|
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
export function createInstallReceipt(
|
|
63
|
+
channel: InstallReceipt["channel"],
|
|
64
|
+
version: string,
|
|
65
|
+
executablePath: string,
|
|
66
|
+
platform: NodeJS.Platform,
|
|
67
|
+
arch: string,
|
|
68
|
+
): InstallReceipt {
|
|
69
|
+
const pathApi = platformPath(platform);
|
|
70
|
+
if (!pathApi.isAbsolute(executablePath)) throw new Error("Install receipt executable path must be absolute");
|
|
71
|
+
return {
|
|
72
|
+
schemaVersion: INSTALL_RECEIPT_SCHEMA_VERSION,
|
|
73
|
+
channel,
|
|
74
|
+
version,
|
|
75
|
+
executablePath: pathApi.normalize(executablePath),
|
|
76
|
+
platform,
|
|
77
|
+
arch,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
function releaseAsset(platform: NodeJS.Platform, arch: string): string | undefined {
|
|
59
82
|
if (platform === "linux" && (arch === "x64" || arch === "arm64")) return `xcsh-linux-${arch}`;
|
|
60
83
|
if (platform === "darwin" && (arch === "x64" || arch === "arm64")) return `xcsh-darwin-${arch}`;
|
|
@@ -82,12 +105,15 @@ function receiptIsValid(
|
|
|
82
105
|
if (!value || typeof value !== "object") return false;
|
|
83
106
|
const receipt = value as Partial<InstallReceipt>;
|
|
84
107
|
if (
|
|
85
|
-
receipt.
|
|
108
|
+
JSON.stringify(Object.keys(receipt).sort()) !== JSON.stringify(INSTALL_RECEIPT_KEYS) ||
|
|
109
|
+
receipt.schemaVersion !== INSTALL_RECEIPT_SCHEMA_VERSION ||
|
|
86
110
|
(receipt.channel !== "standalone" && receipt.channel !== "windows-installer") ||
|
|
87
111
|
receipt.version !== deps.version ||
|
|
88
112
|
receipt.platform !== deps.platform ||
|
|
89
113
|
receipt.arch !== deps.arch ||
|
|
90
114
|
typeof receipt.executablePath !== "string" ||
|
|
115
|
+
!platformPath(deps.platform).isAbsolute(receipt.executablePath) ||
|
|
116
|
+
platformPath(deps.platform).normalize(receipt.executablePath) !== receipt.executablePath ||
|
|
91
117
|
comparablePath(receipt.executablePath, deps.platform) !== comparablePath(resolvedExecPath, deps.platform)
|
|
92
118
|
) {
|
|
93
119
|
return false;
|
|
@@ -233,7 +259,7 @@ export async function resolveInstallChannel(deps: InstallChannelDependencies): P
|
|
|
233
259
|
if (receiptIsValid(parsed, deps, resolvedExecPath)) {
|
|
234
260
|
signals.push({
|
|
235
261
|
channel: parsed.channel,
|
|
236
|
-
evidence: `valid schema-
|
|
262
|
+
evidence: `valid schema-v2 ${parsed.channel} receipt at ${receiptPath}`,
|
|
237
263
|
});
|
|
238
264
|
} else {
|
|
239
265
|
diagnostics.push(`invalid or stale install receipt at ${receiptPath}`);
|
|
@@ -95,6 +95,48 @@ export function matchesBaselineCatalogDiscoveryTerm(term: string, values: readon
|
|
|
95
95
|
return values.some(value => normalizeApiCatalogDiscoveryTerm(value).includes(normalized));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function categoryDocuments(category: ApiCatalogCategory): ApiCatalogDiscoveryDocument[] {
|
|
99
|
+
const destination = `xcsh://api-catalog/${category.name}`;
|
|
100
|
+
if (category.operations.length === 0) {
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
id: `category:${category.name}`,
|
|
104
|
+
categoryName: category.name,
|
|
105
|
+
markdown: [
|
|
106
|
+
`# ${category.displayName}`,
|
|
107
|
+
"",
|
|
108
|
+
`- Category: ${category.name}`,
|
|
109
|
+
`- Destination: ${destination}`,
|
|
110
|
+
"",
|
|
111
|
+
"## Operations",
|
|
112
|
+
"- None",
|
|
113
|
+
"",
|
|
114
|
+
].join("\n"),
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
return category.operations
|
|
119
|
+
.slice()
|
|
120
|
+
.sort((left, right) => left.operationId.localeCompare(right.operationId))
|
|
121
|
+
.map(operation => ({
|
|
122
|
+
id: `operation:${category.name}:${operation.operationId}`,
|
|
123
|
+
categoryName: category.name,
|
|
124
|
+
markdown: [
|
|
125
|
+
`# ${category.displayName}: ${operation.name}`,
|
|
126
|
+
"",
|
|
127
|
+
`- Category: ${category.name}`,
|
|
128
|
+
`- Destination: ${destination}`,
|
|
129
|
+
`- Operation: ${operation.name}`,
|
|
130
|
+
`- Alias: ${(operation.operationAliases ?? []).join(", ")}`,
|
|
131
|
+
`- Description: ${operation.description}`,
|
|
132
|
+
`- Method: ${operation.method.toUpperCase()}`,
|
|
133
|
+
`- Path: ${operation.path}`,
|
|
134
|
+
`- Operation ID: ${operation.operationId}`,
|
|
135
|
+
"",
|
|
136
|
+
].join("\n"),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
98
140
|
function categoryDocument(category: ApiCatalogCategory): ApiCatalogDiscoveryDocument {
|
|
99
141
|
const destination = `xcsh://api-catalog/${category.name}`;
|
|
100
142
|
const operations = category.operations.flatMap(operation => [
|
|
@@ -105,7 +147,6 @@ function categoryDocument(category: ApiCatalogCategory): ApiCatalogDiscoveryDocu
|
|
|
105
147
|
` - Path: ${operation.path}`,
|
|
106
148
|
` - Operation ID: ${operation.operationId}`,
|
|
107
149
|
]);
|
|
108
|
-
|
|
109
150
|
return {
|
|
110
151
|
id: `category:${category.name}`,
|
|
111
152
|
categoryName: category.name,
|
|
@@ -139,6 +180,20 @@ export function buildApiCatalogDiscoveryCorpus(
|
|
|
139
180
|
};
|
|
140
181
|
}
|
|
141
182
|
|
|
183
|
+
/** Candidate-only operation granularity; authoritative results remain category URLs. */
|
|
184
|
+
export function buildApiOperationDiscoveryCorpus(
|
|
185
|
+
index: ApiCatalogIndex,
|
|
186
|
+
data: Readonly<Record<string, ApiCatalogCategory>>,
|
|
187
|
+
): ApiCatalogDiscoveryCorpus {
|
|
188
|
+
return {
|
|
189
|
+
catalogVersion: index.version,
|
|
190
|
+
documents: Object.values(data)
|
|
191
|
+
.slice()
|
|
192
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
193
|
+
.flatMap(categoryDocuments),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
142
197
|
export function fingerprintApiCatalogDiscoveryCorpus(
|
|
143
198
|
corpus: ApiCatalogDiscoveryCorpus,
|
|
144
199
|
sourceSha: string,
|
|
@@ -3,18 +3,20 @@ import path from "node:path";
|
|
|
3
3
|
import { rankQmdBm25CatalogDiscovery } from "./api-catalog-discovery";
|
|
4
4
|
import { QMD_API_CATALOG_PREBUILT_INDEX } from "./api-catalog-qmd-index.generated";
|
|
5
5
|
import type { ApiSpecIndex } from "./api-spec-types";
|
|
6
|
+
import {
|
|
7
|
+
buildKnowledgeSearchPlan,
|
|
8
|
+
classifyKnowledgeRequest,
|
|
9
|
+
type KnowledgeClassifierResource,
|
|
10
|
+
type KnowledgeSearchPlan,
|
|
11
|
+
} from "./knowledge-classifier";
|
|
6
12
|
|
|
7
|
-
export interface ApiCatalogPreflightResource {
|
|
8
|
-
readonly name: string;
|
|
9
|
-
readonly aliases: readonly string[];
|
|
10
|
-
readonly domain: string;
|
|
11
|
-
readonly categories: readonly string[];
|
|
12
|
-
}
|
|
13
|
+
export interface ApiCatalogPreflightResource extends KnowledgeClassifierResource {}
|
|
13
14
|
|
|
14
15
|
export interface ApiCatalogPreflightIntent {
|
|
15
16
|
readonly resource: string;
|
|
16
17
|
readonly domain: string;
|
|
17
18
|
readonly queries: readonly string[];
|
|
19
|
+
readonly structuredQuery?: KnowledgeSearchPlan;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface ApiCatalogPreflightDestination {
|
|
@@ -45,16 +47,6 @@ interface RunApiCatalogPreflightOptions extends ApiCatalogPreflightOptions {
|
|
|
45
47
|
readonly rank?: (query: string) => Promise<readonly string[]>;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
const EXCLUDED_INTENT =
|
|
49
|
-
/\b(?:troubleshoot|debug|not working|returning\s+\d{3}|pricing|price|quote|licen[cs]ing|sales)\b/i;
|
|
50
|
-
const THIRD_PARTY_SCOPE = /\b(?:aws|amazon|azure|gcp|google cloud|kubernetes)\b/i;
|
|
51
|
-
const F5_XC_SCOPE = /\b(?:f5(?:\s+distributed\s+cloud)?|xc)\b/i;
|
|
52
|
-
const ANAPHORIC_RESOURCE = /\b(?:its|it|that|this)\b/i;
|
|
53
|
-
const DOCUMENTATION_ROUTE = /xcsh:\/\/documentation(?:\/|\?)/i;
|
|
54
|
-
const API_METADATA_INTENT =
|
|
55
|
-
/\b(?:endpoint|api\s+path|http\s+method|method|payload|request\s+body|required\s+fields?|enum|allowed\s+values?|constraints?|limits?|maximum|max(?:imum)?|minimum|min(?:imum)?|how\s+many|create|creates|creating|get|gets|list|lists|update|updates|replace|replaces|delete|deletes|clone|import)\b/i;
|
|
56
|
-
const EXPLICIT_API_INTENT = /\b(?:api|endpoint|method|payload|schema)\b/i;
|
|
57
|
-
|
|
58
50
|
let generatedResources: readonly ApiCatalogPreflightResource[] | undefined;
|
|
59
51
|
let generatedCatalogVersion: string | undefined;
|
|
60
52
|
|
|
@@ -67,10 +59,6 @@ function normalizedPhrase(value: string): string {
|
|
|
67
59
|
.replace(/\s+/g, " ");
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
function preferredAlias(resource: { name: string; aliases: readonly string[] }): string {
|
|
71
|
-
return normalizedPhrase(resource.aliases[0] ?? resource.name);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
62
|
function defaultAliases(name: string, descriptionShort?: string): string[] {
|
|
75
63
|
const canonical = normalizedPhrase(name);
|
|
76
64
|
const aliases = [normalizedPhrase(descriptionShort ?? ""), canonical].filter(Boolean);
|
|
@@ -97,56 +85,40 @@ function loadGeneratedMetadata(): { resources: readonly ApiCatalogPreflightResou
|
|
|
97
85
|
return { resources: generatedResources, catalogVersion: generatedCatalogVersion };
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
function crudVerb(prompt: string): string | undefined {
|
|
101
|
-
const value = normalizedPhrase(prompt);
|
|
102
|
-
if (/\b(?:create|creates|creating)\b/.test(value)) return "create";
|
|
103
|
-
if (/\b(?:get|gets|read|fetch)\b/.test(value)) return "get";
|
|
104
|
-
if (/\b(?:list|lists)\b/.test(value)) return "list";
|
|
105
|
-
if (/\b(?:update|updates)\b/.test(value)) return "update";
|
|
106
|
-
if (/\b(?:replace|replaces)\b/.test(value)) return "replace";
|
|
107
|
-
if (/\b(?:delete|deletes|remove)\b/.test(value)) return "delete";
|
|
108
|
-
if (/\bclone\b/.test(value)) return "clone";
|
|
109
|
-
if (/\bimport\b/.test(value)) return "import";
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
88
|
export function classifyApiCatalogPreflight(
|
|
114
89
|
prompt: string,
|
|
115
90
|
options: ApiCatalogPreflightOptions,
|
|
116
91
|
): ApiCatalogPreflightIntent | null {
|
|
117
|
-
if (!options.toolsEnabled || DOCUMENTATION_ROUTE.test(prompt) || EXCLUDED_INTENT.test(prompt)) return null;
|
|
118
|
-
if (THIRD_PARTY_SCOPE.test(prompt) && !F5_XC_SCOPE.test(prompt)) return null;
|
|
119
|
-
if (/\bterraform\b/i.test(prompt) && !EXPLICIT_API_INTENT.test(prompt)) return null;
|
|
120
|
-
if (!API_METADATA_INTENT.test(prompt)) return null;
|
|
121
|
-
|
|
122
92
|
const resources = options.resources ?? loadGeneratedMetadata().resources;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
140
|
-
|
|
141
|
-
|
|
93
|
+
const previous = options.previousResource
|
|
94
|
+
? {
|
|
95
|
+
route: "api" as const,
|
|
96
|
+
confidence: 0.99,
|
|
97
|
+
resource: options.previousResource.resource,
|
|
98
|
+
domain: options.previousResource.domain,
|
|
99
|
+
constraints: { domain: options.previousResource.domain },
|
|
100
|
+
query:
|
|
101
|
+
options.previousResource.structuredQuery ??
|
|
102
|
+
buildKnowledgeSearchPlan(
|
|
103
|
+
"api",
|
|
104
|
+
options.previousResource.queries[0] ?? options.previousResource.resource,
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
const classified = classifyKnowledgeRequest(prompt, { toolsEnabled: options.toolsEnabled, resources, previous });
|
|
109
|
+
if (classified.route !== "api" || !classified.resource || !classified.domain) return null;
|
|
110
|
+
if (
|
|
111
|
+
options.previousResource &&
|
|
112
|
+
classified.resource === options.previousResource.resource &&
|
|
113
|
+
/\b(?:its|it|that|this|those|them|the same)\b/i.test(prompt)
|
|
114
|
+
) {
|
|
115
|
+
return options.previousResource;
|
|
142
116
|
}
|
|
143
|
-
|
|
144
|
-
const alias = preferredAlias(selected);
|
|
145
|
-
const verb = crudVerb(prompt);
|
|
146
117
|
return {
|
|
147
|
-
resource:
|
|
148
|
-
domain:
|
|
149
|
-
queries:
|
|
118
|
+
resource: classified.resource,
|
|
119
|
+
domain: classified.domain,
|
|
120
|
+
queries: classified.query.lex,
|
|
121
|
+
structuredQuery: classified.query,
|
|
150
122
|
};
|
|
151
123
|
}
|
|
152
124
|
|