arisa 2.3.55 → 3.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 (62) hide show
  1. package/AGENTS.md +102 -0
  2. package/README.md +120 -165
  3. package/bin/arisa.js +2 -643
  4. package/cli/openai-transcribe/index.js +51 -0
  5. package/cli/openai-transcribe/package.json +6 -0
  6. package/cli/openai-transcribe/tool.manifest.json +15 -0
  7. package/cli/openai-tts/index.js +58 -0
  8. package/cli/openai-tts/package.json +6 -0
  9. package/cli/openai-tts/tool.manifest.json +20 -0
  10. package/cli/web-browser/index.js +146 -0
  11. package/cli/web-browser/package.json +6 -0
  12. package/cli/web-browser/tool.manifest.json +8 -0
  13. package/package.json +26 -44
  14. package/src/core/agent/agent-manager.js +218 -0
  15. package/src/core/artifacts/artifact-store.js +102 -0
  16. package/src/core/config/config-store.js +20 -0
  17. package/src/core/tools/tool-registry.js +117 -0
  18. package/src/index.js +27 -0
  19. package/src/runtime/bootstrap.js +213 -0
  20. package/src/runtime/create-app.js +22 -0
  21. package/src/transport/telegram/auth.js +13 -0
  22. package/src/transport/telegram/bot.js +214 -0
  23. package/src/transport/telegram/media.js +75 -0
  24. package/CLAUDE.md +0 -191
  25. package/SOUL.md +0 -36
  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
package/bin/arisa.js CHANGED
@@ -1,644 +1,3 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
 
