@aisa-one/cli 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +165 -1
  2. package/LICENSE +21 -0
  3. package/README.md +164 -170
  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 +45 -11
  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 +59 -0
  38. package/dist/commands/mcp.js +122 -44
  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 +306 -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.d.ts +3 -0
  64. package/dist/config.js +80 -3
  65. package/dist/constants.d.ts +51 -38
  66. package/dist/constants.js +53 -109
  67. package/dist/index.js +152 -355
  68. package/dist/json-text.d.ts +12 -0
  69. package/dist/json-text.js +60 -0
  70. package/dist/router.d.ts +39 -0
  71. package/dist/router.js +59 -0
  72. package/dist/skills-registry.js +12 -3
  73. package/dist/utils/banner.d.ts +9 -0
  74. package/dist/utils/banner.js +41 -0
  75. package/dist/utils/browser.d.ts +19 -0
  76. package/dist/utils/browser.js +31 -0
  77. package/dist/utils/exec.d.ts +43 -0
  78. package/dist/utils/exec.js +61 -0
  79. package/dist/utils/http.d.ts +53 -0
  80. package/dist/utils/http.js +124 -0
  81. package/dist/utils/install-method.d.ts +36 -0
  82. package/dist/utils/install-method.js +48 -0
  83. package/dist/utils/journal.d.ts +57 -0
  84. package/dist/utils/journal.js +188 -0
  85. package/dist/utils/key-check.d.ts +20 -0
  86. package/dist/utils/key-check.js +24 -0
  87. package/dist/utils/update-check.d.ts +23 -0
  88. package/dist/utils/update-check.js +116 -0
  89. package/dist/utils/width.d.ts +32 -0
  90. package/dist/utils/width.js +73 -0
  91. package/package.json +8 -4
  92. package/vscode-extension/README.md +6 -0
  93. package/vscode-extension/aisa-vscode.vsix +0 -0
  94. package/dist/commands/finance.d.ts +0 -17
  95. package/dist/commands/finance.js +0 -342
  96. package/dist/commands/run.d.ts +0 -28
  97. package/dist/commands/run.js +0 -182
  98. package/dist/commands/search.d.ts +0 -9
  99. package/dist/commands/search.js +0 -81
  100. package/dist/commands/twitter.d.ts +0 -138
  101. package/dist/commands/twitter.js +0 -374
  102. package/dist/commands/video.d.ts +0 -49
  103. 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
+ }
@@ -1,8 +1,67 @@
1
+ /**
2
+ * `aisa mcp setup` — write real, per-client MCP entries from the live manifest.
3
+ *
4
+ * Three rules, each one a lesson from the version this replaces:
5
+ *
6
+ * 1. The server list comes from the platform's discovery manifest at setup
7
+ * time, never from this file. Servers ship without a CLI release.
8
+ * 2. Nothing is written unless the manifest was actually fetched. The old
9
+ * command wrote first and verified never, which is how a dead hostname
10
+ * sat in users' configs across releases.
11
+ * 3. Each client gets the shape it executes: `{url}` for clients that dial
12
+ * HTTP themselves, an `npx mcp-remote` stdio bridge for the ones that only
13
+ * spawn processes. One shape for all three was the old bug.
14
+ *
15
+ * Auth is a choice the user already made: with an API key configured, entries
16
+ * carry it as a Bearer header (zero-interaction, but the key lands in each
17
+ * client's config file); without one, entries carry no credentials and the
18
+ * server's 401 challenge drives the client through the Clerk OAuth flow.
19
+ */
20
+ export interface LiveServer {
21
+ slug: string;
22
+ name: string;
23
+ endpoint: string;
24
+ toolCount: number;
25
+ /** One-paragraph capability description from the manifest — the single
26
+ * source of truth for what a server does; rendered on the connect page. */
27
+ description: string;
28
+ category: string;
29
+ }
30
+ export declare const stripped: (name: string) => string;
31
+ /**
32
+ * Which servers exist, asked of the host that runs them.
33
+ *
34
+ * The index is preferred and the manifest is the fallback, in that order for
35
+ * one reason: the manifest is a snapshot committed by hand, and on 2026-08-25
36
+ * it was three servers and 72 tools behind what the host was actually serving.
37
+ * Anything derived from a running process cannot drift that way.
38
+ *
39
+ * The fallback is not defensive padding — the index is newer than some
40
+ * deployments, and a self-hosted gateway configured through `baseUrl` may
41
+ * never grow one. A user in either case should still get a working setup from
42
+ * a slightly stale list rather than an error.
43
+ */
44
+ export declare function fetchLiveServers(): Promise<LiveServer[]>;
1
45
  /** One config entry in the shape the client actually executes. */
2
46
  export declare function buildEntry(shape: "url" | "stdio", endpoint: string, key: string | undefined): Record<string, unknown>;
47
+ export type WriteResult = {
48
+ ok: true;
49
+ written: number;
50
+ } | {
51
+ ok: false;
52
+ reason: string;
53
+ };
54
+ /**
55
+ * Write the chosen servers (plus the docs server) into one client's config
56
+ * file. Shared by `aisa mcp setup` and `aisa connect` so the two entry points
57
+ * cannot drift in how they treat existing files: unparseable JSON is refused,
58
+ * never replaced, and only the known-dead legacy entry is cleaned up.
59
+ */
60
+ export declare function writeClientConfig(agent: string, chosen: LiveServer[], key: string | undefined): WriteResult;
3
61
  export declare function mcpSetupAction(options: {
4
62
  agent?: string;
5
63
  all?: boolean;
64
+ yes?: boolean;
6
65
  }): Promise<void>;
7
66
  /**
8
67
  * `aisa mcp status` — what is configured, and does it actually answer.