@aisa-one/cli 0.2.4 → 0.5.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/CHANGELOG.md +172 -1
- package/LICENSE +21 -0
- package/README.md +163 -169
- package/dist/api.d.ts +10 -0
- package/dist/api.js +14 -3
- package/dist/catalog.d.ts +1 -18
- package/dist/catalog.js +1 -24
- package/dist/cli-error.d.ts +17 -0
- package/dist/cli-error.js +25 -0
- package/dist/cli-guidance.d.ts +14 -0
- package/dist/cli-guidance.js +55 -0
- package/dist/commands/account.d.ts +17 -0
- package/dist/commands/account.js +45 -1
- package/dist/commands/api.d.ts +0 -11
- package/dist/commands/api.js +6 -175
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +43 -9
- package/dist/commands/brand-logos.d.ts +6 -0
- package/dist/commands/brand-logos.js +21 -0
- package/dist/commands/configCmd.js +8 -3
- package/dist/commands/connect-shared.d.ts +136 -0
- package/dist/commands/connect-shared.js +143 -0
- package/dist/commands/connect-t2.d.ts +18 -0
- package/dist/commands/connect-t2.js +1599 -0
- package/dist/commands/connect-terminal.d.ts +65 -0
- package/dist/commands/connect-terminal.js +822 -0
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.js +3181 -0
- package/dist/commands/flow.d.ts +685 -0
- package/dist/commands/flow.js +697 -0
- package/dist/commands/install.d.ts +91 -0
- package/dist/commands/install.js +258 -0
- package/dist/commands/llm-config.d.ts +103 -0
- package/dist/commands/llm-config.js +452 -0
- package/dist/commands/manifest.d.ts +68 -0
- package/dist/commands/manifest.js +90 -0
- package/dist/commands/mcp.d.ts +79 -2
- package/dist/commands/mcp.js +262 -48
- package/dist/commands/oauth-login.d.ts +33 -0
- package/dist/commands/oauth-login.js +333 -0
- package/dist/commands/prompt.d.ts +96 -0
- package/dist/commands/prompt.js +397 -0
- package/dist/commands/serve-results.d.ts +61 -0
- package/dist/commands/serve-results.js +245 -0
- package/dist/commands/serve-signin.d.ts +28 -0
- package/dist/commands/serve-signin.js +90 -0
- package/dist/commands/signin-page.d.ts +49 -0
- package/dist/commands/signin-page.js +250 -0
- package/dist/commands/skills.js +87 -50
- package/dist/commands/tool-help.d.ts +69 -0
- package/dist/commands/tool-help.js +189 -0
- package/dist/commands/tool-input.d.ts +22 -0
- package/dist/commands/tool-input.js +220 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.js +307 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +92 -0
- package/dist/commands/vscode.d.ts +65 -0
- package/dist/commands/vscode.js +173 -0
- package/dist/commands/wrappers.d.ts +23 -0
- package/dist/commands/wrappers.js +102 -0
- package/dist/completion.js +3 -80
- package/dist/config.js +76 -2
- package/dist/constants.d.ts +78 -38
- package/dist/constants.js +80 -102
- package/dist/index.js +155 -357
- package/dist/json-text.d.ts +12 -0
- package/dist/json-text.js +60 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +59 -0
- package/dist/skills-registry.js +12 -3
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/browser.d.ts +19 -0
- package/dist/utils/browser.js +31 -0
- package/dist/utils/exec.d.ts +43 -0
- package/dist/utils/exec.js +61 -0
- package/dist/utils/http.d.ts +53 -0
- package/dist/utils/http.js +124 -0
- package/dist/utils/install-method.d.ts +36 -0
- package/dist/utils/install-method.js +48 -0
- package/dist/utils/journal.d.ts +57 -0
- package/dist/utils/journal.js +188 -0
- package/dist/utils/key-check.d.ts +20 -0
- package/dist/utils/key-check.js +24 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +116 -0
- package/dist/utils/width.d.ts +32 -0
- package/dist/utils/width.js +73 -0
- package/package.json +8 -4
- package/vscode-extension/README.md +6 -0
- package/vscode-extension/aisa-vscode.vsix +0 -0
- package/dist/commands/finance.d.ts +0 -17
- package/dist/commands/finance.js +0 -342
- package/dist/commands/run.d.ts +0 -28
- package/dist/commands/run.js +0 -182
- package/dist/commands/search.d.ts +0 -9
- package/dist/commands/search.js +0 -81
- package/dist/commands/twitter.d.ts +0 -138
- package/dist/commands/twitter.js +0 -374
- package/dist/commands/video.d.ts +0 -49
- package/dist/commands/video.js +0 -311
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { usageError } from "../cli-error.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve one request body from operands, --input, or -f.
|
|
5
|
+
* Conflicting sources are a usage error and must not dispatch.
|
|
6
|
+
*/
|
|
7
|
+
export async function prepareRouterRequest(kind, operands, options) {
|
|
8
|
+
const hasInput = options.input !== undefined;
|
|
9
|
+
const hasFile = options.file !== undefined;
|
|
10
|
+
const hasOperands = operands.length > 0;
|
|
11
|
+
if (hasInput && hasFile) {
|
|
12
|
+
throw usageError("Use either --input or -f/--file, not both");
|
|
13
|
+
}
|
|
14
|
+
if ((hasInput || hasFile) && hasOperands) {
|
|
15
|
+
throw usageError("Do not combine positional arguments with --input or -f/--file");
|
|
16
|
+
}
|
|
17
|
+
if ((hasInput || hasFile) && hasSupplementalFlags(kind, options)) {
|
|
18
|
+
throw usageError("Do not combine --input/-f with field flags; put every field in the JSON request");
|
|
19
|
+
}
|
|
20
|
+
if (hasInput) {
|
|
21
|
+
return fromEnvelope(kind, options.input, " --input");
|
|
22
|
+
}
|
|
23
|
+
if (hasFile) {
|
|
24
|
+
return fromEnvelope(kind, await readRequestFile(options.file), ` -f ${options.file}`);
|
|
25
|
+
}
|
|
26
|
+
if (kind === "search") {
|
|
27
|
+
return buildSearch(operands, options);
|
|
28
|
+
}
|
|
29
|
+
if (kind === "schema") {
|
|
30
|
+
return buildSchema(operands, options);
|
|
31
|
+
}
|
|
32
|
+
throw usageError(`${kind} requires --input '<json>' or -f FILE (use -f - for stdin)`);
|
|
33
|
+
}
|
|
34
|
+
async function readRequestFile(path) {
|
|
35
|
+
if (path === "-") {
|
|
36
|
+
if (process.stdin.isTTY) {
|
|
37
|
+
throw usageError("No JSON on stdin. Pipe a request or pass -f FILE");
|
|
38
|
+
}
|
|
39
|
+
const chunks = [];
|
|
40
|
+
for await (const chunk of process.stdin) {
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
}
|
|
43
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(path, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw usageError(`Cannot read ${path}: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function fromEnvelope(kind, raw, source) {
|
|
53
|
+
const text = raw.trim();
|
|
54
|
+
if (!text) {
|
|
55
|
+
throw usageError(`Empty JSON request from${source}`);
|
|
56
|
+
}
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw usageError(`Invalid JSON from${source}`);
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(value)) {
|
|
65
|
+
throw usageError(`Request from${source} must be a JSON object`);
|
|
66
|
+
}
|
|
67
|
+
validateEnvelope(kind, value);
|
|
68
|
+
// Send the original text so numeric tokens in arguments are not rewritten.
|
|
69
|
+
return { body: text, value };
|
|
70
|
+
}
|
|
71
|
+
function buildSearch(operands, options) {
|
|
72
|
+
if (operands.length !== 1 || !operands[0].trim()) {
|
|
73
|
+
throw usageError("search requires a query, --input '<json>', or -f FILE");
|
|
74
|
+
}
|
|
75
|
+
const request = { query: operands[0] };
|
|
76
|
+
if (options.limit !== undefined) {
|
|
77
|
+
request.limit = parseLimit(options.limit);
|
|
78
|
+
}
|
|
79
|
+
if (options.provider && options.provider.length > 0) {
|
|
80
|
+
request.provider_filters = options.provider;
|
|
81
|
+
}
|
|
82
|
+
validateEnvelope("search", request);
|
|
83
|
+
return { body: JSON.stringify(request), value: request };
|
|
84
|
+
}
|
|
85
|
+
function buildSchema(operands, options) {
|
|
86
|
+
const tools = operands.map((t) => t.trim()).filter(Boolean);
|
|
87
|
+
if (tools.length === 0) {
|
|
88
|
+
throw usageError("schema requires one or more tool names, --input '<json>', or -f FILE");
|
|
89
|
+
}
|
|
90
|
+
const request = { tools };
|
|
91
|
+
// Only send include_* when the caller set them so server defaults remain.
|
|
92
|
+
if (options.includeArgumentsSchema === false) {
|
|
93
|
+
request.include_arguments_schema = false;
|
|
94
|
+
}
|
|
95
|
+
if (options.includeResponseSchema === true) {
|
|
96
|
+
request.include_response_schema = true;
|
|
97
|
+
}
|
|
98
|
+
if (options.includeArgumentsSchema === false && options.includeResponseSchema !== true) {
|
|
99
|
+
throw usageError("At least one of arguments or response schema must be requested");
|
|
100
|
+
}
|
|
101
|
+
validateEnvelope("schema", request);
|
|
102
|
+
return { body: JSON.stringify(request), value: request };
|
|
103
|
+
}
|
|
104
|
+
function parseLimit(raw) {
|
|
105
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
106
|
+
throw usageError("limit must be an integer from 1 to 20");
|
|
107
|
+
}
|
|
108
|
+
const n = Number(raw);
|
|
109
|
+
if (n < 1 || n > 20) {
|
|
110
|
+
throw usageError("limit must be an integer from 1 to 20");
|
|
111
|
+
}
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
const SEARCH_KEYS = ["query", "known_fields", "provider_filters", "limit"];
|
|
115
|
+
const SCHEMA_KEYS = ["tools", "include_arguments_schema", "include_response_schema"];
|
|
116
|
+
const BATCH_KEYS = ["search_id", "calls"];
|
|
117
|
+
const CALL_KEYS = ["call_id", "tool", "arguments"];
|
|
118
|
+
export function validateEnvelope(kind, value) {
|
|
119
|
+
if (kind === "search") {
|
|
120
|
+
rejectUnknownKeys(value, SEARCH_KEYS, "search request");
|
|
121
|
+
if (typeof value.query !== "string" || value.query.trim().length === 0) {
|
|
122
|
+
throw usageError("search request requires a non-empty query string");
|
|
123
|
+
}
|
|
124
|
+
if (value.query.length > 2000) {
|
|
125
|
+
throw usageError("query must be at most 2000 characters");
|
|
126
|
+
}
|
|
127
|
+
if (value.known_fields !== undefined && !isPlainObject(value.known_fields)) {
|
|
128
|
+
throw usageError("known_fields must be a JSON object");
|
|
129
|
+
}
|
|
130
|
+
if (value.provider_filters !== undefined) {
|
|
131
|
+
if (!isStringArray(value.provider_filters)) {
|
|
132
|
+
throw usageError("provider_filters must be an array of strings");
|
|
133
|
+
}
|
|
134
|
+
if (value.provider_filters.length > 50) {
|
|
135
|
+
throw usageError("provider_filters must have at most 50 entries");
|
|
136
|
+
}
|
|
137
|
+
if (new Set(value.provider_filters).size !== value.provider_filters.length) {
|
|
138
|
+
throw usageError("provider_filters must be unique");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (value.limit !== undefined) {
|
|
142
|
+
if (typeof value.limit !== "number" || !Number.isInteger(value.limit) || value.limit < 1 || value.limit > 20) {
|
|
143
|
+
throw usageError("limit must be an integer from 1 to 20");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (kind === "schema") {
|
|
149
|
+
rejectUnknownKeys(value, SCHEMA_KEYS, "schema request");
|
|
150
|
+
if (!isStringArray(value.tools) || value.tools.length < 1 || value.tools.length > 20) {
|
|
151
|
+
throw usageError("schema request requires tools: 1–20 non-empty strings");
|
|
152
|
+
}
|
|
153
|
+
if (new Set(value.tools).size !== value.tools.length) {
|
|
154
|
+
throw usageError("schema tools must be unique");
|
|
155
|
+
}
|
|
156
|
+
if (value.tools.some((t) => t.trim().length === 0 || /\s/.test(t[0]) || /\s/.test(t[t.length - 1]))) {
|
|
157
|
+
throw usageError("each tool name must be a non-empty string without surrounding whitespace");
|
|
158
|
+
}
|
|
159
|
+
const includeArgs = value.include_arguments_schema;
|
|
160
|
+
const includeResp = value.include_response_schema;
|
|
161
|
+
if (includeArgs !== undefined && typeof includeArgs !== "boolean") {
|
|
162
|
+
throw usageError("include_arguments_schema must be a boolean");
|
|
163
|
+
}
|
|
164
|
+
if (includeResp !== undefined && typeof includeResp !== "boolean") {
|
|
165
|
+
throw usageError("include_response_schema must be a boolean");
|
|
166
|
+
}
|
|
167
|
+
if (includeArgs === false && includeResp === false) {
|
|
168
|
+
throw usageError("At least one of arguments or response schema must be requested");
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
rejectUnknownKeys(value, BATCH_KEYS, "quote/call request");
|
|
173
|
+
if (!Array.isArray(value.calls) || value.calls.length < 1 || value.calls.length > 20) {
|
|
174
|
+
throw usageError("quote/call request requires calls: 1–20 items");
|
|
175
|
+
}
|
|
176
|
+
if (value.search_id !== undefined && typeof value.search_id !== "string") {
|
|
177
|
+
throw usageError("search_id must be a string");
|
|
178
|
+
}
|
|
179
|
+
const callIds = [];
|
|
180
|
+
for (const [i, call] of value.calls.entries()) {
|
|
181
|
+
if (!isPlainObject(call)) {
|
|
182
|
+
throw usageError(`calls[${i}] must be an object`);
|
|
183
|
+
}
|
|
184
|
+
rejectUnknownKeys(call, CALL_KEYS, `calls[${i}]`);
|
|
185
|
+
if (typeof call.call_id !== "string" || call.call_id.length < 1 || call.call_id.length > 128) {
|
|
186
|
+
throw usageError(`calls[${i}].call_id must be a string of 1–128 characters`);
|
|
187
|
+
}
|
|
188
|
+
if (typeof call.tool !== "string" || call.tool.length < 1) {
|
|
189
|
+
throw usageError(`calls[${i}].tool must be a non-empty string`);
|
|
190
|
+
}
|
|
191
|
+
if (!isPlainObject(call.arguments)) {
|
|
192
|
+
throw usageError(`calls[${i}].arguments must be a JSON object`);
|
|
193
|
+
}
|
|
194
|
+
callIds.push(call.call_id);
|
|
195
|
+
}
|
|
196
|
+
if (new Set(callIds).size !== callIds.length) {
|
|
197
|
+
throw usageError("call_id values must be unique within the batch");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
201
|
+
const extra = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
202
|
+
if (extra.length > 0) {
|
|
203
|
+
throw usageError(`${label} has unknown field(s): ${extra.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function hasSupplementalFlags(kind, options) {
|
|
207
|
+
if (kind === "search") {
|
|
208
|
+
return options.limit !== undefined || (options.provider !== undefined && options.provider.length > 0);
|
|
209
|
+
}
|
|
210
|
+
if (kind === "schema") {
|
|
211
|
+
return options.includeArgumentsSchema !== undefined || options.includeResponseSchema !== undefined;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function isPlainObject(value) {
|
|
216
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
217
|
+
}
|
|
218
|
+
function isStringArray(value) {
|
|
219
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
220
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RouterIoOptions } from "./tool-input.js";
|
|
2
|
+
export declare function searchAction(query: string | undefined, options: RouterIoOptions): Promise<void>;
|
|
3
|
+
export declare function schemaAction(tools: string[] | undefined, options: RouterIoOptions): Promise<void>;
|
|
4
|
+
export declare function quoteAction(options: RouterIoOptions): Promise<void>;
|
|
5
|
+
export declare function callAction(options: RouterIoOptions): Promise<void>;
|
|
6
|
+
/** Unmodified application body. No parse/stringify round trip. */
|
|
7
|
+
export declare function writeRawStdout(raw: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Partial batch: any failed item in a 200 envelope. Counts are small integers
|
|
10
|
+
* so a structural parse is safe; the raw body has already been written for --json.
|
|
11
|
+
*/
|
|
12
|
+
export declare function batchHasFailure(raw: string): boolean;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getApiKey } from "../config.js";
|
|
4
|
+
import { ENV_VAR_NAME } from "../constants.js";
|
|
5
|
+
import { CliError, EXIT_PARTIAL, EXIT_TRANSPORT, transportError } from "../cli-error.js";
|
|
6
|
+
import { routerPost } from "../router.js";
|
|
7
|
+
import { error as printError } from "../utils/display.js";
|
|
8
|
+
import { numberToken, parseJsonKeepingNumberTokens } from "../json-text.js";
|
|
9
|
+
import { projectMcpIdentifiersToCli } from "../cli-guidance.js";
|
|
10
|
+
import { prepareRouterRequest, } from "./tool-input.js";
|
|
11
|
+
export async function searchAction(query, options) {
|
|
12
|
+
const operands = query ? [query] : [];
|
|
13
|
+
await runToolCommand("search", operands, options, { auth: "optional" });
|
|
14
|
+
}
|
|
15
|
+
export async function schemaAction(tools, options) {
|
|
16
|
+
await runToolCommand("schema", tools ?? [], options, { auth: "optional" });
|
|
17
|
+
}
|
|
18
|
+
export async function quoteAction(options) {
|
|
19
|
+
await runToolCommand("quote", [], options, { auth: "required" });
|
|
20
|
+
}
|
|
21
|
+
export async function callAction(options) {
|
|
22
|
+
await runToolCommand("call", [], options, { auth: "required" });
|
|
23
|
+
}
|
|
24
|
+
async function runToolCommand(kind, operands, options, auth) {
|
|
25
|
+
const prepared = await prepareRouterRequest(kind, operands, options);
|
|
26
|
+
const apiKey = auth.auth === "required" ? requireRouterKey(kind) : getApiKey();
|
|
27
|
+
const spinner = options.json ? undefined : ora(spinnerText(kind)).start();
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
result = await routerPost({
|
|
31
|
+
operation: kind,
|
|
32
|
+
body: prepared.body,
|
|
33
|
+
apiKey,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
spinner?.fail("Request failed");
|
|
38
|
+
throw transportError(err.message);
|
|
39
|
+
}
|
|
40
|
+
spinner?.stop();
|
|
41
|
+
const applicationOk = result.status >= 200 && result.status < 300;
|
|
42
|
+
if (options.json) {
|
|
43
|
+
writeRawStdout(result.raw);
|
|
44
|
+
}
|
|
45
|
+
else if (applicationOk) {
|
|
46
|
+
renderHuman(kind, result.raw);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
printError(httpErrorMessage(result.status, result.raw));
|
|
50
|
+
if (result.raw.trim()) {
|
|
51
|
+
console.error(result.raw);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!applicationOk) {
|
|
55
|
+
throw new CliError("", EXIT_TRANSPORT);
|
|
56
|
+
}
|
|
57
|
+
if (batchHasFailure(result.raw)) {
|
|
58
|
+
process.exitCode = EXIT_PARTIAL;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function requireRouterKey(kind) {
|
|
62
|
+
const key = getApiKey();
|
|
63
|
+
if (!key) {
|
|
64
|
+
throw new CliError(`No API key found. Run "aisa login --key <key>" or set ${ENV_VAR_NAME}. ` +
|
|
65
|
+
`search and schema may be anonymous; ${kind} will not run without a key. ` +
|
|
66
|
+
`Do not invent a business result.`, EXIT_TRANSPORT);
|
|
67
|
+
}
|
|
68
|
+
return key;
|
|
69
|
+
}
|
|
70
|
+
function spinnerText(kind) {
|
|
71
|
+
switch (kind) {
|
|
72
|
+
case "search":
|
|
73
|
+
return "Searching published tools...";
|
|
74
|
+
case "schema":
|
|
75
|
+
return "Fetching tool schemas...";
|
|
76
|
+
case "quote":
|
|
77
|
+
return "Quoting published tools...";
|
|
78
|
+
case "call":
|
|
79
|
+
return "Calling published tools...";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Unmodified application body. No parse/stringify round trip. */
|
|
83
|
+
export function writeRawStdout(raw) {
|
|
84
|
+
const text = raw.endsWith("\n") ? raw : raw + "\n";
|
|
85
|
+
process.stdout.write(text);
|
|
86
|
+
}
|
|
87
|
+
function httpErrorMessage(status, raw) {
|
|
88
|
+
const code = rawFieldString(raw, "code");
|
|
89
|
+
const message = rawFieldString(raw, "message");
|
|
90
|
+
if (code && message)
|
|
91
|
+
return `${status}: ${code}: ${message}`;
|
|
92
|
+
if (message)
|
|
93
|
+
return `${status}: ${message}`;
|
|
94
|
+
return `${status}: Router request failed`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Partial batch: any failed item in a 200 envelope. Counts are small integers
|
|
98
|
+
* so a structural parse is safe; the raw body has already been written for --json.
|
|
99
|
+
*/
|
|
100
|
+
export function batchHasFailure(raw) {
|
|
101
|
+
try {
|
|
102
|
+
const value = JSON.parse(raw);
|
|
103
|
+
if (typeof value.error_count === "number" && value.error_count > 0)
|
|
104
|
+
return true;
|
|
105
|
+
if (Array.isArray(value.results)) {
|
|
106
|
+
return value.results.some((item) => isRecord(item) && item.successful === false);
|
|
107
|
+
}
|
|
108
|
+
if (isRecord(value.tools)) {
|
|
109
|
+
return Object.values(value.tools).some((item) => isRecord(item) && item.successful === false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function renderHuman(kind, raw) {
|
|
118
|
+
let value;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = parseJsonKeepingNumberTokens(raw);
|
|
121
|
+
if (!isRecord(parsed)) {
|
|
122
|
+
console.log(raw);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
value = parsed;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
console.log(raw);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (kind === "search") {
|
|
132
|
+
renderSearch(value);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (kind === "schema") {
|
|
136
|
+
renderSchema(value);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
renderBatch(kind, value);
|
|
140
|
+
}
|
|
141
|
+
function renderSearch(value) {
|
|
142
|
+
if (typeof value.search_id === "string") {
|
|
143
|
+
console.log(`\n ${chalk.gray("search_id")} ${value.search_id}`);
|
|
144
|
+
}
|
|
145
|
+
renderPlan(value.plan);
|
|
146
|
+
const tools = Array.isArray(value.tools) ? value.tools : [];
|
|
147
|
+
if (tools.length === 0) {
|
|
148
|
+
console.log(`\n ${chalk.yellow("No executable tools returned.")}`);
|
|
149
|
+
}
|
|
150
|
+
for (const item of tools) {
|
|
151
|
+
if (!isRecord(item))
|
|
152
|
+
continue;
|
|
153
|
+
const name = typeof item.tool === "string" ? item.tool : "(unknown tool)";
|
|
154
|
+
const summary = typeof item.summary === "string" ? item.summary : "";
|
|
155
|
+
const full = item.has_full_schema === true;
|
|
156
|
+
console.log(`\n ${chalk.cyan.bold(name)} ${chalk.gray(full ? "full schema" : "schema incomplete")}`);
|
|
157
|
+
if (summary)
|
|
158
|
+
console.log(` ${summary}`);
|
|
159
|
+
if (item.has_full_schema === false) {
|
|
160
|
+
console.log(chalk.gray(" Next: aisa schema " + name));
|
|
161
|
+
}
|
|
162
|
+
if (isRecord(item.price)) {
|
|
163
|
+
console.log(chalk.gray(" Price is catalog metadata — not an exact charge. Use --json for tokens."));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
printGuidance(value.next_steps_guidance);
|
|
167
|
+
console.log();
|
|
168
|
+
}
|
|
169
|
+
function renderSchema(value) {
|
|
170
|
+
const tools = isRecord(value.tools) ? value.tools : {};
|
|
171
|
+
console.log();
|
|
172
|
+
for (const [name, item] of Object.entries(tools)) {
|
|
173
|
+
if (!isRecord(item))
|
|
174
|
+
continue;
|
|
175
|
+
if (item.successful === false) {
|
|
176
|
+
const err = isRecord(item.error) ? item.error : {};
|
|
177
|
+
const code = typeof err.code === "string" ? err.code : "error";
|
|
178
|
+
const message = typeof err.message === "string" ? err.message : "lookup failed";
|
|
179
|
+
console.log(` ${chalk.red.bold(name)} failed (${code})`);
|
|
180
|
+
console.log(` ${message}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
console.log(` ${chalk.cyan.bold(name)} ${chalk.green("ok")}`);
|
|
184
|
+
if (item.arguments_schema !== undefined) {
|
|
185
|
+
console.log(chalk.gray(" arguments_schema present — use --json for the contract"));
|
|
186
|
+
}
|
|
187
|
+
if (item.response_schema !== undefined) {
|
|
188
|
+
console.log(chalk.gray(" response_schema present — use --json for the contract"));
|
|
189
|
+
}
|
|
190
|
+
printStringList(" pitfalls", item.known_pitfalls);
|
|
191
|
+
}
|
|
192
|
+
printCounts(value);
|
|
193
|
+
printGuidance(value.next_steps_guidance);
|
|
194
|
+
console.log();
|
|
195
|
+
}
|
|
196
|
+
function renderPlan(plan) {
|
|
197
|
+
if (!isRecord(plan))
|
|
198
|
+
return;
|
|
199
|
+
if (typeof plan.plan_ref === "string") {
|
|
200
|
+
console.log(`\n ${chalk.bold("Plan")} ${plan.plan_ref}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(`\n ${chalk.bold("Plan")}`);
|
|
204
|
+
}
|
|
205
|
+
printStringList(" step", plan.recommended_steps);
|
|
206
|
+
printStringList(" pitfall", plan.known_pitfalls);
|
|
207
|
+
}
|
|
208
|
+
function renderBatch(kind, value) {
|
|
209
|
+
if (typeof value.batch_id === "string") {
|
|
210
|
+
console.log(`\n ${chalk.gray("batch_id")} ${value.batch_id}`);
|
|
211
|
+
}
|
|
212
|
+
const results = Array.isArray(value.results) ? value.results : [];
|
|
213
|
+
for (const item of results) {
|
|
214
|
+
if (!isRecord(item))
|
|
215
|
+
continue;
|
|
216
|
+
const callId = typeof item.call_id === "string" ? item.call_id : "?";
|
|
217
|
+
const tool = typeof item.tool === "string" ? item.tool : "?";
|
|
218
|
+
const requestId = typeof item.request_id === "string" ? item.request_id : "";
|
|
219
|
+
if (item.successful === false) {
|
|
220
|
+
const err = isRecord(item.error) ? item.error : {};
|
|
221
|
+
const type = typeof err.type === "string" ? err.type : "error";
|
|
222
|
+
const message = typeof err.message === "string" ? err.message : "call failed";
|
|
223
|
+
console.log(`\n ${chalk.red.bold(callId)} ${tool} failed (${type})`);
|
|
224
|
+
console.log(` ${message}`);
|
|
225
|
+
if (requestId)
|
|
226
|
+
console.log(chalk.gray(` request_id ${requestId}`));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
console.log(`\n ${chalk.green.bold(callId)} ${tool}`);
|
|
230
|
+
if (requestId)
|
|
231
|
+
console.log(chalk.gray(` request_id ${requestId}`));
|
|
232
|
+
if (kind === "quote") {
|
|
233
|
+
renderQuoteData(item.data);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const charged = numberToken(item.customer_cost_micros_usd);
|
|
237
|
+
if (charged) {
|
|
238
|
+
console.log(` customer_cost_micros_usd ${charged} (as reported; not converted to dollars)`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(chalk.gray(" customer cost not reported — not treated as zero"));
|
|
242
|
+
}
|
|
243
|
+
console.log(chalk.gray(" result data: use --json for the unmodified payload"));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
printCounts(value);
|
|
247
|
+
printGuidance(value.next_steps_guidance);
|
|
248
|
+
if (kind === "quote") {
|
|
249
|
+
console.log(chalk.gray("\n Quote is a price observation, not authorization to execute."));
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
function renderQuoteData(data) {
|
|
254
|
+
if (!isRecord(data)) {
|
|
255
|
+
console.log(chalk.gray(" quote data missing — not treated as zero"));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const kind = typeof data.estimate_kind === "string" ? data.estimate_kind : "unknown";
|
|
259
|
+
const mayExceed = data.may_exceed_estimate === true;
|
|
260
|
+
const estimated = numberToken(data.estimated_cost_micros_usd);
|
|
261
|
+
const max = numberToken(data.max_cost_micros_usd);
|
|
262
|
+
console.log(` estimate_kind ${kind}${mayExceed ? " (may exceed estimate)" : ""}`);
|
|
263
|
+
if (estimated) {
|
|
264
|
+
console.log(` estimated_cost_micros_usd ${estimated} (as reported; not an exact dollar price)`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
console.log(chalk.gray(" estimated cost not reported — not treated as zero"));
|
|
268
|
+
}
|
|
269
|
+
if (max) {
|
|
270
|
+
console.log(` max_cost_micros_usd ${max} (ceiling token, not converted)`);
|
|
271
|
+
}
|
|
272
|
+
if (kind === "estimate" || mayExceed) {
|
|
273
|
+
console.log(chalk.gray(" This figure may be exceeded; do not treat it as a firm price."));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function printCounts(value) {
|
|
277
|
+
const total = numberToken(value.total_count);
|
|
278
|
+
const ok = numberToken(value.success_count);
|
|
279
|
+
const err = numberToken(value.error_count);
|
|
280
|
+
if (total !== undefined || ok !== undefined || err !== undefined) {
|
|
281
|
+
console.log(`\n ${ok ?? "?"} ok · ${err ?? "?"} failed · ${total ?? "?"} total`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function printGuidance(guidance) {
|
|
285
|
+
if (!Array.isArray(guidance) || guidance.length === 0)
|
|
286
|
+
return;
|
|
287
|
+
console.log(chalk.bold(`\n Next steps`));
|
|
288
|
+
for (const line of guidance) {
|
|
289
|
+
if (typeof line === "string")
|
|
290
|
+
console.log(` ${projectMcpIdentifiersToCli(line)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function printStringList(label, value) {
|
|
294
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
295
|
+
return;
|
|
296
|
+
for (const line of value) {
|
|
297
|
+
if (typeof line === "string")
|
|
298
|
+
console.log(`${label}: ${projectMcpIdentifiersToCli(line)}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function rawFieldString(raw, key) {
|
|
302
|
+
const match = raw.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`));
|
|
303
|
+
return match?.[1];
|
|
304
|
+
}
|
|
305
|
+
function isRecord(value) {
|
|
306
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
307
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update this copy of the CLI, whichever way it was installed.
|
|
3
|
+
*
|
|
4
|
+
* No confirmation prompt. Typing `aisa update` is the intent — asking "are
|
|
5
|
+
* you sure" after it is a speed bump, not a safeguard. What does get asked is
|
|
6
|
+
* the question the user cannot answer from here: when the install cannot be
|
|
7
|
+
* changed by this process (no write access, or a package manager whose global
|
|
8
|
+
* install this process should not guess at), the command to run is printed
|
|
9
|
+
* rather than attempted.
|
|
10
|
+
*/
|
|
11
|
+
export declare function updateAction(): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { VERSION } from "../constants.js";
|
|
2
|
+
import { success, error, info, hint } from "../utils/display.js";
|
|
3
|
+
import { runShell, runSync, QUICK_TIMEOUT_MS } from "../utils/exec.js";
|
|
4
|
+
import { pickNpmChannel, npmPrefixWritable } from "./install.js";
|
|
5
|
+
import { detectInstall } from "../utils/install-method.js";
|
|
6
|
+
import { checkForUpdate } from "../utils/update-check.js";
|
|
7
|
+
const PACKAGE = "@aisa-one/cli";
|
|
8
|
+
/** The version actually on PATH — may differ from the VERSION this process
|
|
9
|
+
* was built with if another copy shadows it, so ask the binary, not the build. */
|
|
10
|
+
function installedVersion() {
|
|
11
|
+
const r = runSync("aisa", ["--version"], { timeout: QUICK_TIMEOUT_MS });
|
|
12
|
+
return r.status === 0 ? r.stdout.trim() : VERSION;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Update this copy of the CLI, whichever way it was installed.
|
|
16
|
+
*
|
|
17
|
+
* No confirmation prompt. Typing `aisa update` is the intent — asking "are
|
|
18
|
+
* you sure" after it is a speed bump, not a safeguard. What does get asked is
|
|
19
|
+
* the question the user cannot answer from here: when the install cannot be
|
|
20
|
+
* changed by this process (no write access, or a package manager whose global
|
|
21
|
+
* install this process should not guess at), the command to run is printed
|
|
22
|
+
* rather than attempted.
|
|
23
|
+
*/
|
|
24
|
+
export async function updateAction() {
|
|
25
|
+
const before = installedVersion();
|
|
26
|
+
const install = detectInstall();
|
|
27
|
+
// npx resolves the published version on every invocation, so there is
|
|
28
|
+
// nothing here to update — and installing a global would leave the user
|
|
29
|
+
// with a second copy they did not ask for and did not know about.
|
|
30
|
+
if (install.kind === "npx") {
|
|
31
|
+
info(`Current version: ${before}`);
|
|
32
|
+
success("Nothing to update — npx fetches the published version every time");
|
|
33
|
+
hint(`for a copy that stays put: npm install -g ${PACKAGE}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// A checkout run from its own dist. Updating it means pulling, and this
|
|
37
|
+
// process has no business guessing at someone's working tree.
|
|
38
|
+
if (install.kind === "source") {
|
|
39
|
+
info(`Current version: ${before}`);
|
|
40
|
+
error("This is a working copy, not an installed package");
|
|
41
|
+
hint(`running from ${install.path} — update it with git`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const latest = await checkForUpdate({ current: before });
|
|
46
|
+
info(`Current version: ${before}${latest ? ` — ${latest} is available` : ""}`);
|
|
47
|
+
if (!install.command) {
|
|
48
|
+
error("Could not tell how this copy was installed");
|
|
49
|
+
hint(`running from ${install.path}`);
|
|
50
|
+
hint(`if you used npm: npm install -g ${PACKAGE}@latest`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// The registry probe and the writability check are npm's business; the
|
|
55
|
+
// other managers have their own resolution and their own global roots, and
|
|
56
|
+
// a guess about either is worse than letting the manager speak for itself.
|
|
57
|
+
let command = install.command;
|
|
58
|
+
if (install.kind === "npm") {
|
|
59
|
+
const channel = await pickNpmChannel(PACKAGE);
|
|
60
|
+
if (channel.kind === "offline") {
|
|
61
|
+
error("Could not reach the npm registry (official or mirror)");
|
|
62
|
+
hint("check your network and try again");
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (channel.kind === "mirror")
|
|
67
|
+
command += ` --registry=${channel.registry}`;
|
|
68
|
+
if (!npmPrefixWritable()) {
|
|
69
|
+
error("npm's global prefix is not writable by this user");
|
|
70
|
+
hint(`run this yourself: ${command}`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
info(command);
|
|
76
|
+
const result = await runShell(command);
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
error("Update failed");
|
|
79
|
+
if (result.detail)
|
|
80
|
+
hint(result.detail);
|
|
81
|
+
hint(`you can also run it yourself: ${command}`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const after = installedVersion();
|
|
86
|
+
if (after === before) {
|
|
87
|
+
success(`Already on the latest version (${after})`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
success(`Updated ${before} → ${after}`);
|
|
91
|
+
}
|
|
92
|
+
}
|