@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +172 -1
  2. package/LICENSE +21 -0
  3. package/README.md +163 -169
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +14 -3
  6. package/dist/catalog.d.ts +1 -18
  7. package/dist/catalog.js +1 -24
  8. package/dist/cli-error.d.ts +17 -0
  9. package/dist/cli-error.js +25 -0
  10. package/dist/cli-guidance.d.ts +14 -0
  11. package/dist/cli-guidance.js +55 -0
  12. package/dist/commands/account.d.ts +17 -0
  13. package/dist/commands/account.js +45 -1
  14. package/dist/commands/api.d.ts +0 -11
  15. package/dist/commands/api.js +6 -175
  16. package/dist/commands/auth.d.ts +2 -1
  17. package/dist/commands/auth.js +43 -9
  18. package/dist/commands/brand-logos.d.ts +6 -0
  19. package/dist/commands/brand-logos.js +21 -0
  20. package/dist/commands/configCmd.js +8 -3
  21. package/dist/commands/connect-shared.d.ts +136 -0
  22. package/dist/commands/connect-shared.js +143 -0
  23. package/dist/commands/connect-t2.d.ts +18 -0
  24. package/dist/commands/connect-t2.js +1599 -0
  25. package/dist/commands/connect-terminal.d.ts +65 -0
  26. package/dist/commands/connect-terminal.js +822 -0
  27. package/dist/commands/connect.d.ts +31 -0
  28. package/dist/commands/connect.js +3181 -0
  29. package/dist/commands/flow.d.ts +685 -0
  30. package/dist/commands/flow.js +697 -0
  31. package/dist/commands/install.d.ts +91 -0
  32. package/dist/commands/install.js +258 -0
  33. package/dist/commands/llm-config.d.ts +103 -0
  34. package/dist/commands/llm-config.js +452 -0
  35. package/dist/commands/manifest.d.ts +68 -0
  36. package/dist/commands/manifest.js +90 -0
  37. package/dist/commands/mcp.d.ts +79 -2
  38. package/dist/commands/mcp.js +262 -48
  39. package/dist/commands/oauth-login.d.ts +33 -0
  40. package/dist/commands/oauth-login.js +333 -0
  41. package/dist/commands/prompt.d.ts +96 -0
  42. package/dist/commands/prompt.js +397 -0
  43. package/dist/commands/serve-results.d.ts +61 -0
  44. package/dist/commands/serve-results.js +245 -0
  45. package/dist/commands/serve-signin.d.ts +28 -0
  46. package/dist/commands/serve-signin.js +90 -0
  47. package/dist/commands/signin-page.d.ts +49 -0
  48. package/dist/commands/signin-page.js +250 -0
  49. package/dist/commands/skills.js +87 -50
  50. package/dist/commands/tool-help.d.ts +69 -0
  51. package/dist/commands/tool-help.js +189 -0
  52. package/dist/commands/tool-input.d.ts +22 -0
  53. package/dist/commands/tool-input.js +220 -0
  54. package/dist/commands/tools.d.ts +12 -0
  55. package/dist/commands/tools.js +307 -0
  56. package/dist/commands/update.d.ts +11 -0
  57. package/dist/commands/update.js +92 -0
  58. package/dist/commands/vscode.d.ts +65 -0
  59. package/dist/commands/vscode.js +173 -0
  60. package/dist/commands/wrappers.d.ts +23 -0
  61. package/dist/commands/wrappers.js +102 -0
  62. package/dist/completion.js +3 -80
  63. package/dist/config.js +76 -2
  64. package/dist/constants.d.ts +78 -38
  65. package/dist/constants.js +80 -102
  66. package/dist/index.js +155 -357
  67. package/dist/json-text.d.ts +12 -0
  68. package/dist/json-text.js +60 -0
  69. package/dist/router.d.ts +39 -0
  70. package/dist/router.js +59 -0
  71. package/dist/skills-registry.js +12 -3
  72. package/dist/utils/banner.d.ts +9 -0
  73. package/dist/utils/banner.js +41 -0
  74. package/dist/utils/browser.d.ts +19 -0
  75. package/dist/utils/browser.js +31 -0
  76. package/dist/utils/exec.d.ts +43 -0
  77. package/dist/utils/exec.js +61 -0
  78. package/dist/utils/http.d.ts +53 -0
  79. package/dist/utils/http.js +124 -0
  80. package/dist/utils/install-method.d.ts +36 -0
  81. package/dist/utils/install-method.js +48 -0
  82. package/dist/utils/journal.d.ts +57 -0
  83. package/dist/utils/journal.js +188 -0
  84. package/dist/utils/key-check.d.ts +20 -0
  85. package/dist/utils/key-check.js +24 -0
  86. package/dist/utils/update-check.d.ts +23 -0
  87. package/dist/utils/update-check.js +116 -0
  88. package/dist/utils/width.d.ts +32 -0
  89. package/dist/utils/width.js +73 -0
  90. package/package.json +8 -4
  91. package/vscode-extension/README.md +6 -0
  92. package/vscode-extension/aisa-vscode.vsix +0 -0
  93. package/dist/commands/finance.d.ts +0 -17
  94. package/dist/commands/finance.js +0 -342
  95. package/dist/commands/run.d.ts +0 -28
  96. package/dist/commands/run.js +0 -182
  97. package/dist/commands/search.d.ts +0 -9
  98. package/dist/commands/search.js +0 -81
  99. package/dist/commands/twitter.d.ts +0 -138
  100. package/dist/commands/twitter.js +0 -374
  101. package/dist/commands/video.d.ts +0 -49
  102. 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.2.4",
3
+ "version": "0.5.0",
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
- "prepublishOnly": "npm run build"
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.
@@ -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>;