arisa 2.3.55 → 3.0.0

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 (62) hide show
  1. package/AGENTS.md +102 -0
  2. package/README.md +120 -165
  3. package/cli/openai-transcribe/index.js +51 -0
  4. package/cli/openai-transcribe/package.json +6 -0
  5. package/cli/openai-transcribe/tool.manifest.json +15 -0
  6. package/cli/openai-tts/index.js +58 -0
  7. package/cli/openai-tts/package.json +6 -0
  8. package/cli/openai-tts/tool.manifest.json +20 -0
  9. package/cli/web-browser/index.js +146 -0
  10. package/cli/web-browser/package.json +6 -0
  11. package/cli/web-browser/tool.manifest.json +8 -0
  12. package/package.json +26 -44
  13. package/src/core/agent/agent-manager.js +218 -0
  14. package/src/core/artifacts/artifact-store.js +102 -0
  15. package/src/core/config/config-store.js +20 -0
  16. package/src/core/tools/tool-registry.js +117 -0
  17. package/src/index.js +27 -0
  18. package/src/runtime/bootstrap.js +213 -0
  19. package/src/runtime/create-app.js +22 -0
  20. package/src/transport/telegram/auth.js +13 -0
  21. package/src/transport/telegram/bot.js +214 -0
  22. package/src/transport/telegram/media.js +75 -0
  23. package/CLAUDE.md +0 -191
  24. package/SOUL.md +0 -36
  25. package/bin/arisa.js +0 -644
  26. package/scripts/dump-commands.ts +0 -26
  27. package/scripts/test-secrets.ts +0 -22
  28. package/src/core/attachments.ts +0 -104
  29. package/src/core/auth.ts +0 -58
  30. package/src/core/context.ts +0 -30
  31. package/src/core/file-detector.ts +0 -39
  32. package/src/core/format.ts +0 -159
  33. package/src/core/index.ts +0 -456
  34. package/src/core/intent.ts +0 -119
  35. package/src/core/media.ts +0 -144
  36. package/src/core/onboarding.ts +0 -102
  37. package/src/core/processor.ts +0 -305
  38. package/src/core/router.ts +0 -64
  39. package/src/core/scheduler.ts +0 -193
  40. package/src/daemon/agent-cli.ts +0 -130
  41. package/src/daemon/auto-install.ts +0 -158
  42. package/src/daemon/autofix.ts +0 -116
  43. package/src/daemon/bridge.ts +0 -166
  44. package/src/daemon/channels/base.ts +0 -10
  45. package/src/daemon/channels/telegram.ts +0 -306
  46. package/src/daemon/claude-login.ts +0 -218
  47. package/src/daemon/codex-login.ts +0 -172
  48. package/src/daemon/fallback.ts +0 -73
  49. package/src/daemon/index.ts +0 -272
  50. package/src/daemon/lifecycle.ts +0 -313
  51. package/src/daemon/setup.ts +0 -329
  52. package/src/shared/ai-cli.ts +0 -165
  53. package/src/shared/config.ts +0 -137
  54. package/src/shared/db.ts +0 -304
  55. package/src/shared/deepbase-secure.ts +0 -39
  56. package/src/shared/ink-shim.js +0 -14
  57. package/src/shared/logger.ts +0 -42
  58. package/src/shared/paths.ts +0 -90
  59. package/src/shared/ports.ts +0 -120
  60. package/src/shared/secrets.ts +0 -136
  61. package/src/shared/types.ts +0 -103
  62. package/tsconfig.json +0 -19
