@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
@@ -0,0 +1,108 @@
1
+ import { readCache, recordProviderRuntimeFailure, recordProviderRuntimeSuccess } from "../../core/config/cache-store.js";
2
+ import { writeLog } from "../../core/logs/log-service.js";
3
+ import { refreshAllProviders } from "../../core/providers/provider-service.js";
4
+ import { resolveProviderCandidatesForTool } from "../../core/runner/fallback.js";
5
+ import { runTool } from "../../core/runner/tool-runner.js";
6
+ import { promptConfirm, promptSelect } from "../prompts/prompt.js";
7
+ export async function runUseCommand(args) {
8
+ const tool = await getTool(args[0]);
9
+ const providerName = args[1];
10
+ const modelName = args[2];
11
+ await refreshAllProviders();
12
+ const candidates = await resolveProviderCandidatesForTool(tool, providerName, modelName);
13
+ if (!candidates.length) {
14
+ throw new Error("No usable provider/model found. Run `ai status` or `ai doctor`.");
15
+ }
16
+ const code = await runResolvedProviderCandidates(tool, candidates, {
17
+ confirm: promptConfirm,
18
+ runner: (candidate, fallbackFrom) => runTool(tool, candidate.provider, candidate.model, { fallbackFrom })
19
+ });
20
+ process.exitCode = code;
21
+ }
22
+ export async function runLastCommand() {
23
+ const cache = await readCache();
24
+ if (!cache.lastSelection) {
25
+ console.log("No last selection found.");
26
+ return;
27
+ }
28
+ await runUseCommand([cache.lastSelection.tool, cache.lastSelection.provider, cache.lastSelection.model]);
29
+ }
30
+ async function getTool(value) {
31
+ if (value === "claude" || value === "codex") {
32
+ return value;
33
+ }
34
+ return promptSelect("Select tool", [
35
+ { label: "claude", value: "claude" },
36
+ { label: "codex", value: "codex" }
37
+ ]);
38
+ }
39
+ export async function runResolvedProviderCandidates(tool, candidates, options = {}) {
40
+ const confirm = options.confirm ?? promptConfirm;
41
+ const runner = options.runner ?? ((candidate, fallbackFrom) => runTool(tool, candidate.provider, candidate.model, { fallbackFrom }));
42
+ let lastExitCode = 1;
43
+ let runtimeFallbackFrom = null;
44
+ for (const [index, candidate] of candidates.entries()) {
45
+ const fallbackFrom = runtimeFallbackFrom ?? candidate.fallbackFrom ?? null;
46
+ if (fallbackFrom) {
47
+ console.log(`Provider ${fallbackFrom} unavailable, fallback to ${candidate.provider.name}.`);
48
+ await writeProviderFallbackLog({
49
+ action: "fallback_applied",
50
+ from: fallbackFrom,
51
+ to: candidate.provider.name,
52
+ tool,
53
+ model: candidate.model
54
+ });
55
+ }
56
+ console.log(`Using ${tool}: ${candidate.provider.name} / ${candidate.model}`);
57
+ await writeLog({
58
+ category: "usage",
59
+ action: "tool_start",
60
+ message: `Starting ${tool}: ${candidate.provider.name} / ${candidate.model}`,
61
+ metadata: { tool, provider: candidate.provider.name, model: candidate.model, mode: candidate.provider.mode }
62
+ });
63
+ if (candidate.provider.mode === "ask") {
64
+ const confirmed = await confirm("Start CLI?");
65
+ if (!confirmed)
66
+ return 0;
67
+ }
68
+ const code = await runner(candidate, fallbackFrom);
69
+ if (code === 0) {
70
+ await recordProviderRuntimeSuccess(candidate.provider.name);
71
+ return 0;
72
+ }
73
+ lastExitCode = code;
74
+ await recordProviderRuntimeFailure(candidate.provider.name, `exit_code_${code}`);
75
+ const next = candidates[index + 1];
76
+ if (next) {
77
+ runtimeFallbackFrom = candidate.provider.name;
78
+ await writeProviderFallbackLog({
79
+ level: "warn",
80
+ action: "runtime_fallback_retry",
81
+ from: candidate.provider.name,
82
+ to: next.provider.name,
83
+ tool,
84
+ model: candidate.model,
85
+ nextModel: next.model,
86
+ error: `exit_code_${code}`
87
+ });
88
+ console.log(`Provider ${candidate.provider.name} failed with exit code ${code}, trying ${next.provider.name}.`);
89
+ }
90
+ }
91
+ return lastExitCode;
92
+ }
93
+ async function writeProviderFallbackLog(input) {
94
+ await writeLog({
95
+ level: input.level,
96
+ category: "strategy",
97
+ action: input.action,
98
+ message: `Provider fallback ${input.from} -> ${input.to} for ${input.tool}.`,
99
+ metadata: {
100
+ from: input.from,
101
+ to: input.to,
102
+ engine: input.tool,
103
+ model: input.model,
104
+ nextModel: input.nextModel ?? null,
105
+ error: input.error ?? null
106
+ }
107
+ });
108
+ }
@@ -0,0 +1,12 @@
1
+ import { createRequire } from "node:module";
2
+ export function getRuntimeVersion() {
3
+ const require = createRequire(import.meta.url);
4
+ const metadata = require("../../../package.json");
5
+ if (typeof metadata.version !== "string" || !metadata.version.trim()) {
6
+ throw new Error("Runtime package version is unavailable.");
7
+ }
8
+ return metadata.version;
9
+ }
10
+ export async function runVersionCommand() {
11
+ console.log(getRuntimeVersion());
12
+ }
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ import { runAddCommand } from "./commands/add.js";
3
+ import { runContextCommand } from "./commands/context.js";
4
+ import { runDoctorCommand } from "./commands/doctor.js";
5
+ import { runProviderDeleteCommand, runProviderEditCommand } from "./commands/edit.js";
6
+ import { runHelpCommand } from "./commands/help.js";
7
+ import { runListCommand } from "./commands/list.js";
8
+ import { runLogCommand } from "./commands/log.js";
9
+ import { runMemoryCommand } from "./commands/memory.js";
10
+ import { runReportCommand } from "./commands/report.js";
11
+ import { runLastCommand, runUseCommand } from "./commands/use.js";
12
+ import { runSessionCommand } from "./commands/session.js";
13
+ import { runSetupCommand } from "./commands/setup.js";
14
+ import { runStatusCommand } from "./commands/status.js";
15
+ import { runTelegramCommand } from "./commands/telegram.js";
16
+ import { runVersionCommand } from "./commands/version.js";
17
+ import { writeLog } from "../core/logs/log-service.js";
18
+ import { promptSelect } from "./prompts/prompt.js";
19
+ import { getCliCommand, registerCliCommand } from "./registry.js";
20
+ registerBuiltInCommands();
21
+ async function main() {
22
+ const [, , command, ...args] = process.argv;
23
+ const registered = command === undefined ? undefined : getCliCommand(command);
24
+ if (registered?.logUsage === false) {
25
+ await registered.run(args);
26
+ return;
27
+ }
28
+ const safeArgs = sanitizeArgs(command, args);
29
+ await writeLog({
30
+ category: "usage",
31
+ action: "command_start",
32
+ message: `Command started: ${command ?? "interactive"}`,
33
+ metadata: { command: command ?? "interactive", args: safeArgs }
34
+ });
35
+ if (command === undefined) {
36
+ await runInteractive();
37
+ return;
38
+ }
39
+ if (registered) {
40
+ await registered.run(args);
41
+ return;
42
+ }
43
+ await writeLog({
44
+ level: "warn",
45
+ category: "usage",
46
+ action: "unknown_command",
47
+ message: `Unknown command: ${command}`,
48
+ metadata: { command, args: safeArgs }
49
+ });
50
+ console.error(`Unknown command: ${command}`);
51
+ console.error("Run `ai help` for usage.");
52
+ process.exitCode = 1;
53
+ }
54
+ function sanitizeArgs(command, args) {
55
+ if (command === "add" && args.length >= 3) {
56
+ return [args[0], args[1], maskSecretArg(args[2])];
57
+ }
58
+ if (command === "provider" && args[0] === "add" && args.length >= 4) {
59
+ return [args[0], args[1], args[2], maskSecretArg(args[3])];
60
+ }
61
+ if (command === "memory" && (args[0] === "embedding" || args[0] === "vector") && (args[1] === "set" || args[1] === "edit")) {
62
+ return maskMemoryEmbeddingSetArgs(args);
63
+ }
64
+ if (command === "telegram") {
65
+ return maskTelegramConfigSecretArgs(args);
66
+ }
67
+ return args.map((arg, index) => {
68
+ const previous = args[index - 1]?.toLowerCase();
69
+ if (previous === "--api-key" || previous === "--apikey" || previous === "--key") {
70
+ return maskSecretArg(arg);
71
+ }
72
+ return arg;
73
+ });
74
+ }
75
+ function maskTelegramConfigSecretArgs(args) {
76
+ const masked = [...args];
77
+ let state = "config";
78
+ for (let index = 0; index < args.length; index += 1) {
79
+ const arg = args[index];
80
+ if (arg === "--bot") {
81
+ index += 1;
82
+ continue;
83
+ }
84
+ if (arg === "--all" || arg.startsWith("--bot="))
85
+ continue;
86
+ if (state === "config" && arg === "config") {
87
+ state = "set";
88
+ continue;
89
+ }
90
+ if (state === "set" && arg === "set") {
91
+ state = "token";
92
+ continue;
93
+ }
94
+ if (state === "token" && arg === "token") {
95
+ state = "done";
96
+ continue;
97
+ }
98
+ if (state === "done") {
99
+ masked[index] = maskSecretArg(arg);
100
+ break;
101
+ }
102
+ }
103
+ return masked;
104
+ }
105
+ function maskMemoryEmbeddingSetArgs(args) {
106
+ const valueOptions = new Set(["--type", "--provider-type", "--query-task", "--passage-task", "--dimensions", "--api-key", "--apikey", "--key"]);
107
+ const masked = [...args];
108
+ let positional = 0;
109
+ for (let index = 0; index < args.length; index += 1) {
110
+ const arg = args[index];
111
+ if (arg.startsWith("--"))
112
+ continue;
113
+ const previous = args[index - 1]?.toLowerCase();
114
+ if (previous === "--api-key" || previous === "--apikey" || previous === "--key") {
115
+ masked[index] = maskSecretArg(arg);
116
+ continue;
117
+ }
118
+ if (previous && valueOptions.has(previous))
119
+ continue;
120
+ if (index >= 2) {
121
+ positional += 1;
122
+ if (positional === 3)
123
+ masked[index] = maskSecretArg(arg);
124
+ }
125
+ }
126
+ return masked;
127
+ }
128
+ function maskSecretArg(value) {
129
+ if (value.length <= 8)
130
+ return "****";
131
+ return `${value.slice(0, 4)}****${value.slice(-4)}`;
132
+ }
133
+ async function runInteractive() {
134
+ const interactiveCommandNames = ["setup", "use", "add", "memory", "telegram", "status", "session", "report", "doctor", "log", "context", "help", "last"];
135
+ const command = await promptSelect("AI Gateway", [
136
+ ...interactiveCommandNames
137
+ .flatMap((name) => {
138
+ const item = getCliCommand(name);
139
+ return item ? [{ label: item.description, value: item.name }] : [];
140
+ }),
141
+ { label: "provider list", value: "provider:list" },
142
+ { label: "provider edit", value: "provider:edit" },
143
+ { label: "provider delete", value: "provider:delete" },
144
+ { label: "exit", value: "exit" }
145
+ ]);
146
+ if (command === "exit")
147
+ return;
148
+ if (command === "provider:list")
149
+ await runListCommand();
150
+ if (command === "provider:edit")
151
+ await runProviderEditCommand();
152
+ if (command === "provider:delete")
153
+ await runProviderDeleteCommand();
154
+ const registered = getCliCommand(command);
155
+ if (registered)
156
+ await registered.run([]);
157
+ }
158
+ async function runProviderCommand(args) {
159
+ const [subcommand, name] = args;
160
+ if (subcommand === "add") {
161
+ await runAddCommand(args.slice(1));
162
+ return;
163
+ }
164
+ if (subcommand === "list") {
165
+ await runListCommand();
166
+ return;
167
+ }
168
+ if (subcommand === "edit") {
169
+ await runProviderEditCommand(name);
170
+ return;
171
+ }
172
+ if (subcommand === "delete" || subcommand === "remove") {
173
+ await runProviderDeleteCommand(name);
174
+ return;
175
+ }
176
+ throw new Error(`Unknown provider command: ${subcommand ?? "(missing)"}`);
177
+ }
178
+ function registerBuiltInCommands() {
179
+ registerCliCommand({
180
+ name: "version",
181
+ aliases: ["--version", "-v", "-V"],
182
+ description: "version",
183
+ interactive: false,
184
+ logUsage: false,
185
+ run: () => runVersionCommand()
186
+ });
187
+ registerCliCommand({
188
+ name: "setup",
189
+ description: "setup",
190
+ run: (args) => runSetupCommand(args)
191
+ });
192
+ registerCliCommand({
193
+ name: "help",
194
+ description: "help",
195
+ run: () => runHelpCommand()
196
+ });
197
+ registerCliCommand({
198
+ name: "add",
199
+ description: "provider add",
200
+ run: (args) => runAddCommand(args)
201
+ });
202
+ registerCliCommand({
203
+ name: "status",
204
+ description: "status",
205
+ run: () => runStatusCommand()
206
+ });
207
+ registerCliCommand({
208
+ name: "provider",
209
+ description: "provider add",
210
+ interactive: false,
211
+ run: (args) => runProviderCommand(args)
212
+ });
213
+ registerCliCommand({
214
+ name: "session",
215
+ description: "session",
216
+ run: (args) => runSessionCommand(args)
217
+ });
218
+ registerCliCommand({
219
+ name: "report",
220
+ description: "report",
221
+ run: (args) => runReportCommand(args)
222
+ });
223
+ registerCliCommand({
224
+ name: "log",
225
+ description: "log",
226
+ run: (args) => runLogCommand(args)
227
+ });
228
+ registerCliCommand({
229
+ name: "context",
230
+ description: "context",
231
+ run: (args) => runContextCommand(args)
232
+ });
233
+ registerCliCommand({
234
+ name: "telegram",
235
+ description: "telegram",
236
+ run: (args) => runTelegramCommand(args)
237
+ });
238
+ registerCliCommand({
239
+ name: "memory",
240
+ description: "memory",
241
+ run: (args) => runMemoryCommand(args)
242
+ });
243
+ registerCliCommand({
244
+ name: "doctor",
245
+ description: "doctor",
246
+ run: (args) => runDoctorCommand(args)
247
+ });
248
+ registerCliCommand({
249
+ name: "use",
250
+ description: "use",
251
+ run: (args) => runUseCommand(args)
252
+ });
253
+ registerCliCommand({
254
+ name: "last",
255
+ description: "last",
256
+ run: () => runLastCommand()
257
+ });
258
+ registerCliCommand({
259
+ name: "web",
260
+ description: "web",
261
+ interactive: false,
262
+ run: async () => {
263
+ console.log("`ai web` is planned but not implemented in this foundation build.");
264
+ }
265
+ });
266
+ }
267
+ main().catch(async (error) => {
268
+ await writeLog({
269
+ level: "error",
270
+ category: "error",
271
+ action: "command_error",
272
+ message: error instanceof Error ? error.message : String(error)
273
+ });
274
+ console.error(error instanceof Error ? error.message : String(error));
275
+ process.exitCode = 1;
276
+ });
@@ -0,0 +1,22 @@
1
+ export function printTable(rows) {
2
+ if (!rows.length) {
3
+ console.log("No data.");
4
+ return;
5
+ }
6
+ const headers = Object.keys(rows[0]);
7
+ const widths = headers.map((header) => Math.max(header.length, ...rows.map((row) => String(row[header] ?? "").length)));
8
+ const line = headers.map((header, index) => header.padEnd(widths[index])).join(" ");
9
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
10
+ console.log(line);
11
+ console.log(separator);
12
+ for (const row of rows) {
13
+ console.log(headers.map((header, index) => String(row[header] ?? "").padEnd(widths[index])).join(" "));
14
+ }
15
+ }
16
+ export function maskSecret(value) {
17
+ if (!value)
18
+ return "";
19
+ if (value.length <= 8)
20
+ return "****";
21
+ return `${value.slice(0, 4)}****${value.slice(-4)}`;
22
+ }
@@ -0,0 +1,37 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ export async function promptText(message, options = {}) {
4
+ const rl = readline.createInterface({ input, output });
5
+ try {
6
+ const suffix = options.defaultValue ? ` (${options.defaultValue})` : "";
7
+ const answer = await rl.question(`${message}${suffix}: `);
8
+ return answer.trim() || options.defaultValue || "";
9
+ }
10
+ finally {
11
+ rl.close();
12
+ }
13
+ }
14
+ export async function promptConfirm(message, defaultValue = false) {
15
+ const fallback = defaultValue ? "Y/n" : "y/N";
16
+ const answer = (await promptText(`${message} [${fallback}]`)).toLowerCase();
17
+ if (!answer)
18
+ return defaultValue;
19
+ return answer === "y" || answer === "yes";
20
+ }
21
+ export async function promptSelect(message, choices) {
22
+ if (!choices.length) {
23
+ throw new Error(`No choices available for: ${message}`);
24
+ }
25
+ console.log(`\n${message}`);
26
+ choices.forEach((choice, index) => {
27
+ console.log(` ${index + 1}. ${choice.label}`);
28
+ });
29
+ while (true) {
30
+ const raw = await promptText("Select");
31
+ const index = Number(raw);
32
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length) {
33
+ return choices[index - 1].value;
34
+ }
35
+ console.log(`Please enter a number between 1 and ${choices.length}.`);
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ const commands = new Map();
2
+ export function registerCliCommand(command) {
3
+ commands.set(command.name, command);
4
+ for (const alias of command.aliases ?? []) {
5
+ commands.set(alias, command);
6
+ }
7
+ }
8
+ export function getCliCommand(name) {
9
+ return commands.get(name);
10
+ }
11
+ export function listCliCommands() {
12
+ return [...new Set(commands.values())];
13
+ }
14
+ export function listInteractiveCliCommands() {
15
+ return listCliCommands().filter((command) => command.interactive !== false);
16
+ }
@@ -0,0 +1,193 @@
1
+ import { getCachePath } from "./paths.js";
2
+ import { readJsonFile, updateJsonFile, writeJsonFile } from "./json-store.js";
3
+ const EMPTY_CACHE = { providers: {} };
4
+ const PROVIDER_CIRCUIT_FAILURE_THRESHOLD = 3;
5
+ const PROVIDER_CIRCUIT_OPEN_MS = 10 * 60 * 1000;
6
+ const DEFAULT_HEALTH_SCORE = 50;
7
+ const HEALTH_SCORE_SUCCESS_STEP = 25;
8
+ const HEALTH_SCORE_FAILURE_STEP = 35;
9
+ export async function readCache() {
10
+ const cache = await readJsonFile(getCachePath(), EMPTY_CACHE);
11
+ return normalizeCache(cache);
12
+ }
13
+ export async function writeCache(cache) {
14
+ await writeJsonFile(getCachePath(), normalizeCache(cache));
15
+ }
16
+ export async function upsertProviderCache(providerName, providerCache) {
17
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => {
18
+ const next = normalizeCache(cache);
19
+ next.providers[providerName] = normalizeProviderCache(providerCache);
20
+ return next;
21
+ });
22
+ }
23
+ export async function recordProviderRuntimeSuccess(providerName) {
24
+ const checkedAt = new Date().toISOString();
25
+ let updated = null;
26
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => {
27
+ const next = normalizeCache(cache);
28
+ updated = applyProviderHealth(next.providers[providerName], {
29
+ status: "ok",
30
+ models: next.providers[providerName]?.models ?? [],
31
+ lastCheckedAt: checkedAt,
32
+ lastError: null
33
+ });
34
+ next.providers[providerName] = updated;
35
+ return next;
36
+ });
37
+ return updated;
38
+ }
39
+ export async function recordProviderRuntimeFailure(providerName, error) {
40
+ const checkedAt = new Date().toISOString();
41
+ const lastError = summarizeHealthError(error);
42
+ let updated = null;
43
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => {
44
+ const next = normalizeCache(cache);
45
+ const previous = next.providers[providerName] ? normalizeProviderCache(next.providers[providerName]) : undefined;
46
+ const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
47
+ const circuitOpenUntil = consecutiveFailures >= PROVIDER_CIRCUIT_FAILURE_THRESHOLD
48
+ ? new Date(Date.now() + PROVIDER_CIRCUIT_OPEN_MS).toISOString()
49
+ : null;
50
+ updated = {
51
+ status: circuitOpenUntil ? "error" : previous?.status ?? "unknown",
52
+ models: previous?.models ?? [],
53
+ lastCheckedAt: checkedAt,
54
+ lastError,
55
+ consecutiveFailures,
56
+ lastFailureAt: checkedAt,
57
+ circuitOpenUntil,
58
+ healthScore: clampHealthScore((previous?.healthScore ?? DEFAULT_HEALTH_SCORE) - HEALTH_SCORE_FAILURE_STEP)
59
+ };
60
+ next.providers[providerName] = updated;
61
+ return next;
62
+ });
63
+ return updated;
64
+ }
65
+ export function buildProviderHealthCache(previous, next) {
66
+ return applyProviderHealth(previous, normalizeProviderCache(next));
67
+ }
68
+ export function isProviderCircuitOpen(providerCache, now = Date.now()) {
69
+ if (!providerCache?.circuitOpenUntil)
70
+ return false;
71
+ const openUntil = Date.parse(providerCache.circuitOpenUntil);
72
+ return Number.isFinite(openUntil) && openUntil > now;
73
+ }
74
+ export async function renameProviderCache(from, to) {
75
+ if (from === to)
76
+ return;
77
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => {
78
+ const next = normalizeCache(cache);
79
+ if (next.providers[from]) {
80
+ next.providers[to] = next.providers[from];
81
+ delete next.providers[from];
82
+ }
83
+ if (next.lastSelection?.provider === from) {
84
+ next.lastSelection.provider = to;
85
+ }
86
+ return next;
87
+ });
88
+ }
89
+ export async function deleteProviderCache(providerName) {
90
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => {
91
+ const next = normalizeCache(cache);
92
+ delete next.providers[providerName];
93
+ if (next.lastSelection?.provider === providerName) {
94
+ delete next.lastSelection;
95
+ }
96
+ return next;
97
+ });
98
+ }
99
+ export async function saveLastSelection(tool, provider, model) {
100
+ await updateJsonFile(getCachePath(), EMPTY_CACHE, (cache) => ({
101
+ ...normalizeCache(cache),
102
+ lastSelection: {
103
+ tool,
104
+ provider,
105
+ model,
106
+ usedAt: new Date().toISOString()
107
+ }
108
+ }));
109
+ }
110
+ function normalizeCache(cache) {
111
+ const providers = {};
112
+ for (const [name, value] of Object.entries(cache.providers ?? {})) {
113
+ providers[name] = normalizeProviderCache(value);
114
+ }
115
+ return {
116
+ providers,
117
+ lastSelection: cache.lastSelection
118
+ };
119
+ }
120
+ function applyProviderHealth(previous, next) {
121
+ const normalizedPrevious = previous ? normalizeProviderCache(previous) : undefined;
122
+ const normalizedNext = normalizeProviderCache(next);
123
+ const previousScore = normalizedPrevious?.healthScore ?? DEFAULT_HEALTH_SCORE;
124
+ if (normalizedNext.status === "ok") {
125
+ return {
126
+ ...normalizedNext,
127
+ consecutiveFailures: 0,
128
+ lastFailureAt: null,
129
+ circuitOpenUntil: null,
130
+ healthScore: clampHealthScore(previousScore + HEALTH_SCORE_SUCCESS_STEP)
131
+ };
132
+ }
133
+ if (normalizedNext.status === "error") {
134
+ const consecutiveFailures = (normalizedPrevious?.consecutiveFailures ?? 0) + 1;
135
+ return {
136
+ ...normalizedNext,
137
+ consecutiveFailures,
138
+ lastFailureAt: normalizedNext.lastCheckedAt,
139
+ circuitOpenUntil: consecutiveFailures >= PROVIDER_CIRCUIT_FAILURE_THRESHOLD
140
+ ? new Date(Date.now() + PROVIDER_CIRCUIT_OPEN_MS).toISOString()
141
+ : null,
142
+ healthScore: clampHealthScore(previousScore - HEALTH_SCORE_FAILURE_STEP)
143
+ };
144
+ }
145
+ return {
146
+ ...normalizedNext,
147
+ consecutiveFailures: normalizedPrevious?.consecutiveFailures ?? 0,
148
+ lastFailureAt: normalizedPrevious?.lastFailureAt ?? null,
149
+ circuitOpenUntil: normalizedPrevious?.circuitOpenUntil ?? null,
150
+ healthScore: clampHealthScore(previousScore)
151
+ };
152
+ }
153
+ function normalizeProviderCache(value) {
154
+ const status = normalizeHealthStatus(value.status);
155
+ return {
156
+ status,
157
+ models: Array.isArray(value.models) ? value.models.map(String).filter(Boolean) : [],
158
+ lastCheckedAt: normalizeNullableString(value.lastCheckedAt),
159
+ lastError: normalizeNullableString(value.lastError),
160
+ consecutiveFailures: normalizeNonNegativeInteger(value.consecutiveFailures),
161
+ lastFailureAt: normalizeNullableString(value.lastFailureAt),
162
+ circuitOpenUntil: normalizeNullableString(value.circuitOpenUntil),
163
+ healthScore: clampHealthScore(typeof value.healthScore === "number" && Number.isFinite(value.healthScore)
164
+ ? value.healthScore
165
+ : status === "ok"
166
+ ? 100
167
+ : status === "error"
168
+ ? 0
169
+ : DEFAULT_HEALTH_SCORE)
170
+ };
171
+ }
172
+ function normalizeHealthStatus(value) {
173
+ return value === "ok" || value === "error" || value === "unknown" ? value : "unknown";
174
+ }
175
+ function normalizeNullableString(value) {
176
+ if (value === null || value === undefined)
177
+ return null;
178
+ const text = String(value).trim();
179
+ return text || null;
180
+ }
181
+ function normalizeNonNegativeInteger(value) {
182
+ const numberValue = Number(value ?? 0);
183
+ return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
184
+ }
185
+ function clampHealthScore(value) {
186
+ if (!Number.isFinite(value))
187
+ return DEFAULT_HEALTH_SCORE;
188
+ return Math.max(0, Math.min(100, Math.round(value)));
189
+ }
190
+ function summarizeHealthError(error) {
191
+ const message = error instanceof Error ? error.message : String(error ?? "Provider failed.");
192
+ return message.replace(/\s+/g, " ").trim().slice(0, 240) || "Provider failed.";
193
+ }