@aisa-one/cli 0.3.0 → 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 +143 -1
- package/LICENSE +21 -0
- package/README.md +151 -162
- 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 +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 +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 +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,65 @@
|
|
|
1
|
+
import type { LiveServer } from "./mcp.js";
|
|
2
|
+
import type { ConfigResult } from "./llm-config.js";
|
|
3
|
+
/**
|
|
4
|
+
* VS Code: both of its AI surfaces are plain files in the user profile
|
|
5
|
+
* directory, so both can be written without the UI.
|
|
6
|
+
*
|
|
7
|
+
* - Models: `chatLanguageModels.json` (VS Code ≥ 1.117, the "Custom
|
|
8
|
+
* Endpoint" provider). Top level is an array of provider groups; ours is
|
|
9
|
+
* one group `{vendor: "customendpoint", name: "AIsa", apiType, models}`.
|
|
10
|
+
* Copilot's own models are untouched — the group sits beside them in the
|
|
11
|
+
* model picker, so this is always an "add", never a switch. Inline
|
|
12
|
+
* completions stay on Copilot; the provider only covers chat and agent.
|
|
13
|
+
*
|
|
14
|
+
* The API key is deliberately NOT written. `apiKey` is a `secret: true`
|
|
15
|
+
* property: at run time VS Code treats the value as a reference into its
|
|
16
|
+
* own secret storage (`${input:chat.lm.secret.<id>}`), so a literal key
|
|
17
|
+
* in the file is never sent (verified on 1.134 — the gateway saw an empty
|
|
18
|
+
* bearer and answered 401). The key is entered once in VS Code's Manage
|
|
19
|
+
* Models UI, which stores it; there is no command or URI to do that from
|
|
20
|
+
* outside, and writing its encrypted store is off the table.
|
|
21
|
+
* - MCP: `mcp.json` in the same directory, `{servers: {name: {type: "http",
|
|
22
|
+
* url, headers}}}`. With a key the entry carries the bearer; without one
|
|
23
|
+
* VS Code runs the server's OAuth itself on first use.
|
|
24
|
+
*
|
|
25
|
+
* Both writers are key-exact: the AIsa group / `aisa-*` entries are replaced,
|
|
26
|
+
* anything else in either file is kept byte-for-byte equivalent.
|
|
27
|
+
*/
|
|
28
|
+
export declare const VSCODE_GROUP_NAME = "AIsa";
|
|
29
|
+
/** A representative spread of what the gateway offers, one per lab. */
|
|
30
|
+
export declare const VSCODE_MODELS: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function vscodeUserDir(): string;
|
|
35
|
+
/** Present when VS Code has been run at least once on this machine. */
|
|
36
|
+
export declare function vscodeDetected(): boolean;
|
|
37
|
+
export declare function writeVSCodeLLM(models?: {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}[]): ConfigResult;
|
|
41
|
+
export declare function removeVSCodeLLM(): ConfigResult;
|
|
42
|
+
/** The `code` command: on PATH when the user enabled it, otherwise inside
|
|
43
|
+
* the app bundle on macOS or the usual Linux locations. */
|
|
44
|
+
export declare function vscodeBinary(): string | undefined;
|
|
45
|
+
/** The packaged AIsa extension shipped with the CLI. */
|
|
46
|
+
export declare function vscodeExtensionPath(): string;
|
|
47
|
+
/**
|
|
48
|
+
* The fully automatic route for the key: install the AIsa extension, which
|
|
49
|
+
* asks VS Code itself to store the key from ~/.aisa/key and add the model
|
|
50
|
+
* group (see vscode-extension/extension.js). Works on a running VS Code —
|
|
51
|
+
* new extensions activate without a reload.
|
|
52
|
+
*/
|
|
53
|
+
export declare function installVSCodeExtension(): {
|
|
54
|
+
ok: true;
|
|
55
|
+
bin: string;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
/** Open VS Code on a folder (the one connect was launched in). Resolves to
|
|
61
|
+
* whether the command could be started; VS Code itself detaches. */
|
|
62
|
+
export declare function launchVSCode(dir: string): boolean;
|
|
63
|
+
export declare function writeVSCodeMCP(chosen: LiveServer[], apiKey: string | undefined): ConfigResult & {
|
|
64
|
+
written?: number;
|
|
65
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { runSync, DEFAULT_TIMEOUT_MS } from "../utils/exec.js";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensureDir } from "../utils/file.js";
|
|
7
|
+
import { LLM_BASE_URL } from "../constants.js";
|
|
8
|
+
/**
|
|
9
|
+
* VS Code: both of its AI surfaces are plain files in the user profile
|
|
10
|
+
* directory, so both can be written without the UI.
|
|
11
|
+
*
|
|
12
|
+
* - Models: `chatLanguageModels.json` (VS Code ≥ 1.117, the "Custom
|
|
13
|
+
* Endpoint" provider). Top level is an array of provider groups; ours is
|
|
14
|
+
* one group `{vendor: "customendpoint", name: "AIsa", apiType, models}`.
|
|
15
|
+
* Copilot's own models are untouched — the group sits beside them in the
|
|
16
|
+
* model picker, so this is always an "add", never a switch. Inline
|
|
17
|
+
* completions stay on Copilot; the provider only covers chat and agent.
|
|
18
|
+
*
|
|
19
|
+
* The API key is deliberately NOT written. `apiKey` is a `secret: true`
|
|
20
|
+
* property: at run time VS Code treats the value as a reference into its
|
|
21
|
+
* own secret storage (`${input:chat.lm.secret.<id>}`), so a literal key
|
|
22
|
+
* in the file is never sent (verified on 1.134 — the gateway saw an empty
|
|
23
|
+
* bearer and answered 401). The key is entered once in VS Code's Manage
|
|
24
|
+
* Models UI, which stores it; there is no command or URI to do that from
|
|
25
|
+
* outside, and writing its encrypted store is off the table.
|
|
26
|
+
* - MCP: `mcp.json` in the same directory, `{servers: {name: {type: "http",
|
|
27
|
+
* url, headers}}}`. With a key the entry carries the bearer; without one
|
|
28
|
+
* VS Code runs the server's OAuth itself on first use.
|
|
29
|
+
*
|
|
30
|
+
* Both writers are key-exact: the AIsa group / `aisa-*` entries are replaced,
|
|
31
|
+
* anything else in either file is kept byte-for-byte equivalent.
|
|
32
|
+
*/
|
|
33
|
+
export const VSCODE_GROUP_NAME = "AIsa";
|
|
34
|
+
/** A representative spread of what the gateway offers, one per lab. */
|
|
35
|
+
export const VSCODE_MODELS = [
|
|
36
|
+
{ id: "claude-sonnet-5", name: "Claude Sonnet 5 (AIsa)" },
|
|
37
|
+
{ id: "claude-opus-5", name: "Claude Opus 5 (AIsa)" },
|
|
38
|
+
{ id: "gpt-5.5", name: "GPT-5.5 (AIsa)" },
|
|
39
|
+
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro (AIsa)" },
|
|
40
|
+
{ id: "kimi-k3", name: "Kimi K3 (AIsa)" },
|
|
41
|
+
{ id: "glm-5.2", name: "GLM-5.2 (AIsa)" },
|
|
42
|
+
{ id: "qwen3.7-max", name: "Qwen3.7 Max (AIsa)" },
|
|
43
|
+
];
|
|
44
|
+
export function vscodeUserDir() {
|
|
45
|
+
const home = homedir();
|
|
46
|
+
if (process.platform === "darwin")
|
|
47
|
+
return join(home, "Library", "Application Support", "Code", "User");
|
|
48
|
+
if (process.platform === "win32")
|
|
49
|
+
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Code", "User");
|
|
50
|
+
return join(home, ".config", "Code", "User");
|
|
51
|
+
}
|
|
52
|
+
/** Present when VS Code has been run at least once on this machine. */
|
|
53
|
+
export function vscodeDetected() {
|
|
54
|
+
return existsSync(vscodeUserDir());
|
|
55
|
+
}
|
|
56
|
+
function readJson(path) {
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return undefined;
|
|
59
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
60
|
+
}
|
|
61
|
+
export function writeVSCodeLLM(models = VSCODE_MODELS) {
|
|
62
|
+
const dir = vscodeUserDir();
|
|
63
|
+
const path = join(dir, "chatLanguageModels.json");
|
|
64
|
+
let groups;
|
|
65
|
+
try {
|
|
66
|
+
groups = readJson(path) ?? [];
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(groups))
|
|
72
|
+
return { ok: false, reason: `${path} is not the expected array` };
|
|
73
|
+
const ours = {
|
|
74
|
+
vendor: "customendpoint",
|
|
75
|
+
name: VSCODE_GROUP_NAME,
|
|
76
|
+
apiType: "chat-completions",
|
|
77
|
+
models: models.map((m) => ({
|
|
78
|
+
id: m.id,
|
|
79
|
+
name: m.name,
|
|
80
|
+
url: `${LLM_BASE_URL}/v1`,
|
|
81
|
+
toolCalling: true,
|
|
82
|
+
vision: false,
|
|
83
|
+
maxInputTokens: 200_000,
|
|
84
|
+
maxOutputTokens: 32_000,
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
const isOurs = (g) => typeof g === "object" && g !== null && g.vendor === "customendpoint" &&
|
|
88
|
+
g.name === VSCODE_GROUP_NAME;
|
|
89
|
+
// A key reference VS Code already stored for our group survives the
|
|
90
|
+
// rewrite — the user should paste it once, not once per run.
|
|
91
|
+
const prior = groups.find(isOurs);
|
|
92
|
+
const next = groups.filter((g) => !isOurs(g));
|
|
93
|
+
next.push(typeof prior?.apiKey === "string" && prior.apiKey.startsWith("${input:") ? { ...ours, apiKey: prior.apiKey } : ours);
|
|
94
|
+
ensureDir(dir);
|
|
95
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
96
|
+
return { ok: true, path };
|
|
97
|
+
}
|
|
98
|
+
export function removeVSCodeLLM() {
|
|
99
|
+
const path = join(vscodeUserDir(), "chatLanguageModels.json");
|
|
100
|
+
const groups = readJson(path);
|
|
101
|
+
if (!Array.isArray(groups))
|
|
102
|
+
return { ok: true, path };
|
|
103
|
+
const next = groups.filter((g) => !(typeof g === "object" && g !== null && g.name === VSCODE_GROUP_NAME));
|
|
104
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
105
|
+
return { ok: true, path };
|
|
106
|
+
}
|
|
107
|
+
/** The `code` command: on PATH when the user enabled it, otherwise inside
|
|
108
|
+
* the app bundle on macOS or the usual Linux locations. */
|
|
109
|
+
export function vscodeBinary() {
|
|
110
|
+
const candidates = process.platform === "darwin"
|
|
111
|
+
? ["code", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
|
112
|
+
join(homedir(), "Applications/Visual Studio Code.app/Contents/Resources/app/bin/code")]
|
|
113
|
+
: ["code", "/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code"];
|
|
114
|
+
for (const c of candidates) {
|
|
115
|
+
const r = runSync(c, ["--version"]);
|
|
116
|
+
if (r.status === 0)
|
|
117
|
+
return c;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
/** The packaged AIsa extension shipped with the CLI. */
|
|
122
|
+
export function vscodeExtensionPath() {
|
|
123
|
+
return fileURLToPath(new URL("../../vscode-extension/aisa-vscode.vsix", import.meta.url));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The fully automatic route for the key: install the AIsa extension, which
|
|
127
|
+
* asks VS Code itself to store the key from ~/.aisa/key and add the model
|
|
128
|
+
* group (see vscode-extension/extension.js). Works on a running VS Code —
|
|
129
|
+
* new extensions activate without a reload.
|
|
130
|
+
*/
|
|
131
|
+
export function installVSCodeExtension() {
|
|
132
|
+
const bin = vscodeBinary();
|
|
133
|
+
if (!bin)
|
|
134
|
+
return { ok: false, reason: "the code command was not found" };
|
|
135
|
+
const vsix = vscodeExtensionPath();
|
|
136
|
+
if (!existsSync(vsix))
|
|
137
|
+
return { ok: false, reason: `extension package missing at ${vsix}` };
|
|
138
|
+
const r = runSync(bin, ["--install-extension", vsix, "--force"], { timeout: DEFAULT_TIMEOUT_MS });
|
|
139
|
+
if (r.status !== 0)
|
|
140
|
+
return { ok: false, reason: (r.stderr || r.stdout || `exit ${r.status}`).trim().split("\n")[0] };
|
|
141
|
+
return { ok: true, bin };
|
|
142
|
+
}
|
|
143
|
+
/** Open VS Code on a folder (the one connect was launched in). Resolves to
|
|
144
|
+
* whether the command could be started; VS Code itself detaches. */
|
|
145
|
+
export function launchVSCode(dir) {
|
|
146
|
+
const bin = vscodeBinary();
|
|
147
|
+
if (!bin)
|
|
148
|
+
return false;
|
|
149
|
+
const r = runSync(bin, [dir], { timeout: 30_000 });
|
|
150
|
+
return r.status === 0;
|
|
151
|
+
}
|
|
152
|
+
export function writeVSCodeMCP(chosen, apiKey) {
|
|
153
|
+
const dir = vscodeUserDir();
|
|
154
|
+
const path = join(dir, "mcp.json");
|
|
155
|
+
let doc;
|
|
156
|
+
try {
|
|
157
|
+
const raw = readJson(path);
|
|
158
|
+
doc = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
162
|
+
}
|
|
163
|
+
const servers = (doc.servers ?? {});
|
|
164
|
+
for (const s of chosen) {
|
|
165
|
+
servers[`aisa-${s.slug}`] = apiKey
|
|
166
|
+
? { type: "http", url: s.endpoint, headers: { Authorization: `Bearer ${apiKey}` } }
|
|
167
|
+
: { type: "http", url: s.endpoint };
|
|
168
|
+
}
|
|
169
|
+
doc.servers = servers;
|
|
170
|
+
ensureDir(dir);
|
|
171
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
172
|
+
return { ok: true, path, written: chosen.length };
|
|
173
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ModelChoice } from "./llm-config.js";
|
|
2
|
+
export declare const CLAUDE_AISA_SETTINGS: () => string;
|
|
3
|
+
/** The --settings document claude-aisa loads: gateway env + model tiers. */
|
|
4
|
+
export declare function writeClaudeAisaSettings(apiKey: string, models: ModelChoice): string;
|
|
5
|
+
/**
|
|
6
|
+
* Where wrappers go: npm's global bin first — on this platform it is the
|
|
7
|
+
* directory the agents themselves live in, so it is on PATH by definition.
|
|
8
|
+
* ~/.local/bin is the fallback (standard on Linux, needs one PATH line on
|
|
9
|
+
* macOS — reported to the caller, never written into anyone's shell rc).
|
|
10
|
+
*/
|
|
11
|
+
export declare function wrapperBinDir(): {
|
|
12
|
+
dir: string;
|
|
13
|
+
onPath: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type WrapperResult = {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
dir: string;
|
|
18
|
+
wrote: string[];
|
|
19
|
+
/** Set when the chosen dir is not on PATH — the one line the user can add. */
|
|
20
|
+
pathHint?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Write one or both wrappers, idempotently. */
|
|
23
|
+
export declare function installWrappers(which: Array<"claude-aisa" | "codex-aisa">): WrapperResult;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { runSync, QUICK_TIMEOUT_MS } from "../utils/exec.js";
|
|
2
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { LLM_BASE_URL } from "../constants.js";
|
|
6
|
+
/**
|
|
7
|
+
* The claude-aisa / codex-aisa companion commands: run the agent the user
|
|
8
|
+
* already has, on AIsa, without touching a byte of their own configuration.
|
|
9
|
+
*
|
|
10
|
+
* Verified mechanics (2026-08-22, three loopback experiments):
|
|
11
|
+
* - Claude Code's settings.json env OVERRIDES real environment variables,
|
|
12
|
+
* so an env-prefix wrapper cannot work — but `--settings <file>` overrides
|
|
13
|
+
* everything, and it is the same flag Sapiom runs its whole product on.
|
|
14
|
+
* claude-aisa therefore points --settings at a file under ~/.aisa.
|
|
15
|
+
* - Codex has native profiles: codex-aisa is just `--profile aisa`, whose
|
|
16
|
+
* definition lives in ~/.codex/config.toml (profiles cannot be external).
|
|
17
|
+
*
|
|
18
|
+
* The wrappers contain no credentials. Claude's key sits in the settings
|
|
19
|
+
* file (same directory and permissions as the key itself); Codex's sits in
|
|
20
|
+
* its provider entry. Removing a wrapper is deleting one small file.
|
|
21
|
+
*/
|
|
22
|
+
const aisaDir = () => join(homedir(), ".aisa");
|
|
23
|
+
export const CLAUDE_AISA_SETTINGS = () => join(aisaDir(), "claude-aisa.settings.json");
|
|
24
|
+
/** The --settings document claude-aisa loads: gateway env + model tiers. */
|
|
25
|
+
export function writeClaudeAisaSettings(apiKey, models) {
|
|
26
|
+
mkdirSync(aisaDir(), { recursive: true });
|
|
27
|
+
const path = CLAUDE_AISA_SETTINGS();
|
|
28
|
+
const doc = {
|
|
29
|
+
env: {
|
|
30
|
+
ANTHROPIC_BASE_URL: LLM_BASE_URL,
|
|
31
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
32
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.smallModel,
|
|
33
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: models.model,
|
|
34
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: models.model,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + "\n", { mode: 0o600 });
|
|
38
|
+
chmodSync(path, 0o600);
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Where wrappers go: npm's global bin first — on this platform it is the
|
|
43
|
+
* directory the agents themselves live in, so it is on PATH by definition.
|
|
44
|
+
* ~/.local/bin is the fallback (standard on Linux, needs one PATH line on
|
|
45
|
+
* macOS — reported to the caller, never written into anyone's shell rc).
|
|
46
|
+
*/
|
|
47
|
+
export function wrapperBinDir() {
|
|
48
|
+
let dir;
|
|
49
|
+
try {
|
|
50
|
+
const r = runSync("npm", ["config", "get", "prefix"], { timeout: QUICK_TIMEOUT_MS });
|
|
51
|
+
const prefix = r.status === 0 ? r.stdout.trim() : "";
|
|
52
|
+
if (prefix && existsSync(join(prefix, "bin"))) {
|
|
53
|
+
accessSync(join(prefix, "bin"), constants.W_OK);
|
|
54
|
+
dir = join(prefix, "bin");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* fall through to ~/.local/bin */
|
|
59
|
+
}
|
|
60
|
+
if (!dir) {
|
|
61
|
+
dir = join(homedir(), ".local", "bin");
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
const onPath = (process.env.PATH ?? "").split(":").includes(dir);
|
|
65
|
+
return { dir, onPath };
|
|
66
|
+
}
|
|
67
|
+
/** Write one or both wrappers, idempotently. */
|
|
68
|
+
export function installWrappers(which) {
|
|
69
|
+
const { dir, onPath } = wrapperBinDir();
|
|
70
|
+
const scripts = {
|
|
71
|
+
"claude-aisa": `#!/bin/sh
|
|
72
|
+
# AIsa companion for Claude Code. Your own claude setup is untouched:
|
|
73
|
+
# this one session loads AIsa's settings via --settings and nothing else.
|
|
74
|
+
exec claude --settings "$HOME/.aisa/claude-aisa.settings.json" "$@"
|
|
75
|
+
`,
|
|
76
|
+
"codex-aisa": `#!/bin/sh
|
|
77
|
+
# AIsa companion for Codex: your default setup stays as it is; this runs
|
|
78
|
+
# the aisa profile defined in ~/.codex/config.toml for this session only.
|
|
79
|
+
exec codex --profile aisa "$@"
|
|
80
|
+
`,
|
|
81
|
+
};
|
|
82
|
+
const wrote = [];
|
|
83
|
+
try {
|
|
84
|
+
for (const name of which) {
|
|
85
|
+
const p = join(dir, name);
|
|
86
|
+
writeFileSync(p, scripts[name], { mode: 0o755 });
|
|
87
|
+
chmodSync(p, 0o755);
|
|
88
|
+
wrote.push(name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { ok: false, dir, wrote };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
dir,
|
|
97
|
+
wrote,
|
|
98
|
+
...(onPath
|
|
99
|
+
? {}
|
|
100
|
+
: { pathHint: `echo 'export PATH="${dir}:$PATH"' >> ~/.zshrc && source ~/.zshrc` }),
|
|
101
|
+
};
|
|
102
|
+
}
|
package/dist/completion.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readCache } from "./cache.js";
|
|
2
2
|
import { cacheScope } from "./api.js";
|
|
3
|
-
import { AGENT_DIRS, API_CATEGORIES,
|
|
3
|
+
import { AGENT_DIRS, API_CATEGORIES, MCP_CONFIGS } from "./constants.js";
|
|
4
4
|
const SHELLS = ["bash", "zsh", "fish"];
|
|
5
5
|
export function isShell(value) {
|
|
6
6
|
return SHELLS.includes(value);
|
|
@@ -9,46 +9,12 @@ export function isShell(value) {
|
|
|
9
9
|
function cachedProviders() {
|
|
10
10
|
return readCache(`catalog/${cacheScope()}/category.json`)?.data.apis || [];
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Slugs usable with `aisa run`. A provider's id is not always its URL slug
|
|
14
|
-
* (`brave-search` serves `/apis/v1/brave/...`), so prefer the real slug from any
|
|
15
|
-
* cached detail and fall back to the id.
|
|
16
|
-
*/
|
|
17
|
-
function runSlugs() {
|
|
18
|
-
const out = new Map();
|
|
19
|
-
for (const provider of cachedProviders()) {
|
|
20
|
-
const detail = readCache(`catalog/${cacheScope()}/${provider.id.replace(/[^\w.-]/g, "_")}.json`)?.data.api;
|
|
21
|
-
const first = detail?.endpoint_groups?.flatMap((g) => g.endpoints || [])[0];
|
|
22
|
-
const slug = first
|
|
23
|
-
? first.path.replace(/^\/apis\/v1\//, "").split("/")[0]
|
|
24
|
-
: provider.id;
|
|
25
|
-
out.set(slug, `${provider.endpoint_count} endpoints`);
|
|
26
|
-
}
|
|
27
|
-
return [...out].map(([value, description]) => ({ value, description }));
|
|
28
|
-
}
|
|
29
12
|
function providerIds() {
|
|
30
13
|
return cachedProviders().map((p) => ({
|
|
31
14
|
value: p.id,
|
|
32
15
|
description: `${p.endpoint_count} endpoints`,
|
|
33
16
|
}));
|
|
34
17
|
}
|
|
35
|
-
/** Endpoint paths within one provider, for `aisa run <slug> <TAB>`. */
|
|
36
|
-
function endpointPaths(slug) {
|
|
37
|
-
for (const provider of cachedProviders()) {
|
|
38
|
-
const detail = readCache(`catalog/${cacheScope()}/${provider.id.replace(/[^\w.-]/g, "_")}.json`)?.data.api;
|
|
39
|
-
if (!detail)
|
|
40
|
-
continue;
|
|
41
|
-
const endpoints = (detail.endpoint_groups || []).flatMap((g) => g.endpoints || []);
|
|
42
|
-
const matching = endpoints.filter((e) => e.path.replace(/^\/apis\/v1\//, "").startsWith(`${slug}/`));
|
|
43
|
-
if (matching.length > 0) {
|
|
44
|
-
return matching.map((e) => ({
|
|
45
|
-
value: `/${e.path.replace(/^\/apis\/v1\//, "").split("/").slice(1).join("/")}`,
|
|
46
|
-
description: e.name,
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
18
|
function skillSlugs() {
|
|
53
19
|
const index = readCache("skills/tree.json")?.data;
|
|
54
20
|
if (!index)
|
|
@@ -73,38 +39,10 @@ function modelIds() {
|
|
|
73
39
|
const STATIC = {
|
|
74
40
|
agent: Object.keys(AGENT_DIRS).concat("all").map((value) => ({ value })),
|
|
75
41
|
mcpAgent: Object.keys(MCP_CONFIGS).concat("all").map((value) => ({ value })),
|
|
76
|
-
|
|
77
|
-
searchType: [
|
|
78
|
-
{ value: "tavily" },
|
|
79
|
-
{ value: "youtube" },
|
|
80
|
-
{ value: "scholar" },
|
|
81
|
-
{ value: "smart", description: "currently degraded" },
|
|
82
|
-
{ value: "full", description: "currently degraded" },
|
|
83
|
-
],
|
|
84
|
-
stockField: [
|
|
85
|
-
"info",
|
|
86
|
-
"estimates",
|
|
87
|
-
"financials",
|
|
88
|
-
"filings",
|
|
89
|
-
"insider",
|
|
90
|
-
"institutional",
|
|
91
|
-
"news",
|
|
92
|
-
].map((value) => ({ value })),
|
|
93
|
-
configKey: ["defaultModel", "baseUrl", "outputFormat", "twitterCookies", "twitterProxy"].map((value) => ({ value })),
|
|
42
|
+
configKey: ["defaultModel", "baseUrl", "routerUrl", "outputFormat"].map((value) => ({ value })),
|
|
94
43
|
template: ["default", "llm", "search", "finance", "twitter", "video"].map((value) => ({ value })),
|
|
95
|
-
videoModel: Object.entries(VIDEO_MODELS).map(([value, spec]) => ({
|
|
96
|
-
value,
|
|
97
|
-
description: spec.requiresMedia ? `needs --media ${spec.requiresMedia}` : undefined,
|
|
98
|
-
})),
|
|
99
|
-
mediaType: MEDIA_TYPES.map((value) => ({ value: `${value}=` })),
|
|
100
44
|
apiCategory: API_CATEGORIES.map((value) => ({ value })),
|
|
101
|
-
period: ["current", "1d", "7d", "30d", "90d", "1y"].map((value) => ({ value })),
|
|
102
|
-
cryptoSource: ["coingecko", "financial"].map((value) => ({ value })),
|
|
103
45
|
shell: SHELLS.map((value) => ({ value })),
|
|
104
|
-
crypto: Object.keys(COINGECKO_IDS).map((value) => ({
|
|
105
|
-
value: value.toUpperCase(),
|
|
106
|
-
description: COINGECKO_IDS[value],
|
|
107
|
-
})),
|
|
108
46
|
};
|
|
109
47
|
/**
|
|
110
48
|
* Values for an option that takes an argument, keyed by "command path --option".
|
|
@@ -117,36 +55,21 @@ const OPTION_VALUES = [
|
|
|
117
55
|
["skills list --category", () => skillCategories()],
|
|
118
56
|
["skills init --template", () => STATIC.template],
|
|
119
57
|
["api list --category", () => STATIC.apiCategory],
|
|
120
|
-
["api search --provider", () => providerIds()],
|
|
121
|
-
["api code --lang", () => STATIC.lang],
|
|
122
|
-
["code --lang", () => STATIC.lang],
|
|
123
|
-
["video create --model", () => STATIC.videoModel],
|
|
124
|
-
["video create --media", () => STATIC.mediaType],
|
|
125
58
|
["chat --model", () => modelIds()],
|
|
126
|
-
["web-search --type", () => STATIC.searchType],
|
|
127
|
-
["stock --field", () => STATIC.stockField],
|
|
128
|
-
["crypto --period", () => STATIC.period],
|
|
129
|
-
["crypto --source", () => STATIC.cryptoSource],
|
|
130
59
|
["models --provider", () => [...new Set(modelIds().map((m) => m.description))].filter(Boolean).map((value) => ({ value: value }))],
|
|
131
60
|
];
|
|
132
61
|
/**
|
|
133
62
|
* Positional-argument candidates, keyed by command path and argument index.
|
|
134
63
|
* Callbacks receive the positionals parsed so far — never the raw word list,
|
|
135
64
|
* whose last entry may be an option or an option's value
|
|
136
|
-
* (`
|
|
65
|
+
* (`api show financial --json <TAB>`).
|
|
137
66
|
*/
|
|
138
67
|
const POSITIONAL = [
|
|
139
|
-
["run", 0, () => runSlugs()],
|
|
140
|
-
["run", 1, (positionals) => endpointPaths(positionals[0])],
|
|
141
68
|
["api show", 0, () => providerIds()],
|
|
142
|
-
["api code", 0, () => runSlugs()],
|
|
143
|
-
["api code", 1, (positionals) => endpointPaths(positionals[0])],
|
|
144
|
-
["code", 0, () => runSlugs()],
|
|
145
69
|
["models show", 0, () => modelIds()],
|
|
146
70
|
["skills show", 0, () => skillSlugs()],
|
|
147
71
|
["skills install", 0, () => skillSlugs()],
|
|
148
72
|
["skills remove", 0, () => skillSlugs()],
|
|
149
|
-
["crypto", 0, () => STATIC.crypto],
|
|
150
73
|
["config set", 0, () => STATIC.configKey],
|
|
151
74
|
["config get", 0, () => STATIC.configKey],
|
|
152
75
|
["completion", 0, () => STATIC.shell],
|
package/dist/config.js
CHANGED
|
@@ -1,22 +1,88 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { ENV_VAR_NAME } from "./constants.js";
|
|
6
|
+
// ~/.aisa is the credential home: one visible, portable, platform-stable
|
|
7
|
+
// place for the key, next door to ~/.claude and ~/.codex. The conf store
|
|
8
|
+
// (platform-specific path, invisible to other tools) remains a legacy read
|
|
9
|
+
// source and a sync target so older CLI versions keep working.
|
|
10
|
+
const aisaDir = () => join(homedir(), ".aisa");
|
|
11
|
+
const keyFile = () => join(aisaDir(), "key");
|
|
12
|
+
function readKeyFile() {
|
|
13
|
+
try {
|
|
14
|
+
const k = readFileSync(keyFile(), "utf-8").trim();
|
|
15
|
+
return k || undefined;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeKeyFile(key) {
|
|
22
|
+
mkdirSync(aisaDir(), { recursive: true });
|
|
23
|
+
writeFileSync(keyFile(), key + "\n", { mode: 0o600 });
|
|
24
|
+
chmodSync(keyFile(), 0o600);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 0600, because this file holds credentials.
|
|
28
|
+
*
|
|
29
|
+
* conf defaults to 0o666 (0644 after a typical umask), so until 2026-08-24
|
|
30
|
+
* every install left the API key — and the Twitter session cookies, which are
|
|
31
|
+
* account access rather than something revocable — world-readable on shared
|
|
32
|
+
* machines. The key file next door was already 0600; this closes the gap on
|
|
33
|
+
* the store that mirrors it.
|
|
34
|
+
*
|
|
35
|
+
* The mirror itself stays: published CLIs up to 0.3.0 know nothing about
|
|
36
|
+
* ~/.aisa/key and read the key only from here, so dropping the write would
|
|
37
|
+
* silently log out anyone still running one alongside a newer copy.
|
|
38
|
+
*/
|
|
39
|
+
const CONFIG_FILE_MODE = 0o600;
|
|
3
40
|
const config = new Conf({
|
|
4
41
|
projectName: "aisa-cli",
|
|
42
|
+
configFileMode: CONFIG_FILE_MODE,
|
|
5
43
|
schema: {
|
|
6
44
|
apiKey: { type: "string", default: "" },
|
|
7
45
|
defaultModel: { type: "string", default: "gpt-4.1-mini" },
|
|
8
46
|
baseUrl: { type: "string", default: "https://api.aisa.one/v1" },
|
|
47
|
+
routerUrl: { type: "string", default: "" },
|
|
9
48
|
outputFormat: { type: "string", default: "text" },
|
|
49
|
+
// Set by --lang or the page's picker; read by both renderers so they
|
|
50
|
+
// never end up in different languages.
|
|
51
|
+
lang: { type: "string", default: "" },
|
|
10
52
|
twitterCookies: { type: "string", default: "" },
|
|
11
53
|
twitterProxy: { type: "string", default: "" },
|
|
12
54
|
},
|
|
13
55
|
});
|
|
56
|
+
// configFileMode only applies when conf writes. Every install that ran before
|
|
57
|
+
// this change already has the file on disk at 0644, and a user who never logs
|
|
58
|
+
// in again would keep it — so tighten what is already there, once, at startup.
|
|
59
|
+
// Best-effort: a config we cannot chmod is still a config we can read.
|
|
60
|
+
try {
|
|
61
|
+
chmodSync(config.path, CONFIG_FILE_MODE);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* not ours to chmod, or gone — neither is worth failing a command over */
|
|
65
|
+
}
|
|
14
66
|
export function getApiKey() {
|
|
15
67
|
const envKey = process.env[ENV_VAR_NAME];
|
|
16
68
|
if (envKey)
|
|
17
69
|
return envKey;
|
|
70
|
+
const fileKey = readKeyFile();
|
|
71
|
+
if (fileKey)
|
|
72
|
+
return fileKey;
|
|
18
73
|
const stored = config.get("apiKey");
|
|
19
|
-
|
|
74
|
+
if (stored) {
|
|
75
|
+
// Legacy location — migrate on first read so every other tool (wrappers,
|
|
76
|
+
// scripts) finds the key at the one agreed place from now on.
|
|
77
|
+
try {
|
|
78
|
+
writeKeyFile(stored);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* migration is best-effort; the key itself is still returned */
|
|
82
|
+
}
|
|
83
|
+
return stored;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
20
86
|
}
|
|
21
87
|
export function requireApiKey() {
|
|
22
88
|
const key = getApiKey();
|
|
@@ -27,15 +93,23 @@ export function requireApiKey() {
|
|
|
27
93
|
return key;
|
|
28
94
|
}
|
|
29
95
|
export function setApiKey(key) {
|
|
96
|
+
writeKeyFile(key);
|
|
30
97
|
config.set("apiKey", key);
|
|
31
98
|
}
|
|
32
99
|
export function clearApiKey() {
|
|
100
|
+
try {
|
|
101
|
+
if (existsSync(keyFile()))
|
|
102
|
+
unlinkSync(keyFile());
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* the conf delete below still applies */
|
|
106
|
+
}
|
|
33
107
|
config.delete("apiKey");
|
|
34
108
|
}
|
|
35
109
|
export function getKeySource() {
|
|
36
110
|
if (process.env[ENV_VAR_NAME])
|
|
37
111
|
return "env";
|
|
38
|
-
if (config.get("apiKey"))
|
|
112
|
+
if (readKeyFile() || config.get("apiKey"))
|
|
39
113
|
return "config";
|
|
40
114
|
return "none";
|
|
41
115
|
}
|