@@ -1,165 +0,0 @@
1
- /**
2
- * @module shared/ai-cli
3
- * @role Resolve agent CLI binaries and execute them via Bun runtime.
4
- * When running as root, wraps calls with su arisa to satisfy
5
- * Claude CLI's non-root requirement.
6
- */
7
-
8
- import { existsSync, openSync, readSync, closeSync } from "fs";
9
- import { delimiter, dirname, join } from "path";
10
-
11
- export type AgentCliName = "claude" | "codex";
12
-
13
- const ARISA_HOME = "/home/arisa";
14
- // Use root's bun — arisa user has traverse+read access via chmod o+x /root, o+rX /root/.bun
15
- const ROOT_BUN_INSTALL = process.env.BUN_INSTALL || "/root/.bun";
16
- const ROOT_BUN_BIN = `${ROOT_BUN_INSTALL}/bin`;
17
- const ARISA_BUN_ENV = `export HOME=${ARISA_HOME} && export BUN_INSTALL=${ROOT_BUN_INSTALL} && export PATH=${ROOT_BUN_BIN}:$PATH`;
18
-
19
- export function isRunningAsRoot(): boolean {
20
- return process.getuid?.() === 0;
21
- }
22
-
23
- function shellEscape(arg: string): string {
24
- return "'" + arg.replace(/'/g, "'\\''") + "'";
25
- }
26
-
27
- function unique(paths: Array<string | null | undefined>): string[] {
28
- const seen = new Set<string>();
29
- const out: string[] = [];
30
- for (const p of paths) {
31
- if (!p) continue;
32
- if (seen.has(p)) continue;
33
- seen.add(p);
34
- out.push(p);
35
- }
36
- return out;
37
- }
38
-
39
- function cliOverrideEnvVar(cli: AgentCliName): string | undefined {
40
- return cli === "codex" ? process.env.ARISA_CODEX_BIN : process.env.ARISA_CLAUDE_BIN;
41
- }
42
-
43
- function candidatePaths(cli: AgentCliName): string[] {
44
- const arisaBunBin = `${ARISA_HOME}/.bun/bin`;
45
-
46
- if (isRunningAsRoot()) {
47
- return unique([
48
- cliOverrideEnvVar(cli),
49
- join(ROOT_BUN_BIN, cli),
50
- join(arisaBunBin, cli),
51
- ]);
52
- }
53
-
54
- const bunInstall = process.env.BUN_INSTALL?.trim();
55
- const bunDir = dirname(process.execPath);
56
- const fromPath = Bun.which(cli);
57
- const fromEnvPath = (process.env.PATH || "")
58
- .split(delimiter)
59
- .map((entry) => entry.trim())
60
- .filter(Boolean)
61
- .map((entry) => join(entry, cli));
62
-
63
- return unique([
64
- cliOverrideEnvVar(cli),
65
- bunInstall ? join(bunInstall, "bin", cli) : null,
66
- join(arisaBunBin, cli),
67
- join(bunDir, cli),
68
- fromPath,
69
- ...fromEnvPath,
70
- ]);
71
- }
72
-
73
- export function resolveAgentCliPath(cli: AgentCliName): string | null {
74
- for (const candidate of candidatePaths(cli)) {
75
- if (existsSync(candidate)) return candidate;
76
- }
77
- return null;
78
- }
79
-
80
- export function isAgentCliInstalled(cli: AgentCliName): boolean {
81
- return resolveAgentCliPath(cli) !== null;
82
- }
83
-
84
- const INK_SHIM = join(dirname(new URL(import.meta.url).pathname), "ink-shim.js");
85
-
86
- // Env vars that must survive the su - login shell reset.
87
- // CLAUDE_CODE_OAUTH_TOKEN is a long-lived (1 year) token from `claude setup-token`
88
- // — the headless auth method. It must be passed through to the CLI.
89
- const PASSTHROUGH_VARS = [
90
- "CLAUDE_CODE_OAUTH_TOKEN",
91
- "ANTHROPIC_API_KEY",
92
- "OPENAI_API_KEY",
93
- ];
94
-
95
- function buildEnvExports(): string {
96
- const exports: string[] = [];
97
- for (const key of PASSTHROUGH_VARS) {
98
- const val = process.env[key];
99
- if (val) exports.push(`export ${key}=${shellEscape(val)}`);
100
- }
101
- return exports.length > 0 ? exports.join(" && ") + " && " : "";
102
- }
103
-
104
- export interface AgentCliOptions {
105
- /** Skip the ink-shim preload (useful for interactive login flows). Default: false */
106
- skipPreload?: boolean;
107
- }
108
-
109
- /** Env vars to suppress Ink/TTY in non-interactive CLI spawns. Merge into Bun.spawn env. */
110
- export const CLI_SPAWN_ENV: Record<string, string> = { CI: "true", TERM: "dumb" };
111
-
112
- /**
113
- * Detect native executables (Mach-O, ELF) by reading magic bytes.
114
- * Claude Code CLI v2+ ships as a native binary, not a JS script.
115
- */
116
- function isNativeBinary(filePath: string): boolean {
117
- try {
118
- const fd = openSync(filePath, "r");
119
- const buf = Buffer.alloc(4);
120
- readSync(fd, buf, 0, 4, 0);
121
- closeSync(fd);
122
- const magic = buf.readUInt32BE(0);
123
- return (
124
- magic === 0xCFFAEDFE || // Mach-O 64-bit LE (macOS arm64/x86_64)
125
- magic === 0xCEFAEDFE || // Mach-O 32-bit LE
126
- magic === 0xFEEDFACF || // Mach-O 64-bit BE
127
- magic === 0xFEEDFACE || // Mach-O 32-bit BE
128
- magic === 0xCAFEBABE || // Mach-O Universal
129
- magic === 0x7F454C46 // ELF (Linux)
130
- );
131
- } catch {
132
- return false;
133
- }
134
- }
135
-
136
- export function buildBunWrappedAgentCliCommand(cli: AgentCliName, args: string[], options?: AgentCliOptions): string[] {
137
- const cliPath = isRunningAsRoot()
138
- ? (resolveAgentCliPath(cli) || join(ROOT_BUN_BIN, cli))
139
- : resolveAgentCliPath(cli);
140
-
141
- if (!cliPath) {
142
- throw new Error(`${cli} CLI not found`);
143
- }
144
-
145
- // Native binaries (Mach-O, ELF) must be executed directly — bun can't parse them as JS
146
- const native = isNativeBinary(cliPath);
147
-
148
- if (isRunningAsRoot()) {
149
- // Run as arisa user — Claude CLI refuses to run as root.
150
- const ciEnv = options?.skipPreload ? "" : "export CI=true && export TERM=dumb && ";
151
- const inner = native
152
- ? [cliPath, ...args].map(shellEscape).join(" ")
153
- : ["bun", "--bun", ...(!options?.skipPreload ? ["--preload", INK_SHIM] : []), cliPath, ...args].map(shellEscape).join(" ");
154
- // su without "-" preserves parent env (tokens, keys); explicit HOME/PATH for arisa
155
- return ["su", "arisa", "-s", "/bin/bash", "-c", `${ARISA_BUN_ENV} && ${ciEnv}${buildEnvExports()}${inner}`];
156
- }
157
-
158
- if (native) {
159
- return [cliPath, ...args];
160
- }
161
-
162
- // JS/Node scripts: wrap with bun for performance + optional ink-shim preload
163
- const preloadArgs = !options?.skipPreload ? ["--preload", INK_SHIM] : [];
164
- return ["bun", "--bun", ...preloadArgs, cliPath, ...args];
165
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * @module shared/config
3
- * @role Centralized configuration from encrypted secrets + env vars fallback
4
- * @responsibilities
5
- * - Load API keys from encrypted secrets DB (with .env fallback)
6
- * - Define ports, paths, timeouts
7
- * @dependencies secrets.ts
8
- * @effects Reads encrypted secrets on first access
9
- */
10
-
11
- import { existsSync, readFileSync } from "fs";
12
- import { join } from "path";
13
- import { secrets } from "./secrets";
14
- import { dataDir, legacyDataDir, preferredDataDir, projectDir } from "./paths";
15
-
16
- const ENV_PATH_CANDIDATES = Array.from(new Set([
17
- join(dataDir, ".env"),
18
- join(preferredDataDir, ".env"),
19
- join(legacyDataDir, ".env"),
20
- ]));
21
-
22
- function loadEnvFile(): Record<string, string> {
23
- for (const envPath of ENV_PATH_CANDIDATES) {
24
- if (!existsSync(envPath)) continue;
25
-
26
- const content = readFileSync(envPath, "utf8");
27
- const vars: Record<string, string> = {};
28
- for (const line of content.split("\n")) {
29
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
30
- if (match) {
31
- vars[match[1]] = match[2].trim();
32
- }
33
- }
34
- return vars;
35
- }
36
-
37
- return {};
38
- }
39
-
40
- const envFile = loadEnvFile();
41
-
42
- // Inject .env vars into process.env so child processes inherit them
43
- for (const [key, value] of Object.entries(envFile)) {
44
- if (!process.env[key]) process.env[key] = value;
45
- }
46
-
47
- function env(key: string): string | undefined {
48
- return process.env[key] || envFile[key];
49
- }
50
-
51
- /**
52
- * Lazy-loaded API keys from encrypted secrets with fallback to .env
53
- */
54
- class SecureConfig {
55
- private _telegramBotToken?: string;
56
- private _openaiApiKey?: string;
57
- private _elevenlabsApiKey?: string;
58
- private _initialized = false;
59
-
60
- async initialize(): Promise<void> {
61
- if (this._initialized) return;
62
-
63
- // Load all secrets in parallel
64
- const [telegram, openai, elevenlabs] = await Promise.all([
65
- secrets.telegram(),
66
- secrets.openai(),
67
- secrets.elevenlabs(),
68
- ]);
69
-
70
- this._telegramBotToken = telegram || env("TELEGRAM_BOT_TOKEN") || "";
71
- this._openaiApiKey = openai || env("OPENAI_API_KEY") || "";
72
- this._elevenlabsApiKey = elevenlabs || env("ELEVENLABS_API_KEY") || "";
73
- this._initialized = true;
74
- }
75
-
76
- async getTelegramBotToken(): Promise<string> {
77
- await this.initialize();
78
- return this._telegramBotToken!;
79
- }
80
-
81
- async getOpenaiApiKey(): Promise<string> {
82
- await this.initialize();
83
- return this._openaiApiKey!;
84
- }
85
-
86
- async getElevenlabsApiKey(): Promise<string> {
87
- await this.initialize();
88
- return this._elevenlabsApiKey!;
89
- }
90
-
91
- // Synchronous getters for backwards compatibility (will use cached values)
92
- get telegramBotToken(): string {
93
- return this._telegramBotToken || "";
94
- }
95
-
96
- get openaiApiKey(): string {
97
- return this._openaiApiKey || "";
98
- }
99
-
100
- get elevenlabsApiKey(): string {
101
- return this._elevenlabsApiKey || "";
102
- }
103
- }
104
-
105
- const secureConfig = new SecureConfig();
106
-
107
- export const config = {
108
- projectDir,
109
- arisaDir: dataDir,
110
- // Backward-compatible alias for existing modules.
111
- tinyclawDir: dataDir,
112
-
113
- corePort: 51777,
114
- daemonPort: 51778,
115
- coreSocket: join(dataDir, "core.sock"),
116
- daemonSocket: join(dataDir, "daemon.sock"),
117
-
118
- // API keys - use async getters for first load
119
- get telegramBotToken() { return secureConfig.telegramBotToken; },
120
- get openaiApiKey() { return secureConfig.openaiApiKey; },
121
- get elevenlabsApiKey() { return secureConfig.elevenlabsApiKey; },
122
-
123
- elevenlabsVoiceId: "BpjGufoPiobT79j2vtj4",
124
-
125
- logsDir: join(dataDir, "logs"),
126
- tasksFile: join(dataDir, "scheduler", "tasks.json"),
127
- resetFlagPath: join(dataDir, "reset_flag"),
128
- voiceTempDir: join(dataDir, "voice_temp"),
129
- attachmentsDir: join(dataDir, "attachments"),
130
- attachmentMaxAgeDays: 30,
131
-
132
- claudeTimeout: 120_000,
133
- maxResponseLength: 4000,
134
-
135
- // Async API key loaders
136
- secrets: secureConfig,
137
- } as const;
package/src/shared/db.ts DELETED
@@ -1,304 +0,0 @@
1
- /**
2
- * @module shared/db
3
- * @role Unified persistence layer using deepbase
4
- * @responsibilities
5
- * - Persist scheduled tasks, authorized users, onboarded users, queue messages
6
- * - Provide type-safe operations with JSON storage
7
- * - Auto-connect on first operation
8
- * @dependencies deepbase, shared/types, shared/config
9
- * @effects Disk I/O (runtime db directory)
10
- */
11
-
12
- import DeepBase from "deepbase";
13
- import { config } from "./config";
14
- import type { ScheduledTask, AttachmentRecord, MessageRecord } from "./types";
15
- import { DeepbaseSecure } from "./deepbase-secure";
16
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
17
- import { randomBytes } from "crypto";
18
- import { dirname } from "path";
19
-
20
- const DB_PATH = `${config.arisaDir}/db`;
21
- const ARISA_DB_FILE = `${DB_PATH}/arisa.json`;
22
- const LEGACY_DB_FILE = `${DB_PATH}/tinyclaw.json`;
23
-
24
- function readDbJson(path: string): Record<string, any> {
25
- try {
26
- if (!existsSync(path)) return {};
27
- const raw = readFileSync(path, "utf8").trim();
28
- if (!raw) return {};
29
- const parsed = JSON.parse(raw);
30
- return parsed && typeof parsed === "object" ? parsed : {};
31
- } catch {
32
- return {};
33
- }
34
- }
35
-
36
- function mergeLegacyIntoArisa(): void {
37
- if (!existsSync(LEGACY_DB_FILE)) return;
38
-
39
- // If arisa DB doesn't exist yet, seed it from legacy and keep legacy file as backup/history.
40
- if (!existsSync(ARISA_DB_FILE)) {
41
- try {
42
- mkdirSync(DB_PATH, { recursive: true });
43
- copyFileSync(LEGACY_DB_FILE, ARISA_DB_FILE);
44
- } catch {
45
- // Best-effort migration; if copy fails, app can still run on a fresh arisa DB.
46
- }
47
- return;
48
- }
49
-
50
- const arisa = readDbJson(ARISA_DB_FILE);
51
- const legacy = readDbJson(LEGACY_DB_FILE);
52
- let changed = false;
53
-
54
- for (const [collection, legacyCollection] of Object.entries(legacy)) {
55
- if (!legacyCollection || typeof legacyCollection !== "object") continue;
56
-
57
- const arisaCollection = arisa[collection];
58
- if (!arisaCollection || typeof arisaCollection !== "object") {
59
- arisa[collection] = legacyCollection;
60
- changed = true;
61
- continue;
62
- }
63
-
64
- for (const [key, value] of Object.entries(legacyCollection as Record<string, any>)) {
65
- if (!(key in arisaCollection)) {
66
- arisaCollection[key] = value;
67
- changed = true;
68
- }
69
- }
70
- }
71
-
72
- if (changed) {
73
- try {
74
- writeFileSync(ARISA_DB_FILE, JSON.stringify(arisa, null, 4));
75
- } catch {
76
- // Ignore write failures; runtime will continue with current DB state.
77
- }
78
- }
79
- }
80
-
81
- // Ensure legacy data is available in arisa DB before DeepBase picks a file.
82
- mergeLegacyIntoArisa();
83
-
84
- // Initialize deepbase with the storage directory
85
- const db = new DeepBase({
86
- path: DB_PATH,
87
- name: "arisa",
88
- });
89
-
90
- // Initialize encrypted secrets database
91
- function getOrCreateEncryptionKey(): string {
92
- const keyPath = `${config.arisaDir}/.encryption_key`;
93
- const keyDir = dirname(keyPath);
94
-
95
- if (!existsSync(keyDir)) {
96
- mkdirSync(keyDir, { recursive: true });
97
- }
98
-
99
- if (existsSync(keyPath)) {
100
- return readFileSync(keyPath, 'utf-8').trim();
101
- }
102
-
103
- const key = randomBytes(32).toString('hex');
104
- writeFileSync(keyPath, key, { mode: 0o600 });
105
- return key;
106
- }
107
-
108
- const secretsDb = new DeepbaseSecure({
109
- path: DB_PATH,
110
- name: "secrets",
111
- encryptionKey: getOrCreateEncryptionKey(),
112
- });
113
-
114
- /**
115
- * Helper functions for common operations
116
- */
117
-
118
- // Tasks
119
- export async function getTasks(): Promise<ScheduledTask[]> {
120
- const tasks = await db.values("tasks");
121
- return tasks || [];
122
- }
123
-
124
- export async function getTask(id: string): Promise<ScheduledTask | null> {
125
- return await db.get("tasks", id);
126
- }
127
-
128
- export async function addTask(task: ScheduledTask): Promise<void> {
129
- await db.set("tasks", task.id, task);
130
- }
131
-
132
- export async function updateTask(id: string, updates: Partial<ScheduledTask>): Promise<void> {
133
- const existing = await db.get("tasks", id);
134
- if (existing) {
135
- await db.set("tasks", id, { ...existing, ...updates });
136
- }
137
- }
138
-
139
- export async function deleteTask(id: string): Promise<void> {
140
- await db.del("tasks", id);
141
- }
142
-
143
- export async function deleteTasks(filter: Partial<ScheduledTask>): Promise<number> {
144
- const tasks = await getTasks();
145
- let deleted = 0;
146
-
147
- for (const task of tasks) {
148
- const matches = Object.entries(filter).every(([key, value]) => {
149
- return task[key as keyof ScheduledTask] === value;
150
- });
151
-
152
- if (matches) {
153
- await db.del("tasks", task.id);
154
- deleted++;
155
- }
156
- }
157
-
158
- return deleted;
159
- }
160
-
161
- // Authorized users
162
- export async function isAuthorized(userId: string): Promise<boolean> {
163
- const user = await db.get("authorized", userId);
164
- return user !== null && user !== undefined;
165
- }
166
-
167
- export async function addAuthorized(userId: string): Promise<void> {
168
- await db.set("authorized", userId, { userId });
169
- }
170
-
171
- export async function getAuthorizedUsers(): Promise<string[]> {
172
- const users = await db.keys("authorized");
173
- return users || [];
174
- }
175
-
176
- // Onboarded users
177
- export async function isOnboarded(userId: string): Promise<boolean> {
178
- const user = await db.get("onboarded", userId);
179
- return user !== null && user !== undefined;
180
- }
181
-
182
- export async function addOnboarded(userId: string): Promise<void> {
183
- await db.set("onboarded", userId, { userId });
184
- }
185
-
186
- export async function getOnboardedUsers(): Promise<string[]> {
187
- const users = await db.keys("onboarded");
188
- return users || [];
189
- }
190
-
191
- // Queue operations
192
- export async function enqueueMessage(message: {
193
- id: string;
194
- chatId: string | number;
195
- text: string;
196
- type: "heartbeat" | "message";
197
- }): Promise<void> {
198
- await db.set("queue", message.id, {
199
- ...message,
200
- timestamp: Date.now(),
201
- });
202
- }
203
-
204
- export async function dequeueMessages(limit?: number): Promise<any[]> {
205
- const messages = await db.values("queue");
206
- if (!messages || messages.length === 0) return [];
207
-
208
- const sorted = messages.sort((a: any, b: any) => a.timestamp - b.timestamp);
209
- const batch = limit ? sorted.slice(0, limit) : sorted;
210
-
211
- // Delete the dequeued messages
212
- for (const msg of batch) {
213
- await db.del("queue", msg.id);
214
- }
215
-
216
- return batch;
217
- }
218
-
219
- export async function getQueueSize(): Promise<number> {
220
- const messages = await db.keys("queue");
221
- return messages ? messages.length : 0;
222
- }
223
-
224
- export async function clearQueue(): Promise<void> {
225
- const keys = await db.keys("queue");
226
- if (keys) {
227
- for (const key of keys) {
228
- await db.del("queue", key);
229
- }
230
- }
231
- }
232
-
233
- // Settings (auth token, etc.)
234
- export async function getSetting(key: string): Promise<string | null> {
235
- const val = await db.get("settings", key);
236
- return val?.value ?? null;
237
- }
238
-
239
- export async function setSetting(key: string, value: string): Promise<void> {
240
- await db.set("settings", key, { value });
241
- }
242
-
243
- // Attachments
244
- export async function addAttachment(record: AttachmentRecord): Promise<void> {
245
- await db.set("attachments", record.id, record);
246
- }
247
-
248
- export async function getAttachments(chatId?: string): Promise<AttachmentRecord[]> {
249
- const all = (await db.values("attachments")) || [];
250
- if (!chatId) return all;
251
- return all.filter((a: AttachmentRecord) => a.chatId === chatId);
252
- }
253
-
254
- export async function getAttachment(id: string): Promise<AttachmentRecord | null> {
255
- return await db.get("attachments", id);
256
- }
257
-
258
- export async function deleteAttachment(id: string): Promise<void> {
259
- await db.del("attachments", id);
260
- }
261
-
262
- export async function getExpiredAttachments(maxAgeDays: number): Promise<AttachmentRecord[]> {
263
- const all = (await db.values("attachments")) || [];
264
- const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
265
- return all.filter((a: AttachmentRecord) => a.createdAt < cutoff);
266
- }
267
-
268
- // Messages (ledger)
269
- export async function saveMessageRecord(record: MessageRecord): Promise<void> {
270
- await db.set("messages", record.id, record);
271
- }
272
-
273
- export async function getMessageRecord(chatId: string, messageId: number): Promise<MessageRecord | null> {
274
- return await db.get("messages", `${chatId}_${messageId}`);
275
- }
276
-
277
- export async function cleanupOldMessages(maxAgeDays: number): Promise<number> {
278
- const all = (await db.values("messages")) || [];
279
- const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
280
- let deleted = 0;
281
- for (const record of all) {
282
- if ((record as MessageRecord).timestamp < cutoff) {
283
- await db.del("messages", (record as MessageRecord).id);
284
- deleted++;
285
- }
286
- }
287
- return deleted;
288
- }
289
-
290
- // Secrets (API keys stored encrypted)
291
- export async function getSecret(key: string): Promise<string | null> {
292
- const val = await secretsDb.get("secrets", key);
293
- return val?.value ?? null;
294
- }
295
-
296
- export async function setSecret(key: string, value: string): Promise<void> {
297
- await secretsDb.set("secrets", key, { value });
298
- }
299
-
300
- export async function deleteSecret(key: string): Promise<void> {
301
- await secretsDb.del("secrets", key);
302
- }
303
-
304
- export { db, secretsDb };
@@ -1,39 +0,0 @@
1
- import CryptoJS from 'crypto-js';
2
- import DeepBase from 'deepbase';
3
- import { JsonDriver } from 'deepbase-json';
4
-
5
- interface DeepbaseSecureOptions {
6
- encryptionKey: string;
7
- path: string;
8
- name: string;
9
- }
10
-
11
- export class DeepbaseSecure extends DeepBase {
12
- constructor(opts: DeepbaseSecureOptions) {
13
- const encryptionKey = opts.encryptionKey;
14
- const { path, name } = opts;
15
-
16
- // Create JSON driver with encryption
17
- const driver = new JsonDriver({
18
- path,
19
- name,
20
- stringify: (obj: any) => {
21
- const iv = CryptoJS.lib.WordArray.random(128 / 8);
22
- const encrypted = CryptoJS.AES.encrypt(
23
- JSON.stringify(obj),
24
- encryptionKey,
25
- { iv }
26
- );
27
- return iv.toString(CryptoJS.enc.Hex) + ':' + encrypted.toString();
28
- },
29
- parse: (encryptedData: string) => {
30
- const [ivHex, encrypted] = encryptedData.split(':');
31
- const iv = CryptoJS.enc.Hex.parse(ivHex);
32
- const bytes = CryptoJS.AES.decrypt(encrypted, encryptionKey, { iv });
33
- return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
34
- }
35
- });
36
-
37
- super(driver);
38
- }
39
- }
@@ -1,14 +0,0 @@
1
- // Shim for running CLI tools that use Ink without a TTY.
2
- // Prevents "Raw mode is not supported" crash by providing a no-op setRawMode.
3
- // Uses Object.defineProperty for robustness (Bun may make stdin props non-writable).
4
- if (process.stdin) {
5
- if (!process.stdin.isTTY) {
6
- try { Object.defineProperty(process.stdin, "isTTY", { value: true, writable: true, configurable: true }); }
7
- catch { process.stdin.isTTY = true; }
8
- }
9
- if (typeof process.stdin.setRawMode !== "function") {
10
- const noop = function () { return process.stdin; };
11
- try { Object.defineProperty(process.stdin, "setRawMode", { value: noop, writable: true, configurable: true }); }
12
- catch { process.stdin.setRawMode = noop; }
13
- }
14
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * @module shared/logger
3
- * @role Structured logging to runtime logs dir and stdout.
4
- * @responsibilities
5
- * - Create named loggers per component (core, daemon, telegram, scheduler)
6
- * - Write timestamped log lines to file + console
7
- * - Ensure log directory exists
8
- * @dependencies shared/config
9
- * @effects Writes to disk (logs dir), writes to stdout
10
- */
11
-
12
- import { appendFileSync, mkdirSync, existsSync } from "fs";
13
- import { join } from "path";
14
- import { config } from "./config";
15
-
16
- type Level = "DEBUG" | "INFO" | "WARN" | "ERROR";
17
-
18
- export function createLogger(component: string) {
19
- const logFile = join(config.logsDir, `${component}.log`);
20
-
21
- if (!existsSync(config.logsDir)) {
22
- mkdirSync(config.logsDir, { recursive: true });
23
- }
24
-
25
- function write(level: Level, message: string) {
26
- const timestamp = new Date().toISOString();
27
- const line = `[${timestamp}] [${level}] ${message}\n`;
28
- console.log(line.trim());
29
- try {
30
- appendFileSync(logFile, line);
31
- } catch {
32
- // If we can't write to log file, at least console output happened
33
- }
34
- }
35
-
36
- return {
37
- debug: (msg: string) => write("DEBUG", msg),
38
- info: (msg: string) => write("INFO", msg),
39
- warn: (msg: string) => write("WARN", msg),
40
- error: (msg: string) => write("ERROR", msg),
41
- };
42
- }