@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,319 @@
1
+ import http from "node:http";
2
+ import { Agent, fetch } from "undici";
3
+ import { writeLog } from "../logs/log-service.js";
4
+ import { buildUpstreamHeaders, buildUpstreamUrl, methodMayHaveBody, readRequestBody, streamResponse, writeResponseHead } from "./openai-usage-http.js";
5
+ import { buildChatCompletionsResponsesSse, isResponsesStreamRequest, shouldFallbackResponsesToChat } from "./openai-usage-responses.js";
6
+ import { extractRequestModel, recordUsageFromResponse } from "./openai-usage-recording.js";
7
+ import { getToolBaseUrl } from "./tool-runner.js";
8
+ const DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS = 45_000;
9
+ const MAX_CONSECUTIVE_HEADERS_TIMEOUTS = 2;
10
+ export async function startOpenAiUsageProxy(input) {
11
+ const upstreamBaseUrl = getToolBaseUrl("codex", input.provider);
12
+ const upstreamHeadersTimeoutMs = normalizePositiveTimeout(input.upstreamHeadersTimeoutMs, DEFAULT_UPSTREAM_HEADERS_TIMEOUT_MS);
13
+ // Codex already retries failed Responses requests. Give those retries fresh
14
+ // upstream connections so a stale gateway keep-alive socket cannot poison an
15
+ // entire turn with immediate fetch failures.
16
+ const upstreamDispatcher = new Agent({
17
+ connections: 4,
18
+ pipelining: 0,
19
+ headersTimeout: upstreamHeadersTimeoutMs,
20
+ bodyTimeout: 0
21
+ });
22
+ const records = [];
23
+ const metrics = createEmptyMetrics();
24
+ let consecutiveHeadersTimeouts = 0;
25
+ const server = http.createServer(async (req, res) => {
26
+ const startedAt = Date.now();
27
+ let requestModel = input.model;
28
+ let requestBytes = 0;
29
+ let requestShape = {};
30
+ let upstreamPath = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
31
+ metrics.requestCount += 1;
32
+ let finalized = false;
33
+ const finalizeMetrics = (success, upstreamStatus) => {
34
+ if (finalized)
35
+ return;
36
+ finalized = true;
37
+ if (success) {
38
+ metrics.successCount += 1;
39
+ }
40
+ else {
41
+ metrics.failureCount += 1;
42
+ }
43
+ metrics.totalDurationMs += Date.now() - startedAt;
44
+ if (upstreamStatus !== undefined) {
45
+ incrementStatusCount(metrics, upstreamStatus);
46
+ }
47
+ };
48
+ try {
49
+ const requestBody = await readRequestBody(req);
50
+ requestModel = extractRequestModel(requestBody) ?? input.model;
51
+ requestBytes = requestBody.length;
52
+ requestShape = summarizeRequestShape(requestBody);
53
+ const responsesStream = isResponsesStreamRequest(req.url ?? "/", requestBody);
54
+ void writeLog({
55
+ category: "usage",
56
+ action: "openai_usage_proxy_request",
57
+ message: `Proxying ${input.tool} request for ${input.provider.name} / ${requestModel}`,
58
+ metadata: {
59
+ tool: input.tool,
60
+ provider: input.provider.name,
61
+ model: requestModel,
62
+ path: new URL(req.url ?? "/", "http://127.0.0.1").pathname
63
+ }
64
+ });
65
+ const upstreamUrl = buildUpstreamUrl(upstreamBaseUrl, req.url ?? "/");
66
+ upstreamPath = new URL(upstreamUrl).pathname;
67
+ if (consecutiveHeadersTimeouts >= MAX_CONSECUTIVE_HEADERS_TIMEOUTS) {
68
+ await writeLog({
69
+ level: "warn",
70
+ category: "usage",
71
+ action: "openai_usage_proxy_timeout_short_circuit",
72
+ message: `Skipping another stalled upstream attempt for ${input.provider.name} / ${requestModel}.`,
73
+ metadata: {
74
+ tool: input.tool,
75
+ provider: input.provider.name,
76
+ model: requestModel,
77
+ upstreamPath,
78
+ requestBytes,
79
+ ...requestShape,
80
+ timeoutMs: upstreamHeadersTimeoutMs,
81
+ consecutiveTimeouts: consecutiveHeadersTimeouts
82
+ }
83
+ });
84
+ sendProxyError(res, 504, "AI Gateway timed out waiting for provider response headers.");
85
+ finalizeMetrics(false, "headers_timeout_short_circuit");
86
+ return;
87
+ }
88
+ const upstreamResponse = await fetch(upstreamUrl, {
89
+ method: req.method,
90
+ headers: buildUpstreamHeaders(req.headers, input.provider.apiKey),
91
+ body: requestBody.length && methodMayHaveBody(req.method) ? requestBody : undefined,
92
+ dispatcher: upstreamDispatcher
93
+ });
94
+ consecutiveHeadersTimeouts = 0;
95
+ // Native Responses is attempted first. Only when the upstream cannot speak
96
+ // the Responses protocol do we translate the request through chat
97
+ // completions; transient/no-channel errors are passed through untouched so
98
+ // provider rotation can react to them.
99
+ if (responsesStream && !isUsableEventStream(upstreamResponse)) {
100
+ const nativeBody = await upstreamResponse.text().catch(() => "");
101
+ const outcome = {
102
+ status: upstreamResponse.status,
103
+ contentType: upstreamResponse.headers.get("content-type") ?? "",
104
+ bodyText: nativeBody
105
+ };
106
+ if (shouldFallbackResponsesToChat(outcome)) {
107
+ void writeLog({
108
+ category: "usage",
109
+ action: "openai_usage_proxy_responses_chat_fallback",
110
+ message: `Responses protocol is unsupported for ${input.provider.name} / ${requestModel}; falling back to chat completions.`,
111
+ metadata: {
112
+ tool: input.tool,
113
+ provider: input.provider.name,
114
+ model: requestModel,
115
+ status: outcome.status
116
+ }
117
+ });
118
+ await sendChatCompletionsResponsesSse({
119
+ upstreamBaseUrl,
120
+ requestBody,
121
+ apiKey: input.provider.apiKey,
122
+ dispatcher: upstreamDispatcher,
123
+ requestModel,
124
+ provider: input.provider.name,
125
+ tool: input.tool,
126
+ records,
127
+ onUsage: input.onUsage,
128
+ metrics,
129
+ res
130
+ }).then((result) => finalizeMetrics(result.status >= 200 && result.status < 400, result.status));
131
+ return;
132
+ }
133
+ // Not a protocol problem (e.g. 503 no-channel): pass the native error
134
+ // through so the caller and provider rotation see the real cause.
135
+ res.statusCode = outcome.status;
136
+ res.statusMessage = upstreamResponse.statusText;
137
+ res.setHeader("content-type", outcome.contentType || "application/json; charset=utf-8");
138
+ const recordedUsage = await recordUsageFromResponse({
139
+ text: nativeBody,
140
+ contentType: outcome.contentType,
141
+ fallbackModel: requestModel,
142
+ provider: input.provider.name,
143
+ tool: input.tool,
144
+ records,
145
+ onUsage: input.onUsage
146
+ });
147
+ recordUsageMetric(metrics, recordedUsage);
148
+ res.end(nativeBody);
149
+ finalizeMetrics(outcome.status >= 200 && outcome.status < 400, outcome.status);
150
+ return;
151
+ }
152
+ writeResponseHead(res, upstreamResponse);
153
+ const captured = await streamResponse(upstreamResponse, res);
154
+ const recordedUsage = await recordUsageFromResponse({
155
+ text: captured.toString("utf8"),
156
+ contentType: upstreamResponse.headers.get("content-type") ?? "",
157
+ fallbackModel: requestModel,
158
+ provider: input.provider.name,
159
+ tool: input.tool,
160
+ records,
161
+ onUsage: input.onUsage
162
+ });
163
+ recordUsageMetric(metrics, recordedUsage);
164
+ res.end();
165
+ finalizeMetrics(upstreamResponse.status >= 200 && upstreamResponse.status < 400, upstreamResponse.status);
166
+ }
167
+ catch (error) {
168
+ const diagnostic = summarizeUpstreamError(error);
169
+ const headersTimedOut = diagnostic.code === "UND_ERR_HEADERS_TIMEOUT";
170
+ consecutiveHeadersTimeouts = headersTimedOut ? consecutiveHeadersTimeouts + 1 : 0;
171
+ await writeLog({
172
+ level: "error",
173
+ category: "usage",
174
+ action: "openai_usage_proxy_upstream_error",
175
+ message: `OpenAI upstream request failed for ${input.provider.name} / ${requestModel}: ${diagnostic.message}`,
176
+ metadata: {
177
+ tool: input.tool,
178
+ provider: input.provider.name,
179
+ model: requestModel,
180
+ upstreamPath,
181
+ requestBytes,
182
+ ...requestShape,
183
+ errorName: diagnostic.name,
184
+ errorCode: diagnostic.code,
185
+ errorSyscall: diagnostic.syscall,
186
+ timeoutMs: headersTimedOut ? upstreamHeadersTimeoutMs : null,
187
+ consecutiveTimeouts: headersTimedOut ? consecutiveHeadersTimeouts : 0
188
+ }
189
+ });
190
+ const code = diagnostic.code ? ` (${diagnostic.code})` : "";
191
+ sendProxyError(res, headersTimedOut ? 504 : 502, `AI Gateway proxy failed: ${diagnostic.message}${code}`);
192
+ finalizeMetrics(false, headersTimedOut ? "headers_timeout" : "proxy_error");
193
+ }
194
+ });
195
+ await new Promise((resolve, reject) => {
196
+ const onListenError = (error) => {
197
+ void upstreamDispatcher.close();
198
+ reject(error);
199
+ };
200
+ server.once("error", onListenError);
201
+ server.listen(0, "127.0.0.1", () => {
202
+ server.off("error", onListenError);
203
+ resolve();
204
+ });
205
+ });
206
+ const address = server.address();
207
+ if (!address || typeof address === "string") {
208
+ throw new Error("Failed to start AI Gateway OpenAI proxy.");
209
+ }
210
+ return {
211
+ baseUrl: `http://127.0.0.1:${address.port}/v1`,
212
+ async close() {
213
+ const closeServer = server.listening
214
+ ? new Promise((resolve, reject) => {
215
+ server.close((error) => error ? reject(error) : resolve());
216
+ })
217
+ : Promise.resolve();
218
+ await Promise.all([closeServer, upstreamDispatcher.close()]);
219
+ },
220
+ getRecords() {
221
+ return [...records];
222
+ },
223
+ getMetrics() {
224
+ return snapshotMetrics(metrics);
225
+ }
226
+ };
227
+ }
228
+ function normalizePositiveTimeout(value, fallback) {
229
+ return typeof value === "number" && Number.isFinite(value) && value > 0
230
+ ? Math.floor(value)
231
+ : fallback;
232
+ }
233
+ function sendProxyError(res, status, message) {
234
+ res.statusCode = status;
235
+ res.setHeader("content-type", "application/json; charset=utf-8");
236
+ res.end(JSON.stringify({ error: { message } }));
237
+ }
238
+ function summarizeRequestShape(requestBody) {
239
+ try {
240
+ const body = JSON.parse(requestBody.toString("utf8"));
241
+ return {
242
+ stream: body.stream === true,
243
+ inputItems: Array.isArray(body.input) ? body.input.length : typeof body.input === "string" ? 1 : 0,
244
+ toolCount: Array.isArray(body.tools) ? body.tools.length : 0
245
+ };
246
+ }
247
+ catch {
248
+ return { stream: null, inputItems: null, toolCount: null };
249
+ }
250
+ }
251
+ function summarizeUpstreamError(error) {
252
+ const top = error instanceof Error ? error : new Error(String(error));
253
+ const cause = top.cause && typeof top.cause === "object" ? top.cause : null;
254
+ return {
255
+ name: cause && typeof cause.name === "string" ? cause.name : top.name,
256
+ message: cause && typeof cause.message === "string" ? cause.message : top.message,
257
+ code: cause && typeof cause.code === "string" ? cause.code : null,
258
+ syscall: cause && typeof cause.syscall === "string" ? cause.syscall : null
259
+ };
260
+ }
261
+ async function sendChatCompletionsResponsesSse(input) {
262
+ const response = await buildChatCompletionsResponsesSse({
263
+ upstreamBaseUrl: input.upstreamBaseUrl,
264
+ requestBody: input.requestBody,
265
+ apiKey: input.apiKey,
266
+ dispatcher: input.dispatcher
267
+ });
268
+ input.res.statusCode = response.status;
269
+ input.res.statusMessage = response.statusText;
270
+ input.res.setHeader("content-type", response.contentType);
271
+ const recordedUsage = await recordUsageFromResponse({
272
+ text: response.body.toString("utf8"),
273
+ contentType: response.contentType,
274
+ fallbackModel: input.requestModel,
275
+ provider: input.provider,
276
+ tool: input.tool,
277
+ records: input.records,
278
+ onUsage: input.onUsage
279
+ });
280
+ recordUsageMetric(input.metrics, recordedUsage);
281
+ input.res.end(response.body);
282
+ return { status: response.status };
283
+ }
284
+ function isUsableEventStream(response) {
285
+ if (!response.ok)
286
+ return false;
287
+ return (response.headers.get("content-type") ?? "").toLowerCase().includes("text/event-stream");
288
+ }
289
+ function createEmptyMetrics() {
290
+ return {
291
+ requestCount: 0,
292
+ successCount: 0,
293
+ failureCount: 0,
294
+ noUsageCount: 0,
295
+ usageRecordCount: 0,
296
+ totalDurationMs: 0,
297
+ averageDurationMs: 0,
298
+ upstreamStatusCounts: {}
299
+ };
300
+ }
301
+ function recordUsageMetric(metrics, recordedUsage) {
302
+ if (recordedUsage) {
303
+ metrics.usageRecordCount += 1;
304
+ }
305
+ else {
306
+ metrics.noUsageCount += 1;
307
+ }
308
+ }
309
+ function incrementStatusCount(metrics, status) {
310
+ const key = String(status);
311
+ metrics.upstreamStatusCounts[key] = (metrics.upstreamStatusCounts[key] ?? 0) + 1;
312
+ }
313
+ function snapshotMetrics(metrics) {
314
+ return {
315
+ ...metrics,
316
+ averageDurationMs: metrics.requestCount ? metrics.totalDurationMs / metrics.requestCount : 0,
317
+ upstreamStatusCounts: { ...metrics.upstreamStatusCounts }
318
+ };
319
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,138 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { saveLastSelection } from "../config/cache-store.js";
6
+ import { writeLog } from "../logs/log-service.js";
7
+ import { isRecord } from "../utils/is-record.js";
8
+ import { resolveToolCommand } from "./command-resolver.js";
9
+ import { getRunnerEngine } from "./engine-registry.js";
10
+ import { finishSession, startSession } from "../sessions/session-service.js";
11
+ export async function runTool(tool, provider, model, options = {}) {
12
+ await saveLastSelection(tool, provider.name, model);
13
+ const command = resolveToolCommand(tool);
14
+ if (!command) {
15
+ throw new Error(`Failed to find ${tool} CLI. Run \`ai doctor\` to check local CLI installation.`);
16
+ }
17
+ const engine = getRunnerEngine(tool);
18
+ const env = buildRunnerEnv(tool, provider, model);
19
+ const temporaryFiles = [];
20
+ const claudeSettingsPath = engine.requiresSettingsFile ? writeTemporaryClaudeSettings(provider) : null;
21
+ if (claudeSettingsPath)
22
+ temporaryFiles.push(claudeSettingsPath);
23
+ const args = buildRunnerArgs(tool, provider, model, claudeSettingsPath);
24
+ const effectiveBaseUrl = getToolBaseUrl(tool, provider);
25
+ await writeLog({
26
+ category: "usage",
27
+ action: "tool_spawn",
28
+ message: `Spawning ${tool}: ${provider.name} / ${model}`,
29
+ metadata: {
30
+ tool,
31
+ provider: provider.name,
32
+ model,
33
+ mode: provider.mode,
34
+ baseUrl: effectiveBaseUrl,
35
+ configuredBaseUrl: provider.baseUrl,
36
+ apiKeyFingerprint: getSecretFingerprint(provider.apiKey),
37
+ command,
38
+ args,
39
+ authInjection: engine.authInjection
40
+ }
41
+ });
42
+ const session = await startSession({
43
+ tool,
44
+ provider,
45
+ model,
46
+ projectPath: process.cwd(),
47
+ fallbackFrom: options.fallbackFrom ?? null
48
+ });
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn(command, args, {
51
+ stdio: "inherit",
52
+ env
53
+ });
54
+ child.on("error", (error) => {
55
+ cleanupTemporaryFiles(temporaryFiles);
56
+ void finishSession(session.id, 1);
57
+ reject(new Error(`Failed to start ${command}. Run \`ai doctor\` to check local CLI installation. ${error.message}`));
58
+ });
59
+ child.on("close", (code) => {
60
+ cleanupTemporaryFiles(temporaryFiles);
61
+ const exitCode = code ?? 0;
62
+ void finishSession(session.id, exitCode).finally(() => resolve(exitCode));
63
+ });
64
+ });
65
+ }
66
+ export function getSecretFingerprint(value) {
67
+ if (!value)
68
+ return "(empty)";
69
+ if (value.length <= 12)
70
+ return `len=${value.length}`;
71
+ return `${value.slice(0, 8)}...${value.slice(-6)} len=${value.length}`;
72
+ }
73
+ export function buildRunnerArgs(tool, provider, model, claudeSettingsPath) {
74
+ return getRunnerEngine(tool).buildArgs({ provider, model, settingsPath: claudeSettingsPath });
75
+ }
76
+ const AUTO_PERMISSIONS = {
77
+ defaultMode: "bypassPermissions",
78
+ allow: [
79
+ "Bash",
80
+ "Edit",
81
+ "Write",
82
+ "Read",
83
+ "Glob",
84
+ "Grep",
85
+ "WebFetch",
86
+ "WebSearch",
87
+ "MultiEdit",
88
+ "NotebookEdit"
89
+ ],
90
+ deny: []
91
+ };
92
+ const READ_ONLY_PERMISSIONS = {
93
+ defaultMode: "default",
94
+ allow: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
95
+ deny: ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]
96
+ };
97
+ export function writeTemporaryClaudeSettings(provider, options = {}) {
98
+ const userSettingsPath = path.join(os.homedir(), ".claude", "settings.json");
99
+ const settings = readClaudeSettings(userSettingsPath);
100
+ const settingsEnv = isRecord(settings.env) ? settings.env : {};
101
+ const baseUrl = getToolBaseUrl("claude", provider);
102
+ settings.env = {
103
+ ...settingsEnv,
104
+ ANTHROPIC_API_KEY: provider.apiKey,
105
+ ANTHROPIC_BASE_URL: baseUrl
106
+ };
107
+ if (options.permissionMode === "readOnly") {
108
+ settings.permissions = { ...READ_ONLY_PERMISSIONS };
109
+ }
110
+ else if (provider.mode === "auto") {
111
+ settings.permissions = { ...AUTO_PERMISSIONS };
112
+ }
113
+ const settingsPath = path.join(os.tmpdir(), `ai-gateway-claude-settings-${process.pid}-${Date.now()}.json`);
114
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
115
+ return settingsPath;
116
+ }
117
+ function readClaudeSettings(filePath) {
118
+ if (!fs.existsSync(filePath)) {
119
+ return {};
120
+ }
121
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
122
+ }
123
+ export function cleanupTemporaryFiles(filePaths) {
124
+ for (const filePath of filePaths) {
125
+ try {
126
+ fs.unlinkSync(filePath);
127
+ }
128
+ catch {
129
+ // Best-effort cleanup for temporary launch settings.
130
+ }
131
+ }
132
+ }
133
+ export function buildRunnerEnv(tool, provider, model) {
134
+ return getRunnerEngine(tool).buildEnv(provider, model, process.env);
135
+ }
136
+ export function getToolBaseUrl(tool, provider) {
137
+ return getRunnerEngine(tool).normalizeBaseUrl(provider.baseUrl);
138
+ }
@@ -0,0 +1,47 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { addSession, clearSessions, readSessions, updateSession } from "../config/session-store.js";
3
+ export async function startSession(input) {
4
+ const now = new Date();
5
+ const session = {
6
+ id: buildSessionId(now),
7
+ tool: input.tool,
8
+ provider: input.provider.name,
9
+ model: input.model,
10
+ mode: input.provider.mode,
11
+ projectPath: input.projectPath,
12
+ startedAt: now.toISOString(),
13
+ endedAt: null,
14
+ status: "running",
15
+ exitCode: null,
16
+ fallbackFrom: input.fallbackFrom ?? null
17
+ };
18
+ await addSession(session);
19
+ return session;
20
+ }
21
+ export async function finishSession(sessionId, exitCode) {
22
+ await updateSession(sessionId, {
23
+ endedAt: new Date().toISOString(),
24
+ status: exitCode === 0 ? "completed" : "failed",
25
+ exitCode
26
+ });
27
+ }
28
+ export async function listRecentSessions(limit = 20) {
29
+ const file = await readSessions();
30
+ return [...file.sessions]
31
+ .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
32
+ .slice(0, limit);
33
+ }
34
+ export async function getSession(id) {
35
+ const file = await readSessions();
36
+ return file.sessions.find((session) => session.id === id) ?? null;
37
+ }
38
+ export async function getLastSession() {
39
+ return (await listRecentSessions(1))[0] ?? null;
40
+ }
41
+ export async function clearAllSessions() {
42
+ return clearSessions();
43
+ }
44
+ function buildSessionId(date) {
45
+ const stamp = date.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "_");
46
+ return `ses_${stamp}_${randomBytes(3).toString("hex")}`;
47
+ }