@offerpilot/axiomruntime 0.0.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 (99) hide show
  1. package/README.md +185 -0
  2. package/dist/cli/commands/add.js +29 -0
  3. package/dist/cli/commands/context.js +29 -0
  4. package/dist/cli/commands/doctor.js +62 -0
  5. package/dist/cli/commands/edit.js +85 -0
  6. package/dist/cli/commands/help.js +16 -0
  7. package/dist/cli/commands/list.js +25 -0
  8. package/dist/cli/commands/log.js +63 -0
  9. package/dist/cli/commands/memory.js +241 -0
  10. package/dist/cli/commands/report.js +35 -0
  11. package/dist/cli/commands/session.js +74 -0
  12. package/dist/cli/commands/setup.js +86 -0
  13. package/dist/cli/commands/status.js +50 -0
  14. package/dist/cli/commands/telegram.js +701 -0
  15. package/dist/cli/commands/use.js +108 -0
  16. package/dist/cli/commands/version.js +12 -0
  17. package/dist/cli/index.js +276 -0
  18. package/dist/cli/output/table.js +22 -0
  19. package/dist/cli/prompts/prompt.js +37 -0
  20. package/dist/cli/registry.js +16 -0
  21. package/dist/core/config/cache-store.js +193 -0
  22. package/dist/core/config/json-store.js +114 -0
  23. package/dist/core/config/paths.js +85 -0
  24. package/dist/core/config/providers-store.js +89 -0
  25. package/dist/core/config/schema.js +60 -0
  26. package/dist/core/config/session-store.js +30 -0
  27. package/dist/core/config/usage-store.js +18 -0
  28. package/dist/core/context/context-service.js +186 -0
  29. package/dist/core/integrations/integration-state.js +105 -0
  30. package/dist/core/logs/log-service.js +56 -0
  31. package/dist/core/memory/embedding-check.js +121 -0
  32. package/dist/core/memory/memory-config.js +122 -0
  33. package/dist/core/models/model-discovery.js +430 -0
  34. package/dist/core/models/model-filter.js +13 -0
  35. package/dist/core/providers/provider-service.js +212 -0
  36. package/dist/core/reports/report-service.js +166 -0
  37. package/dist/core/runner/command-resolver.js +60 -0
  38. package/dist/core/runner/engine-registry.js +93 -0
  39. package/dist/core/runner/fallback.js +114 -0
  40. package/dist/core/runner/openai-usage-http.js +82 -0
  41. package/dist/core/runner/openai-usage-proxy.js +1 -0
  42. package/dist/core/runner/openai-usage-recording.js +172 -0
  43. package/dist/core/runner/openai-usage-responses.js +469 -0
  44. package/dist/core/runner/openai-usage-server.js +319 -0
  45. package/dist/core/runner/openai-usage-types.js +1 -0
  46. package/dist/core/runner/tool-runner.js +138 -0
  47. package/dist/core/sessions/session-service.js +47 -0
  48. package/dist/core/status/doctor-service.js +391 -0
  49. package/dist/core/status/status-service.js +60 -0
  50. package/dist/core/types.js +1 -0
  51. package/dist/core/usage/pricing.js +113 -0
  52. package/dist/core/usage/usage-service.js +30 -0
  53. package/dist/core/utils/is-record.js +3 -0
  54. package/dist/server/index.js +28 -0
  55. package/dist/server/runtime-server.js +430 -0
  56. package/dist/telegram/bot-registry.js +80 -0
  57. package/dist/telegram/bot.js +128 -0
  58. package/dist/telegram/config.js +235 -0
  59. package/dist/telegram/engine/claude-engine.js +240 -0
  60. package/dist/telegram/engine/codex-engine.js +437 -0
  61. package/dist/telegram/engine/engine-utils.js +67 -0
  62. package/dist/telegram/engine/process-utils.js +132 -0
  63. package/dist/telegram/engine/registry.js +31 -0
  64. package/dist/telegram/engine/types.js +1 -0
  65. package/dist/telegram/handler-registry.js +28 -0
  66. package/dist/telegram/handlers/callback.js +311 -0
  67. package/dist/telegram/handlers/command.js +272 -0
  68. package/dist/telegram/handlers/document.js +108 -0
  69. package/dist/telegram/handlers/memory.js +305 -0
  70. package/dist/telegram/handlers/message.js +701 -0
  71. package/dist/telegram/handlers/provider.js +332 -0
  72. package/dist/telegram/handlers/setup.js +527 -0
  73. package/dist/telegram/handlers/usage.js +124 -0
  74. package/dist/telegram/index.js +93 -0
  75. package/dist/telegram/interaction/approval.js +108 -0
  76. package/dist/telegram/interaction/command-menu.js +253 -0
  77. package/dist/telegram/interaction/formatter.js +487 -0
  78. package/dist/telegram/interaction/keyboards.js +145 -0
  79. package/dist/telegram/interaction/progress-reporter.js +160 -0
  80. package/dist/telegram/interaction/prompt-middleware.js +168 -0
  81. package/dist/telegram/interaction/result-store.js +41 -0
  82. package/dist/telegram/interaction/token-budget.js +21 -0
  83. package/dist/telegram/interaction/tool-name.js +41 -0
  84. package/dist/telegram/lifecycle-registry.js +47 -0
  85. package/dist/telegram/log.js +46 -0
  86. package/dist/telegram/memory/memory-inject.js +52 -0
  87. package/dist/telegram/memory/memory-service.js +413 -0
  88. package/dist/telegram/memory/memory-store.js +216 -0
  89. package/dist/telegram/memory/types.js +1 -0
  90. package/dist/telegram/network-retry.js +22 -0
  91. package/dist/telegram/network.js +53 -0
  92. package/dist/telegram/session/manager.js +229 -0
  93. package/dist/telegram/session/store.js +363 -0
  94. package/dist/telegram/session/types.js +1 -0
  95. package/dist/telegram/supervisor.js +57 -0
  96. package/dist/telegram/templates/messages.js +1 -0
  97. package/docs/README.md +98 -0
  98. package/docs/USAGE.html +853 -0
  99. package/package.json +57 -0
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # AI Runtime(当前阶段:AI Gateway)
2
+
3
+ 本项目的长期目标是构建一套企业级 AI Runtime:让模型、Agent、记忆、知识、技能、工作流和工具在统一的运行、治理与学习体系中协作,并持续把个人经验沉淀为组织能力。
4
+
5
+ Runtime 才是产品。Memory、Agent 和 LLM 都是可以替换和组合的能力模块。
6
+
7
+ 当前可用版本是 Runtime 的 Gateway 基础层:通过统一的 `ai` 命令管理 Claude Code 和 Codex CLI 的 provider、模型、优先级、健康度、故障转移、用量、会话、诊断与 Telegram 入口。后续能力会在保持现有命令稳定的基础上,逐步演进到统一执行、受治理学习、Skill/Capability 沉淀和组织级共享。
8
+
9
+ 北极星目标:**每一次交互,都让 Runtime 更有能力。**
10
+
11
+ ## 文档导航
12
+
13
+ - [中文文档总览](./docs/README.md)
14
+ - [doc_2.0 文档层总览](./docs/README.md)
15
+ - [中文使用指南](./docs/usage.md)
16
+ - [HTML 命令手册](./docs/USAGE.html)
17
+ - [整体架构与当前实现快照](./docs/00-overview.md)
18
+
19
+ AI 与维护者使用的英文权威文档位于 `AGENTS.md`、`docs/AI_RUNTIME_ARCHITECTURE.md` 和 `docs/CONVENTIONS.md`;产品规范、宪法摘要、记忆/学习与 Telegram 设计等构成 `doc_2.0` 文档层(现已展开在 `docs/` 与 `docs/modules/` 下,索引见 `docs/README.md`)。
20
+
21
+ ## 当前边界
22
+
23
+ - 已交付:本地 Provider Gateway、Claude/Codex 启动与 fallback、Telegram、会话、用量、诊断和早期 Memory。
24
+ - 演进中:统一 Runtime 执行上下文、自动学习与记忆治理、Skill/Capability 生命周期、插件体系。
25
+ - 长期方向:组织与租户隔离、能力注册与共享、企业集成、Web/Desktop/API 治理界面。
26
+
27
+ ## 通过 npm 安装
28
+
29
+ 运行环境需要 Node.js `22.19.0` 或更高版本。全局安装后,npm 会把本包注册为 `ai` 命令:
30
+
31
+ ```bash
32
+ npm install --global @offerpilot/axiomruntime
33
+ ai --version
34
+ ai setup
35
+ ```
36
+
37
+ `ai --version` 用于验证 npm 包和命令安装成功;`ai setup` 初始化 Runtime 配置,并可在确认后安装缺失的 Claude Code/Codex。之后可直接运行 `ai`。
38
+
39
+ ## 通过 Homebrew 安装
40
+
41
+ 发布流程会在 npm 发布成功后把 `Formula/axiomruntime.rb` 同步到 `offerpilot/homebrew-tap`。Tap 建立后可使用:
42
+
43
+ ```bash
44
+ brew tap offerpilot/tap
45
+ brew install axiomruntime
46
+ ai --version
47
+ ai setup
48
+ ```
49
+
50
+ 如果 Tap 不托管在 GitHub,需要在首次添加时显式提供公开 Git 地址:
51
+
52
+ ```bash
53
+ brew tap offerpilot/tap <homebrew-tap-public-git-url>
54
+ ```
55
+
56
+ ## 安装开发依赖
57
+
58
+ 运行和开发需要 Node.js `22.19.0` 或更高版本。该下限与锁定依赖的运行时要求一致;Node.js 20 不受支持。
59
+
60
+ ```bash
61
+ npm install
62
+ ```
63
+
64
+ ## 本机 CLI 前置条件
65
+
66
+ AI Gateway 不内置 Claude Code 或 Codex。它们作为彼此独立的外部 CLI 使用,需要先在本机安装并能通过 `PATH` 或常见安装路径找到。
67
+
68
+ 可用下面的命令初始化和检查:
69
+
70
+ ```bash
71
+ ai setup
72
+ ai doctor
73
+ ```
74
+
75
+ ## 构建
76
+
77
+ ```bash
78
+ npm run build
79
+ ```
80
+
81
+ ## 本地测试
82
+
83
+ ```bash
84
+ npm test
85
+ ```
86
+
87
+ ## 内网 Runtime 服务
88
+
89
+ 常驻服务为其他应用提供两类受鉴权接口,同时保留现有 `ai` CLI 和 Telegram 行为:
90
+
91
+ - `POST /v1/chat/completions`、`/v1/responses`、`/v1/embeddings`:OpenAI 兼容模型调用,按 provider 优先级、模型目录、健康度和熔断状态自动切换。
92
+ - `POST /v1/agent/runs`:复用 Telegram 背后的 Claude/Codex Engine。只接受受控 `workspace_id`,默认 `read_only`,服务端限制并发和超时。
93
+ - `GET /health/live`、`/health/ready`:不含 Secret 的存活与就绪检查;其他接口必须使用 Bearer Token。
94
+
95
+ ```bash
96
+ npm run build
97
+ AI_GATEWAY_SERVER_TOKEN='replace-me' npm run server
98
+ ```
99
+
100
+ 生产 Compose 位于 `deploy/docker-compose.prod.yml`,默认仅映射宿主机 `127.0.0.1:8020`,并加入外部 Docker 网络 `ai-runtime`。Provider 配置挂载到 `/var/lib/ai-gateway`,服务 Token 通过只读文件挂载,不写入镜像或仓库。
101
+
102
+ ### GitLab 自动部署
103
+
104
+ 默认分支提交通过测试后,GitLab CI 会构建以 commit SHA 标记的镜像,并由锁定当前项目的受保护 Runner(标签 `axiomruntime-prod`)部署到 `154.201.73.41:/opt/AxiomRuntime`。部署只操作 `axiomruntime` Compose 项目;健康检查失败时恢复上一镜像,不清理服务器上的其他容器或镜像。
105
+
106
+ 首次部署前,服务器必须已有 `/opt/AxiomRuntime/config/providers.json` 和 `/opt/AxiomRuntime/secrets/server-token`,且容器用户 `10001:10001` 可以读写配置目录。CI 只校验这些文件,不生成或覆盖凭据。
107
+
108
+ Telegram 容器默认不启动,以免与现有 long polling 进程争抢消息。完成迁移并停止旧 Bot 后,在服务器 `/opt/AxiomRuntime/.env` 中设置:
109
+
110
+ ```dotenv
111
+ COMPOSE_PROFILES=telegram
112
+ ```
113
+
114
+ 启用前还要把各 Bot 的 `defaultCwd` 改为容器内的 `/workspace/...`,对应宿主机 `/opt/AxiomRuntime/workspaces/...`。之后的自动部署会同时更新 Runtime API 和所有位于 `config/telegram/bots/*.json` 的 Telegram Bot。资源上限和工作目录可参考 `.env.server.example` 调整。
115
+
116
+ ## 本机注册 `ai` 命令
117
+
118
+ ```bash
119
+ npm link
120
+ ```
121
+
122
+ 注册后可直接使用:
123
+
124
+ ```bash
125
+ ai
126
+ ai --version
127
+ ai setup
128
+ ai help
129
+ ai provider add
130
+ ai provider list
131
+ ai provider edit
132
+ ai provider delete
133
+ ai telegram
134
+ ai telegram list
135
+ ai telegram start --bot ops
136
+ ai telegram status --bot ops
137
+ ai status
138
+ ai log
139
+ ai log --print
140
+ ai context
141
+ ai context --print
142
+ ai session
143
+ ai session list
144
+ ai session clear
145
+ ai report
146
+ ai doctor
147
+ ```
148
+
149
+ ## 配置文件
150
+
151
+ 默认从全局配置目录读取:
152
+
153
+ ```text
154
+ ~/.ai-gateway/providers.json
155
+ ~/.ai-gateway/.ai-cache.json
156
+ ~/.ai-gateway/usage.json
157
+ ~/.ai-gateway/model-pricing.json
158
+ ~/.ai-gateway/sessions.json
159
+ ~/.ai-gateway/telegram.json
160
+ ~/.ai-gateway/telegram.sqlite
161
+ ~/.ai-gateway/telegram.log
162
+ ~/.ai-gateway/reports/
163
+ ~/.ai-gateway/exports/
164
+ ~/.ai-gateway/.ai-gateway.log
165
+ ```
166
+
167
+ 可通过 `AI_GATEWAY_HOME` 覆盖配置目录。
168
+
169
+ `providers.json` 包含 provider 配置和 SecretStore 引用,不要提交到仓库。
170
+
171
+ Telegram Bot 集成说明:
172
+
173
+ - 多 Bot 使用 `ai telegram add <name>` 创建,并以统一的 `--bot <name>` 选择目标;例如 `ai telegram edit --bot ops`、`start|stop|status --bot ops`、`delete --bot ops`。裸 `ai telegram` 会先选操作再选 Bot,删除有二次确认;批量生命周期操作使用 `--all`。
174
+ - `ai telegram config set <key> <value> --bot <name>` 只修改指定 Bot;旧位置参数写法继续兼容。
175
+ - 如果本机不能直连 Telegram API,可设置 `ai telegram config set proxyUrl http://127.0.0.1:7890 --bot <name>`。
176
+ - Bot 首次 `/start` 或 `/reset` 会先选择工作路径,再选择 Claude Code/Codex,并展示当前会话、provider 和模型卡片。
177
+ - Bot 会记录选择过的工作路径;自定义路径会按 `~/输入名称` 创建或复用。
178
+ - Telegram 默认命令菜单只保留 `/claude`、`/codex`、`/reset`、`/cwd` 和 `/help`;选择 CLI 后,会在当前 chat 的 `/` 菜单追加该 CLI 的 slash 命令映射。
179
+ - Telegram 远程执行复用 AI Gateway 顶层 provider,不维护另一套 provider 配置。
180
+ - Telegram Codex 执行会自动走本地 OpenAI 兼容代理记录 provider usage;`/usage` 有 provider cost 时直接展示,否则按 `model-pricing.json` 估算。
181
+
182
+ 更多命令用法见:
183
+
184
+ - [docs/usage.md](./docs/usage.md)
185
+ - [docs/USAGE.html](./docs/USAGE.html)
@@ -0,0 +1,29 @@
1
+ import { addProvider } from "../../core/providers/provider-service.js";
2
+ import { getCachedModels, updateProvider } from "../../core/providers/provider-service.js";
3
+ import { writeLog } from "../../core/logs/log-service.js";
4
+ import { promptSelect, promptText } from "../prompts/prompt.js";
5
+ export async function runAddCommand(args) {
6
+ const [nameArg, baseUrlArg, apiKeyArg] = args;
7
+ const interactive = !nameArg || !baseUrlArg || !apiKeyArg;
8
+ const name = nameArg ?? await promptText("Provider name");
9
+ const baseUrl = baseUrlArg ?? await promptText("Base URL");
10
+ const apiKey = apiKeyArg ?? await promptText("API key");
11
+ let provider = await addProvider({ name, baseUrl, apiKey });
12
+ if (interactive) {
13
+ const models = await getCachedModels(provider.name);
14
+ if (models.length) {
15
+ const model = await promptSelect("Select default model", models.map((item) => ({ label: item, value: item })));
16
+ provider = await updateProvider(provider.name, { model });
17
+ }
18
+ }
19
+ console.log(`Added provider: ${provider.name}`);
20
+ console.log(`Level: ${provider.level}`);
21
+ console.log(`Mode: ${provider.mode}`);
22
+ console.log(`Default model: ${provider.model || "(not detected)"}`);
23
+ await writeLog({
24
+ category: "provider",
25
+ action: "provider_add",
26
+ message: `Added provider: ${provider.name}`,
27
+ metadata: { provider: provider.name, baseUrl: provider.baseUrl, level: provider.level, model: provider.model || null }
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ import { spawn } from "node:child_process";
2
+ import { generateProjectContext } from "../../core/context/context-service.js";
3
+ import { writeLog } from "../../core/logs/log-service.js";
4
+ export async function runContextCommand(args) {
5
+ const shouldPrint = args.includes("--print");
6
+ const context = await generateProjectContext();
7
+ await writeLog({
8
+ category: "usage",
9
+ action: "context_generated",
10
+ message: `Generated project context: ${context.filePath}`,
11
+ metadata: { filePath: context.filePath, printed: shouldPrint }
12
+ });
13
+ if (shouldPrint) {
14
+ console.log(context.content);
15
+ return;
16
+ }
17
+ await openFile(context.filePath);
18
+ console.log(`Opened context file: ${context.filePath}`);
19
+ }
20
+ async function openFile(filePath) {
21
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
22
+ const args = process.platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
23
+ await new Promise((resolve) => {
24
+ const child = spawn(opener, args, { stdio: "ignore", detached: true });
25
+ child.on("error", () => resolve());
26
+ child.on("close", () => resolve());
27
+ child.unref();
28
+ });
29
+ }
@@ -0,0 +1,62 @@
1
+ import { runDeepDoctor, runDoctor } from "../../core/status/doctor-service.js";
2
+ import { writeLog } from "../../core/logs/log-service.js";
3
+ import { promptConfirm } from "../prompts/prompt.js";
4
+ import { printTable } from "../output/table.js";
5
+ export async function runDoctorCommand(args) {
6
+ const deep = args.includes("--deep");
7
+ const yes = args.includes("--yes");
8
+ const strict = args.includes("--strict");
9
+ const local = args.includes("--local") || args.includes("--no-network");
10
+ const fix = !args.includes("--no-fix");
11
+ const tool = readTool(args);
12
+ if (deep && local) {
13
+ throw new Error("`--deep` cannot be combined with `--local` or `--no-network`.");
14
+ }
15
+ const result = deep
16
+ ? await runDeep(yes, tool, fix)
17
+ : { items: await runDoctor({ fix, checkProviders: !local }), deepPerformed: false };
18
+ await writeLog({
19
+ category: "usage",
20
+ action: result.deepPerformed ? "doctor_deep" : "doctor",
21
+ message: `Doctor completed with ${result.items.length} item(s).`,
22
+ metadata: {
23
+ deep: result.deepPerformed,
24
+ tool: result.deepPerformed ? tool : null,
25
+ fix,
26
+ local,
27
+ fixed: result.items.filter((item) => item.fixed).length,
28
+ errors: result.items.filter((item) => item.status === "error").length,
29
+ warnings: result.items.filter((item) => item.status === "warn").length
30
+ }
31
+ });
32
+ printDoctorItems(result.items);
33
+ if (strict && result.items.some((item) => item.status === "error")) {
34
+ process.exitCode = 1;
35
+ }
36
+ }
37
+ async function runDeep(yes, tool, fix) {
38
+ if (!yes) {
39
+ const confirmed = await promptConfirm("`ai doctor --deep` may consume tokens. Continue?");
40
+ if (!confirmed) {
41
+ return { items: await runDoctor({ fix }), deepPerformed: false };
42
+ }
43
+ }
44
+ return { items: await runDeepDoctor(tool, { fix }), deepPerformed: true };
45
+ }
46
+ export function printDoctorItems(items) {
47
+ printTable(items.map((item) => ({
48
+ group: item.group,
49
+ status: item.fixed ? "fixed" : item.status,
50
+ message: item.message,
51
+ next: item.suggestion ?? ""
52
+ })));
53
+ }
54
+ function readTool(args) {
55
+ const inline = args.find((arg) => arg.startsWith("--tool="));
56
+ const index = args.indexOf("--tool");
57
+ const value = inline?.slice("--tool=".length) ?? (index >= 0 ? args[index + 1] : undefined) ?? "codex";
58
+ if (value !== "claude" && value !== "codex") {
59
+ throw new Error("`--tool` must be `claude` or `codex`.");
60
+ }
61
+ return value;
62
+ }
@@ -0,0 +1,85 @@
1
+ import { readCache } from "../../core/config/cache-store.js";
2
+ import { writeLog } from "../../core/logs/log-service.js";
3
+ import { deleteProvider, listProviders, updateProvider } from "../../core/providers/provider-service.js";
4
+ import { promptConfirm, promptSelect, promptText } from "../prompts/prompt.js";
5
+ export async function runProviderEditCommand(name) {
6
+ const providers = await listProviders();
7
+ if (!providers.length) {
8
+ console.log("No providers configured. Run `ai add` first.");
9
+ return;
10
+ }
11
+ const providerName = name ?? await selectProviderName(providers);
12
+ const provider = providers.find((item) => item.name === providerName);
13
+ if (!provider) {
14
+ throw new Error(`Provider not found: ${providerName}`);
15
+ }
16
+ const field = await promptSelect("Select field", [
17
+ { label: "name", value: "name" },
18
+ { label: "baseUrl", value: "baseUrl" },
19
+ { label: "apiKey", value: "apiKey" },
20
+ { label: "mode", value: "mode" },
21
+ { label: "level", value: "level" },
22
+ { label: "model", value: "model" }
23
+ ]);
24
+ const patch = await buildPatch(provider, providers, field);
25
+ const updated = await updateProvider(provider.name, patch);
26
+ console.log(`Updated provider: ${updated.name}`);
27
+ await writeLog({
28
+ category: "provider",
29
+ action: "provider_edit",
30
+ message: `Updated provider: ${updated.name}`,
31
+ metadata: { provider: provider.name, field, renamedTo: updated.name !== provider.name ? updated.name : undefined }
32
+ });
33
+ }
34
+ export async function runProviderDeleteCommand(name) {
35
+ const providers = await listProviders();
36
+ if (!providers.length) {
37
+ console.log("No providers configured. Run `ai add` first.");
38
+ return;
39
+ }
40
+ const providerName = name ?? await selectProviderName(providers);
41
+ const provider = providers.find((item) => item.name === providerName);
42
+ if (!provider) {
43
+ throw new Error(`Provider not found: ${providerName}`);
44
+ }
45
+ const confirmed = await promptConfirm(`Delete provider ${provider.name}?`, false);
46
+ if (!confirmed) {
47
+ console.log("Delete cancelled.");
48
+ return;
49
+ }
50
+ await deleteProvider(provider.name);
51
+ console.log(`Deleted provider: ${provider.name}`);
52
+ await writeLog({
53
+ category: "provider",
54
+ action: "provider_delete",
55
+ message: `Deleted provider: ${provider.name}`,
56
+ metadata: { provider: provider.name }
57
+ });
58
+ }
59
+ async function selectProviderName(providers) {
60
+ return promptSelect("Select provider", providers.map((provider) => ({ label: `${provider.name} (level ${provider.level})`, value: provider.name })));
61
+ }
62
+ async function buildPatch(provider, providers, field) {
63
+ if (field === "mode") {
64
+ const mode = await promptSelect("Select mode", [
65
+ { label: "auto", value: "auto" },
66
+ { label: "ask", value: "ask" }
67
+ ]);
68
+ return { mode };
69
+ }
70
+ if (field === "level") {
71
+ const level = await promptSelect("Select level", providers.map((item) => ({ label: `${item.level}${item.name === provider.name ? " (current)" : ` (${item.name})`}`, value: item.level })));
72
+ return { level };
73
+ }
74
+ if (field === "model") {
75
+ const cache = await readCache();
76
+ const models = cache.providers[provider.name]?.models ?? [];
77
+ if (!models.length) {
78
+ throw new Error("No cached models found. Run `ai status` first.");
79
+ }
80
+ const model = await promptSelect("Select model", models.map((model) => ({ label: model, value: model })));
81
+ return { model };
82
+ }
83
+ const value = await promptText(`New ${field}`, { defaultValue: provider[field] });
84
+ return { [field]: value };
85
+ }
@@ -0,0 +1,16 @@
1
+ import { spawn } from "node:child_process";
2
+ import { getUsageHtmlPath } from "../../core/config/paths.js";
3
+ export async function runHelpCommand() {
4
+ const filePath = getUsageHtmlPath();
5
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
6
+ const args = process.platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
7
+ await new Promise((resolve) => {
8
+ const child = spawn(opener, args, { stdio: "ignore", detached: true });
9
+ child.on("error", () => {
10
+ console.log(filePath);
11
+ resolve();
12
+ });
13
+ child.on("close", () => resolve());
14
+ child.unref();
15
+ });
16
+ }
@@ -0,0 +1,25 @@
1
+ import { listProviders } from "../../core/providers/provider-service.js";
2
+ import { maskSecret } from "../output/table.js";
3
+ export async function runListCommand() {
4
+ const providers = await listProviders();
5
+ if (!providers.length) {
6
+ console.log("No providers configured. Run `ai add` first.");
7
+ return;
8
+ }
9
+ console.log(`AI Gateway Providers (${providers.length})`);
10
+ console.log("=".repeat(72));
11
+ providers.forEach((provider, index) => {
12
+ const isDefault = provider.level === 1;
13
+ const title = `${provider.name}${isDefault ? " [default]" : ""}`;
14
+ const header = `${provider.level.toString().padStart(2, "0")} ${title}`;
15
+ console.log(header);
16
+ console.log("-".repeat(Math.min(72, Math.max(24, header.length))));
17
+ console.log(` Base URL ${provider.baseUrl}`);
18
+ console.log(` API Key ${maskSecret(provider.apiKey)}`);
19
+ console.log(` Mode ${provider.mode}`);
20
+ console.log(` Model ${provider.model || "(none)"}`);
21
+ if (index < providers.length - 1) {
22
+ console.log("");
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,63 @@
1
+ import { spawn } from "node:child_process";
2
+ import { ensureLogFile, readRecentLogs } from "../../core/logs/log-service.js";
3
+ export async function runLogCommand(args) {
4
+ if (!args.includes("--print")) {
5
+ const filePath = await ensureLogFile();
6
+ await openFile(filePath);
7
+ console.log(`Opened log file: ${filePath}`);
8
+ return;
9
+ }
10
+ const printableArgs = args.filter((arg) => arg !== "--print");
11
+ const limit = parseLimit(printableArgs[0]);
12
+ const logs = await readRecentLogs(limit);
13
+ printLogs(logs);
14
+ }
15
+ async function openFile(filePath) {
16
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
17
+ const args = process.platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
18
+ await new Promise((resolve) => {
19
+ const child = spawn(opener, args, { stdio: "ignore", detached: true });
20
+ child.on("error", () => resolve());
21
+ child.on("close", () => resolve());
22
+ child.unref();
23
+ });
24
+ }
25
+ function parseLimit(value) {
26
+ if (!value)
27
+ return 50;
28
+ const parsed = Number(value);
29
+ if (!Number.isInteger(parsed) || parsed <= 0) {
30
+ return 50;
31
+ }
32
+ return Math.min(parsed, 500);
33
+ }
34
+ function printLogs(logs) {
35
+ if (!logs.length) {
36
+ console.log("No logs found.");
37
+ return;
38
+ }
39
+ console.log(`AI Gateway Logs (${logs.length})`);
40
+ console.log("=".repeat(88));
41
+ logs.forEach((event, index) => {
42
+ const badge = formatBadge(event.level);
43
+ console.log(`${index + 1}. ${badge} ${event.timestamp}`);
44
+ console.log(` ${event.category} / ${event.action}`);
45
+ console.log(` ${event.message}`);
46
+ if (event.metadata && Object.keys(event.metadata).length) {
47
+ console.log(` metadata: ${formatMetadata(event.metadata)}`);
48
+ }
49
+ if (index < logs.length - 1) {
50
+ console.log("-".repeat(88));
51
+ }
52
+ });
53
+ }
54
+ function formatBadge(level) {
55
+ if (level === "error")
56
+ return "[ERROR]";
57
+ if (level === "warn")
58
+ return "[WARN ]";
59
+ return "[INFO ]";
60
+ }
61
+ function formatMetadata(metadata) {
62
+ return JSON.stringify(metadata);
63
+ }