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,104 +0,0 @@
1
- /**
2
- * @module core/attachments
3
- * @role Persist media attachments so the model can access them later.
4
- * @responsibilities
5
- * - Save base64 attachments to runtime attachments/{chatId}/
6
- * - Track metadata in deepbase (collection: "attachments")
7
- * - Clean up files older than configured max age
8
- * @dependencies shared/config, shared/db
9
- * @effects Disk I/O in runtime attachments dir, deepbase writes
10
- */
11
-
12
- import { mkdirSync, existsSync, unlinkSync, rmdirSync, readdirSync } from "fs";
13
- import { join, dirname, relative } from "path";
14
- import { config } from "../shared/config";
15
- import { createLogger } from "../shared/logger";
16
- import { addAttachment, getExpiredAttachments, deleteAttachment, cleanupOldMessages } from "../shared/db";
17
- import type { AttachmentRecord } from "../shared/types";
18
-
19
- const log = createLogger("core");
20
-
21
- const EXT_MAP: Record<string, string> = {
22
- image: "jpg",
23
- audio: "ogg",
24
- document: "bin",
25
- };
26
-
27
- export async function initAttachments(): Promise<void> {
28
- if (!existsSync(config.attachmentsDir)) {
29
- mkdirSync(config.attachmentsDir, { recursive: true });
30
- }
31
- await cleanupOldAttachments();
32
- const msgsCleaned = await cleanupOldMessages(config.attachmentMaxAgeDays);
33
- if (msgsCleaned > 0) {
34
- log.info(`Cleaned up ${msgsCleaned} expired message record(s)`);
35
- }
36
- }
37
-
38
- export async function saveAttachment(
39
- chatId: string,
40
- type: "image" | "audio" | "document",
41
- base64: string,
42
- filename?: string,
43
- mimeType?: string,
44
- ): Promise<string> {
45
- const chatDir = join(config.attachmentsDir, chatId);
46
- if (!existsSync(chatDir)) {
47
- mkdirSync(chatDir, { recursive: true });
48
- }
49
-
50
- const ext = filename ? filename.split(".").pop() || EXT_MAP[type] : EXT_MAP[type];
51
- const hex = Math.random().toString(16).slice(2, 6);
52
- const prefix = type === "image" ? "img" : type === "audio" ? "aud" : "doc";
53
- const outName = `${prefix}_${Date.now()}_${hex}.${ext}`;
54
- const outPath = join(chatDir, outName);
55
-
56
- const buffer = Buffer.from(base64, "base64");
57
- await Bun.write(outPath, buffer);
58
-
59
- let relPath = relative(config.projectDir, outPath).replace(/\\/g, "/");
60
- if (relPath.startsWith("..")) {
61
- relPath = outPath;
62
- }
63
-
64
- const record: AttachmentRecord = {
65
- id: `${chatId}_${outName}`,
66
- chatId,
67
- type,
68
- filename: filename || outName,
69
- relPath,
70
- mimeType,
71
- sizeBytes: buffer.length,
72
- createdAt: Date.now(),
73
- };
74
- await addAttachment(record);
75
-
76
- log.info(`Saved ${type} attachment: ${relPath} (${buffer.length} bytes)`);
77
- return relPath;
78
- }
79
-
80
- async function cleanupOldAttachments(): Promise<void> {
81
- const expired = await getExpiredAttachments(config.attachmentMaxAgeDays);
82
- if (expired.length === 0) return;
83
-
84
- let cleaned = 0;
85
- for (const record of expired) {
86
- const absPath = join(config.projectDir, record.relPath);
87
- try {
88
- if (existsSync(absPath)) {
89
- unlinkSync(absPath);
90
- }
91
- // Remove empty chat dir
92
- const dir = dirname(absPath);
93
- if (existsSync(dir) && readdirSync(dir).length === 0) {
94
- rmdirSync(dir);
95
- }
96
- } catch {
97
- // File already gone — just clean the record
98
- }
99
- await deleteAttachment(record.id);
100
- cleaned++;
101
- }
102
-
103
- log.info(`Cleaned up ${cleaned} expired attachment(s)`);
104
- }
package/src/core/auth.ts DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * @module core/auth
3
- * @role Gate access to the bot via a one-time token shown in the console.
4
- * @responsibilities
5
- * - Generate and persist an auth token via deepbase (settings.auth_token)
6
- * - Track authorized chat IDs via deepbase (authorized collection)
7
- * - Validate tokens from new chats
8
- * @dependencies shared/db
9
- * @effects deepbase writes, console output
10
- */
11
-
12
- import { createLogger } from "../shared/logger";
13
- import { getAuthorizedUsers, addAuthorized, getSetting, setSetting } from "../shared/db";
14
-
15
- const log = createLogger("auth");
16
-
17
- let authToken = "";
18
- let authorizedChats: Set<string> = new Set();
19
-
20
- async function loadToken(): Promise<string> {
21
- const existing = await getSetting("auth_token");
22
- if (existing) return existing;
23
-
24
- const token = crypto.randomUUID().split("-")[0];
25
- await setSetting("auth_token", token);
26
- return token;
27
- }
28
-
29
- async function loadAuthorized(): Promise<Set<string>> {
30
- try {
31
- const users = await getAuthorizedUsers();
32
- return new Set(users);
33
- } catch (error) {
34
- log.error(`Failed to load authorized users: ${error}`);
35
- return new Set();
36
- }
37
- }
38
-
39
- export async function initAuth() {
40
- authToken = await loadToken();
41
- authorizedChats = await loadAuthorized();
42
- log.info(`Auth token: ${authToken}`);
43
- console.log(`\n🔑 Auth token: ${authToken}\n Send this token to the bot on Telegram to authorize a chat.\n`);
44
- }
45
-
46
- export function isAuthorized(chatId: string): boolean {
47
- return authorizedChats.has(chatId);
48
- }
49
-
50
- export async function tryAuthorize(chatId: string, message: string): Promise<boolean> {
51
- if (message.trim() === authToken) {
52
- authorizedChats.add(chatId);
53
- await addAuthorized(chatId);
54
- log.info(`Chat ${chatId} authorized`);
55
- return true;
56
- }
57
- return false;
58
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * @module core/context
3
- * @role Manage Claude conversation continuity via the -c flag and reset_flag.
4
- * @responsibilities
5
- * - Check if reset_flag exists (user sent /reset)
6
- * - Return whether to use -c (continue) flag
7
- * - Clear reset_flag after consuming it
8
- * @dependencies shared/config
9
- * @effects Reads/deletes reset_flag from runtime data dir
10
- * @contract shouldContinue() => boolean
11
- */
12
-
13
- import { existsSync, unlinkSync } from "fs";
14
- import { config } from "../shared/config";
15
- import { createLogger } from "../shared/logger";
16
-
17
- const log = createLogger("core");
18
-
19
- export function shouldContinue(): boolean {
20
- if (existsSync(config.resetFlagPath)) {
21
- log.info("Reset flag found — starting fresh conversation");
22
- try {
23
- unlinkSync(config.resetFlagPath);
24
- } catch {
25
- // Already deleted, race condition
26
- }
27
- return false;
28
- }
29
- return true;
30
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * @module core/file-detector
3
- * @role Detect file paths mentioned in Claude responses that exist on disk.
4
- * @responsibilities
5
- * - Scan response text for absolute file paths
6
- * - Verify they exist and are sendable (< 50MB, not directories)
7
- * - Return list of unique valid file paths
8
- * @dependencies None
9
- * @effects Reads file system to check existence/size
10
- * @contract detectFiles(text) => string[]
11
- */
12
-
13
- import { statSync } from "fs";
14
-
15
- const FILE_PATH_REGEX = /(\/[\w./-]+\.\w{1,10})/gm;
16
- const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
17
-
18
- export function detectFiles(text: string): string[] {
19
- const matches = [...text.matchAll(FILE_PATH_REGEX)];
20
- const seen = new Set<string>();
21
- const files: string[] = [];
22
-
23
- for (const match of matches) {
24
- const filePath = match[1];
25
- if (seen.has(filePath)) continue;
26
- seen.add(filePath);
27
-
28
- try {
29
- const stat = statSync(filePath);
30
- if (stat.isFile() && stat.size < MAX_FILE_SIZE) {
31
- files.push(filePath);
32
- }
33
- } catch {
34
- // File doesn't exist or can't be read
35
- }
36
- }
37
-
38
- return files;
39
- }
@@ -1,159 +0,0 @@
1
- /**
2
- * @module core/format
3
- * @role Format responses for Telegram (HTML) and chunk long messages.
4
- * @responsibilities
5
- * - Split text into chunks respecting Telegram's 4096 char limit
6
- * - Safe HTML sending with plain-text fallback marker
7
- * @dependencies None
8
- * @effects None (pure functions)
9
- * @contract chunkMessage(text) => string[]
10
- */
11
-
12
- const MAX_TELEGRAM_LENGTH = 4096;
13
-
14
- function escapeHtml(s: string): string {
15
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16
- }
17
-
18
- /** Decode HTML entities so we don't double-encode them when escapeHtml runs. */
19
- function unescapeHtml(s: string): string {
20
- return s
21
- .replace(/&amp;/g, "&")
22
- .replace(/&lt;/g, "<")
23
- .replace(/&gt;/g, ">")
24
- .replace(/&quot;/g, '"')
25
- .replace(/&#39;/g, "'");
26
- }
27
-
28
- function protectTelegramHtmlTags(text: string): { text: string; tags: string[] } {
29
- const tags: string[] = [];
30
- const tagPattern = /<\/?(?:b|i|u|s|code|pre|blockquote)>|<a\s+href="[^"]+">|<\/a>/gi;
31
- const protectedText = text.replace(tagPattern, (tag) => {
32
- tags.push(tag);
33
- return `\x00HTMLTAG${tags.length - 1}\x00`;
34
- });
35
- return { text: protectedText, tags };
36
- }
37
-
38
- /**
39
- * Convert Markdown (from Claude CLI) to Telegram-safe HTML.
40
- * Telegram supports: <b>, <i>, <code>, <pre>, <a>, <s>, <u>, <blockquote>
41
- * Must also escape HTML entities in non-tag content.
42
- */
43
- export function markdownToTelegramHtml(text: string): string {
44
- // Step 0: Decode any pre-escaped HTML entities to avoid double-encoding
45
- // (e.g. &lt;code&gt; → <code> so protectTelegramHtmlTags can detect them)
46
- text = unescapeHtml(text);
47
-
48
- // Step 1: Extract code blocks and links before escaping to protect them
49
- const codeBlocks: string[] = [];
50
- const inlineCodes: string[] = [];
51
- const links: string[] = [];
52
-
53
- // Preserve already-valid Telegram HTML tags instead of escaping them.
54
- const protectedHtml = protectTelegramHtmlTags(text);
55
-
56
- // Protect fenced code blocks: ```lang\n...\n``` or ```...```
57
- let result = protectedHtml.text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => {
58
- codeBlocks.push(code.trimEnd());
59
- return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
60
- });
61
-
62
- // Protect inline code: `...`
63
- result = result.replace(/`([^`\n]+)`/g, (_match, code) => {
64
- inlineCodes.push(code);
65
- return `\x00INLINE${inlineCodes.length - 1}\x00`;
66
- });
67
-
68
- // Protect links: [text](url)
69
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
70
- links.push(`<a href="${url}">${escapeHtml(linkText)}</a>`);
71
- return `\x00LINK${links.length - 1}\x00`;
72
- });
73
-
74
- // Step 2: Now escape HTML entities in the remaining text
75
- result = escapeHtml(result);
76
-
77
- // Step 3: Convert markdown patterns to HTML tags
78
-
79
- // Bold+Italic: ***text*** or ___text___
80
- result = result.replace(/\*{3}(.+?)\*{3}/g, "<b><i>$1</i></b>");
81
- result = result.replace(/_{3}(.+?)_{3}/g, "<b><i>$1</i></b>");
82
-
83
- // Bold: **text** or __text__
84
- result = result.replace(/\*{2}(.+?)\*{2}/g, "<b>$1</b>");
85
- result = result.replace(/_{2}(.+?)_{2}/g, "<b>$1</b>");
86
-
87
- // Italic: *text* or _text_ (not inside words for underscore)
88
- result = result.replace(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, "<i>$1</i>");
89
- result = result.replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, "<i>$1</i>");
90
-
91
- // Strikethrough: ~~text~~
92
- result = result.replace(/~~(.+?)~~/g, "<s>$1</s>");
93
-
94
- // Step 4: Restore protected elements
95
- result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, i) => `<pre>${escapeHtml(codeBlocks[+i])}</pre>`);
96
- result = result.replace(/\x00INLINE(\d+)\x00/g, (_m, i) => `<code>${escapeHtml(inlineCodes[+i])}</code>`);
97
- result = result.replace(/\x00LINK(\d+)\x00/g, (_m, i) => links[+i]);
98
- result = result.replace(/\x00HTMLTAG(\d+)\x00/g, (_m, i) => protectedHtml.tags[+i]);
99
-
100
- return result;
101
- }
102
-
103
- export function chunkMessage(text: string): string[] {
104
- if (text.length <= MAX_TELEGRAM_LENGTH) {
105
- return [text];
106
- }
107
-
108
- const chunks: string[] = [];
109
- let remaining = text;
110
-
111
- // Tags that Telegram supports and we use
112
- const tagNames = ["pre", "code", "b", "i", "s", "a"];
113
-
114
- while (remaining.length > 0) {
115
- if (remaining.length <= MAX_TELEGRAM_LENGTH) {
116
- chunks.push(remaining);
117
- break;
118
- }
119
-
120
- let splitAt = remaining.lastIndexOf("\n", MAX_TELEGRAM_LENGTH);
121
- if (splitAt === -1 || splitAt < MAX_TELEGRAM_LENGTH * 0.5) {
122
- splitAt = MAX_TELEGRAM_LENGTH;
123
- }
124
-
125
- // Check if splitting here would break an HTML tag
126
- let candidate = remaining.substring(0, splitAt);
127
-
128
- // Find unclosed tags in the candidate chunk
129
- const openTags: string[] = [];
130
- const tagRegex = /<\/?([a-z]+)(?:\s[^>]*)?\/?>/gi;
131
- let match: RegExpExecArray | null;
132
- while ((match = tagRegex.exec(candidate)) !== null) {
133
- const fullTag = match[0];
134
- const tagName = match[1].toLowerCase();
135
- if (!tagNames.includes(tagName)) continue;
136
- if (fullTag.startsWith("</")) {
137
- // Closing tag - pop if matching
138
- const idx = openTags.lastIndexOf(tagName);
139
- if (idx !== -1) openTags.splice(idx, 1);
140
- } else if (!fullTag.endsWith("/>")) {
141
- openTags.push(tagName);
142
- }
143
- }
144
-
145
- // If there are unclosed tags, close them at end of this chunk and reopen in next
146
- if (openTags.length > 0) {
147
- const closingTags = [...openTags].reverse().map(t => `</${t}>`).join("");
148
- const openingTags = openTags.map(t => `<${t}>`).join("");
149
- candidate = candidate + closingTags;
150
- remaining = openingTags + remaining.substring(splitAt).trimStart();
151
- } else {
152
- remaining = remaining.substring(splitAt).trimStart();
153
- }
154
-
155
- chunks.push(candidate);
156
- }
157
-
158
- return chunks;
159
- }