arisa 2.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.
- package/CLAUDE.md +191 -0
- package/README.md +200 -0
- package/SOUL.md +36 -0
- package/bin/arisa.js +85 -0
- package/package.json +43 -0
- package/scripts/test-secrets.ts +22 -0
- package/src/core/attachments.ts +104 -0
- package/src/core/auth.ts +58 -0
- package/src/core/context.ts +30 -0
- package/src/core/file-detector.ts +39 -0
- package/src/core/format.ts +159 -0
- package/src/core/history.ts +193 -0
- package/src/core/index.ts +437 -0
- package/src/core/intent.ts +112 -0
- package/src/core/media.ts +144 -0
- package/src/core/onboarding.ts +115 -0
- package/src/core/processor.ts +268 -0
- package/src/core/router.ts +64 -0
- package/src/core/scheduler.ts +192 -0
- package/src/daemon/agent-cli.ts +119 -0
- package/src/daemon/autofix.ts +116 -0
- package/src/daemon/bridge.ts +162 -0
- package/src/daemon/channels/base.ts +10 -0
- package/src/daemon/channels/telegram.ts +306 -0
- package/src/daemon/fallback.ts +49 -0
- package/src/daemon/index.ts +213 -0
- package/src/daemon/lifecycle.ts +288 -0
- package/src/daemon/setup.ts +79 -0
- package/src/shared/config.ts +130 -0
- package/src/shared/db.ts +304 -0
- package/src/shared/deepbase-secure.ts +39 -0
- package/src/shared/logger.ts +42 -0
- package/src/shared/paths.ts +90 -0
- package/src/shared/ports.ts +98 -0
- package/src/shared/secrets.ts +136 -0
- package/src/shared/types.ts +103 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/agent-cli
|
|
3
|
+
* @role Run AI CLI commands directly from Daemon with local fallback order.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Detect available CLIs (Claude, Codex)
|
|
6
|
+
* - Execute prompt with timeout
|
|
7
|
+
* - Fallback from Claude to Codex when needed
|
|
8
|
+
* @dependencies shared/config
|
|
9
|
+
* @effects Spawns external CLI processes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { config } from "../shared/config";
|
|
13
|
+
import { createLogger } from "../shared/logger";
|
|
14
|
+
|
|
15
|
+
const log = createLogger("daemon");
|
|
16
|
+
|
|
17
|
+
export type AgentCli = "claude" | "codex";
|
|
18
|
+
|
|
19
|
+
export interface CliExecutionResult {
|
|
20
|
+
cli: AgentCli;
|
|
21
|
+
output: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
exitCode: number;
|
|
24
|
+
partial: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CliFallbackOutcome {
|
|
28
|
+
result: CliExecutionResult | null;
|
|
29
|
+
attempted: AgentCli[];
|
|
30
|
+
failures: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getAvailableAgentCli(): AgentCli[] {
|
|
34
|
+
const order: AgentCli[] = [];
|
|
35
|
+
if (Bun.which("claude") !== null) order.push("claude");
|
|
36
|
+
if (Bun.which("codex") !== null) order.push("codex");
|
|
37
|
+
return order;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getAgentCliLabel(cli: AgentCli): string {
|
|
41
|
+
return cli === "claude" ? "Claude" : "Codex";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildCommand(cli: AgentCli, prompt: string): string[] {
|
|
45
|
+
if (cli === "claude") {
|
|
46
|
+
return ["claude", "--dangerously-skip-permissions", "--model", "sonnet", "-p", prompt];
|
|
47
|
+
}
|
|
48
|
+
return ["codex", "exec", "--dangerously-bypass-approvals-and-sandbox", "-C", config.projectDir, prompt];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function runSingleCli(
|
|
52
|
+
cli: AgentCli,
|
|
53
|
+
prompt: string,
|
|
54
|
+
timeoutMs: number,
|
|
55
|
+
): Promise<Omit<CliExecutionResult, "partial">> {
|
|
56
|
+
const cmd = buildCommand(cli, prompt);
|
|
57
|
+
log.info(`Daemon AI: trying ${getAgentCliLabel(cli)} CLI`);
|
|
58
|
+
|
|
59
|
+
const proc = Bun.spawn(cmd, {
|
|
60
|
+
cwd: config.projectDir,
|
|
61
|
+
stdout: "pipe",
|
|
62
|
+
stderr: "pipe",
|
|
63
|
+
env: { ...process.env },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const timeout = setTimeout(() => proc.kill(), timeoutMs);
|
|
67
|
+
const stdoutPromise = new Response(proc.stdout).text();
|
|
68
|
+
const stderrPromise = new Response(proc.stderr).text();
|
|
69
|
+
const exitCode = await proc.exited;
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
|
|
72
|
+
const [output, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
73
|
+
return { cli, output, stderr, exitCode };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runWithCliFallback(prompt: string, timeoutMs: number): Promise<CliFallbackOutcome> {
|
|
77
|
+
const candidates = getAvailableAgentCli();
|
|
78
|
+
const attempted: AgentCli[] = [];
|
|
79
|
+
const failures: string[] = [];
|
|
80
|
+
let partial: CliExecutionResult | null = null;
|
|
81
|
+
|
|
82
|
+
for (const cli of candidates) {
|
|
83
|
+
attempted.push(cli);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await runSingleCli(cli, prompt, timeoutMs);
|
|
87
|
+
const output = result.output.trim();
|
|
88
|
+
|
|
89
|
+
if (result.exitCode === 0 && output) {
|
|
90
|
+
return {
|
|
91
|
+
result: { ...result, output, partial: false },
|
|
92
|
+
attempted,
|
|
93
|
+
failures,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.exitCode !== 0 && output && partial === null) {
|
|
98
|
+
partial = { ...result, output, partial: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const reason = result.exitCode === 0
|
|
102
|
+
? "empty output"
|
|
103
|
+
: `exit=${result.exitCode}: ${summarizeError(result.stderr || result.output)}`;
|
|
104
|
+
failures.push(`${getAgentCliLabel(cli)} ${reason}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
107
|
+
failures.push(`${getAgentCliLabel(cli)} error: ${summarizeError(msg)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { result: partial, attempted, failures };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function summarizeError(raw: string): string {
|
|
115
|
+
const clean = raw.replace(/\s+/g, " ").trim();
|
|
116
|
+
if (!clean) return "no details";
|
|
117
|
+
return clean.length > 200 ? `${clean.slice(0, 200)}...` : clean;
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/autofix
|
|
3
|
+
* @role Auto-diagnose and fix Core crashes using available AI CLI.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Spawn Claude/Codex CLI to analyze crash errors and edit code
|
|
6
|
+
* - Rate-limit attempts (cooldown + max attempts)
|
|
7
|
+
* - Notify via callback (Telegram)
|
|
8
|
+
* @dependencies shared/config
|
|
9
|
+
* @effects Spawns AI CLI process which may edit project files
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { config } from "../shared/config";
|
|
13
|
+
import { createLogger } from "../shared/logger";
|
|
14
|
+
import { getAgentCliLabel, runWithCliFallback } from "./agent-cli";
|
|
15
|
+
|
|
16
|
+
const log = createLogger("daemon");
|
|
17
|
+
|
|
18
|
+
let lastAttemptAt = 0;
|
|
19
|
+
let attemptCount = 0;
|
|
20
|
+
|
|
21
|
+
const COOLDOWN_MS = 120_000; // 2min between batches
|
|
22
|
+
const MAX_ATTEMPTS = 3;
|
|
23
|
+
const AUTOFIX_TIMEOUT = 180_000; // 3min for autofix
|
|
24
|
+
|
|
25
|
+
type NotifyFn = (text: string) => Promise<void>;
|
|
26
|
+
let notifyFn: NotifyFn | null = null;
|
|
27
|
+
|
|
28
|
+
export function setAutoFixNotify(fn: NotifyFn) {
|
|
29
|
+
notifyFn = fn;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Attempt to auto-fix a Core crash. Returns true if any CLI produced a usable result.
|
|
34
|
+
*/
|
|
35
|
+
export async function attemptAutoFix(error: string): Promise<boolean> {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
|
|
38
|
+
// Reset attempts after cooldown
|
|
39
|
+
if (now - lastAttemptAt > COOLDOWN_MS) {
|
|
40
|
+
attemptCount = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (attemptCount >= MAX_ATTEMPTS) {
|
|
44
|
+
log.warn(`Auto-fix: max attempts (${MAX_ATTEMPTS}) reached, waiting for cooldown`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
attemptCount++;
|
|
49
|
+
lastAttemptAt = now;
|
|
50
|
+
|
|
51
|
+
log.info(`Auto-fix: attempt ${attemptCount}/${MAX_ATTEMPTS}`);
|
|
52
|
+
await notifyFn?.(`Auto-fix: intento ${attemptCount}/${MAX_ATTEMPTS}. Analizando error...`);
|
|
53
|
+
|
|
54
|
+
// Extract file paths from the error to help the fallback model focus
|
|
55
|
+
const projectPathPattern = new RegExp(`${escapeRegExp(config.projectDir)}[^\\s:)]+`, "g");
|
|
56
|
+
const fileRefs = error.match(projectPathPattern) || [];
|
|
57
|
+
const uniqueFiles = [...new Set(fileRefs)].slice(0, 5);
|
|
58
|
+
const fileHint = uniqueFiles.length > 0
|
|
59
|
+
? `\nKey files from the stack trace: ${uniqueFiles.join(", ")}`
|
|
60
|
+
: "";
|
|
61
|
+
|
|
62
|
+
const prompt = `Arisa Core error on startup. Fix it.
|
|
63
|
+
|
|
64
|
+
Error:
|
|
65
|
+
\`\`\`
|
|
66
|
+
${error.slice(-1500)}
|
|
67
|
+
\`\`\`
|
|
68
|
+
${fileHint}
|
|
69
|
+
|
|
70
|
+
Rules:
|
|
71
|
+
- If it's a corrupted JSON/data file: delete or recreate it
|
|
72
|
+
- If it's a bad import: fix the import
|
|
73
|
+
- If it's a code bug: fix the minimal code
|
|
74
|
+
- Do NOT refactor, improve, or change anything beyond the fix
|
|
75
|
+
- Be fast — read only the files mentioned in the error`;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const outcome = await runWithCliFallback(prompt, AUTOFIX_TIMEOUT);
|
|
79
|
+
const result = outcome.result;
|
|
80
|
+
|
|
81
|
+
if (!result) {
|
|
82
|
+
if (outcome.attempted.length === 0) {
|
|
83
|
+
log.error("Auto-fix: no AI CLI available (claude/codex)");
|
|
84
|
+
await notifyFn?.("Auto-fix: no hay CLI disponible (Claude/Codex).");
|
|
85
|
+
} else {
|
|
86
|
+
const detail = outcome.failures.join(" | ").slice(0, 400);
|
|
87
|
+
log.error(`Auto-fix: all CLIs failed: ${detail}`);
|
|
88
|
+
await notifyFn?.("Auto-fix: Claude y Codex fallaron. Revisá los logs.");
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cli = getAgentCliLabel(result.cli);
|
|
94
|
+
const summary = result.output.slice(0, 300);
|
|
95
|
+
if (result.partial) {
|
|
96
|
+
log.warn(`Auto-fix: ${cli} produced output but exited with code ${result.exitCode}`);
|
|
97
|
+
} else {
|
|
98
|
+
log.info(`Auto-fix: ${cli} completed successfully`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await notifyFn?.(`Auto-fix aplicado con ${cli}. Core reiniciando...\n<pre>${escapeHtml(summary)}</pre>`);
|
|
102
|
+
return true;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log.error(`Auto-fix: error: ${err}`);
|
|
105
|
+
await notifyFn?.("Auto-fix: error interno. Revisá los logs.");
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function escapeHtml(s: string): string {
|
|
111
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function escapeRegExp(s: string): string {
|
|
115
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
116
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/bridge
|
|
3
|
+
* @role HTTP client from Daemon to Core with smart fallback to local AI CLI.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - POST messages to Core at :51777/message
|
|
6
|
+
* - Respect Core lifecycle state (starting/up/down)
|
|
7
|
+
* - Wait for Core during startup, fallback only when truly down
|
|
8
|
+
* - Serialize fallback calls (one CLI process at a time)
|
|
9
|
+
* @dependencies shared/config, shared/types, daemon/fallback, daemon/lifecycle
|
|
10
|
+
* @effects Network (HTTP to Core), may spawn AI CLI process
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { config } from "../shared/config";
|
|
14
|
+
import { createLogger } from "../shared/logger";
|
|
15
|
+
import type { IncomingMessage, CoreResponse } from "../shared/types";
|
|
16
|
+
import { fallbackClaude } from "./fallback";
|
|
17
|
+
import { getCoreState, getCoreError, waitForCoreReady } from "./lifecycle";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("daemon");
|
|
20
|
+
|
|
21
|
+
const CORE_URL = `http://localhost:${config.corePort}`;
|
|
22
|
+
const STARTUP_WAIT_MS = 15_000;
|
|
23
|
+
const RETRY_DELAY = 3000;
|
|
24
|
+
|
|
25
|
+
type StatusCallback = (text: string) => Promise<void>;
|
|
26
|
+
|
|
27
|
+
// Serialize fallback calls — only one fallback CLI process at a time
|
|
28
|
+
let fallbackQueue: Promise<string> = Promise.resolve("");
|
|
29
|
+
|
|
30
|
+
export async function sendToCore(
|
|
31
|
+
message: IncomingMessage,
|
|
32
|
+
onStatus?: StatusCallback,
|
|
33
|
+
): Promise<CoreResponse> {
|
|
34
|
+
const state = getCoreState();
|
|
35
|
+
|
|
36
|
+
if (state === "starting") {
|
|
37
|
+
return await handleStarting(message, onStatus);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (state === "up") {
|
|
41
|
+
return await handleUp(message, onStatus);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// state === "down" — go straight to fallback
|
|
45
|
+
log.warn("Core is down, using fallback");
|
|
46
|
+
return await runFallback(message, onStatus);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Core is starting — wait for it, then send.
|
|
51
|
+
*/
|
|
52
|
+
async function handleStarting(
|
|
53
|
+
message: IncomingMessage,
|
|
54
|
+
onStatus?: StatusCallback,
|
|
55
|
+
): Promise<CoreResponse> {
|
|
56
|
+
log.info("Core is starting, waiting for it to be ready...");
|
|
57
|
+
await onStatus?.("Core iniciando, esperando...");
|
|
58
|
+
|
|
59
|
+
const ready = await waitForCoreReady(STARTUP_WAIT_MS);
|
|
60
|
+
|
|
61
|
+
if (ready) {
|
|
62
|
+
try {
|
|
63
|
+
return await postToCore(message);
|
|
64
|
+
} catch {
|
|
65
|
+
log.warn("Core ready but request failed, retrying...");
|
|
66
|
+
await sleep(RETRY_DELAY);
|
|
67
|
+
try {
|
|
68
|
+
return await postToCore(message);
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall through to fallback
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.warn("Core didn't start in time, using fallback");
|
|
76
|
+
return await runFallback(message, onStatus);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Core is up — normal path with one retry.
|
|
81
|
+
*/
|
|
82
|
+
async function handleUp(
|
|
83
|
+
message: IncomingMessage,
|
|
84
|
+
onStatus?: StatusCallback,
|
|
85
|
+
): Promise<CoreResponse> {
|
|
86
|
+
try {
|
|
87
|
+
return await postToCore(message);
|
|
88
|
+
} catch {
|
|
89
|
+
// First failure
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.warn("Core unreachable, retrying in 3s...");
|
|
93
|
+
await onStatus?.("Core no responde, reintentando...");
|
|
94
|
+
await sleep(RETRY_DELAY);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return await postToCore(message);
|
|
98
|
+
} catch {
|
|
99
|
+
// Still down
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log.warn("Core still unreachable after retry, using fallback");
|
|
103
|
+
return await runFallback(message, onStatus);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fallback: call local CLI directly (Claude -> Codex). Serialized so only one runs at a time.
|
|
108
|
+
*/
|
|
109
|
+
async function runFallback(
|
|
110
|
+
message: IncomingMessage,
|
|
111
|
+
onStatus?: StatusCallback,
|
|
112
|
+
): Promise<CoreResponse> {
|
|
113
|
+
const coreError = getCoreError();
|
|
114
|
+
|
|
115
|
+
if (coreError) {
|
|
116
|
+
const preview = coreError.length > 300 ? coreError.slice(-300) : coreError;
|
|
117
|
+
await onStatus?.(`Core caido. Error:\n<pre>${escapeHtml(preview)}</pre>\nConsultando fallback directo (Claude/Codex)...`);
|
|
118
|
+
} else {
|
|
119
|
+
await onStatus?.("Core caido. Consultando fallback directo (Claude/Codex)...");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const text = message.text || "[non-text message — media not available in fallback mode]";
|
|
123
|
+
|
|
124
|
+
// Chain onto the queue so only one fallback CLI runs at a time
|
|
125
|
+
const result = fallbackQueue.then(() => fallbackClaude(text, coreError ?? undefined));
|
|
126
|
+
fallbackQueue = result.catch(() => "");
|
|
127
|
+
|
|
128
|
+
const response = await result;
|
|
129
|
+
return { text: response };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function postToCore(message: IncomingMessage): Promise<CoreResponse> {
|
|
133
|
+
const response = await fetch(`${CORE_URL}/message`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify({ message }),
|
|
137
|
+
signal: AbortSignal.timeout(config.claudeTimeout + 5000),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Core returned ${response.status}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (await response.json()) as CoreResponse;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function escapeHtml(s: string): string {
|
|
148
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sleep(ms: number): Promise<void> {
|
|
152
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function isCoreHealthy(): Promise<boolean> {
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`${CORE_URL}/health`, { signal: AbortSignal.timeout(2000) });
|
|
158
|
+
return response.ok;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module daemon/channels/base
|
|
3
|
+
* @role Re-export the Channel interface from shared types.
|
|
4
|
+
* @responsibilities
|
|
5
|
+
* - Provide the Channel contract that all adapters must implement
|
|
6
|
+
* @dependencies shared/types
|
|
7
|
+
* @effects None
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type { Channel, IncomingMessage } from "../../shared/types";
|