@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,114 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ const LOCK_OPTIONS = {
6
+ realpath: false,
7
+ stale: 10_000,
8
+ update: 5_000,
9
+ retries: {
10
+ retries: 100,
11
+ factor: 1.2,
12
+ minTimeout: 25,
13
+ maxTimeout: 500
14
+ }
15
+ };
16
+ export async function readJsonFile(filePath, fallback) {
17
+ try {
18
+ const raw = await fs.readFile(filePath, "utf8");
19
+ return parseJsonWithTrailingRecovery(raw);
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT") {
23
+ return fallback;
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ export async function writeJsonFile(filePath, value) {
29
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
30
+ await withJsonFileLock(filePath, () => writeJsonFileUnlocked(filePath, value));
31
+ }
32
+ export async function updateJsonFile(filePath, fallback, updater) {
33
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
34
+ return withJsonFileLock(filePath, async () => {
35
+ const current = await readJsonFile(filePath, fallback);
36
+ const next = await updater(current);
37
+ await writeJsonFileUnlocked(filePath, next);
38
+ return next;
39
+ });
40
+ }
41
+ export async function withJsonFileLock(filePath, fn) {
42
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
43
+ const release = await lockfile.lock(filePath, LOCK_OPTIONS);
44
+ try {
45
+ return await fn();
46
+ }
47
+ finally {
48
+ await release();
49
+ }
50
+ }
51
+ async function writeJsonFileUnlocked(filePath, value) {
52
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
53
+ try {
54
+ await fs.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
55
+ await fs.rename(temporaryPath, filePath);
56
+ }
57
+ catch (error) {
58
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
59
+ throw error;
60
+ }
61
+ }
62
+ function parseJsonWithTrailingRecovery(raw) {
63
+ try {
64
+ return JSON.parse(raw);
65
+ }
66
+ catch (error) {
67
+ if (!(error instanceof SyntaxError))
68
+ throw error;
69
+ const prefix = extractFirstJsonValue(raw);
70
+ if (!prefix)
71
+ throw error;
72
+ return JSON.parse(prefix);
73
+ }
74
+ }
75
+ function extractFirstJsonValue(raw) {
76
+ const start = raw.search(/\S/);
77
+ if (start < 0)
78
+ return null;
79
+ const opener = raw[start];
80
+ const closer = opener === "{" ? "}" : opener === "[" ? "]" : "";
81
+ if (!closer)
82
+ return null;
83
+ let depth = 0;
84
+ let inString = false;
85
+ let escaped = false;
86
+ for (let index = start; index < raw.length; index += 1) {
87
+ const char = raw[index];
88
+ if (inString) {
89
+ if (escaped) {
90
+ escaped = false;
91
+ }
92
+ else if (char === "\\") {
93
+ escaped = true;
94
+ }
95
+ else if (char === "\"") {
96
+ inString = false;
97
+ }
98
+ continue;
99
+ }
100
+ if (char === "\"") {
101
+ inString = true;
102
+ }
103
+ else if (char === opener) {
104
+ depth += 1;
105
+ }
106
+ else if (char === closer) {
107
+ depth -= 1;
108
+ if (depth === 0) {
109
+ return raw.slice(start, index + 1);
110
+ }
111
+ }
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,85 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ export function getConfigRoot() {
4
+ return process.env.AI_GATEWAY_HOME
5
+ ? path.resolve(process.env.AI_GATEWAY_HOME)
6
+ : path.join(os.homedir(), ".ai-gateway");
7
+ }
8
+ export function getProjectRoot() {
9
+ return process.cwd();
10
+ }
11
+ export function getProvidersPath() {
12
+ return path.join(getConfigRoot(), "providers.json");
13
+ }
14
+ export function getCachePath() {
15
+ return path.join(getConfigRoot(), ".ai-cache.json");
16
+ }
17
+ export function getUsagePath() {
18
+ return path.join(getConfigRoot(), "usage.json");
19
+ }
20
+ export function getModelPricingPath() {
21
+ return path.join(getConfigRoot(), "model-pricing.json");
22
+ }
23
+ export function getSessionsPath() {
24
+ return path.join(getConfigRoot(), "sessions.json");
25
+ }
26
+ export function getIntegrationStatePath() {
27
+ return path.join(getConfigRoot(), "integrations.json");
28
+ }
29
+ export function getReportsDir() {
30
+ return path.join(getConfigRoot(), "reports");
31
+ }
32
+ export function getReportPath(sessionId) {
33
+ return path.join(getReportsDir(), `${sessionId}.md`);
34
+ }
35
+ export function getLogPath() {
36
+ return path.join(getConfigRoot(), ".ai-gateway.log");
37
+ }
38
+ export function getTelegramConfigPath() {
39
+ return path.join(getConfigRoot(), "telegram.json");
40
+ }
41
+ export function getTelegramPidPath() {
42
+ return path.join(getConfigRoot(), "telegram.pid");
43
+ }
44
+ export function getTelegramLogPath() {
45
+ return path.join(getConfigRoot(), "telegram.log");
46
+ }
47
+ export function getTelegramDbPath() {
48
+ return path.join(getConfigRoot(), "telegram.sqlite");
49
+ }
50
+ export function getTelegramBotDir() {
51
+ return path.join(getConfigRoot(), "telegram", "bots");
52
+ }
53
+ export function getTelegramBotConfigPath(name) {
54
+ return path.join(getTelegramBotDir(), `${name}.json`);
55
+ }
56
+ export function getTelegramBotPidPath(name) {
57
+ return path.join(getTelegramBotDir(), `${name}.pid`);
58
+ }
59
+ export function getTelegramBotLogPath(name) {
60
+ return path.join(getTelegramBotDir(), `${name}.log`);
61
+ }
62
+ export function getTelegramBotDbPath(name) {
63
+ return path.join(getTelegramBotDir(), `${name}.sqlite`);
64
+ }
65
+ export function getMemoryDbPath() {
66
+ return path.join(getConfigRoot(), "memory.sqlite");
67
+ }
68
+ export function getMemoryConfigPath() {
69
+ return path.join(getConfigRoot(), "memory.json");
70
+ }
71
+ export function getMemoryBackupDir() {
72
+ return path.join(getConfigRoot(), "memory.backup");
73
+ }
74
+ export function getContextDir() {
75
+ return path.join(getProjectRoot(), ".ai-gateway");
76
+ }
77
+ export function getContextPath() {
78
+ return path.join(getContextDir(), "context.md");
79
+ }
80
+ export function getContextPathForRoot(root) {
81
+ return path.join(root, ".ai-gateway", "context.md");
82
+ }
83
+ export function getUsageHtmlPath() {
84
+ return path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../docs/USAGE.html");
85
+ }
@@ -0,0 +1,89 @@
1
+ import { getProvidersPath } from "./paths.js";
2
+ import { readJsonFile, updateJsonFile, writeJsonFile } from "./json-store.js";
3
+ import { readNumber, readRecord, readString } from "./schema.js";
4
+ const VALID_MODES = new Set(["ask", "auto"]);
5
+ export async function readProviders() {
6
+ const raw = await readJsonFile(getProvidersPath(), []);
7
+ if (!Array.isArray(raw)) {
8
+ throw new Error("providers.json must contain a JSON array.");
9
+ }
10
+ return normalizeProviders(raw.map(normalizeProviderShape));
11
+ }
12
+ export async function writeProviders(providers) {
13
+ await writeJsonFile(getProvidersPath(), normalizeProviders(providers));
14
+ }
15
+ export async function updateProviders(updater) {
16
+ return updateJsonFile(getProvidersPath(), [], async (raw) => {
17
+ if (!Array.isArray(raw)) {
18
+ throw new Error("providers.json must contain a JSON array.");
19
+ }
20
+ return normalizeProviders(await updater(normalizeProviders(raw.map(normalizeProviderShape))));
21
+ });
22
+ }
23
+ export function normalizeProviders(providers) {
24
+ const sorted = [...providers].sort((a, b) => {
25
+ const aLevel = Number.isFinite(a.level) && a.level > 0 ? a.level : Number.MAX_SAFE_INTEGER;
26
+ const bLevel = Number.isFinite(b.level) && b.level > 0 ? b.level : Number.MAX_SAFE_INTEGER;
27
+ return aLevel - bLevel || a.name.localeCompare(b.name);
28
+ });
29
+ const seenNames = new Set();
30
+ return sorted.map((provider, index) => {
31
+ const name = provider.name.trim();
32
+ if (!name) {
33
+ throw new Error("Provider name cannot be empty.");
34
+ }
35
+ if (seenNames.has(name)) {
36
+ throw new Error(`Duplicate provider name: ${name}`);
37
+ }
38
+ seenNames.add(name);
39
+ const baseUrl = provider.baseUrl.trim().replace(/\/+$/, "");
40
+ const apiKey = provider.apiKey.trim();
41
+ if (!baseUrl) {
42
+ throw new Error(`Provider ${name} baseUrl cannot be empty.`);
43
+ }
44
+ if (!apiKey) {
45
+ throw new Error(`Provider ${name} apiKey cannot be empty.`);
46
+ }
47
+ return {
48
+ name,
49
+ baseUrl,
50
+ apiKey,
51
+ mode: VALID_MODES.has(provider.mode) ? provider.mode : "auto",
52
+ level: index + 1,
53
+ model: provider.model.trim()
54
+ };
55
+ });
56
+ }
57
+ export function swapProviderLevel(providers, providerName, targetLevel) {
58
+ const normalized = normalizeProviders(providers);
59
+ const source = normalized.find((provider) => provider.name === providerName);
60
+ if (!source) {
61
+ throw new Error(`Provider not found: ${providerName}`);
62
+ }
63
+ const target = normalized.find((provider) => provider.level === targetLevel);
64
+ if (!target) {
65
+ throw new Error(`Invalid level: ${targetLevel}`);
66
+ }
67
+ const sourceLevel = source.level;
68
+ return normalizeProviders(normalized.map((provider) => {
69
+ if (provider.name === source.name) {
70
+ return { ...provider, level: targetLevel };
71
+ }
72
+ if (provider.name === target.name) {
73
+ return { ...provider, level: sourceLevel };
74
+ }
75
+ return provider;
76
+ }));
77
+ }
78
+ function normalizeProviderShape(value) {
79
+ const provider = readRecord(value, "provider");
80
+ const mode = readString(provider, "mode");
81
+ return {
82
+ name: readString(provider, "name"),
83
+ baseUrl: readString(provider, "baseUrl"),
84
+ apiKey: readString(provider, "apiKey"),
85
+ mode: mode === "ask" ? "ask" : "auto",
86
+ level: readNumber(provider, "level", Number.MAX_SAFE_INTEGER),
87
+ model: readString(provider, "model")
88
+ };
89
+ }
@@ -0,0 +1,60 @@
1
+ import { isRecord } from "../utils/is-record.js";
2
+ export class ConfigValidationError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "ConfigValidationError";
6
+ }
7
+ }
8
+ export function readRecord(value, path) {
9
+ if (!isRecord(value)) {
10
+ throw new ConfigValidationError(`${path} must be an object.`);
11
+ }
12
+ return value;
13
+ }
14
+ export function readOptionalRecord(value, path) {
15
+ if (value === null || value === undefined)
16
+ return undefined;
17
+ return readRecord(value, path);
18
+ }
19
+ export function readString(record, key, options = {}) {
20
+ const value = record[key] ?? options.fallback ?? "";
21
+ const text = String(value);
22
+ return options.trim === false ? text : text.trim();
23
+ }
24
+ export function readRequiredString(record, key, path = key) {
25
+ const value = readString(record, key);
26
+ if (!value) {
27
+ throw new ConfigValidationError(`${path} cannot be empty.`);
28
+ }
29
+ return value;
30
+ }
31
+ export function readOptionalString(record, key) {
32
+ const value = record[key];
33
+ if (value === null || value === undefined)
34
+ return undefined;
35
+ const text = String(value).trim();
36
+ return text || undefined;
37
+ }
38
+ export function readNumber(record, key, fallback) {
39
+ const value = Number(record[key] ?? fallback);
40
+ return Number.isFinite(value) ? value : fallback;
41
+ }
42
+ export function readEnum(record, key, values, fallback) {
43
+ const value = String(record[key] ?? fallback);
44
+ return values.includes(value) ? value : fallback;
45
+ }
46
+ export function readBoolean(value) {
47
+ if (typeof value === "boolean")
48
+ return value;
49
+ const text = String(value ?? "").trim().toLowerCase();
50
+ return text === "true" || text === "1" || text === "yes" || text === "y" || text === "on";
51
+ }
52
+ export function readArray(value) {
53
+ return Array.isArray(value) ? value : [];
54
+ }
55
+ export function readRequiredArray(value, path) {
56
+ if (!Array.isArray(value)) {
57
+ throw new ConfigValidationError(`${path} must be an array.`);
58
+ }
59
+ return value;
60
+ }
@@ -0,0 +1,30 @@
1
+ import { getSessionsPath } from "./paths.js";
2
+ import { readJsonFile, updateJsonFile, writeJsonFile } from "./json-store.js";
3
+ export async function readSessions() {
4
+ const file = await readJsonFile(getSessionsPath(), { sessions: [] });
5
+ return normalizeSessionsFile(file);
6
+ }
7
+ export async function writeSessions(file) {
8
+ await writeJsonFile(getSessionsPath(), normalizeSessionsFile(file));
9
+ }
10
+ export async function addSession(session) {
11
+ await updateJsonFile(getSessionsPath(), { sessions: [] }, (file) => ({
12
+ sessions: [...normalizeSessionsFile(file).sessions, session]
13
+ }));
14
+ }
15
+ export async function updateSession(sessionId, patch) {
16
+ await updateJsonFile(getSessionsPath(), { sessions: [] }, (file) => ({
17
+ sessions: normalizeSessionsFile(file).sessions.map((session) => session.id === sessionId ? { ...session, ...patch } : session)
18
+ }));
19
+ }
20
+ export async function clearSessions() {
21
+ let count = 0;
22
+ await updateJsonFile(getSessionsPath(), { sessions: [] }, (file) => {
23
+ count = normalizeSessionsFile(file).sessions.length;
24
+ return { sessions: [] };
25
+ });
26
+ return count;
27
+ }
28
+ function normalizeSessionsFile(file) {
29
+ return { sessions: Array.isArray(file.sessions) ? file.sessions : [] };
30
+ }
@@ -0,0 +1,18 @@
1
+ import { getUsagePath } from "./paths.js";
2
+ import { readJsonFile, updateJsonFile, writeJsonFile } from "./json-store.js";
3
+ export async function readUsage() {
4
+ const usage = await readJsonFile(getUsagePath(), { events: [] });
5
+ return normalizeUsageFile(usage);
6
+ }
7
+ export async function appendUsageEvent(event) {
8
+ await updateJsonFile(getUsagePath(), { events: [] }, (usage) => ({
9
+ events: [...normalizeUsageFile(usage).events, event]
10
+ }));
11
+ }
12
+ export async function ensureUsageFile() {
13
+ const usage = await readUsage();
14
+ await writeJsonFile(getUsagePath(), usage);
15
+ }
16
+ function normalizeUsageFile(usage) {
17
+ return { events: Array.isArray(usage.events) ? usage.events : [] };
18
+ }
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { getContextPathForRoot, getProjectRoot } from "../config/paths.js";
6
+ import { readRecentLogs } from "../logs/log-service.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const EXCLUDED_DIRS = new Set([
9
+ ".git",
10
+ ".ai-gateway",
11
+ "node_modules",
12
+ "dist",
13
+ "build",
14
+ "coverage",
15
+ ".next",
16
+ ".turbo",
17
+ ".cache"
18
+ ]);
19
+ const EXCLUDED_FILES = new Set([
20
+ "providers.json",
21
+ ".ai-cache.json",
22
+ "usage.json",
23
+ ".ai-gateway.log",
24
+ ".DS_Store",
25
+ ".env"
26
+ ]);
27
+ const DOC_CANDIDATES = [
28
+ "README.md",
29
+ "AGENTS.md",
30
+ "CLAUDE.md",
31
+ "docs/00-overview.md",
32
+ "docs/CONVENTIONS.md",
33
+ "docs/usage.md"
34
+ ];
35
+ export async function generateProjectContext(root = getProjectRoot()) {
36
+ const content = [
37
+ "# AI Gateway Project Context",
38
+ "",
39
+ `Generated at: ${new Date().toISOString()}`,
40
+ `Project root: ${root}`,
41
+ "",
42
+ await renderGitSection(root),
43
+ await renderPackageSection(root),
44
+ await renderFileTree(root),
45
+ await renderDocsSection(root),
46
+ await renderLogsSection()
47
+ ].join("\n");
48
+ const filePath = getContextPathForRoot(root);
49
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
50
+ await fs.writeFile(filePath, `${content.trim()}\n`, "utf8");
51
+ return { filePath, content: `${content.trim()}\n` };
52
+ }
53
+ async function renderGitSection(root) {
54
+ const [branch, status, diffStat, latestCommit] = await Promise.all([
55
+ runGit(root, ["branch", "--show-current"]),
56
+ runGit(root, ["status", "--short"]),
57
+ runGit(root, ["diff", "--stat"]),
58
+ runGit(root, ["log", "-1", "--oneline"])
59
+ ]);
60
+ return [
61
+ "## Git",
62
+ "",
63
+ `Branch: ${branch || "(unknown)"}`,
64
+ `Latest commit: ${latestCommit || "(none)"}`,
65
+ "",
66
+ "### Status",
67
+ "",
68
+ fenced(status || "clean"),
69
+ "",
70
+ "### Diff Stat",
71
+ "",
72
+ fenced(diffStat || "no working tree diff")
73
+ ].join("\n");
74
+ }
75
+ async function renderPackageSection(root) {
76
+ const packagePath = path.join(root, "package.json");
77
+ try {
78
+ const pkg = JSON.parse(await fs.readFile(packagePath, "utf8"));
79
+ const scripts = Object.entries(pkg.scripts ?? {})
80
+ .map(([name, script]) => `- ${name}: ${script}`)
81
+ .join("\n") || "- none";
82
+ return [
83
+ "## Project",
84
+ "",
85
+ `Name: ${pkg.name ?? "(unknown)"}`,
86
+ `Version: ${pkg.version ?? "(unknown)"}`,
87
+ `Type: ${pkg.type ?? "(unknown)"}`,
88
+ "",
89
+ "### Scripts",
90
+ "",
91
+ scripts
92
+ ].join("\n");
93
+ }
94
+ catch {
95
+ return ["## Project", "", "No package.json detected."].join("\n");
96
+ }
97
+ }
98
+ async function renderFileTree(root) {
99
+ const entries = await collectTree(root, root, 0, 3, 180);
100
+ return [
101
+ "## File Tree",
102
+ "",
103
+ "Excluded: node_modules, dist, build, .git, .ai-gateway, env/key/cache files.",
104
+ "",
105
+ fenced(entries.join("\n") || "(empty)")
106
+ ].join("\n");
107
+ }
108
+ async function renderDocsSection(root) {
109
+ const sections = ["## Key Documents", ""];
110
+ for (const relativePath of DOC_CANDIDATES) {
111
+ const absolutePath = path.join(root, relativePath);
112
+ const text = await readTextIfExists(absolutePath);
113
+ if (!text)
114
+ continue;
115
+ sections.push(`### ${relativePath}`, "", truncate(text, 1600), "");
116
+ }
117
+ if (sections.length === 2) {
118
+ sections.push("No key documents found.");
119
+ }
120
+ return sections.join("\n").trimEnd();
121
+ }
122
+ async function renderLogsSection() {
123
+ const logs = await readRecentLogs(10);
124
+ return [
125
+ "## Recent AI Gateway Logs",
126
+ "",
127
+ logs.length
128
+ ? logs.map((event) => `- ${event.timestamp} [${event.level}] ${event.category}/${event.action}: ${event.message}`).join("\n")
129
+ : "No logs found."
130
+ ].join("\n");
131
+ }
132
+ async function collectTree(root, current, depth, maxDepth, maxEntries, state = { count: 0 }) {
133
+ if (depth > maxDepth || state.count >= maxEntries)
134
+ return [];
135
+ const dirents = await fs.readdir(current, { withFileTypes: true });
136
+ const lines = [];
137
+ for (const dirent of dirents.sort((a, b) => a.name.localeCompare(b.name))) {
138
+ if (state.count >= maxEntries)
139
+ break;
140
+ if (shouldExclude(dirent.name))
141
+ continue;
142
+ const absolutePath = path.join(current, dirent.name);
143
+ const relativePath = path.relative(root, absolutePath);
144
+ lines.push(`${" ".repeat(depth)}${dirent.isDirectory() ? "+ " : "- "}${relativePath}`);
145
+ state.count += 1;
146
+ if (dirent.isDirectory()) {
147
+ lines.push(...await collectTree(root, absolutePath, depth + 1, maxDepth, maxEntries, state));
148
+ }
149
+ }
150
+ return lines;
151
+ }
152
+ function shouldExclude(name) {
153
+ if (EXCLUDED_DIRS.has(name) || EXCLUDED_FILES.has(name))
154
+ return true;
155
+ if (name.endsWith(".log"))
156
+ return true;
157
+ if (name.endsWith(".pem") || name.endsWith(".key") || name.endsWith(".p12"))
158
+ return true;
159
+ return false;
160
+ }
161
+ async function runGit(cwd, args) {
162
+ try {
163
+ const { stdout } = await execFileAsync("git", args, { cwd });
164
+ return stdout.trim();
165
+ }
166
+ catch {
167
+ return "";
168
+ }
169
+ }
170
+ async function readTextIfExists(filePath) {
171
+ try {
172
+ return await fs.readFile(filePath, "utf8");
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ function truncate(value, maxLength) {
179
+ const trimmed = value.trim();
180
+ if (trimmed.length <= maxLength)
181
+ return trimmed;
182
+ return `${trimmed.slice(0, maxLength)}\n\n... truncated ...`;
183
+ }
184
+ function fenced(value) {
185
+ return `\`\`\`text\n${value}\n\`\`\``;
186
+ }
@@ -0,0 +1,105 @@
1
+ import { getIntegrationStatePath } from "../config/paths.js";
2
+ import { readJsonFile, updateJsonFile, writeJsonFile } from "../config/json-store.js";
3
+ const EMPTY_STATE = {
4
+ providerRevision: 0,
5
+ integrations: {}
6
+ };
7
+ const registeredIntegrations = new Set();
8
+ export async function readIntegrationState() {
9
+ const raw = await readJsonFile(getIntegrationStatePath(), EMPTY_STATE);
10
+ return normalizeIntegrationState(raw);
11
+ }
12
+ export async function writeIntegrationState(state) {
13
+ await writeJsonFile(getIntegrationStatePath(), normalizeIntegrationState(state));
14
+ }
15
+ export async function recordProviderChange(change) {
16
+ return updateJsonFile(getIntegrationStatePath(), EMPTY_STATE, (raw) => {
17
+ const state = normalizeIntegrationState(raw);
18
+ const nextRevision = state.providerRevision + 1;
19
+ const providerChange = { ...change, at: new Date().toISOString() };
20
+ const integrations = { ...state.integrations };
21
+ for (const name of registeredIntegrations) {
22
+ integrations[name] = appendProviderChange(getIntegrationProviderState(state, name), providerChange);
23
+ }
24
+ return {
25
+ providerRevision: nextRevision,
26
+ integrations
27
+ };
28
+ });
29
+ }
30
+ export async function markIntegrationProvidersSynced(integrationName) {
31
+ const name = normalizeIntegrationName(integrationName);
32
+ await updateJsonFile(getIntegrationStatePath(), EMPTY_STATE, (raw) => {
33
+ const state = normalizeIntegrationState(raw);
34
+ const synced = {
35
+ syncedProviderRevision: state.providerRevision,
36
+ pendingProviderChanges: []
37
+ };
38
+ const integrations = { ...state.integrations, [name]: synced };
39
+ return {
40
+ providerRevision: state.providerRevision,
41
+ integrations
42
+ };
43
+ });
44
+ }
45
+ export async function hasPendingProviderChanges(integrationName) {
46
+ const state = await readIntegrationState();
47
+ const integration = getIntegrationProviderState(state, integrationName);
48
+ return state.providerRevision !== integration.syncedProviderRevision || integration.pendingProviderChanges.length > 0;
49
+ }
50
+ export function registerIntegration(registration) {
51
+ registeredIntegrations.add(normalizeIntegrationName(registration.name));
52
+ }
53
+ export function getIntegrationProviderState(state, integrationName) {
54
+ const name = normalizeIntegrationName(integrationName);
55
+ return state.integrations[name] ?? {
56
+ syncedProviderRevision: state.providerRevision,
57
+ pendingProviderChanges: []
58
+ };
59
+ }
60
+ function normalizeIntegrationState(value) {
61
+ const integrations = normalizeIntegrationMap(value.integrations);
62
+ return {
63
+ providerRevision: Number(value.providerRevision ?? 0),
64
+ integrations
65
+ };
66
+ }
67
+ function normalizeIntegrationMap(value) {
68
+ if (typeof value !== "object" || value === null || Array.isArray(value))
69
+ return {};
70
+ const integrations = {};
71
+ for (const [name, raw] of Object.entries(value)) {
72
+ const normalizedName = normalizeIntegrationName(name);
73
+ integrations[normalizedName] = normalizeIntegrationProviderSyncState(raw);
74
+ }
75
+ return integrations;
76
+ }
77
+ function normalizeIntegrationProviderSyncState(value) {
78
+ return {
79
+ syncedProviderRevision: Number(value.syncedProviderRevision ?? 0),
80
+ pendingProviderChanges: Array.isArray(value.pendingProviderChanges)
81
+ ? value.pendingProviderChanges.filter(isProviderChange)
82
+ : []
83
+ };
84
+ }
85
+ function appendProviderChange(state, change) {
86
+ return {
87
+ syncedProviderRevision: state.syncedProviderRevision,
88
+ pendingProviderChanges: [...state.pendingProviderChanges, change]
89
+ };
90
+ }
91
+ function normalizeIntegrationName(value) {
92
+ const name = value.trim();
93
+ if (!name) {
94
+ throw new Error("Integration name cannot be empty.");
95
+ }
96
+ return name;
97
+ }
98
+ function isProviderChange(value) {
99
+ if (typeof value !== "object" || value === null)
100
+ return false;
101
+ const change = value;
102
+ return (change.type === "added" || change.type === "updated" || change.type === "deleted")
103
+ && typeof change.provider === "string"
104
+ && typeof change.at === "string";
105
+ }