@aisa-one/cli 0.2.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -1
- package/LICENSE +21 -0
- package/README.md +163 -169
- package/dist/api.d.ts +10 -0
- package/dist/api.js +14 -3
- package/dist/catalog.d.ts +1 -18
- package/dist/catalog.js +1 -24
- package/dist/cli-error.d.ts +17 -0
- package/dist/cli-error.js +25 -0
- package/dist/cli-guidance.d.ts +14 -0
- package/dist/cli-guidance.js +55 -0
- package/dist/commands/account.d.ts +17 -0
- package/dist/commands/account.js +45 -1
- package/dist/commands/api.d.ts +0 -11
- package/dist/commands/api.js +6 -175
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +43 -9
- package/dist/commands/brand-logos.d.ts +6 -0
- package/dist/commands/brand-logos.js +21 -0
- package/dist/commands/configCmd.js +8 -3
- package/dist/commands/connect-shared.d.ts +136 -0
- package/dist/commands/connect-shared.js +143 -0
- package/dist/commands/connect-t2.d.ts +18 -0
- package/dist/commands/connect-t2.js +1599 -0
- package/dist/commands/connect-terminal.d.ts +65 -0
- package/dist/commands/connect-terminal.js +822 -0
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.js +3181 -0
- package/dist/commands/flow.d.ts +685 -0
- package/dist/commands/flow.js +697 -0
- package/dist/commands/install.d.ts +91 -0
- package/dist/commands/install.js +258 -0
- package/dist/commands/llm-config.d.ts +103 -0
- package/dist/commands/llm-config.js +452 -0
- package/dist/commands/manifest.d.ts +68 -0
- package/dist/commands/manifest.js +90 -0
- package/dist/commands/mcp.d.ts +79 -2
- package/dist/commands/mcp.js +262 -48
- package/dist/commands/oauth-login.d.ts +33 -0
- package/dist/commands/oauth-login.js +333 -0
- package/dist/commands/prompt.d.ts +96 -0
- package/dist/commands/prompt.js +397 -0
- package/dist/commands/serve-results.d.ts +61 -0
- package/dist/commands/serve-results.js +245 -0
- package/dist/commands/serve-signin.d.ts +28 -0
- package/dist/commands/serve-signin.js +90 -0
- package/dist/commands/signin-page.d.ts +49 -0
- package/dist/commands/signin-page.js +250 -0
- package/dist/commands/skills.js +87 -50
- package/dist/commands/tool-help.d.ts +69 -0
- package/dist/commands/tool-help.js +189 -0
- package/dist/commands/tool-input.d.ts +22 -0
- package/dist/commands/tool-input.js +220 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.js +307 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +92 -0
- package/dist/commands/vscode.d.ts +65 -0
- package/dist/commands/vscode.js +173 -0
- package/dist/commands/wrappers.d.ts +23 -0
- package/dist/commands/wrappers.js +102 -0
- package/dist/completion.js +3 -80
- package/dist/config.js +76 -2
- package/dist/constants.d.ts +78 -38
- package/dist/constants.js +80 -102
- package/dist/index.js +155 -357
- package/dist/json-text.d.ts +12 -0
- package/dist/json-text.js +60 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +59 -0
- package/dist/skills-registry.js +12 -3
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/browser.d.ts +19 -0
- package/dist/utils/browser.js +31 -0
- package/dist/utils/exec.d.ts +43 -0
- package/dist/utils/exec.js +61 -0
- package/dist/utils/http.d.ts +53 -0
- package/dist/utils/http.js +124 -0
- package/dist/utils/install-method.d.ts +36 -0
- package/dist/utils/install-method.js +48 -0
- package/dist/utils/journal.d.ts +57 -0
- package/dist/utils/journal.js +188 -0
- package/dist/utils/key-check.d.ts +20 -0
- package/dist/utils/key-check.js +24 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +116 -0
- package/dist/utils/width.d.ts +32 -0
- package/dist/utils/width.js +73 -0
- package/package.json +8 -4
- package/vscode-extension/README.md +6 -0
- package/vscode-extension/aisa-vscode.vsix +0 -0
- package/dist/commands/finance.d.ts +0 -17
- package/dist/commands/finance.js +0 -342
- package/dist/commands/run.d.ts +0 -28
- package/dist/commands/run.js +0 -182
- package/dist/commands/search.d.ts +0 -9
- package/dist/commands/search.js +0 -81
- package/dist/commands/twitter.d.ts +0 -138
- package/dist/commands/twitter.js +0 -374
- package/dist/commands/video.d.ts +0 -49
- package/dist/commands/video.js +0 -311
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as TOML from "smol-toml";
|
|
4
|
+
import { expandHome, ensureDir } from "../utils/file.js";
|
|
5
|
+
import { LLM_BASE_URL, LLM_RESPONSES_BASE_URL, AISA_PROVIDER_ID } from "../constants.js";
|
|
6
|
+
/**
|
|
7
|
+
* Pointing a coding agent's model traffic at AIsa.
|
|
8
|
+
*
|
|
9
|
+
* Both agents support this through their own documented settings — no wrapper,
|
|
10
|
+
* no proxy process, nothing injected at runtime. Claude Code reads an `env`
|
|
11
|
+
* block from settings.json; Codex takes a named provider in config.toml. We
|
|
12
|
+
* write one block each and can remove exactly what we wrote.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is reversible by key, never by rewriting the file: a user's
|
|
15
|
+
* own settings live in the same documents and must survive both directions.
|
|
16
|
+
*/
|
|
17
|
+
/** Written into every managed block so removal never has to guess. */
|
|
18
|
+
const MANAGED_KEYS_CLAUDE = [
|
|
19
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
20
|
+
"ANTHROPIC_BASE_URL",
|
|
21
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
22
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
23
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Defaults follow the agent, not our preference.
|
|
27
|
+
*
|
|
28
|
+
* Each coding agent is tuned around a model family: its system prompt, tool
|
|
29
|
+
* schemas and patch format were written and evaluated against one. Codex is
|
|
30
|
+
* OpenAI's, and `gpt-5.3-codex` is the model trained for it; Claude Code is
|
|
31
|
+
* Anthropic's. Sending either somewhere else works — the gateway speaks both
|
|
32
|
+
* protocols — but it is not the pairing the agent was built for, and it is
|
|
33
|
+
* not what a user installing that agent expects. Both remain switchable.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_MODELS = {
|
|
36
|
+
model: "claude-sonnet-5",
|
|
37
|
+
smallModel: "claude-haiku-4-5-20251001",
|
|
38
|
+
contextWindow: 200_000,
|
|
39
|
+
};
|
|
40
|
+
export const CODEX_DEFAULT_MODELS = {
|
|
41
|
+
model: "gpt-5.3-codex",
|
|
42
|
+
smallModel: "gpt-5.4-mini",
|
|
43
|
+
contextWindow: 400_000,
|
|
44
|
+
};
|
|
45
|
+
/** The default pairing for one agent. */
|
|
46
|
+
export function defaultModelsFor(client) {
|
|
47
|
+
return client === "codex" ? CODEX_DEFAULT_MODELS : DEFAULT_MODELS;
|
|
48
|
+
}
|
|
49
|
+
// ── Claude Code ─────────────────────────────────────────────────────────────
|
|
50
|
+
const claudeSettingsPath = () => expandHome("~/.claude/settings.json");
|
|
51
|
+
function readJson(path) {
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return {};
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A file we cannot parse is a file we do not own: refuse rather than
|
|
59
|
+
// replace someone's hand-edited settings.
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function writeClaudeCodeLLM(apiKey, models = DEFAULT_MODELS) {
|
|
64
|
+
const path = claudeSettingsPath();
|
|
65
|
+
const settings = readJson(path);
|
|
66
|
+
if (settings === null)
|
|
67
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
68
|
+
const env = { ...(settings.env ?? {}) };
|
|
69
|
+
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
70
|
+
env.ANTHROPIC_BASE_URL = LLM_BASE_URL;
|
|
71
|
+
// Claude Code asks for three tiers by name; map the two it will actually
|
|
72
|
+
// reach for and leave opus pointed at the same workhorse.
|
|
73
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = models.smallModel;
|
|
74
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = models.model;
|
|
75
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = models.model;
|
|
76
|
+
settings.env = env;
|
|
77
|
+
ensureDir(join(path, ".."));
|
|
78
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
79
|
+
return { ok: true, path };
|
|
80
|
+
}
|
|
81
|
+
export function removeClaudeCodeLLM() {
|
|
82
|
+
const path = claudeSettingsPath();
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return { ok: true, path };
|
|
85
|
+
const settings = readJson(path);
|
|
86
|
+
if (settings === null)
|
|
87
|
+
return { ok: false, reason: `${path} is not valid JSON` };
|
|
88
|
+
const env = settings.env;
|
|
89
|
+
if (!env)
|
|
90
|
+
return { ok: true, path };
|
|
91
|
+
for (const key of MANAGED_KEYS_CLAUDE)
|
|
92
|
+
delete env[key];
|
|
93
|
+
// An env block that only ever held our keys should not linger as `{}`.
|
|
94
|
+
if (Object.keys(env).length === 0)
|
|
95
|
+
delete settings.env;
|
|
96
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
97
|
+
return { ok: true, path };
|
|
98
|
+
}
|
|
99
|
+
// ── Codex ───────────────────────────────────────────────────────────────────
|
|
100
|
+
const codexConfigPath = () => expandHome("~/.codex/config.toml");
|
|
101
|
+
/** Codex ≥0.149: `--profile <name>` layers this file on top of the base
|
|
102
|
+
* config rather than reading a `[profiles.<name>]` table — see
|
|
103
|
+
* writeCodexAisaProfile for the migration this replaced. */
|
|
104
|
+
const codexAisaProfilePath = () => expandHome("~/.codex/aisa.config.toml");
|
|
105
|
+
function readToml(path) {
|
|
106
|
+
if (!existsSync(path))
|
|
107
|
+
return {};
|
|
108
|
+
try {
|
|
109
|
+
return TOML.parse(readFileSync(path, "utf-8"));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function writeToml(path, config) {
|
|
116
|
+
ensureDir(join(path, ".."));
|
|
117
|
+
writeFileSync(path, TOML.stringify(config) + "\n", "utf-8");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Codex talks the Responses API, not Chat Completions, so its base URL is the
|
|
121
|
+
* `/v1` root our gateway serves `/v1/responses` from — hence the separate
|
|
122
|
+
* constant and `wire_api = "responses"`.
|
|
123
|
+
*/
|
|
124
|
+
export function writeCodexLLM(apiKey, models = CODEX_DEFAULT_MODELS) {
|
|
125
|
+
const path = codexConfigPath();
|
|
126
|
+
const config = readToml(path);
|
|
127
|
+
if (config === null)
|
|
128
|
+
return { ok: false, reason: `${path} exists but is not valid TOML` };
|
|
129
|
+
// Declare the model before selecting it. Codex looks its metadata up in
|
|
130
|
+
// models.json and, finding nothing, warns and falls back to defaults —
|
|
131
|
+
// observed live against a gateway model on 2026-08-20.
|
|
132
|
+
writeCodexModelCatalog(models);
|
|
133
|
+
const providers = { ...(config.model_providers ?? {}) };
|
|
134
|
+
providers[AISA_PROVIDER_ID] = {
|
|
135
|
+
name: "AIsa",
|
|
136
|
+
base_url: LLM_RESPONSES_BASE_URL,
|
|
137
|
+
// Codex's own field name for a bearer credential on a custom provider.
|
|
138
|
+
experimental_bearer_token: apiKey,
|
|
139
|
+
wire_api: "responses",
|
|
140
|
+
};
|
|
141
|
+
config.model_providers = providers;
|
|
142
|
+
config.model_provider = AISA_PROVIDER_ID;
|
|
143
|
+
config.model = models.model;
|
|
144
|
+
config.model_catalog_json = CODEX_MODELS_PATH;
|
|
145
|
+
writeToml(path, config);
|
|
146
|
+
return { ok: true, path };
|
|
147
|
+
}
|
|
148
|
+
/** `~/.codex/models.json`, as Codex spells it in config. */
|
|
149
|
+
const CODEX_MODELS_PATH = "~/.codex/models.json";
|
|
150
|
+
/**
|
|
151
|
+
* Declare one model in Codex's catalog, preserving any other entries.
|
|
152
|
+
* Best-effort: a catalog we cannot read is left alone and Codex falls back to
|
|
153
|
+
* its warning, which is noisy but not fatal — that must not cost the user the
|
|
154
|
+
* provider config that does work.
|
|
155
|
+
*/
|
|
156
|
+
function writeCodexModelCatalog(models) {
|
|
157
|
+
const path = expandHome(CODEX_MODELS_PATH);
|
|
158
|
+
let catalog = { models: [] };
|
|
159
|
+
if (existsSync(path)) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
162
|
+
if (parsed && Array.isArray(parsed.models))
|
|
163
|
+
catalog = parsed;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const others = catalog.models.filter((m) => m.slug !== models.model);
|
|
170
|
+
catalog.models = [
|
|
171
|
+
...others,
|
|
172
|
+
{
|
|
173
|
+
slug: models.model,
|
|
174
|
+
display_name: models.model,
|
|
175
|
+
description: `${models.model} through the AIsa gateway`,
|
|
176
|
+
// Required by Codex, and each level is a struct rather than a string:
|
|
177
|
+
// it rejects the catalog outright otherwise (both shapes observed live).
|
|
178
|
+
default_reasoning_level: "medium",
|
|
179
|
+
supported_reasoning_levels: [
|
|
180
|
+
{ effort: "low", description: "Light reasoning" },
|
|
181
|
+
{ effort: "medium", description: "Balanced reasoning" },
|
|
182
|
+
{ effort: "high", description: "Deep reasoning" },
|
|
183
|
+
],
|
|
184
|
+
shell_type: "shell_command",
|
|
185
|
+
visibility: "list",
|
|
186
|
+
supported_in_api: true,
|
|
187
|
+
priority: 0,
|
|
188
|
+
base_instructions: "",
|
|
189
|
+
supports_reasoning_summaries: true,
|
|
190
|
+
default_reasoning_summary: "none",
|
|
191
|
+
support_verbosity: false,
|
|
192
|
+
apply_patch_tool_type: "freeform",
|
|
193
|
+
// Codex's own output-truncation ceiling in bytes; unrelated to context.
|
|
194
|
+
truncation_policy: { mode: "bytes", limit: 10_000 },
|
|
195
|
+
context_window: models.contextWindow,
|
|
196
|
+
max_context_window: models.contextWindow,
|
|
197
|
+
effective_context_window_percent: 95,
|
|
198
|
+
supports_parallel_tool_calls: true,
|
|
199
|
+
experimental_supported_tools: [],
|
|
200
|
+
input_modalities: ["text"],
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
ensureDir(join(path, ".."));
|
|
204
|
+
writeFileSync(path, JSON.stringify(catalog, null, 2) + "\n", "utf-8");
|
|
205
|
+
}
|
|
206
|
+
export function removeCodexLLM() {
|
|
207
|
+
const path = codexConfigPath();
|
|
208
|
+
if (!existsSync(path))
|
|
209
|
+
return { ok: true, path };
|
|
210
|
+
const config = readToml(path);
|
|
211
|
+
if (config === null)
|
|
212
|
+
return { ok: false, reason: `${path} is not valid TOML` };
|
|
213
|
+
const providers = config.model_providers;
|
|
214
|
+
if (providers) {
|
|
215
|
+
delete providers[AISA_PROVIDER_ID];
|
|
216
|
+
if (Object.keys(providers).length === 0)
|
|
217
|
+
delete config.model_providers;
|
|
218
|
+
}
|
|
219
|
+
// Only surrender the top-level selection if it is still ours: a user who
|
|
220
|
+
// switched back to another provider keeps their choice.
|
|
221
|
+
if (config.model_provider === AISA_PROVIDER_ID) {
|
|
222
|
+
delete config.model_provider;
|
|
223
|
+
delete config.model;
|
|
224
|
+
delete config.model_catalog_json;
|
|
225
|
+
}
|
|
226
|
+
// models.json is declarative metadata: harmless if left, and removing an
|
|
227
|
+
// entry Codex may share with another provider would be the riskier move.
|
|
228
|
+
writeToml(path, config);
|
|
229
|
+
return { ok: true, path };
|
|
230
|
+
}
|
|
231
|
+
// ── Codex MCP ───────────────────────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Codex keeps MCP servers in the same config.toml. Note `http_headers`, not
|
|
234
|
+
* `headers` — Codex's field name, and getting it wrong fails silently at the
|
|
235
|
+
* first authenticated call rather than at load.
|
|
236
|
+
*/
|
|
237
|
+
export function writeCodexMCP(servers, apiKey) {
|
|
238
|
+
const path = codexConfigPath();
|
|
239
|
+
const config = readToml(path);
|
|
240
|
+
if (config === null)
|
|
241
|
+
return { ok: false, reason: `${path} exists but is not valid TOML` };
|
|
242
|
+
const mcp = { ...(config.mcp_servers ?? {}) };
|
|
243
|
+
for (const s of servers) {
|
|
244
|
+
mcp[`aisa-${s.slug}`] = {
|
|
245
|
+
type: "http",
|
|
246
|
+
url: s.endpoint,
|
|
247
|
+
...(apiKey ? { http_headers: { Authorization: `Bearer ${apiKey}` } } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
config.mcp_servers = mcp;
|
|
251
|
+
writeToml(path, config);
|
|
252
|
+
return { ok: true, path };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Swap an entry's env-var token reference for the literal Authorization header.
|
|
256
|
+
*
|
|
257
|
+
* `codex mcp add --bearer-token-env-var` is the official way to add a keyed
|
|
258
|
+
* entry without triggering the OAuth flow, but it records only the variable
|
|
259
|
+
* NAME — at runtime Codex reads the token from its own process environment,
|
|
260
|
+
* and nothing guarantees the user's shell exports it. `http_headers` is the
|
|
261
|
+
* field Codex accepts for a literal header (`bearer_token` is explicitly
|
|
262
|
+
* rejected by its config loader), and it matches what the Claude Code path
|
|
263
|
+
* already stores via --header. The env-var reference is removed so the entry
|
|
264
|
+
* has exactly one source of credentials.
|
|
265
|
+
*/
|
|
266
|
+
export function patchCodexMCPAuth(name, apiKey) {
|
|
267
|
+
const path = codexConfigPath();
|
|
268
|
+
const config = readToml(path);
|
|
269
|
+
if (config === null)
|
|
270
|
+
return { ok: false, reason: `${path} exists but is not valid TOML` };
|
|
271
|
+
const servers = config.mcp_servers;
|
|
272
|
+
const entry = servers?.[name];
|
|
273
|
+
if (!entry)
|
|
274
|
+
return { ok: false, reason: `no MCP entry named ${name} in ${path}` };
|
|
275
|
+
delete entry.bearer_token_env_var;
|
|
276
|
+
entry.http_headers = { Authorization: `Bearer ${apiKey}` };
|
|
277
|
+
writeToml(path, config);
|
|
278
|
+
return { ok: true, path };
|
|
279
|
+
}
|
|
280
|
+
// ── opencode ────────────────────────────────────────────────────────────────
|
|
281
|
+
//
|
|
282
|
+
// opencode has no `mcp add` command at all — its whole surface is one JSON
|
|
283
|
+
// document, ~/.config/opencode/opencode.json, validated against the schema
|
|
284
|
+
// at opencode.ai/config.json. Both the model provider and the MCP servers
|
|
285
|
+
// are keys in that document, so this section is two writers over the same
|
|
286
|
+
// file, with the same removal-by-key discipline as the other clients.
|
|
287
|
+
// `opencode mcp add` creates opencode.jsonc; the schema site says .json.
|
|
288
|
+
// Both are read by opencode — write whichever already exists (jsonc wins,
|
|
289
|
+
// since the official command creates it), so there is never a second file
|
|
290
|
+
// fighting the first.
|
|
291
|
+
function opencodeConfigPath() {
|
|
292
|
+
const jsonc = expandHome("~/.config/opencode/opencode.jsonc");
|
|
293
|
+
if (existsSync(jsonc))
|
|
294
|
+
return jsonc;
|
|
295
|
+
return expandHome("~/.config/opencode/opencode.json");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Register AIsa as a custom provider and point the default models at it.
|
|
299
|
+
*
|
|
300
|
+
* `@ai-sdk/anthropic` is the runtime (opencode loads providers as Vercel AI
|
|
301
|
+
* SDK packages): our gateway's /v1/messages face is live and verified with a
|
|
302
|
+
* real Claude Code session, and the anthropic SDK is the pairing that face
|
|
303
|
+
* was built for. Models are declared explicitly — a custom provider has no
|
|
304
|
+
* models.dev entry to inherit from.
|
|
305
|
+
*/
|
|
306
|
+
export function writeOpencodeLLM(apiKey, models = DEFAULT_MODELS) {
|
|
307
|
+
const path = opencodeConfigPath();
|
|
308
|
+
const config = readJson(path);
|
|
309
|
+
if (config === null)
|
|
310
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
311
|
+
config.$schema ??= "https://opencode.ai/config.json";
|
|
312
|
+
const provider = { ...(config.provider ?? {}) };
|
|
313
|
+
provider[AISA_PROVIDER_ID] = {
|
|
314
|
+
npm: "@ai-sdk/anthropic",
|
|
315
|
+
name: "AIsa",
|
|
316
|
+
options: { baseURL: `${LLM_BASE_URL}/v1`, apiKey },
|
|
317
|
+
models: {
|
|
318
|
+
[models.model]: { name: models.model },
|
|
319
|
+
[models.smallModel]: { name: models.smallModel },
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
config.provider = provider;
|
|
323
|
+
config.model = `${AISA_PROVIDER_ID}/${models.model}`;
|
|
324
|
+
config.small_model = `${AISA_PROVIDER_ID}/${models.smallModel}`;
|
|
325
|
+
ensureDir(join(path, ".."));
|
|
326
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
327
|
+
return { ok: true, path };
|
|
328
|
+
}
|
|
329
|
+
export function removeOpencodeLLM() {
|
|
330
|
+
const path = opencodeConfigPath();
|
|
331
|
+
if (!existsSync(path))
|
|
332
|
+
return { ok: true, path };
|
|
333
|
+
const config = readJson(path);
|
|
334
|
+
if (config === null)
|
|
335
|
+
return { ok: false, reason: `${path} is not valid JSON` };
|
|
336
|
+
const provider = config.provider;
|
|
337
|
+
if (provider) {
|
|
338
|
+
delete provider[AISA_PROVIDER_ID];
|
|
339
|
+
if (Object.keys(provider).length === 0)
|
|
340
|
+
delete config.provider;
|
|
341
|
+
}
|
|
342
|
+
// Only unset the defaults if they still point at us — a user who switched
|
|
343
|
+
// to another provider keeps their choice.
|
|
344
|
+
if (typeof config.model === "string" && config.model.startsWith(`${AISA_PROVIDER_ID}/`))
|
|
345
|
+
delete config.model;
|
|
346
|
+
if (typeof config.small_model === "string" && config.small_model.startsWith(`${AISA_PROVIDER_ID}/`))
|
|
347
|
+
delete config.small_model;
|
|
348
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
349
|
+
return { ok: true, path };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Write the MCP entries. With a key the entry carries the literal
|
|
353
|
+
* Authorization header; without one, `oauth: {}` tells opencode to run its
|
|
354
|
+
* own MCP OAuth (dynamic client registration per its schema).
|
|
355
|
+
*/
|
|
356
|
+
export function writeOpencodeMCP(servers, apiKey) {
|
|
357
|
+
const path = opencodeConfigPath();
|
|
358
|
+
const config = readJson(path);
|
|
359
|
+
if (config === null)
|
|
360
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
361
|
+
config.$schema ??= "https://opencode.ai/config.json";
|
|
362
|
+
const mcp = { ...(config.mcp ?? {}) };
|
|
363
|
+
for (const s of servers) {
|
|
364
|
+
mcp[`aisa-${s.slug}`] = {
|
|
365
|
+
type: "remote",
|
|
366
|
+
url: s.endpoint,
|
|
367
|
+
enabled: true,
|
|
368
|
+
...(apiKey ? { headers: { Authorization: `Bearer ${apiKey}` } } : { oauth: {} }),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
config.mcp = mcp;
|
|
372
|
+
ensureDir(join(path, ".."));
|
|
373
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
374
|
+
return { ok: true, path };
|
|
375
|
+
}
|
|
376
|
+
// ── backup mode ─────────────────────────────────────────────────────────────
|
|
377
|
+
//
|
|
378
|
+
// "Add AIsa as a backup" for a user who already has working models: nothing
|
|
379
|
+
// they rely on changes. Codex gets the provider plus an aisa.config.toml
|
|
380
|
+
// profile (its native mechanism — `codex --profile aisa` layers it in for
|
|
381
|
+
// one session, the default stays theirs); opencode gets the provider only
|
|
382
|
+
// (its TUI lists models across providers, switching is built in). Claude
|
|
383
|
+
// Code has no coexistence mechanism in its config, so its backup lives
|
|
384
|
+
// entirely outside: a settings file under ~/.aisa driven through --settings
|
|
385
|
+
// (see wrappers.ts).
|
|
386
|
+
/**
|
|
387
|
+
* Codex backup: provider + catalog in the base config, top-level defaults
|
|
388
|
+
* untouched, and a standalone aisa.config.toml that `codex --profile aisa`
|
|
389
|
+
* (or the codex-aisa wrapper) layers on top of it.
|
|
390
|
+
*
|
|
391
|
+
* That standalone file is Codex ≥0.149's replacement for the old
|
|
392
|
+
* `[profiles.aisa]` table — `--profile <name>` now means "layer
|
|
393
|
+
* `$CODEX_HOME/<name>.config.toml`", and Codex refuses to start at all if
|
|
394
|
+
* the legacy table for that name still exists in config.toml alongside it
|
|
395
|
+
* (hit live 2026-08-24, codex-cli 0.149.1: "cannot be used while
|
|
396
|
+
* config.toml contains legacy `profile = \"aisa\"` or `[profiles.aisa]`").
|
|
397
|
+
* A machine still carrying that table from an earlier version of this
|
|
398
|
+
* writer gets it removed here — a one-time, one-way migration; nothing a
|
|
399
|
+
* user typed into it themselves would collide with a section this narrow.
|
|
400
|
+
*/
|
|
401
|
+
export function writeCodexAisaProfile(apiKey, models = CODEX_DEFAULT_MODELS) {
|
|
402
|
+
const path = codexConfigPath();
|
|
403
|
+
const config = readToml(path);
|
|
404
|
+
if (config === null)
|
|
405
|
+
return { ok: false, reason: `${path} exists but is not valid TOML` };
|
|
406
|
+
writeCodexModelCatalog(models);
|
|
407
|
+
const providers = { ...(config.model_providers ?? {}) };
|
|
408
|
+
providers[AISA_PROVIDER_ID] = {
|
|
409
|
+
name: "AIsa",
|
|
410
|
+
base_url: LLM_RESPONSES_BASE_URL,
|
|
411
|
+
experimental_bearer_token: apiKey,
|
|
412
|
+
wire_api: "responses",
|
|
413
|
+
};
|
|
414
|
+
config.model_providers = providers;
|
|
415
|
+
config.model_catalog_json = CODEX_MODELS_PATH;
|
|
416
|
+
if (config.profiles && typeof config.profiles === "object") {
|
|
417
|
+
const profiles = { ...config.profiles };
|
|
418
|
+
delete profiles[AISA_PROVIDER_ID];
|
|
419
|
+
if (Object.keys(profiles).length > 0)
|
|
420
|
+
config.profiles = profiles;
|
|
421
|
+
else
|
|
422
|
+
delete config.profiles;
|
|
423
|
+
}
|
|
424
|
+
writeToml(path, config);
|
|
425
|
+
writeToml(codexAisaProfilePath(), { model_provider: AISA_PROVIDER_ID, model: models.model });
|
|
426
|
+
return { ok: true, path: codexAisaProfilePath() };
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* opencode backup: provider declared, `model`/`small_model` left alone — the
|
|
430
|
+
* user picks aisa/<model> in the TUI whenever they want it.
|
|
431
|
+
*/
|
|
432
|
+
export function writeOpencodeAisaBackup(apiKey, models = DEFAULT_MODELS) {
|
|
433
|
+
const path = opencodeConfigPath();
|
|
434
|
+
const config = readJson(path);
|
|
435
|
+
if (config === null)
|
|
436
|
+
return { ok: false, reason: `${path} exists but is not valid JSON` };
|
|
437
|
+
config.$schema ??= "https://opencode.ai/config.json";
|
|
438
|
+
const provider = { ...(config.provider ?? {}) };
|
|
439
|
+
provider[AISA_PROVIDER_ID] = {
|
|
440
|
+
npm: "@ai-sdk/anthropic",
|
|
441
|
+
name: "AIsa",
|
|
442
|
+
options: { baseURL: `${LLM_BASE_URL}/v1`, apiKey },
|
|
443
|
+
models: {
|
|
444
|
+
[models.model]: { name: models.model },
|
|
445
|
+
[models.smallModel]: { name: models.smallModel },
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
config.provider = provider;
|
|
449
|
+
ensureDir(join(path, ".."));
|
|
450
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
451
|
+
return { ok: true, path };
|
|
452
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { ROUTER_EXITS, type RouterCommandContract } from "./tool-help.js";
|
|
3
|
+
/**
|
|
4
|
+
* The whole command tree as JSON, for the reader this CLI actually has most of:
|
|
5
|
+
* another agent.
|
|
6
|
+
*
|
|
7
|
+
* `--help` is written for a person who can drill down. An agent gets one shot
|
|
8
|
+
* at a top-level dump, and Commander's default renders every subcommand as a
|
|
9
|
+
* bare `[options]` — the flags exist in its own metadata but never reach the
|
|
10
|
+
* page. Tested on 2026-08-24 by handing an LLM nothing but `aisa --help` and
|
|
11
|
+
* `aisa twitter --help`: it invented `--json` (the real flag is `--raw`) and
|
|
12
|
+
* could not find `api show` at all, because the `api` subtree is invisible
|
|
13
|
+
* from the top. Both are discovery failures, not reasoning failures, and both
|
|
14
|
+
* disappear once the tree is dumped in full.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is written per command: Commander already holds the flags,
|
|
17
|
+
* defaults, arguments and descriptions used to render help, so this walks that
|
|
18
|
+
* same metadata. New commands appear in the manifest the day they are added,
|
|
19
|
+
* with no second place to update.
|
|
20
|
+
*/
|
|
21
|
+
interface ManifestOption {
|
|
22
|
+
flags: string;
|
|
23
|
+
description: string;
|
|
24
|
+
default?: unknown;
|
|
25
|
+
/** Must this flag be passed at all. Nearly always false. */
|
|
26
|
+
mandatory: boolean;
|
|
27
|
+
/** "none" for a boolean switch, "required" for --f <v>, "optional" for --f [v]. */
|
|
28
|
+
value: "none" | "required" | "optional";
|
|
29
|
+
}
|
|
30
|
+
interface ManifestArgument {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
required: boolean;
|
|
34
|
+
variadic: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface ManifestCommand {
|
|
37
|
+
path: string;
|
|
38
|
+
description: string;
|
|
39
|
+
usage: string;
|
|
40
|
+
arguments: ManifestArgument[];
|
|
41
|
+
options: ManifestOption[];
|
|
42
|
+
subcommands: ManifestCommand[];
|
|
43
|
+
/** MCP operation this CLI command posts to. Absent on non-Router commands. */
|
|
44
|
+
mcp?: {
|
|
45
|
+
identifier: string;
|
|
46
|
+
path: string;
|
|
47
|
+
};
|
|
48
|
+
auth?: RouterCommandContract["auth"];
|
|
49
|
+
flow?: string;
|
|
50
|
+
enforced?: string[];
|
|
51
|
+
safety?: string[];
|
|
52
|
+
exits?: typeof ROUTER_EXITS;
|
|
53
|
+
sameRequestShapeAs?: RouterCommandContract["sameRequestShapeAs"];
|
|
54
|
+
examples?: RouterCommandContract["examples"];
|
|
55
|
+
}
|
|
56
|
+
export declare function buildManifest(program: Command, prefix?: string): ManifestCommand;
|
|
57
|
+
/** Walk down a command path: ["twitter", "search"] -> that one node. */
|
|
58
|
+
export declare function descend(root: ManifestCommand, path: string[]): ManifestCommand | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* A path argument, because the whole tree is ~15k tokens.
|
|
61
|
+
*
|
|
62
|
+
* An agent that needs to post a tweet should not have to read the definition
|
|
63
|
+
* of 87 commands to find out how — `aisa manifest twitter search` is under
|
|
64
|
+
* half a kilobyte. The full dump stays the default so that asking for
|
|
65
|
+
* everything still works, but the help line advertises the narrow form.
|
|
66
|
+
*/
|
|
67
|
+
export declare function manifestAction(program: Command, path?: string[]): void;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mcpForCommand } from "../cli-guidance.js";
|
|
2
|
+
import { ROUTER_EXITS, routerContractFor, } from "./tool-help.js";
|
|
3
|
+
/**
|
|
4
|
+
* Commander's `Option.required` means "this flag's *value* is required", not
|
|
5
|
+
* "this flag is required" — `--limit <n>` sets it while being entirely
|
|
6
|
+
* optional. Reporting it as `required` told an agent that almost every flag
|
|
7
|
+
* had to be passed, which is worse than saying nothing: it turns a correct
|
|
8
|
+
* command into an over-specified one. `mandatory` is the field that means
|
|
9
|
+
* what it sounds like, and value-ness gets its own name.
|
|
10
|
+
*/
|
|
11
|
+
function describeOption(o) {
|
|
12
|
+
return {
|
|
13
|
+
flags: o.flags,
|
|
14
|
+
description: o.description,
|
|
15
|
+
...(o.defaultValue === undefined ? {} : { default: o.defaultValue }),
|
|
16
|
+
mandatory: Boolean(o.mandatory),
|
|
17
|
+
value: o.required ? "required" : o.optional ? "optional" : "none",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function describeArgument(a) {
|
|
21
|
+
return {
|
|
22
|
+
name: a.name(),
|
|
23
|
+
description: a.description,
|
|
24
|
+
required: a.required,
|
|
25
|
+
variadic: a.variadic,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Hidden commands are excluded: `__complete` is shell plumbing, and listing
|
|
29
|
+
* it invites an agent to call it as if it were a feature. */
|
|
30
|
+
function visible(cmd) {
|
|
31
|
+
return cmd.commands.filter((c) => !c._hidden);
|
|
32
|
+
}
|
|
33
|
+
export function buildManifest(program, prefix = "") {
|
|
34
|
+
const path = prefix ? `${prefix} ${program.name()}` : program.name();
|
|
35
|
+
const args = program.registeredArguments ?? [];
|
|
36
|
+
const relative = path.replace(/^aisa\s+/, "");
|
|
37
|
+
const mcp = mcpForCommand(relative);
|
|
38
|
+
const contract = routerContractFor(relative);
|
|
39
|
+
return {
|
|
40
|
+
path,
|
|
41
|
+
description: program.description(),
|
|
42
|
+
usage: `${path} ${program.usage()}`.trim(),
|
|
43
|
+
arguments: args.map(describeArgument),
|
|
44
|
+
options: program.options.map(describeOption),
|
|
45
|
+
subcommands: visible(program).map((c) => buildManifest(c, path)),
|
|
46
|
+
...(mcp ? { mcp: { identifier: mcp.identifier, path: mcp.path } } : {}),
|
|
47
|
+
...(contract
|
|
48
|
+
? {
|
|
49
|
+
auth: contract.auth,
|
|
50
|
+
flow: contract.flow,
|
|
51
|
+
enforced: contract.enforced,
|
|
52
|
+
safety: contract.safety,
|
|
53
|
+
exits: ROUTER_EXITS,
|
|
54
|
+
examples: contract.examples,
|
|
55
|
+
...(contract.sameRequestShapeAs
|
|
56
|
+
? { sameRequestShapeAs: contract.sameRequestShapeAs }
|
|
57
|
+
: {}),
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Walk down a command path: ["twitter", "search"] -> that one node. */
|
|
63
|
+
export function descend(root, path) {
|
|
64
|
+
let node = root;
|
|
65
|
+
for (const step of path) {
|
|
66
|
+
node = node?.subcommands.find((c) => c.path.split(" ").pop() === step);
|
|
67
|
+
if (!node)
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A path argument, because the whole tree is ~15k tokens.
|
|
74
|
+
*
|
|
75
|
+
* An agent that needs to post a tweet should not have to read the definition
|
|
76
|
+
* of 87 commands to find out how — `aisa manifest twitter search` is under
|
|
77
|
+
* half a kilobyte. The full dump stays the default so that asking for
|
|
78
|
+
* everything still works, but the help line advertises the narrow form.
|
|
79
|
+
*/
|
|
80
|
+
export function manifestAction(program, path = []) {
|
|
81
|
+
const root = buildManifest(program);
|
|
82
|
+
const node = path.length ? descend(root, path) : root;
|
|
83
|
+
if (!node) {
|
|
84
|
+
console.error(`Unknown command: aisa ${path.join(" ")}`);
|
|
85
|
+
console.error(`Run \`aisa manifest\` with no arguments to see every command.`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(JSON.stringify(node, null, 2));
|
|
90
|
+
}
|