@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,53 @@
1
+ import { ProxyAgent, setGlobalDispatcher } from "undici";
2
+ import { HttpsProxyAgent } from "https-proxy-agent";
3
+ export function getTelegramProxyUrl(config) {
4
+ return config.proxyUrl
5
+ ?? process.env.AI_GATEWAY_TELEGRAM_PROXY
6
+ ?? process.env.HTTPS_PROXY
7
+ ?? process.env.https_proxy
8
+ ?? process.env.ALL_PROXY
9
+ ?? process.env.all_proxy
10
+ ?? process.env.HTTP_PROXY
11
+ ?? process.env.http_proxy
12
+ ?? null;
13
+ }
14
+ export function configureTelegramNetwork(config) {
15
+ const proxyUrl = getTelegramProxyUrl(config);
16
+ if (!proxyUrl)
17
+ return null;
18
+ setGlobalDispatcher(new ProxyAgent(proxyUrl));
19
+ return proxyUrl;
20
+ }
21
+ export function createTelegramApiClientOptions(proxyUrl) {
22
+ if (!proxyUrl)
23
+ return undefined;
24
+ return {
25
+ baseFetchConfig: {
26
+ agent: new HttpsProxyAgent(proxyUrl)
27
+ }
28
+ };
29
+ }
30
+ export async function checkTelegramApi(token, proxyUrl) {
31
+ if (proxyUrl) {
32
+ setGlobalDispatcher(new ProxyAgent(proxyUrl));
33
+ }
34
+ try {
35
+ const controller = new AbortController();
36
+ const timeout = setTimeout(() => controller.abort(), 10000);
37
+ const response = await fetch(`https://api.telegram.org/bot${token}/getMe`, { signal: controller.signal });
38
+ clearTimeout(timeout);
39
+ const body = await response.json();
40
+ return {
41
+ ok: Boolean(response.ok && body.ok),
42
+ status: response.status,
43
+ username: body.result?.username,
44
+ error: body.description
45
+ };
46
+ }
47
+ catch (error) {
48
+ return {
49
+ ok: false,
50
+ error: error instanceof Error ? `${error.name}: ${error.message}` : String(error)
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,229 @@
1
+ import fs from "node:fs";
2
+ const MAX_CONVERSATION_TURNS = 12;
3
+ const MAX_CONVERSATION_USER_CHARS = 1000;
4
+ const MAX_CONVERSATION_ASSISTANT_CHARS = 1600;
5
+ const MAX_PINNED_TURNS = 3;
6
+ export class SessionManager {
7
+ store;
8
+ config;
9
+ chatQueues = new Map();
10
+ constructor(store, config) {
11
+ this.store = store;
12
+ this.config = config;
13
+ }
14
+ async runExclusive(chatId, task) {
15
+ const previous = this.chatQueues.get(chatId) ?? Promise.resolve();
16
+ const waitForPrevious = previous.catch(() => undefined);
17
+ let release;
18
+ const current = new Promise((resolve) => {
19
+ release = resolve;
20
+ });
21
+ const next = waitForPrevious.then(() => current);
22
+ this.chatQueues.set(chatId, next);
23
+ await waitForPrevious;
24
+ try {
25
+ return await task();
26
+ }
27
+ finally {
28
+ release();
29
+ if (this.chatQueues.get(chatId) === next) {
30
+ this.chatQueues.delete(chatId);
31
+ }
32
+ }
33
+ }
34
+ getOrCreate(chatId, defaults = {}) {
35
+ const existing = this.store.getActiveByChatId(chatId);
36
+ if (existing) {
37
+ this.store.touch(existing.id);
38
+ return existing;
39
+ }
40
+ return this.store.create({
41
+ chatId,
42
+ engine: defaults.engine ?? this.config.defaultEngine,
43
+ cwd: defaults.cwd ?? this.config.defaultCwd,
44
+ permissionMode: defaults.permissionMode ?? this.config.defaultPermissionMode,
45
+ approveAll: defaults.approveAll ?? false,
46
+ setupStep: defaults.setupStep ?? "ready",
47
+ status: defaults.status ?? "idle"
48
+ });
49
+ }
50
+ startSetup(chatId) {
51
+ this.store.closeByChatId(chatId);
52
+ return this.store.create({
53
+ chatId,
54
+ engine: this.config.defaultEngine,
55
+ cwd: this.config.defaultCwd,
56
+ permissionMode: this.config.defaultPermissionMode,
57
+ approveAll: false,
58
+ setupStep: "model",
59
+ status: "idle"
60
+ });
61
+ }
62
+ getById(id) {
63
+ return this.store.getById(id);
64
+ }
65
+ update(id, patch) {
66
+ return this.store.update(id, patch);
67
+ }
68
+ newSession(chatId) {
69
+ const current = this.store.getActiveByChatId(chatId);
70
+ this.store.closeByChatId(chatId);
71
+ return this.store.create({
72
+ chatId,
73
+ engine: current?.engine ?? this.config.defaultEngine,
74
+ cwd: current?.cwd ?? this.config.defaultCwd,
75
+ permissionMode: current?.permissionMode ?? this.config.defaultPermissionMode,
76
+ approveAll: false,
77
+ setupStep: current?.setupStep ?? "ready",
78
+ status: "idle"
79
+ });
80
+ }
81
+ clearConversation(chatId) {
82
+ const current = this.getOrCreate(chatId);
83
+ return this.store.update(current.id, {
84
+ engineSessionId: null,
85
+ botContextFingerprint: null,
86
+ approveAll: false,
87
+ status: "idle",
88
+ messageCount: 0,
89
+ attachments: [],
90
+ conversation: []
91
+ });
92
+ }
93
+ pinTurn(chatId, messageId, turn) {
94
+ const session = this.getOrCreate(chatId);
95
+ const existing = session.pinnedTurns.find((p) => p.messageId === messageId);
96
+ if (existing)
97
+ return session;
98
+ const pinned = {
99
+ messageId,
100
+ user: truncateConversationText(turn.user, MAX_CONVERSATION_USER_CHARS),
101
+ assistant: truncateConversationText(turn.assistant, MAX_CONVERSATION_ASSISTANT_CHARS),
102
+ pinnedAt: new Date().toISOString()
103
+ };
104
+ const next = [...session.pinnedTurns, pinned].slice(-MAX_PINNED_TURNS);
105
+ return this.store.update(session.id, { pinnedTurns: next });
106
+ }
107
+ unpinTurn(chatId, messageId) {
108
+ const session = this.getOrCreate(chatId);
109
+ const next = session.pinnedTurns.filter((p) => p.messageId !== messageId);
110
+ if (next.length === session.pinnedTurns.length)
111
+ return session;
112
+ return this.store.update(session.id, { pinnedTurns: next });
113
+ }
114
+ appendConversationTurn(sessionId, input) {
115
+ const session = this.store.getById(sessionId);
116
+ if (!session) {
117
+ throw new Error(`Telegram session not found: ${sessionId}`);
118
+ }
119
+ const turn = {
120
+ user: truncateConversationText(input.user, MAX_CONVERSATION_USER_CHARS),
121
+ assistant: truncateConversationText(input.assistant, MAX_CONVERSATION_ASSISTANT_CHARS),
122
+ createdAt: new Date().toISOString()
123
+ };
124
+ return this.store.update(sessionId, {
125
+ conversation: [...session.conversation, turn].slice(-MAX_CONVERSATION_TURNS)
126
+ });
127
+ }
128
+ prepareEditedPrompt(chatId, previousPrompt) {
129
+ const session = this.getOrCreate(chatId);
130
+ const conversation = [...session.conversation];
131
+ for (let index = conversation.length - 1; index >= 0; index -= 1) {
132
+ if (conversation[index]?.user === previousPrompt) {
133
+ conversation.splice(index, 1);
134
+ break;
135
+ }
136
+ }
137
+ return this.store.update(session.id, {
138
+ engineSessionId: null,
139
+ conversation
140
+ });
141
+ }
142
+ recordProviderUsage(sessionId, input) {
143
+ const session = this.store.getById(sessionId);
144
+ if (!session) {
145
+ throw new Error(`Telegram session not found: ${sessionId}`);
146
+ }
147
+ const provider = input.provider.trim() || "unknown";
148
+ const model = input.model?.trim() || "(unknown)";
149
+ const hasCost = typeof input.costUsd === "number" && Number.isFinite(input.costUsd) && input.costUsd >= 0;
150
+ const nextUsage = upsertProviderUsage(session.providerUsage, {
151
+ provider,
152
+ model,
153
+ costUsd: hasCost ? input.costUsd : undefined,
154
+ usage: input.usage
155
+ });
156
+ return this.store.update(sessionId, { providerUsage: nextUsage });
157
+ }
158
+ resume(chatId, idPrefix) {
159
+ const sessions = this.getHistory(chatId, 20);
160
+ const target = idPrefix
161
+ ? sessions.find((session) => session.id.startsWith(idPrefix))
162
+ : sessions.find((session) => session.status !== "closed") ?? sessions[0];
163
+ if (!target)
164
+ return null;
165
+ this.store.closeByChatId(chatId);
166
+ return this.store.update(target.id, { status: "idle", approveAll: false });
167
+ }
168
+ getHistory(chatId, limit = 10) {
169
+ return this.store.listByChatId(chatId, limit);
170
+ }
171
+ getActiveSessions(limit = 100) {
172
+ return this.store.listActiveByChat(limit);
173
+ }
174
+ addAttachment(chatId, attachment) {
175
+ const session = this.getOrCreate(chatId);
176
+ return this.store.addAttachment(session.id, attachment);
177
+ }
178
+ clearAttachments(sessionId) {
179
+ return this.store.clearAttachments(sessionId);
180
+ }
181
+ rememberWorkdir(chatId, cwd) {
182
+ this.store.rememberWorkdir(chatId, cwd);
183
+ }
184
+ getWorkdirs(chatId, limit = 8) {
185
+ return this.store.listWorkdirs(chatId, limit).filter((p) => fs.existsSync(p));
186
+ }
187
+ addCustomCommand(chatId, cwd, cmd) {
188
+ this.store.addCustomCommand(chatId, cwd, cmd);
189
+ }
190
+ listCustomCommands(chatId, cwd) {
191
+ return this.store.listCustomCommands(chatId, cwd);
192
+ }
193
+ deleteCustomCommand(chatId, cwd, name) {
194
+ return this.store.deleteCustomCommand(chatId, cwd, name);
195
+ }
196
+ cleanupExpired() {
197
+ return this.store.deleteExpired(this.config.sessionTimeoutHours);
198
+ }
199
+ resetOrphanSessions() {
200
+ return this.store.resetRunningToIdle();
201
+ }
202
+ }
203
+ function truncateConversationText(value, maxLength) {
204
+ const text = value.trim();
205
+ if (text.length <= maxLength)
206
+ return text;
207
+ return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
208
+ }
209
+ function upsertProviderUsage(usage, input) {
210
+ const now = new Date().toISOString();
211
+ const index = usage.findIndex((item) => item.provider === input.provider);
212
+ const current = index >= 0 ? usage[index] : null;
213
+ const hasCost = typeof input.costUsd === "number" && Number.isFinite(input.costUsd) && input.costUsd >= 0;
214
+ const next = {
215
+ provider: input.provider,
216
+ model: input.model || current?.model || "(unknown)",
217
+ requestCount: (current?.requestCount ?? 0) + 1,
218
+ costUsd: (current?.costUsd ?? 0) + (hasCost ? input.costUsd : 0),
219
+ unknownCostCount: (current?.unknownCostCount ?? 0) + (hasCost ? 0 : 1),
220
+ inputTokens: (current?.inputTokens ?? 0) + (input.usage?.inputTokens ?? 0),
221
+ outputTokens: (current?.outputTokens ?? 0) + (input.usage?.outputTokens ?? 0),
222
+ totalTokens: (current?.totalTokens ?? 0) + (input.usage?.totalTokens ?? 0),
223
+ cacheReadInputTokens: (current?.cacheReadInputTokens ?? 0) + (input.usage?.cacheReadInputTokens ?? 0),
224
+ updatedAt: now
225
+ };
226
+ if (index < 0)
227
+ return [...usage, next];
228
+ return usage.map((item, itemIndex) => itemIndex === index ? next : item);
229
+ }
@@ -0,0 +1,363 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import Database from "better-sqlite3";
5
+ export class SessionStore {
6
+ dbPath;
7
+ db;
8
+ constructor(dbPath) {
9
+ this.dbPath = dbPath;
10
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
11
+ this.db = new Database(dbPath);
12
+ this.db.pragma("journal_mode = WAL");
13
+ this.migrate();
14
+ }
15
+ close() {
16
+ this.db.close();
17
+ }
18
+ create(input) {
19
+ const now = new Date().toISOString();
20
+ const session = {
21
+ id: randomUUID(),
22
+ chatId: input.chatId,
23
+ engine: input.engine,
24
+ engineSessionId: null,
25
+ botContextFingerprint: null,
26
+ cwd: input.cwd,
27
+ permissionMode: input.permissionMode,
28
+ approveAll: input.approveAll ?? false,
29
+ setupStep: input.setupStep ?? "ready",
30
+ status: input.status ?? "idle",
31
+ messageCount: 0,
32
+ attachments: [],
33
+ conversation: [],
34
+ pinnedTurns: [],
35
+ providerUsage: [],
36
+ createdAt: now,
37
+ lastActiveAt: now
38
+ };
39
+ this.db.prepare(`insert into sessions (
40
+ id, chat_id, engine, engine_session_id, bot_context_fingerprint, cwd, permission_mode, approve_all, setup_step,
41
+ status, message_count, attachments_json, conversation_json, pinned_turns_json, provider_usage_json, created_at, last_active_at
42
+ ) values (
43
+ @id, @chatId, @engine, @engineSessionId, @botContextFingerprint, @cwd, @permissionMode, @approveAll, @setupStep,
44
+ @status, @messageCount, @attachmentsJson, @conversationJson, @pinnedTurnsJson, @providerUsageJson, @createdAt, @lastActiveAt
45
+ )`).run({
46
+ ...session,
47
+ approveAll: session.approveAll ? 1 : 0,
48
+ setupStep: session.setupStep,
49
+ attachmentsJson: JSON.stringify(session.attachments),
50
+ conversationJson: JSON.stringify(session.conversation),
51
+ pinnedTurnsJson: JSON.stringify(session.pinnedTurns),
52
+ providerUsageJson: JSON.stringify(session.providerUsage)
53
+ });
54
+ return session;
55
+ }
56
+ getById(id) {
57
+ const row = this.db.prepare("select * from sessions where id = ?").get(id);
58
+ return row ? rowToSession(row) : null;
59
+ }
60
+ getActiveByChatId(chatId) {
61
+ const row = this.db.prepare("select * from sessions where chat_id = ? and status != 'closed' order by last_active_at desc limit 1").get(chatId);
62
+ return row ? rowToSession(row) : null;
63
+ }
64
+ listByChatId(chatId, limit = 10) {
65
+ const rows = this.db.prepare("select * from sessions where chat_id = ? order by last_active_at desc limit ?").all(chatId, limit);
66
+ return rows.map(rowToSession);
67
+ }
68
+ listActiveByChat(limit = 100) {
69
+ const rows = this.db.prepare("select * from sessions where status != 'closed' order by chat_id asc, last_active_at desc limit ?").all(limit * 5);
70
+ const seen = new Set();
71
+ const sessions = [];
72
+ for (const row of rows) {
73
+ if (seen.has(row.chat_id))
74
+ continue;
75
+ seen.add(row.chat_id);
76
+ sessions.push(rowToSession(row));
77
+ if (sessions.length >= limit)
78
+ break;
79
+ }
80
+ return sessions;
81
+ }
82
+ update(id, patch) {
83
+ const current = this.getById(id);
84
+ if (!current) {
85
+ throw new Error(`Telegram session not found: ${id}`);
86
+ }
87
+ const next = {
88
+ ...current,
89
+ ...patch,
90
+ attachments: patch.attachments ?? current.attachments,
91
+ conversation: patch.conversation ?? current.conversation,
92
+ pinnedTurns: patch.pinnedTurns ?? current.pinnedTurns,
93
+ providerUsage: patch.providerUsage ?? current.providerUsage,
94
+ lastActiveAt: patch.lastActiveAt ?? new Date().toISOString()
95
+ };
96
+ this.db.prepare(`update sessions set
97
+ engine = @engine,
98
+ engine_session_id = @engineSessionId,
99
+ bot_context_fingerprint = @botContextFingerprint,
100
+ cwd = @cwd,
101
+ permission_mode = @permissionMode,
102
+ approve_all = @approveAll,
103
+ setup_step = @setupStep,
104
+ status = @status,
105
+ message_count = @messageCount,
106
+ attachments_json = @attachmentsJson,
107
+ conversation_json = @conversationJson,
108
+ pinned_turns_json = @pinnedTurnsJson,
109
+ provider_usage_json = @providerUsageJson,
110
+ last_active_at = @lastActiveAt
111
+ where id = @id`).run({
112
+ id,
113
+ engine: next.engine,
114
+ engineSessionId: next.engineSessionId,
115
+ botContextFingerprint: next.botContextFingerprint,
116
+ cwd: next.cwd,
117
+ permissionMode: next.permissionMode,
118
+ approveAll: next.approveAll ? 1 : 0,
119
+ setupStep: next.setupStep,
120
+ status: next.status,
121
+ messageCount: next.messageCount,
122
+ attachmentsJson: JSON.stringify(next.attachments),
123
+ conversationJson: JSON.stringify(next.conversation),
124
+ pinnedTurnsJson: JSON.stringify(next.pinnedTurns),
125
+ providerUsageJson: JSON.stringify(next.providerUsage),
126
+ lastActiveAt: next.lastActiveAt
127
+ });
128
+ return next;
129
+ }
130
+ closeByChatId(chatId) {
131
+ this.db.prepare("update sessions set status = 'closed', last_active_at = ? where chat_id = ? and status != 'closed'")
132
+ .run(new Date().toISOString(), chatId);
133
+ }
134
+ rememberWorkdir(chatId, cwd) {
135
+ const now = new Date().toISOString();
136
+ this.db.prepare(`insert into workdirs (chat_id, cwd, last_used_at)
137
+ values (?, ?, ?)
138
+ on conflict(chat_id, cwd) do update set last_used_at = excluded.last_used_at`).run(chatId, cwd, now);
139
+ }
140
+ listWorkdirs(chatId, limit = 8) {
141
+ const rows = this.db.prepare("select cwd from workdirs where chat_id = ? order by last_used_at desc limit ?").all(chatId, limit);
142
+ return rows.map((row) => row.cwd);
143
+ }
144
+ touch(id) {
145
+ this.db.prepare("update sessions set last_active_at = ? where id = ?").run(new Date().toISOString(), id);
146
+ }
147
+ deleteExpired(timeoutHours) {
148
+ const cutoff = new Date(Date.now() - timeoutHours * 60 * 60 * 1000).toISOString();
149
+ const result = this.db.prepare("delete from sessions where last_active_at < ?").run(cutoff);
150
+ return result.changes;
151
+ }
152
+ resetRunningToIdle() {
153
+ const result = this.db.prepare("update sessions set status = 'idle' where status in ('running', 'waiting_approval')").run();
154
+ return result.changes;
155
+ }
156
+ addAttachment(sessionId, attachment) {
157
+ const session = this.getById(sessionId);
158
+ if (!session) {
159
+ throw new Error(`Telegram session not found: ${sessionId}`);
160
+ }
161
+ return this.update(sessionId, {
162
+ attachments: [...session.attachments, attachment].slice(-5)
163
+ });
164
+ }
165
+ clearAttachments(sessionId) {
166
+ return this.update(sessionId, { attachments: [] });
167
+ }
168
+ migrate() {
169
+ this.db.exec(`
170
+ create table if not exists sessions (
171
+ id text primary key,
172
+ chat_id integer not null,
173
+ engine text not null,
174
+ engine_session_id text,
175
+ bot_context_fingerprint text,
176
+ cwd text not null,
177
+ permission_mode text not null,
178
+ approve_all integer not null default 0,
179
+ setup_step text not null default 'ready',
180
+ status text not null,
181
+ message_count integer not null default 0,
182
+ attachments_json text not null default '[]',
183
+ conversation_json text not null default '[]',
184
+ provider_usage_json text not null default '[]',
185
+ created_at text not null,
186
+ last_active_at text not null
187
+ );
188
+ create index if not exists idx_sessions_chat_active on sessions(chat_id, status, last_active_at);
189
+ create table if not exists workdirs (
190
+ chat_id integer not null,
191
+ cwd text not null,
192
+ last_used_at text not null,
193
+ primary key (chat_id, cwd)
194
+ );
195
+ create index if not exists idx_workdirs_chat_last_used on workdirs(chat_id, last_used_at);
196
+ `);
197
+ this.ensureColumn("sessions", "setup_step", "setup_step text not null default 'ready'");
198
+ this.ensureColumn("sessions", "provider_usage_json", "provider_usage_json text not null default '[]'");
199
+ this.ensureColumn("sessions", "bot_context_fingerprint", "bot_context_fingerprint text");
200
+ this.ensureColumn("sessions", "conversation_json", "conversation_json text not null default '[]'");
201
+ this.ensureColumn("sessions", "pinned_turns_json", "pinned_turns_json text not null default '[]'");
202
+ this.db.exec(`
203
+ create table if not exists custom_commands (
204
+ chat_id integer not null,
205
+ cwd text not null,
206
+ name text not null,
207
+ params_json text not null default '[]',
208
+ template text not null,
209
+ description text not null,
210
+ created_at text not null,
211
+ primary key (chat_id, cwd, name)
212
+ );
213
+ `);
214
+ }
215
+ addCustomCommand(chatId, cwd, cmd) {
216
+ this.db.prepare(`insert or replace into custom_commands (chat_id, cwd, name, params_json, template, description, created_at)
217
+ values (?, ?, ?, ?, ?, ?, ?)`).run(chatId, cwd, cmd.name, JSON.stringify(cmd.params), cmd.template, cmd.description, new Date().toISOString());
218
+ }
219
+ listCustomCommands(chatId, cwd) {
220
+ const rows = this.db.prepare("select name, params_json, template, description from custom_commands where chat_id = ? and cwd = ? order by created_at").all(chatId, cwd);
221
+ return rows.map((r) => ({
222
+ name: r.name,
223
+ params: JSON.parse(r.params_json),
224
+ template: r.template,
225
+ description: r.description
226
+ }));
227
+ }
228
+ deleteCustomCommand(chatId, cwd, name) {
229
+ const result = this.db.prepare("delete from custom_commands where chat_id = ? and cwd = ? and name = ?").run(chatId, cwd, name);
230
+ return result.changes > 0;
231
+ }
232
+ ensureColumn(table, column, definition) {
233
+ const rows = this.db.prepare(`pragma table_info(${table})`).all();
234
+ if (!rows.some((row) => row.name === column)) {
235
+ this.db.exec(`alter table ${table} add column ${definition}`);
236
+ }
237
+ }
238
+ }
239
+ function rowToSession(row) {
240
+ return {
241
+ id: row.id,
242
+ chatId: row.chat_id,
243
+ engine: row.engine === "codex" ? "codex" : "claude",
244
+ engineSessionId: row.engine_session_id,
245
+ botContextFingerprint: row.bot_context_fingerprint ?? null,
246
+ cwd: row.cwd,
247
+ permissionMode: row.permission_mode === "readOnly" || row.permission_mode === "acceptEdits" || row.permission_mode === "bypassPermissions" ? row.permission_mode : "default",
248
+ approveAll: row.approve_all === 1,
249
+ setupStep: normalizeSetupStep(row.setup_step),
250
+ status: row.status === "running" || row.status === "waiting_approval" || row.status === "closed" ? row.status : "idle",
251
+ messageCount: row.message_count,
252
+ attachments: parseAttachments(row.attachments_json),
253
+ conversation: parseConversation(row.conversation_json),
254
+ pinnedTurns: parsePinnedTurns(row.pinned_turns_json),
255
+ providerUsage: parseProviderUsage(row.provider_usage_json),
256
+ createdAt: row.created_at,
257
+ lastActiveAt: row.last_active_at
258
+ };
259
+ }
260
+ function normalizeSetupStep(value) {
261
+ if (value === "model"
262
+ || value === "workdir"
263
+ || value === "custom_workdir"
264
+ || value === "edit_custom_workdir"
265
+ || value === "usage_price_input"
266
+ || value === "remember_global"
267
+ || value === "remember_bot"
268
+ || value === "remember_auto"
269
+ || value === "memory_promote"
270
+ || value === "cli"
271
+ || value === "agent_profile"
272
+ || value === "command_name"
273
+ || value === "command_template"
274
+ || value === "ready")
275
+ return value;
276
+ return "ready";
277
+ }
278
+ function parseAttachments(value) {
279
+ try {
280
+ const parsed = JSON.parse(value);
281
+ return Array.isArray(parsed) ? parsed.filter(isAttachment) : [];
282
+ }
283
+ catch {
284
+ return [];
285
+ }
286
+ }
287
+ function parseConversation(value) {
288
+ try {
289
+ const parsed = JSON.parse(value);
290
+ if (!Array.isArray(parsed))
291
+ return [];
292
+ return parsed.flatMap((item) => isConversationTurn(item) ? [item] : []);
293
+ }
294
+ catch {
295
+ return [];
296
+ }
297
+ }
298
+ function parsePinnedTurns(value) {
299
+ if (!value)
300
+ return [];
301
+ try {
302
+ const parsed = JSON.parse(value);
303
+ if (!Array.isArray(parsed))
304
+ return [];
305
+ return parsed.filter((item) => {
306
+ const t = item;
307
+ return Boolean(t && typeof t.messageId === "number" && typeof t.user === "string" && typeof t.assistant === "string");
308
+ });
309
+ }
310
+ catch {
311
+ return [];
312
+ }
313
+ }
314
+ function parseProviderUsage(value) {
315
+ try {
316
+ const parsed = JSON.parse(value);
317
+ if (!Array.isArray(parsed))
318
+ return [];
319
+ return parsed.flatMap((item) => {
320
+ if (!isProviderUsage(item))
321
+ return [];
322
+ return [{
323
+ provider: item.provider,
324
+ model: item.model,
325
+ requestCount: item.requestCount,
326
+ costUsd: item.costUsd,
327
+ unknownCostCount: item.unknownCostCount,
328
+ inputTokens: readNonNegativeNumber(item.inputTokens),
329
+ outputTokens: readNonNegativeNumber(item.outputTokens),
330
+ totalTokens: readNonNegativeNumber(item.totalTokens),
331
+ cacheReadInputTokens: readNonNegativeNumber(item.cacheReadInputTokens),
332
+ updatedAt: item.updatedAt
333
+ }];
334
+ });
335
+ }
336
+ catch {
337
+ return [];
338
+ }
339
+ }
340
+ function isAttachment(value) {
341
+ const item = value;
342
+ return Boolean(item && typeof item.name === "string" && typeof item.path === "string" && typeof item.text === "string");
343
+ }
344
+ function isConversationTurn(value) {
345
+ const item = value;
346
+ return Boolean(item
347
+ && typeof item.user === "string"
348
+ && typeof item.assistant === "string"
349
+ && typeof item.createdAt === "string");
350
+ }
351
+ function isProviderUsage(value) {
352
+ const item = value;
353
+ return Boolean(item
354
+ && typeof item.provider === "string"
355
+ && typeof item.model === "string"
356
+ && Number.isFinite(item.requestCount)
357
+ && Number.isFinite(item.costUsd)
358
+ && Number.isFinite(item.unknownCostCount)
359
+ && typeof item.updatedAt === "string");
360
+ }
361
+ function readNonNegativeNumber(value) {
362
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
363
+ }
@@ -0,0 +1 @@
1
+ export {};