3
- const { spawn, spawnSync } = require("node:child_process");
4
- const {
5
- closeSync,
6
- existsSync,
7
- mkdirSync,
8
- openSync,
9
- readFileSync,
10
- unlinkSync,
11
- writeFileSync,
12
- } = require("node:fs");
13
- const { homedir, platform } = require("node:os");
14
- const { dirname, join, resolve } = require("node:path");
15
-
16
- const pkgRoot = resolve(__dirname, "..");
17
- const daemonEntry = join(pkgRoot, "src", "daemon", "index.ts");
18
- const coreEntry = join(pkgRoot, "src", "core", "index.ts");
19
- const homeDir = homedir();
20
- const arisaDir = join(homeDir, ".arisa");
21
- const runDir = join(arisaDir, "run");
22
- const logsDir = join(arisaDir, "logs");
23
- const pidFile = join(runDir, "arisa.pid");
24
- const fallbackLogFile = join(logsDir, "service.log");
25
- const systemdServiceName = "arisa.service";
26
- const systemdUserDir = join(homeDir, ".config", "systemd", "user");
27
- const systemdUserUnitPath = join(systemdUserDir, systemdServiceName);
28
-
29
- const args = process.argv.slice(2);
30
- const inputCommand = (args[0] || "").toLowerCase();
31
- const command = inputCommand || "daemon";
32
- const rest = inputCommand ? args.slice(1) : args;
33
- const isDefaultInvocation = inputCommand === "";
34
-
35
- function printHelp() {
36
- process.stdout.write(
37
- `Arisa CLI
38
-
39
- Usage:
40
- arisa Start daemon in foreground (default)
41
- arisa start Start service and enable restart-on-boot
42
- arisa stop Stop service
43
- arisa status Show service status
44
- arisa restart Restart service
45
- arisa daemon Start daemon in foreground
46
- arisa run Start daemon in foreground
47
- arisa start --foreground
48
- Start daemon in foreground (legacy behavior)
49
- arisa core Start core only
50
- arisa dev Start core in watch mode
51
- arisa version Print version
52
- arisa help Show this help
53
- `
54
- );
55
- }
56
-
57
- function printVersion() {
58
- const pkgPath = join(pkgRoot, "package.json");
59
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
60
- process.stdout.write(`${pkg.version}\n`);
61
- }
62
-
63
- function commandExists(binary) {
64
- const probe = spawnSync("sh", ["-lc", `command -v ${binary} >/dev/null 2>&1`], {
65
- stdio: "ignore",
66
- });
67
- return probe.status === 0;
68
- }
69
-
70
- function runCommand(executable, commandArgs, options = {}) {
71
- return spawnSync(executable, commandArgs, {
72
- encoding: "utf8",
73
- ...options,
74
- });
75
- }
76
-
77
- function resolveBunExecutable() {
78
- if (process.env.BUN_BIN && process.env.BUN_BIN.trim()) {
79
- return process.env.BUN_BIN.trim();
80
- }
81
-
82
- const bunInstall = process.env.BUN_INSTALL || join(homeDir, ".bun");
83
- const bunFromInstall = join(bunInstall, "bin", "bun");
84
- if (existsSync(bunFromInstall)) {
85
- return bunFromInstall;
86
- }
87
-
88
- return "bun";
89
- }
90
-
91
- function runWithBun(bunArgs, options = {}) {
92
- const bunExecutable = resolveBunExecutable();
93
- const child = runCommand(bunExecutable, bunArgs, {
94
- stdio: "inherit",
95
- cwd: process.cwd(),
96
- env: {
97
- ...process.env,
98
- ARISA_PROJECT_DIR: process.env.ARISA_PROJECT_DIR || pkgRoot,
99
- },
100
- shell: process.platform === "win32",
101
- ...options,
102
- });
103
-
104
- if (child.error) {
105
- if (child.error.code === "ENOENT") {
106
- process.stderr.write(
107
- "Arisa requires Bun to run. Install it from https://bun.sh/ and retry.\n"
108
- );
109
- process.exit(1);
110
- }
111
- process.stderr.write(`${String(child.error)}\n`);
112
- process.exit(1);
113
- }
114
-
115
- return child;
116
- }
117
-
118
- function ensureRuntimeDirs() {
119
- mkdirSync(runDir, { recursive: true });
120
- mkdirSync(logsDir, { recursive: true });
121
- }
122
-
123
- function readPid() {
124
- if (!existsSync(pidFile)) return null;
125
-
126
- try {
127
- const raw = readFileSync(pidFile, "utf8").trim();
128
- const parsed = Number.parseInt(raw, 10);
129
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
130
- } catch {
131
- return null;
132
- }
133
- }
134
-
135
- function isPidRunning(pid) {
136
- try {
137
- process.kill(pid, 0);
138
- return true;
139
- } catch (error) {
140
- return error && error.code === "EPERM";
141
- }
142
- }
143
-
144
- function removePidFile() {
145
- if (!existsSync(pidFile)) return;
146
- try {
147
- unlinkSync(pidFile);
148
- } catch {
149
- // Best effort cleanup.
150
- }
151
- }
152
-
153
- function cleanupStalePidFile() {
154
- const pid = readPid();
155
- if (!pid) {
156
- removePidFile();
157
- return null;
158
- }
159
-
160
- if (!isPidRunning(pid)) {
161
- removePidFile();
162
- return null;
163
- }
164
-
165
- return pid;
166
- }
167
-
168
- function startDetachedFallback() {
169
- ensureRuntimeDirs();
170
- const runningPid = cleanupStalePidFile();
171
- if (runningPid) {
172
- process.stdout.write(
173
- `Arisa is already running in fallback mode (PID ${runningPid}).\n`
174
- );
175
- return 0;
176
- }
177
-
178
- const bunExecutable = resolveBunExecutable();
179
- const logFd = openSync(fallbackLogFile, "a");
180
- const child = spawn(bunExecutable, [daemonEntry], {
181
- detached: true,
182
- stdio: ["ignore", logFd, logFd],
183
- cwd: pkgRoot,
184
- env: {
185
- ...process.env,
186
- ARISA_PROJECT_DIR: process.env.ARISA_PROJECT_DIR || pkgRoot,
187
- },
188
- shell: process.platform === "win32",
189
- });
190
-
191
- closeSync(logFd);
192
- if (!child.pid) {
193
- process.stderr.write(
194
- "Failed to start Arisa in fallback mode. Ensure Bun is installed and in PATH.\n"
195
- );
196
- return 1;
197
- }
198
-
199
- child.unref();
200
-
201
- writeFileSync(pidFile, `${child.pid}\n`, "utf8");
202
- process.stdout.write(
203
- `Arisa started in fallback mode (PID ${child.pid}). Logs: ${fallbackLogFile}\n`
204
- );
205
- process.stdout.write(
206
- "Autostart on reboot requires systemd user services.\n"
207
- );
208
- return 0;
209
- }
210
-
211
- function stopDetachedFallback() {
212
- const runningPid = cleanupStalePidFile();
213
- if (!runningPid) {
214
- process.stdout.write("Arisa is not running (fallback mode).\n");
215
- return 0;
216
- }
217
-
218
- try {
219
- process.kill(runningPid, "SIGTERM");
220
- removePidFile();
221
- process.stdout.write(`Sent SIGTERM to Arisa (PID ${runningPid}).\n`);
222
- return 0;
223
- } catch (error) {
224
- process.stderr.write(
225
- `Failed to stop Arisa PID ${runningPid}: ${error.message}\n`
226
- );
227
- return 1;
228
- }
229
- }
230
-
231
- function statusDetachedFallback() {
232
- const runningPid = cleanupStalePidFile();
233
- if (!runningPid) {
234
- process.stdout.write("Arisa is not running (fallback mode).\n");
235
- return 1;
236
- }
237
-
238
- process.stdout.write(`Arisa is running in fallback mode (PID ${runningPid}).\n`);
239
- process.stdout.write(`Logs: ${fallbackLogFile}\n`);
240
- return 0;
241
- }
242
-
243
- function canUseSystemdUser() {
244
- if (platform() !== "linux") return false;
245
- if (!commandExists("systemctl")) return false;
246
-
247
- const probe = runCommand("systemctl", ["--user", "show-environment"], {
248
- stdio: "pipe",
249
- });
250
-
251
- return probe.status === 0;
252
- }
253
-
254
- function writeSystemdUserUnit() {
255
- mkdirSync(systemdUserDir, { recursive: true });
256
-
257
- const bunInstall = process.env.BUN_INSTALL || join(homeDir, ".bun");
258
- const pathValue = process.env.PATH || `${join(bunInstall, "bin")}:/usr/local/bin:/usr/bin:/bin`;
259
- const bunExecutable = resolveBunExecutable();
260
-
261
- const unit = `[Unit]
262
- Description=Arisa Daemon Service
263
- After=network-online.target
264
- Wants=network-online.target
265
-
266
- [Service]
267
- Type=simple
268
- WorkingDirectory=${pkgRoot}
269
- ExecStart=${bunExecutable} ${daemonEntry}
270
- Restart=always
271
- RestartSec=3
272
- Environment=ARISA_PROJECT_DIR=${pkgRoot}
273
- Environment=BUN_INSTALL=${bunInstall}
274
- Environment=PATH=${pathValue}
275
-
276
- [Install]
277
- WantedBy=default.target
278
- `;
279
-
280
- writeFileSync(systemdUserUnitPath, unit, "utf8");
281
- }
282
-
283
- function runSystemd(commandArgs) {
284
- const child = runCommand("systemctl", ["--user", ...commandArgs], {
285
- stdio: "pipe",
286
- });
287
-
288
- if (child.status !== 0) {
289
- const stderr = child.stderr || "Unknown systemd error";
290
- process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
291
- return { ok: false, status: child.status ?? 1 };
292
- }
293
-
294
- return { ok: true, status: 0, stdout: child.stdout || "" };
295
- }
296
-
297
- function startSystemdService() {
298
- writeSystemdUserUnit();
299
-
300
- const reload = runSystemd(["daemon-reload"]);
301
- if (!reload.ok) return reload.status;
302
-
303
- const start = runSystemd(["enable", "--now", systemdServiceName]);
304
- if (!start.ok) return start.status;
305
-
306
- process.stdout.write(
307
- "Arisa service started and enabled (systemd --user).\n"
308
- );
309
- process.stdout.write(
310
- "To keep it running after reboot without login, run: sudo loginctl enable-linger $USER\n"
311
- );
312
- return 0;
313
- }
314
-
315
- function stopSystemdService() {
316
- const stop = runSystemd(["stop", systemdServiceName]);
317
- if (!stop.ok) return stop.status;
318
-
319
- process.stdout.write("Arisa service stopped (systemd --user).\n");
320
- return 0;
321
- }
322
-
323
- function restartSystemdService() {
324
- const restart = runSystemd(["restart", systemdServiceName]);
325
- if (!restart.ok) return restart.status;
326
-
327
- process.stdout.write("Arisa service restarted (systemd --user).\n");
328
- return 0;
329
- }
330
-
331
- function statusSystemdService() {
332
- const activeResult = runCommand(
333
- "systemctl",
334
- ["--user", "is-active", systemdServiceName],
335
- { stdio: "pipe" }
336
- );
337
- const enabledResult = runCommand(
338
- "systemctl",
339
- ["--user", "is-enabled", systemdServiceName],
340
- { stdio: "pipe" }
341
- );
342
-
343
- const active = activeResult.status === 0;
344
- const enabled = enabledResult.status === 0;
345
-
346
- if (active) {
347
- process.stdout.write("Arisa service status: active (systemd --user).\n");
348
- } else {
349
- process.stdout.write("Arisa service status: inactive (systemd --user).\n");
350
- }
351
-
352
- if (enabled) {
353
- process.stdout.write("Autostart: enabled\n");
354
- return active ? 0 : 1;
355
- }
356
-
357
- process.stdout.write("Autostart: disabled\n");
358
- return active ? 0 : 1;
359
- }
360
-
361
- function restartDetachedFallback() {
362
- const stopCode = stopDetachedFallback();
363
- if (stopCode !== 0) return stopCode;
364
- return startDetachedFallback();
365
- }
366
-
367
- function startService() {
368
- if (rest.includes("--foreground")) {
369
- const foregroundArgs = rest.filter((arg) => arg !== "--foreground");
370
- const child = runWithBun([daemonEntry, ...foregroundArgs]);
371
- return child.status === null ? 1 : child.status;
372
- }
373
-
374
- if (canUseSystemdUser()) {
375
- return startSystemdService();
376
- }
377
- return startDetachedFallback();
378
- }
379
-
380
- function stopService() {
381
- if (canUseSystemdUser()) {
382
- return stopSystemdService();
383
- }
384
- return stopDetachedFallback();
385
- }
386
-
387
- function statusService() {
388
- if (canUseSystemdUser()) {
389
- return statusSystemdService();
390
- }
391
- return statusDetachedFallback();
392
- }
393
-
394
- function restartService() {
395
- if (canUseSystemdUser()) {
396
- return restartSystemdService();
397
- }
398
- return restartDetachedFallback();
399
- }
400
-
401
-
402
- // ── Root: create arisa user for Core process execution ──────────────
403
- // Daemon runs as root. Core runs as user arisa (Claude CLI refuses root).
404
- // This means Claude/Codex calls from Core are direct — no su wrapping.
405
-
406
- function isRoot() {
407
- return process.getuid?.() === 0;
408
- }
409
-
410
- function arisaUserExists() {
411
- return spawnSync("id", ["arisa"], { stdio: "ignore" }).status === 0;
412
- }
413
-
414
- function isArisaUserProvisioned() {
415
- return arisaUserExists();
416
- }
417
-
418
- function step(ok, msg) {
419
- process.stdout.write(` ${ok ? "\u2713" : "\u2717"} ${msg}\n`);
420
- }
421
-
422
- const ROOT_BUN_INSTALL = process.env.BUN_INSTALL || join(homeDir, ".bun");
423
- const ARISA_BUN_ENV = `export BUN_INSTALL=${ROOT_BUN_INSTALL} && export PATH=${ROOT_BUN_INSTALL}/bin:$PATH`;
424
-
425
- function provisionArisaUser() {
426
- process.stdout.write("Creating user 'arisa' for Claude/Codex CLI execution...\n");
427
-
428
- // 1. Create user with sudo access
429
- const useradd = spawnSync("useradd", ["-m", "-s", "/bin/bash", "arisa"], { stdio: "pipe" });
430
- if (useradd.status !== 0) {
431
- step(false, `Failed to create user: ${(useradd.stderr || "").toString().trim()}`);
432
- process.exit(1);
433
- }
434
- step(true, "User arisa created");
435
-
436
- // 2. Grant passwordless sudo (needed for full tool execution in Claude/Codex)
437
- try {
438
- writeFileSync("/etc/sudoers.d/arisa", "arisa ALL=(ALL) NOPASSWD: ALL\n", { mode: 0o440 });
439
- step(true, "Passwordless sudo granted");
440
- } catch (e) {
441
- // Not fatal — sudo may not be installed in minimal containers
442
- step(false, `Sudo setup skipped: ${e.message || e}`);
443
- }
444
-
445
- // 3. Give arisa access to root's bun (no separate install needed)
446
- grantBunAccess();
447
- step(true, "Access to root's bun granted");
448
-
449
- process.stdout.write(" Done. Core will run as arisa; Claude/Codex calls are direct.\n\n");
450
- }
451
-
452
- function grantBunAccess() {
453
- // Allow arisa to traverse into /root (execute only, not read)
454
- spawnSync("chmod", ["o+x", homeDir], { stdio: "ignore" });
455
- // Allow arisa to read+execute root's bun and globally installed CLIs
456
- spawnSync("chmod", ["-R", "o+rX", ROOT_BUN_INSTALL], { stdio: "ignore" });
457
- }
458
-
459
- // Provision arisa user if running as root and not yet done
460
- if (isRoot() && !isArisaUserProvisioned()) {
461
- provisionArisaUser();
462
- }
463
-
464
- // When root + arisa exists: route all runtime data through arisa's home
465
- // so Core (running as arisa) and Daemon (root) share the same data dir.
466
- if (isRoot() && arisaUserExists()) {
467
- // Ensure arisa can access root's bun on every startup
468
- grantBunAccess();
469
- const arisaDataDir = "/home/arisa/.arisa";
470
- const rootDataDir = join("/root", ".arisa");
471
-
472
- // One-time migration from root's data dir
473
- if (existsSync(rootDataDir) && !existsSync(arisaDataDir)) {
474
- spawnSync("cp", ["-r", rootDataDir, arisaDataDir], { stdio: "ignore" });
475
- spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
476
- }
477
-
478
- // Ensure arisa data dir exists with correct ownership
479
- if (!existsSync(arisaDataDir)) {
480
- mkdirSync(arisaDataDir, { recursive: true });
481
- spawnSync("chown", ["-R", "arisa:arisa", arisaDataDir], { stdio: "ignore" });
482
- }
483
-
484
- // Ensure arisa can traverse to and read project files.
485
- // When installed globally under /root/.bun/..., parent dirs are mode 700.
486
- // Add o+x (traverse only, not read) on each ancestor so arisa can reach pkgRoot.
487
- let traverseDir = pkgRoot;
488
- while (traverseDir !== "/") {
489
- spawnSync("chmod", ["o+x", traverseDir], { stdio: "ignore" });
490
- traverseDir = dirname(traverseDir);
491
- }
492
- spawnSync("chmod", ["-R", "o+rX", pkgRoot], { stdio: "ignore" });
493
-
494
- // All processes use arisa's data dir (inherited by Daemon → Core)
495
- process.env.ARISA_DATA_DIR = arisaDataDir;
496
-
497
- // Permissive umask so files Daemon (root) creates at runtime in the shared
498
- // data dir are readable/writable by Core (arisa). Safe in Docker containers.
499
- process.umask(0o000);
500
- }
501
-
502
- // Pre-flight: install missing CLIs while still root (before su arisa).
503
- // arisa user has read+execute but NOT write access to root's bun dir.
504
- // Uses @inquirer/prompts checkbox when available (same UI as setup.ts).
505
- async function preflightInstallClis() {
506
- const clis = {
507
- claude: "@anthropic-ai/claude-code",
508
- codex: "@openai/codex",
509
- };
510
-
511
- const bunBinDir = join(ROOT_BUN_INSTALL, "bin");
512
- const missing = [];
513
-
514
- for (const [name, pkg] of Object.entries(clis)) {
515
- if (existsSync(join(bunBinDir, name))) continue;
516
- missing.push({ name, pkg });
517
- }
518
-
519
- if (missing.length === 0) return;
520
-
521
- // Show status
522
- process.stdout.write("\nCLI Status:\n");
523
- for (const name of Object.keys(clis)) {
524
- const installed = existsSync(join(bunBinDir, name));
525
- const label = name === "claude" ? "Claude" : "Codex";
526
- process.stdout.write(` ${installed ? "\u2713" : "\u2717"} ${label}${installed ? "" : " \u2014 not installed"}\n`);
527
- }
528
-
529
- let toInstall = missing;
530
-
531
- if (process.stdin.isTTY) {
532
- // Try @inquirer/prompts for the same checkbox UI as setup.ts
533
- let inq = null;
534
- try { inq = await import("@inquirer/prompts"); } catch {}
535
-
536
- if (inq) {
537
- const selected = await inq.checkbox({
538
- message: "Install missing CLIs? (space to select, enter to confirm)",
539
- choices: missing.map((cli) => ({
540
- name: `${cli.name === "claude" ? "Claude" : "Codex"} (${cli.pkg})`,
541
- value: cli,
542
- checked: true,
543
- })),
544
- });
545
- toInstall = selected;
546
- } else {
547
- // Fallback: simple Y/n
548
- const rl = require("node:readline");
549
- const ask = (q) =>
550
- new Promise((resolve) => {
551
- const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
552
- iface.question(q, (a) => { iface.close(); resolve(a.trim()); });
553
- });
554
- const answer = await ask("\nInstall missing CLIs? (Y/n): ");
555
- if (answer.toLowerCase() === "n") toInstall = [];
556
- }
557
- }
558
-
559
- if (toInstall.length === 0) {
560
- process.stdout.write(" Skipping CLI installation.\n");
561
- return;
562
- }
563
-
564
- for (const { name, pkg } of toInstall) {
565
- process.stdout.write(`\nInstalling ${name}...\n`);
566
- const result = spawnSync("bun", ["add", "-g", pkg], {
567
- stdio: "inherit",
568
- timeout: 180000,
569
- });
570
- if (result.status === 0) {
571
- process.stdout.write(` \u2713 ${name} installed\n`);
572
- } else {
573
- process.stdout.write(` \u2717 ${name} install failed\n`);
574
- }
575
- }
576
-
577
- // Re-grant read+execute access after installing new binaries
578
- grantBunAccess();
579
- }
580
-
581
- if (isRoot() && arisaUserExists()) {
582
- await preflightInstallClis();
583
- }
584
-
585
- // Then fall through to normal daemon startup
586
-
587
- // ── Non-root flow (unchanged) ───────────────────────────────────────
588
-
589
- if (command === "help" || command === "--help" || command === "-h") {
590
- printHelp();
591
- process.exit(0);
592
- }
593
-
594
- if (command === "version" || command === "--version" || command === "-v") {
595
- printVersion();
596
- process.exit(0);
597
- }
598
-
599
- switch (command) {
600
- case "start":
601
- process.exit(startService());
602
- break;
603
- case "stop":
604
- process.exit(stopService());
605
- break;
606
- case "status":
607
- process.exit(statusService());
608
- break;
609
- case "restart":
610
- process.exit(restartService());
611
- break;
612
- case "daemon":
613
- case "run": {
614
- // Single bun process: daemon + core in-process with --watch.
615
- // When root, run as arisa (Claude CLI refuses root). su without "-"
616
- // preserves parent env (ARISA_DATA_DIR, tokens, API keys).
617
- let child;
618
- if (isRoot() && arisaUserExists()) {
619
- const bunEnv = `export HOME=/home/arisa && ${ARISA_BUN_ENV}`;
620
- const cmd = `${bunEnv} && cd ${pkgRoot} && exec bun --watch ${daemonEntry}`;
621
- child = spawnSync("su", ["arisa", "-s", "/bin/bash", "-c", cmd], {
622
- stdio: "inherit",
623
- env: process.env,
624
- });
625
- } else {
626
- child = runWithBun(["--watch", daemonEntry, ...rest]);
627
- }
628
- process.exit(child.status === null ? 1 : child.status);
629
- }
630
- case "core": {
631
- await import(coreEntry);
632
- break;
633
- }
634
- case "dev":
635
- {
636
- // dev needs --watch which is a bun CLI flag, so child process required
637
- const child = runWithBun(["--watch", coreEntry, ...rest]);
638
- process.exit(child.status === null ? 1 : child.status);
639
- }
640
- default:
641
- process.stderr.write(`Unknown command: ${command}\n\n`);
642
- printHelp();
643
- process.exit(1);
644
- }
3
+ import "../src/index.js";
@@ -0,0 +1,51 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import config from "./config.js";
4
+
5
+ function printHelp() {
6
+ console.log(`openai-transcribe\n\nUso:\n node index.js --help\n node index.js run --request-file <json>\n\nInput esperado:\n {\n \"artifact\": { \"path\": \"/abs/audio.ogg\", \"mimeType\": \"audio/ogg\" },\n \"args\": {}\n }\n\nConfig en cli/openai-transcribe/config.js:\n OPENAI_API_KEY\n MODEL\n`);
7
+ }
8
+
9
+ async function run(requestFile) {
10
+ if (!config.OPENAI_API_KEY) {
11
+ console.log(JSON.stringify({ ok: false, missingConfig: ["OPENAI_API_KEY"], configPath: path.resolve("config.js") }));
12
+ return;
13
+ }
14
+
15
+ const request = JSON.parse(await readFile(requestFile, "utf8"));
16
+ const artifact = request.artifact;
17
+ if (!artifact?.path) {
18
+ console.log(JSON.stringify({ ok: false, error: "artifact.path is required" }));
19
+ return;
20
+ }
21
+
22
+ await stat(artifact.path);
23
+ const form = new FormData();
24
+ const data = await readFile(artifact.path);
25
+ form.append("file", new Blob([data]), path.basename(artifact.path));
26
+ form.append("model", config.MODEL);
27
+
28
+ const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
29
+ method: "POST",
30
+ headers: { Authorization: `Bearer ${config.OPENAI_API_KEY}` },
31
+ body: form
32
+ });
33
+
34
+ const payload = await response.json();
35
+ if (!response.ok) {
36
+ console.log(JSON.stringify({ ok: false, error: payload.error?.message || "OpenAI transcription failed" }));
37
+ return;
38
+ }
39
+
40
+ console.log(JSON.stringify({ ok: true, output: { text: payload.text || "" } }));
41
+ }
42
+
43
+ const args = process.argv.slice(2);
44
+ if (!args.length || args.includes("--help") || args[0] === "help") {
45
+ printHelp();
46
+ } else if (args[0] === "run") {
47
+ const fileIndex = args.indexOf("--request-file");
48
+ await run(args[fileIndex + 1]);
49
+ } else {
50
+ printHelp();
51
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "openai-transcribe-cli",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "1.0.0"
6
+ }