@aisa-one/cli 0.3.0 → 0.5.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 +165 -1
- package/LICENSE +21 -0
- package/README.md +164 -170
- 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 +45 -11
- 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 +59 -0
- package/dist/commands/mcp.js +122 -44
- 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 +306 -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.d.ts +3 -0
- package/dist/config.js +80 -3
- package/dist/constants.d.ts +51 -38
- package/dist/constants.js +53 -109
- package/dist/index.js +152 -355
- 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,48 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
const PACKAGE = "@aisa-one/cli";
|
|
3
|
+
/**
|
|
4
|
+
* Read the verdict off a path.
|
|
5
|
+
*
|
|
6
|
+
* Separate from the caller so it can be tested against the shapes each
|
|
7
|
+
* manager produces without installing five package managers.
|
|
8
|
+
*/
|
|
9
|
+
export function classifyPath(p) {
|
|
10
|
+
const s = p.replace(/\\/g, "/");
|
|
11
|
+
// npx first: its cache sits under the npm cache directory, so a test for
|
|
12
|
+
// node_modules alone would call it an npm install.
|
|
13
|
+
if (/\/_npx\//.test(s))
|
|
14
|
+
return "npx";
|
|
15
|
+
if (/\/\.bun\//.test(s))
|
|
16
|
+
return "bun";
|
|
17
|
+
// pnpm's global root, and its content-addressed store — a global install
|
|
18
|
+
// links into node_modules/.pnpm rather than copying.
|
|
19
|
+
if (/\/pnpm\/|\/\.pnpm\//.test(s))
|
|
20
|
+
return "pnpm";
|
|
21
|
+
if (/\/\.yarn\/|\/yarn\/(global|link)\//.test(s))
|
|
22
|
+
return "yarn";
|
|
23
|
+
// Anything still inside a node_modules for this package is a plain install;
|
|
24
|
+
// outside one, it is a checkout being run from its own dist.
|
|
25
|
+
if (s.includes(`/node_modules/${PACKAGE}/`))
|
|
26
|
+
return "npm";
|
|
27
|
+
if (/\/node_modules\//.test(s))
|
|
28
|
+
return "npm";
|
|
29
|
+
return "source";
|
|
30
|
+
}
|
|
31
|
+
const COMMAND = {
|
|
32
|
+
npm: `npm install -g ${PACKAGE}@latest`,
|
|
33
|
+
pnpm: `pnpm add -g ${PACKAGE}@latest`,
|
|
34
|
+
bun: `bun add -g ${PACKAGE}@latest`,
|
|
35
|
+
yarn: `yarn global add ${PACKAGE}@latest`,
|
|
36
|
+
};
|
|
37
|
+
/** Where this process is running from, and what would update it. */
|
|
38
|
+
export function detectInstall(from = import.meta.url) {
|
|
39
|
+
let path;
|
|
40
|
+
try {
|
|
41
|
+
path = fileURLToPath(from);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
path = from;
|
|
45
|
+
}
|
|
46
|
+
const kind = classifyPath(path);
|
|
47
|
+
return { kind, path, command: COMMAND[kind] };
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type Mark = "step" | "ok" | "warn" | "fail" | "info" | "write" | "cmd" | "choice";
|
|
2
|
+
export declare class Journal {
|
|
3
|
+
private readonly quiet;
|
|
4
|
+
private file;
|
|
5
|
+
constructor(quiet?: boolean);
|
|
6
|
+
/** Keep the newest few runs; a config log is small but not eternal. */
|
|
7
|
+
private prune;
|
|
8
|
+
get path(): string | null;
|
|
9
|
+
/** A section heading — the phases of a run, in the order they happen. */
|
|
10
|
+
section(title: string): void;
|
|
11
|
+
line(mark: Mark, text: string, detail?: string): void;
|
|
12
|
+
/** An indented continuation under the previous line. */
|
|
13
|
+
note(text: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Detail belonging to the line above it, on screen and in the log.
|
|
16
|
+
*
|
|
17
|
+
* Deeper than note(): a note continues its line at the same level, while
|
|
18
|
+
* this is subordinate to it — a client's own output, quoted under our
|
|
19
|
+
* summary of what it did. Level with the parent's text it read as the next
|
|
20
|
+
* item in the list, which is exactly how an inherited stdout looked before
|
|
21
|
+
* it was captured.
|
|
22
|
+
*/
|
|
23
|
+
sub(text: string): void;
|
|
24
|
+
/** A command the user can copy and run later. */
|
|
25
|
+
command(cmd: string, why?: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* A ruled line on its own, for a moment that deserves separating from
|
|
28
|
+
* whatever an agent session just left on the screen.
|
|
29
|
+
*/
|
|
30
|
+
rule(): void;
|
|
31
|
+
/**
|
|
32
|
+
* The last thing a run says. Everything above is what happened; these are
|
|
33
|
+
* the commands worth remembering, so they get a rule above them, an arrow
|
|
34
|
+
* each and the brightest ink on the screen — a reader who scrolled away
|
|
35
|
+
* and came back should land on them. One entry is the common case; a
|
|
36
|
+
* fresh install or a new wrapper command earns a second line above the
|
|
37
|
+
* standing "run this again" one, most useful first.
|
|
38
|
+
*
|
|
39
|
+
* A blank line closes it, so the shell prompt that follows is not stuck to
|
|
40
|
+
* the last thing we asked the reader to remember.
|
|
41
|
+
*/
|
|
42
|
+
encore(items: Array<{
|
|
43
|
+
cmd: string;
|
|
44
|
+
why: string;
|
|
45
|
+
}>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Log-only: detail worth having in a bug report, noise in a terminal.
|
|
48
|
+
*
|
|
49
|
+
* Indented past the line above rather than level with it. At three spaces
|
|
50
|
+
* it started in the same column as its parent's text, which reads as the
|
|
51
|
+
* next item in a list rather than as detail belonging to the one before —
|
|
52
|
+
* and once it is properly indented the bullet has nothing left to do.
|
|
53
|
+
*/
|
|
54
|
+
record(text: string): void;
|
|
55
|
+
private emit;
|
|
56
|
+
private write;
|
|
57
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { cells } from "./width.js";
|
|
6
|
+
/**
|
|
7
|
+
* The console transcript of a connect run, and its log file.
|
|
8
|
+
*
|
|
9
|
+
* Two audiences, one stream. In the terminal a user should be able to read
|
|
10
|
+
* back what they chose in the browser, what was checked, what was written
|
|
11
|
+
* where, and which commands they now have — the page is transient, the
|
|
12
|
+
* scrollback is not, and someone who reads it has learned how to do the same
|
|
13
|
+
* thing by hand next time. On disk the same lines (without colour) go to
|
|
14
|
+
* ~/.aisa/logs/connect-<stamp>.log, so a user hitting trouble can send one
|
|
15
|
+
* file that says exactly what happened. Configuration runs are rare, so the
|
|
16
|
+
* logs stay small; the last few are kept and older ones dropped.
|
|
17
|
+
*/
|
|
18
|
+
const KEEP_LOGS = 20;
|
|
19
|
+
/**
|
|
20
|
+
* One column of structure on the left, one glyph of outcome on the right.
|
|
21
|
+
*
|
|
22
|
+
* Every reporting line used to open with its own status emoji — ✅ ⚠️ ❌ ℹ️ —
|
|
23
|
+
* four different shapes in the first column, which is the column the eye uses
|
|
24
|
+
* to find the shape of the section. The result was that the *structure* was
|
|
25
|
+
* unreadable: you could not see at a glance that five lines are one list,
|
|
26
|
+
* because no two of them started the same way.
|
|
27
|
+
*
|
|
28
|
+
* So the left column says "here is a line" and nothing else, and the outcome
|
|
29
|
+
* moves to the end where it belongs — you read what happened, then whether it
|
|
30
|
+
* worked. Lines with no outcome to report simply end.
|
|
31
|
+
*
|
|
32
|
+
* A plain bullet, the same one "What you chose" already used. An arrow was
|
|
33
|
+
* tried here first and read as clutter: it points, and there is nothing for
|
|
34
|
+
* it to point at when every line in the log starts with one.
|
|
35
|
+
*
|
|
36
|
+
* `step` keeps its own mark: a heading is not a result, and giving it the
|
|
37
|
+
* same opener would say it was.
|
|
38
|
+
*
|
|
39
|
+
* Every icon is padded to two columns. ✅ was two wide and ▸ was one, so the
|
|
40
|
+
* text in a section of headings started one column left of the text in a
|
|
41
|
+
* section of results — a misalignment nobody could name but everybody saw.
|
|
42
|
+
*
|
|
43
|
+
* And every one carries the section indent. A heading at the left margin
|
|
44
|
+
* with its lines also at the left margin is not a section, it is a run of
|
|
45
|
+
* text with a bolder line every so often; four spaces is what makes the
|
|
46
|
+
* heading own what follows it. Everything else here indents from the same
|
|
47
|
+
* step, so a note sits under its line and a captured transcript sits under
|
|
48
|
+
* that.
|
|
49
|
+
*/
|
|
50
|
+
const MARKS = {
|
|
51
|
+
step: { icon: " ▸", paint: (s) => chalk.bold(s) },
|
|
52
|
+
ok: { icon: " •", paint: (s) => s, tail: chalk.green("✓"), tailPlain: "✓" },
|
|
53
|
+
warn: { icon: " •", paint: (s) => chalk.yellow(s), tail: chalk.yellow("⚠"), tailPlain: "⚠" },
|
|
54
|
+
fail: { icon: " •", paint: (s) => chalk.red(s), tail: chalk.red("✗"), tailPlain: "✗" },
|
|
55
|
+
info: { icon: " •", paint: (s) => s },
|
|
56
|
+
// No tail: the whole "What changed on this machine" section is things that
|
|
57
|
+
// happened, and a tick on every line of it would say once per line what the
|
|
58
|
+
// heading already said.
|
|
59
|
+
write: { icon: " •", paint: (s) => s },
|
|
60
|
+
cmd: { icon: " ⌨️", paint: (s) => s },
|
|
61
|
+
choice: { icon: " •", paint: (s) => s },
|
|
62
|
+
};
|
|
63
|
+
export class Journal {
|
|
64
|
+
quiet;
|
|
65
|
+
file = null;
|
|
66
|
+
constructor(quiet = false) {
|
|
67
|
+
this.quiet = quiet;
|
|
68
|
+
try {
|
|
69
|
+
const dir = join(homedir(), ".aisa", "logs");
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
// Local time in the name, like the lines inside: a user reading the
|
|
72
|
+
// directory should recognise "the run I did after lunch".
|
|
73
|
+
const d = new Date();
|
|
74
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
75
|
+
const stamp = `${d.getFullYear()}${two(d.getMonth() + 1)}${two(d.getDate())}` +
|
|
76
|
+
`-${two(d.getHours())}${two(d.getMinutes())}${two(d.getSeconds())}`;
|
|
77
|
+
this.file = join(dir, `connect-${stamp}.log`);
|
|
78
|
+
this.prune(dir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
this.file = null; // a log we cannot write is a feature we do not get
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Keep the newest few runs; a config log is small but not eternal. */
|
|
85
|
+
prune(dir) {
|
|
86
|
+
try {
|
|
87
|
+
const logs = readdirSync(dir)
|
|
88
|
+
.filter((f) => f.startsWith("connect-") && f.endsWith(".log"))
|
|
89
|
+
.map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs }))
|
|
90
|
+
.sort((a, b) => b.t - a.t);
|
|
91
|
+
for (const old of logs.slice(KEEP_LOGS))
|
|
92
|
+
unlinkSync(join(dir, old.f));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* pruning is a courtesy */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
get path() {
|
|
99
|
+
return this.file;
|
|
100
|
+
}
|
|
101
|
+
/** A section heading — the phases of a run, in the order they happen. */
|
|
102
|
+
section(title) {
|
|
103
|
+
this.emit("", `\n${chalk.bold.underline(title)}`, `\n== ${title}`);
|
|
104
|
+
}
|
|
105
|
+
line(mark, text, detail) {
|
|
106
|
+
const m = MARKS[mark];
|
|
107
|
+
const plain = (detail ? `${text} — ${detail}` : text) + (m.tailPlain ? ` ${m.tailPlain}` : "");
|
|
108
|
+
this.emit(m.icon, `${m.icon} ${m.paint(text)}${detail ? chalk.gray(` — ${detail}`) : ""}${m.tail ? ` ${m.tail}` : ""}`, `${m.icon} ${plain}`);
|
|
109
|
+
}
|
|
110
|
+
/** An indented continuation under the previous line. */
|
|
111
|
+
note(text) {
|
|
112
|
+
this.emit("", chalk.gray(` ${text}`), ` ${text}`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Detail belonging to the line above it, on screen and in the log.
|
|
116
|
+
*
|
|
117
|
+
* Deeper than note(): a note continues its line at the same level, while
|
|
118
|
+
* this is subordinate to it — a client's own output, quoted under our
|
|
119
|
+
* summary of what it did. Level with the parent's text it read as the next
|
|
120
|
+
* item in the list, which is exactly how an inherited stdout looked before
|
|
121
|
+
* it was captured.
|
|
122
|
+
*/
|
|
123
|
+
sub(text) {
|
|
124
|
+
this.emit("", chalk.gray(` ${text}`), ` ${text}`);
|
|
125
|
+
}
|
|
126
|
+
/** A command the user can copy and run later. */
|
|
127
|
+
command(cmd, why) {
|
|
128
|
+
this.emit("", ` ${chalk.cyan(cmd)}${why ? chalk.gray(` ${why}`) : ""}`, ` $ ${cmd}${why ? ` # ${why}` : ""}`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A ruled line on its own, for a moment that deserves separating from
|
|
132
|
+
* whatever an agent session just left on the screen.
|
|
133
|
+
*/
|
|
134
|
+
rule() {
|
|
135
|
+
const line = "\u2500".repeat(70);
|
|
136
|
+
this.emit("", `\n${chalk.gray(line)}`, `\n${line}`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The last thing a run says. Everything above is what happened; these are
|
|
140
|
+
* the commands worth remembering, so they get a rule above them, an arrow
|
|
141
|
+
* each and the brightest ink on the screen — a reader who scrolled away
|
|
142
|
+
* and came back should land on them. One entry is the common case; a
|
|
143
|
+
* fresh install or a new wrapper command earns a second line above the
|
|
144
|
+
* standing "run this again" one, most useful first.
|
|
145
|
+
*
|
|
146
|
+
* A blank line closes it, so the shell prompt that follows is not stuck to
|
|
147
|
+
* the last thing we asked the reader to remember.
|
|
148
|
+
*/
|
|
149
|
+
encore(items) {
|
|
150
|
+
// Columns, not characters: the rule under these lines has to be as long
|
|
151
|
+
// as the longest of them, and a Chinese `why` is twice the width its
|
|
152
|
+
// length claims.
|
|
153
|
+
const width = Math.min(70, Math.max(...items.map((i) => cells(i.cmd) + cells(i.why) + 8)));
|
|
154
|
+
const rule = "─".repeat(width);
|
|
155
|
+
this.emit("", `\n${chalk.gray(rule)}`, `\n${rule}`);
|
|
156
|
+
for (const { cmd, why } of items) {
|
|
157
|
+
this.emit("👉", `👉 ${chalk.bold.cyan(cmd)} ${chalk.bold(why)}`, `👉 ${cmd} ${why}`);
|
|
158
|
+
}
|
|
159
|
+
this.emit("", "", "");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Log-only: detail worth having in a bug report, noise in a terminal.
|
|
163
|
+
*
|
|
164
|
+
* Indented past the line above rather than level with it. At three spaces
|
|
165
|
+
* it started in the same column as its parent's text, which reads as the
|
|
166
|
+
* next item in a list rather than as detail belonging to the one before —
|
|
167
|
+
* and once it is properly indented the bullet has nothing left to do.
|
|
168
|
+
*/
|
|
169
|
+
record(text) {
|
|
170
|
+
this.write(` ${text}`);
|
|
171
|
+
}
|
|
172
|
+
emit(_icon, pretty, plain) {
|
|
173
|
+
if (!this.quiet)
|
|
174
|
+
console.log(pretty);
|
|
175
|
+
this.write(plain);
|
|
176
|
+
}
|
|
177
|
+
write(plain) {
|
|
178
|
+
if (!this.file)
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
const t = new Date().toTimeString().slice(0, 8);
|
|
182
|
+
appendFileSync(this.file, `${t} ${plain}\n`, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
this.file = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the stored key still work?
|
|
3
|
+
*
|
|
4
|
+
* `connect` used to ask only whether a key existed, and a key that exists is
|
|
5
|
+
* not a key that works: one revoked from the console, or belonging to an
|
|
6
|
+
* account that was deleted, sits in ~/.aisa/key looking exactly like a good
|
|
7
|
+
* one. The run then skipped the sign-in step, wrote that dead key into every
|
|
8
|
+
* MCP entry and into the agent's provider settings, and the first thing the
|
|
9
|
+
* user saw was their agent failing to authenticate — long after the setup had
|
|
10
|
+
* declared itself finished.
|
|
11
|
+
*
|
|
12
|
+
* Three answers, not two. "Cannot tell" has to be its own verdict: a laptop
|
|
13
|
+
* on a flaky connection must not be told its key is bad and pushed through a
|
|
14
|
+
* sign-in it does not need. The retries that separate the two are already in
|
|
15
|
+
* httpFetch — an idempotent GET is attempted three times with backoff, and
|
|
16
|
+
* 401/403 are deliberately not retryable because a rejected key does not
|
|
17
|
+
* become accepted on the second ask.
|
|
18
|
+
*/
|
|
19
|
+
export type KeyVerdict = "ok" | "invalid" | "unreachable";
|
|
20
|
+
export declare function verifyKey(key: string | undefined): Promise<KeyVerdict>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { apiRequestRaw } from "../api.js";
|
|
2
|
+
/** Small: this runs while the user is watching, not in the background. */
|
|
3
|
+
const TIMEOUT_MS = 8_000;
|
|
4
|
+
export async function verifyKey(key) {
|
|
5
|
+
if (!key)
|
|
6
|
+
return "invalid";
|
|
7
|
+
try {
|
|
8
|
+
const res = await apiRequestRaw(key, "credits/balance", {
|
|
9
|
+
idempotent: true,
|
|
10
|
+
timeoutMs: TIMEOUT_MS,
|
|
11
|
+
});
|
|
12
|
+
if (res.ok)
|
|
13
|
+
return "ok";
|
|
14
|
+
// The gateway rejected the credential itself. Nothing about the network
|
|
15
|
+
// would change this answer.
|
|
16
|
+
if (res.status === 401 || res.status === 403)
|
|
17
|
+
return "invalid";
|
|
18
|
+
return "unreachable";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Every attempt failed at the network level. The key may well be fine.
|
|
22
|
+
return "unreachable";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Compares dotted numeric versions ("0.3.0" < "0.10.0"); non-numeric parts sort as 0. */
|
|
2
|
+
export declare function isNewer(candidate: string, current: string): boolean;
|
|
3
|
+
export interface CheckForUpdateOptions {
|
|
4
|
+
/** Injectable for tests; defaults to a real npm-registry lookup. */
|
|
5
|
+
fetchLatest?: () => Promise<string | undefined>;
|
|
6
|
+
current?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Returns the newer version string if one is available, otherwise undefined. */
|
|
9
|
+
export declare function checkForUpdate(options?: CheckForUpdateOptions): Promise<string | undefined>;
|
|
10
|
+
/** Mark it said, for a surface that prints its own line (connect does). */
|
|
11
|
+
export declare function markUpdateAnnounced(): void;
|
|
12
|
+
export interface AnnounceOptions {
|
|
13
|
+
current?: string;
|
|
14
|
+
/** Injectable for tests, all of them. */
|
|
15
|
+
isTTY?: boolean;
|
|
16
|
+
updatable?: boolean;
|
|
17
|
+
lookup?: () => Promise<string | undefined>;
|
|
18
|
+
write?: (line: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Returns the line it printed, or undefined when it stayed quiet. */
|
|
21
|
+
export declare function announceUpdate(o?: AnnounceOptions): Promise<string | undefined>;
|
|
22
|
+
/** Tests only: the once-per-process latch has to be resettable. */
|
|
23
|
+
export declare function resetUpdateAnnouncement(): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readCache, writeCache } from "../cache.js";
|
|
2
|
+
import { VERSION } from "../constants.js";
|
|
3
|
+
import { detectInstall } from "./install-method.js";
|
|
4
|
+
/**
|
|
5
|
+
* Throttled, best-effort check for a newer published version.
|
|
6
|
+
*
|
|
7
|
+
* `connect` already makes a network call before it does anything else
|
|
8
|
+
* (fetchLiveServers), so piggybacking one more cheap lookup costs nothing —
|
|
9
|
+
* but only once a day: the npm-registry hit is cached like the API catalog
|
|
10
|
+
* and skills index (see cache.ts), so every other run within the window
|
|
11
|
+
* reads the last answer off disk instead of asking again.
|
|
12
|
+
*
|
|
13
|
+
* Never throws and never blocks longer than FETCH_TIMEOUT_MS. Offline, a
|
|
14
|
+
* timeout, or a non-200 all resolve to "nothing to report" rather than an
|
|
15
|
+
* error — a stale local install is not this function's problem to surface
|
|
16
|
+
* loudly, just to mention.
|
|
17
|
+
*/
|
|
18
|
+
const CACHE_KEY = "update-check";
|
|
19
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
const FETCH_TIMEOUT_MS = 1_500;
|
|
21
|
+
const PACKAGE = "@aisa-one/cli";
|
|
22
|
+
/** Compares dotted numeric versions ("0.3.0" < "0.10.0"); non-numeric parts sort as 0. */
|
|
23
|
+
export function isNewer(candidate, current) {
|
|
24
|
+
const a = candidate.split(".").map((p) => parseInt(p, 10) || 0);
|
|
25
|
+
const b = current.split(".").map((p) => parseInt(p, 10) || 0);
|
|
26
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
27
|
+
const x = a[i] ?? 0;
|
|
28
|
+
const y = b[i] ?? 0;
|
|
29
|
+
if (x !== y)
|
|
30
|
+
return x > y;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
async function defaultFetchLatest() {
|
|
35
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE}/latest`, {
|
|
36
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
return undefined;
|
|
40
|
+
const json = (await res.json());
|
|
41
|
+
return json.version;
|
|
42
|
+
}
|
|
43
|
+
/** Returns the newer version string if one is available, otherwise undefined. */
|
|
44
|
+
export async function checkForUpdate(options = {}) {
|
|
45
|
+
const current = options.current ?? VERSION;
|
|
46
|
+
const fetchLatest = options.fetchLatest ?? defaultFetchLatest;
|
|
47
|
+
const cached = readCache(CACHE_KEY);
|
|
48
|
+
if (cached?.fresh) {
|
|
49
|
+
return isNewer(cached.data.latest, current) ? cached.data.latest : undefined;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const latest = await fetchLatest();
|
|
53
|
+
if (!latest)
|
|
54
|
+
return cached && isNewer(cached.data.latest, current) ? cached.data.latest : undefined;
|
|
55
|
+
writeCache(CACHE_KEY, { latest }, CHECK_TTL_MS);
|
|
56
|
+
return isNewer(latest, current) ? latest : undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return cached && isNewer(cached.data.latest, current) ? cached.data.latest : undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Say it once, at the end of whatever command the user actually ran.
|
|
64
|
+
*
|
|
65
|
+
* The check already existed but only `connect` ever read it, and `connect` is
|
|
66
|
+
* the command a person runs once. Everyone who then lived in `chat`, `run`
|
|
67
|
+
* and `twitter` for a month was never told a release had happened.
|
|
68
|
+
*
|
|
69
|
+
* Three things keep it from becoming noise:
|
|
70
|
+
*
|
|
71
|
+
* · Nothing is printed unless stdout is a terminal. Most commands here emit
|
|
72
|
+
* JSON, and a friendly line appended to a pipe is corrupted output.
|
|
73
|
+
* · Nothing is printed for a copy that cannot be updated — npx is current by
|
|
74
|
+
* definition, and a working copy is updated with git.
|
|
75
|
+
* · The lookup is read from the day-old cache, not the network. A stale
|
|
76
|
+
* cache is refreshed within a tight budget and simply skipped if the
|
|
77
|
+
* registry is slow: no command pays for this at the moment it exits.
|
|
78
|
+
*/
|
|
79
|
+
const ANNOUNCE_BUDGET_MS = 700;
|
|
80
|
+
let announced = false;
|
|
81
|
+
/** Mark it said, for a surface that prints its own line (connect does). */
|
|
82
|
+
export function markUpdateAnnounced() {
|
|
83
|
+
announced = true;
|
|
84
|
+
}
|
|
85
|
+
/** Returns the line it printed, or undefined when it stayed quiet. */
|
|
86
|
+
export async function announceUpdate(o = {}) {
|
|
87
|
+
if (announced)
|
|
88
|
+
return undefined;
|
|
89
|
+
announced = true;
|
|
90
|
+
const current = o.current ?? VERSION;
|
|
91
|
+
if (!(o.isTTY ?? process.stdout.isTTY))
|
|
92
|
+
return undefined;
|
|
93
|
+
if (process.env.AISA_NO_UPDATE_NOTICE)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!(o.updatable ?? Boolean(detectInstall().command)))
|
|
96
|
+
return undefined;
|
|
97
|
+
let latest;
|
|
98
|
+
try {
|
|
99
|
+
latest = await Promise.race([
|
|
100
|
+
(o.lookup ?? (() => checkForUpdate({ current })))(),
|
|
101
|
+
new Promise((r) => setTimeout(() => r(undefined), ANNOUNCE_BUDGET_MS)),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (!latest)
|
|
108
|
+
return undefined;
|
|
109
|
+
const line = `\nA newer aisa is out — ${current} → ${latest}. Run \u001b[1maisa update\u001b[0m\n`;
|
|
110
|
+
(o.write ?? ((l) => process.stdout.write(l)))(line);
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
/** Tests only: the once-per-process latch has to be resettable. */
|
|
114
|
+
export function resetUpdateAnnouncement() {
|
|
115
|
+
announced = false;
|
|
116
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a string is on a terminal.
|
|
3
|
+
*
|
|
4
|
+
* There were three answers to this in the CLI and two of them were wrong, in
|
|
5
|
+
* two different ways:
|
|
6
|
+
*
|
|
7
|
+
* · `prompt.ts` counted characters, so a painted label was charged for its
|
|
8
|
+
* own escape sequences. `dim(" (recommended)")` is fourteen visible
|
|
9
|
+
* columns carried in twenty-three characters, and a four-row menu was cut
|
|
10
|
+
* to "Add AIsa beside it (recomme…" because of it.
|
|
11
|
+
* · `journal.ts` used `.length`, which is right for the English it happens
|
|
12
|
+
* to be given today and wrong the first time a Chinese string reaches it.
|
|
13
|
+
* · `connect-terminal.ts` had the only correct one, and only by never being
|
|
14
|
+
* handed anything painted.
|
|
15
|
+
*
|
|
16
|
+
* Two things are true of a terminal and neither is character count: a CJK
|
|
17
|
+
* glyph occupies two columns in one character, and an escape sequence
|
|
18
|
+
* occupies none in several. One implementation, used by all of them.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stripAnsi(text: string): string;
|
|
21
|
+
/** Columns the string occupies once printed. */
|
|
22
|
+
export declare function cells(text: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Cut to a column count, keeping colour intact.
|
|
25
|
+
*
|
|
26
|
+
* Escape sequences are copied through without being charged for, so a cut
|
|
27
|
+
* never lands inside one — a half-written escape is not a shorter string,
|
|
28
|
+
* it is a terminal printing `[1m` at the user.
|
|
29
|
+
*/
|
|
30
|
+
export declare function truncate(text: string, width: number): string;
|
|
31
|
+
/** Pad to a column count, counting what is printed rather than what is stored. */
|
|
32
|
+
export declare function padTo(text: string, width: number): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a string is on a terminal.
|
|
3
|
+
*
|
|
4
|
+
* There were three answers to this in the CLI and two of them were wrong, in
|
|
5
|
+
* two different ways:
|
|
6
|
+
*
|
|
7
|
+
* · `prompt.ts` counted characters, so a painted label was charged for its
|
|
8
|
+
* own escape sequences. `dim(" (recommended)")` is fourteen visible
|
|
9
|
+
* columns carried in twenty-three characters, and a four-row menu was cut
|
|
10
|
+
* to "Add AIsa beside it (recomme…" because of it.
|
|
11
|
+
* · `journal.ts` used `.length`, which is right for the English it happens
|
|
12
|
+
* to be given today and wrong the first time a Chinese string reaches it.
|
|
13
|
+
* · `connect-terminal.ts` had the only correct one, and only by never being
|
|
14
|
+
* handed anything painted.
|
|
15
|
+
*
|
|
16
|
+
* Two things are true of a terminal and neither is character count: a CJK
|
|
17
|
+
* glyph occupies two columns in one character, and an escape sequence
|
|
18
|
+
* occupies none in several. One implementation, used by all of them.
|
|
19
|
+
*/
|
|
20
|
+
/** SGR sequences — the only escapes this CLI emits. */
|
|
21
|
+
const ANSI = /\[[0-9;]*m/g;
|
|
22
|
+
/**
|
|
23
|
+
* The ranges that take two columns.
|
|
24
|
+
*
|
|
25
|
+
* Hangul jamo, CJK radicals through the Yi syllabary (which contains the
|
|
26
|
+
* ideographs themselves), Hangul syllables, compatibility ideographs,
|
|
27
|
+
* vertical forms, and fullwidth ASCII.
|
|
28
|
+
*/
|
|
29
|
+
const WIDE = /[ᄀ-ᅟ⺀-가-힣豈-︰-﹏-⦆¢-₩]/;
|
|
30
|
+
export function stripAnsi(text) {
|
|
31
|
+
return text.replace(ANSI, "");
|
|
32
|
+
}
|
|
33
|
+
/** Columns the string occupies once printed. */
|
|
34
|
+
export function cells(text) {
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const ch of stripAnsi(text))
|
|
37
|
+
n += WIDE.test(ch) ? 2 : 1;
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Cut to a column count, keeping colour intact.
|
|
42
|
+
*
|
|
43
|
+
* Escape sequences are copied through without being charged for, so a cut
|
|
44
|
+
* never lands inside one — a half-written escape is not a shorter string,
|
|
45
|
+
* it is a terminal printing `[1m` at the user.
|
|
46
|
+
*/
|
|
47
|
+
export function truncate(text, width) {
|
|
48
|
+
let out = "";
|
|
49
|
+
let w = 0;
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < text.length) {
|
|
52
|
+
if (text.charCodeAt(i) === 0x1b) {
|
|
53
|
+
const end = text.indexOf("m", i);
|
|
54
|
+
if (end !== -1) {
|
|
55
|
+
out += text.slice(i, end + 1);
|
|
56
|
+
i = end + 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const ch = String.fromCodePoint(text.codePointAt(i));
|
|
61
|
+
const cw = WIDE.test(ch) ? 2 : 1;
|
|
62
|
+
if (w + cw > width)
|
|
63
|
+
return out + "…";
|
|
64
|
+
out += ch;
|
|
65
|
+
w += cw;
|
|
66
|
+
i += ch.length;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** Pad to a column count, counting what is printed rather than what is stored. */
|
|
71
|
+
export function padTo(text, width) {
|
|
72
|
+
return text + " ".repeat(Math.max(0, width - cells(text)));
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aisa-one/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "CLI for the AIsa unified AI infrastructure platform - one API key for 80+ LLMs and 900+ endpoints across finance, search, social, and video APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"README.md",
|
|
11
|
-
"CHANGELOG.md"
|
|
11
|
+
"CHANGELOG.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"vscode-extension/aisa-vscode.vsix"
|
|
12
14
|
],
|
|
13
15
|
"bin": {
|
|
14
16
|
"aisa": "dist/index.js"
|
|
@@ -20,7 +22,8 @@
|
|
|
20
22
|
"start": "node dist/index.js",
|
|
21
23
|
"test": "vitest run",
|
|
22
24
|
"test:watch": "vitest",
|
|
23
|
-
"
|
|
25
|
+
"package:smoke": "node scripts/package-smoke.mjs",
|
|
26
|
+
"prepack": "npm run build"
|
|
24
27
|
},
|
|
25
28
|
"engines": {
|
|
26
29
|
"node": ">=18"
|
|
@@ -32,7 +35,8 @@
|
|
|
32
35
|
"chalk": "^5.3.0",
|
|
33
36
|
"commander": "^12.0.0",
|
|
34
37
|
"conf": "^12.0.0",
|
|
35
|
-
"ora": "^8.0.1"
|
|
38
|
+
"ora": "^8.0.1",
|
|
39
|
+
"smol-toml": "^1.8.0"
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
42
|
"@types/node": "^20.0.0",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# AIsa for VS Code
|
|
2
|
+
|
|
3
|
+
Adds AIsa's models (Claude, GPT, DeepSeek, Kimi, GLM, Qwen) to VS Code chat
|
|
4
|
+
using the key `aisa login` stored in `~/.aisa/key`. Installed by
|
|
5
|
+
`aisa connect`; nothing to paste. Run **AIsa: Set up AIsa models in chat**
|
|
6
|
+
from the command palette to re-provision after rotating your key.
|
|
Binary file
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export declare function stockAction(symbol: string, options: {
|
|
2
|
-
field?: string;
|
|
3
|
-
raw?: boolean;
|
|
4
|
-
}): Promise<void>;
|
|
5
|
-
export declare function cryptoAction(symbol: string, options: {
|
|
6
|
-
period?: string;
|
|
7
|
-
raw?: boolean;
|
|
8
|
-
id?: string;
|
|
9
|
-
source?: string;
|
|
10
|
-
}): Promise<void>;
|
|
11
|
-
export declare function screenerAction(options: {
|
|
12
|
-
sector?: string;
|
|
13
|
-
minMarketCap?: string;
|
|
14
|
-
filter?: string[];
|
|
15
|
-
limit?: string;
|
|
16
|
-
raw?: boolean;
|
|
17
|
-
}): Promise<void>;
|