@aitne-sh/aitne 0.1.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/LICENSE +21 -0
- package/README.md +464 -0
- package/agent-assets/agent-profiles/_safety.md +26 -0
- package/agent-assets/agent-profiles/conversational.md +33 -0
- package/agent-assets/agent-profiles/docs-qa.md +24 -0
- package/agent-assets/agent-profiles/observer.md +28 -0
- package/agent-assets/agent-profiles/profile-importer.md +63 -0
- package/agent-assets/agent-profiles/proxy.md +28 -0
- package/agent-assets/agent-profiles/routine.md +16 -0
- package/agent-assets/agent-profiles/task.md +18 -0
- package/agent-assets/docs/concepts/agent-day.md +88 -0
- package/agent-assets/docs/concepts/auth-health.md +75 -0
- package/agent-assets/docs/concepts/backends-and-tiers.md +126 -0
- package/agent-assets/docs/concepts/costs-and-quotas.md +103 -0
- package/agent-assets/docs/concepts/delegated-mode.md +223 -0
- package/agent-assets/docs/concepts/memory-model.md +118 -0
- package/agent-assets/docs/concepts/observations.md +80 -0
- package/agent-assets/docs/concepts/process-keys.md +89 -0
- package/agent-assets/docs/concepts/routines.md +108 -0
- package/agent-assets/docs/concepts/safety-and-execution.md +109 -0
- package/agent-assets/docs/concepts/safety-model.md +279 -0
- package/agent-assets/docs/concepts/skills.md +100 -0
- package/agent-assets/docs/features/integrations/calendar.md +92 -0
- package/agent-assets/docs/features/integrations/git.md +95 -0
- package/agent-assets/docs/features/integrations/github.md +170 -0
- package/agent-assets/docs/features/integrations/mail.md +106 -0
- package/agent-assets/docs/features/integrations/notion.md +69 -0
- package/agent-assets/docs/features/integrations/obsidian.md +71 -0
- package/agent-assets/docs/features/lifestyle/git.md +178 -0
- package/agent-assets/docs/features/lifestyle/reading.md +93 -0
- package/agent-assets/docs/features/lifestyle/receipts.md +71 -0
- package/agent-assets/docs/features/lifestyle/travel-bookings.md +44 -0
- package/agent-assets/docs/features/lifestyle/travel-time.md +52 -0
- package/agent-assets/docs/features/memory-files/agent-journal.md +105 -0
- package/agent-assets/docs/features/memory-files/projects.md +56 -0
- package/agent-assets/docs/features/memory-files/roadmap.md +61 -0
- package/agent-assets/docs/features/memory-files/schedule.md +112 -0
- package/agent-assets/docs/features/memory-files/today.md +73 -0
- package/agent-assets/docs/features/memory-files/user-profile.md +81 -0
- package/agent-assets/docs/features/messaging/dashboard-chat.md +93 -0
- package/agent-assets/docs/features/messaging/discord.md +50 -0
- package/agent-assets/docs/features/messaging/overview.md +111 -0
- package/agent-assets/docs/features/messaging/pairing-and-magic-phrase.md +69 -0
- package/agent-assets/docs/features/messaging/slack.md +51 -0
- package/agent-assets/docs/features/messaging/telegram.md +63 -0
- package/agent-assets/docs/features/messaging/whatsapp.md +48 -0
- package/agent-assets/docs/features/operations/activity-and-conversations.md +105 -0
- package/agent-assets/docs/features/operations/approvals.md +58 -0
- package/agent-assets/docs/features/operations/backend-routing.md +62 -0
- package/agent-assets/docs/features/operations/cost-tracking.md +59 -0
- package/agent-assets/docs/features/operations/notifications.md +69 -0
- package/agent-assets/docs/features/operations/quiet-hours.md +106 -0
- package/agent-assets/docs/features/operations/schedule-approaching.md +60 -0
- package/agent-assets/docs/features/routines/custom-routines.md +101 -0
- package/agent-assets/docs/features/routines/evening-review.md +81 -0
- package/agent-assets/docs/features/routines/hourly-check.md +85 -0
- package/agent-assets/docs/features/routines/monthly-review.md +65 -0
- package/agent-assets/docs/features/routines/morning-routine.md +123 -0
- package/agent-assets/docs/features/routines/weekly-review.md +70 -0
- package/agent-assets/docs/getting-started/01-what-is-this.md +192 -0
- package/agent-assets/docs/getting-started/02-first-steps.md +80 -0
- package/agent-assets/docs/getting-started/03-what-can-this-do.md +110 -0
- package/agent-assets/docs/getting-started/04-first-day.md +287 -0
- package/agent-assets/docs/glossary.md +116 -0
- package/agent-assets/docs/guides/add-a-custom-routine.md +71 -0
- package/agent-assets/docs/guides/backup-and-restore.md +54 -0
- package/agent-assets/docs/guides/change-which-model-handles-x.md +47 -0
- package/agent-assets/docs/guides/connect-a-new-mail-account.md +59 -0
- package/agent-assets/docs/guides/import-knowledge-file.md +275 -0
- package/agent-assets/docs/guides/install-and-run.md +72 -0
- package/agent-assets/docs/guides/migrate-machines.md +52 -0
- package/agent-assets/docs/guides/pause-the-agent.md +65 -0
- package/agent-assets/docs/guides/reinstall-cleanly.md +52 -0
- package/agent-assets/docs/guides/setup-wizard.md +107 -0
- package/agent-assets/docs/guides/switch-default-backend.md +60 -0
- package/agent-assets/docs/reference/api.md +51 -0
- package/agent-assets/docs/reference/cli-commands.md +121 -0
- package/agent-assets/docs/reference/config.md +74 -0
- package/agent-assets/docs/reference/disallowed-tools.md +76 -0
- package/agent-assets/docs/reference/keyboard-shortcuts.md +39 -0
- package/agent-assets/docs/reference/process-keys.md +59 -0
- package/agent-assets/docs/reference/skills.md +50 -0
- package/agent-assets/docs/troubleshooting/auth-failed.md +57 -0
- package/agent-assets/docs/troubleshooting/dashboard-shows-degraded.md +55 -0
- package/agent-assets/docs/troubleshooting/fallback-keeps-firing.md +54 -0
- package/agent-assets/docs/troubleshooting/messaging-not-pairing.md +53 -0
- package/agent-assets/docs/troubleshooting/morning-routine-didnt-run.md +75 -0
- package/agent-assets/docs/troubleshooting/observation-not-detected.md +57 -0
- package/agent-assets/docs/troubleshooting/quota-exhausted.md +57 -0
- package/agent-assets/optimizer-skills/drift-analysis/SKILL.md +75 -0
- package/agent-assets/optimizer-skills/knowledge-map/SKILL.md +71 -0
- package/agent-assets/optimizer-skills/skill-curation/SKILL.md +108 -0
- package/agent-assets/project-doc-templates/git-repo.md +21 -0
- package/agent-assets/project-doc-templates/project.md +38 -0
- package/agent-assets/skills/attach/SKILL.md +104 -0
- package/agent-assets/skills/context/SKILL.md +257 -0
- package/agent-assets/skills/context/curation.json +37 -0
- package/agent-assets/skills/context/seeds/file-responsibilities.seed.json +13 -0
- package/agent-assets/skills/context/seeds/frontmatter-requirements.seed.json +40 -0
- package/agent-assets/skills/docs-search/SKILL.md +176 -0
- package/agent-assets/skills/external-services/SKILL.delegated.claude.md +369 -0
- package/agent-assets/skills/external-services/SKILL.delegated.codex.md +349 -0
- package/agent-assets/skills/external-services/SKILL.delegated.gemini.md +347 -0
- package/agent-assets/skills/external-services/SKILL.md +371 -0
- package/agent-assets/skills/mail/SKILL.delegated.claude.md +284 -0
- package/agent-assets/skills/mail/SKILL.delegated.codex.md +261 -0
- package/agent-assets/skills/mail/SKILL.delegated.gemini.md +255 -0
- package/agent-assets/skills/mail/SKILL.md +313 -0
- package/agent-assets/skills/mail/references/errors.md +17 -0
- package/agent-assets/skills/mail/references/providers.md +40 -0
- package/agent-assets/skills/mail/references/query-grammar.md +24 -0
- package/agent-assets/skills/management-policy/SKILL.md +307 -0
- package/agent-assets/skills/management-policy/curation.json +13 -0
- package/agent-assets/skills/management-policy/seeds/policy-file-shape.seed.json +16 -0
- package/agent-assets/skills/management-task-modify/SKILL.md +202 -0
- package/agent-assets/skills/management-task-register/SKILL.md +330 -0
- package/agent-assets/skills/management-task-stop/SKILL.md +166 -0
- package/agent-assets/skills/notify/SKILL.md +196 -0
- package/agent-assets/skills/notion/SKILL.delegated.claude.md +254 -0
- package/agent-assets/skills/notion/SKILL.delegated.codex.md +195 -0
- package/agent-assets/skills/notion/SKILL.delegated.gemini.md +194 -0
- package/agent-assets/skills/notion/SKILL.md +86 -0
- package/agent-assets/skills/observations/SKILL.md +234 -0
- package/agent-assets/skills/observations/curation.json +13 -0
- package/agent-assets/skills/observations/seeds/source-namespacing.seed.json +20 -0
- package/agent-assets/skills/project-doc/SKILL.md +86 -0
- package/agent-assets/skills/project-doc/curation.json +21 -0
- package/agent-assets/skills/project-doc/seeds/project-shape.seed.json +25 -0
- package/agent-assets/skills/project-doc/seeds/slug-grammar.seed.json +20 -0
- package/agent-assets/skills/reading/SKILL.md +198 -0
- package/agent-assets/skills/reading/references/reading-taste.md +197 -0
- package/agent-assets/skills/receipts/SKILL.md +134 -0
- package/agent-assets/skills/roadmap/SKILL.md +276 -0
- package/agent-assets/skills/roadmap/curation.json +13 -0
- package/agent-assets/skills/roadmap/references/horizon-tags.md +40 -0
- package/agent-assets/skills/roadmap/references/preparation-timeline.md +47 -0
- package/agent-assets/skills/roadmap/seeds/entry-types.seed.json +16 -0
- package/agent-assets/skills/schedule/SKILL.md +228 -0
- package/agent-assets/skills/scheduled-managed-task/SKILL.md +392 -0
- package/agent-assets/skills/today/SKILL.md +198 -0
- package/agent-assets/skills/today/curation.json +21 -0
- package/agent-assets/skills/today/seeds/agent-notes-flavors.seed.json +17 -0
- package/agent-assets/skills/today/seeds/section-shape.seed.json +17 -0
- package/agent-assets/skills/travel/SKILL.md +132 -0
- package/agent-assets/skills/travel-time/SKILL.md +149 -0
- package/agent-assets/skills/user-interview/SKILL.md +323 -0
- package/agent-assets/skills/user-interview/references/sweep-and-fallback.md +94 -0
- package/agent-assets/skills/user-profile/SKILL.md +210 -0
- package/agent-assets/skills/user-profile/curation.json +29 -0
- package/agent-assets/skills/user-profile/seeds/learned-context-format.seed.json +14 -0
- package/agent-assets/skills/user-profile/seeds/routing-table.seed.json +53 -0
- package/agent-assets/skills/user-profile/seeds/topic-files.seed.json +27 -0
- package/agent-assets/task-flows/dashboard.docs_qa.md +43 -0
- package/agent-assets/task-flows/default.md +11 -0
- package/agent-assets/task-flows/git.branch.created.md +25 -0
- package/agent-assets/task-flows/git.lifecycle.poll.md +52 -0
- package/agent-assets/task-flows/git.local_ahead.stale.md +34 -0
- package/agent-assets/task-flows/git.merge_to_default.md +30 -0
- package/agent-assets/task-flows/git.project.refresh_architecture.md +100 -0
- package/agent-assets/task-flows/git.project.retemplate.md +73 -0
- package/agent-assets/task-flows/git.push.detected.md +32 -0
- package/agent-assets/task-flows/git.push.force_pushed.md +36 -0
- package/agent-assets/task-flows/git.tag.created.md +24 -0
- package/agent-assets/task-flows/github.assigned.md +43 -0
- package/agent-assets/task-flows/github.pull_request.review_requested.md +57 -0
- package/agent-assets/task-flows/github.security_alert.md +45 -0
- package/agent-assets/task-flows/github.workflow_run.failed.md +57 -0
- package/agent-assets/task-flows/knowledge.import.md +161 -0
- package/agent-assets/task-flows/message.received.dm.md +142 -0
- package/agent-assets/task-flows/message.received.dm_first.md +117 -0
- package/agent-assets/task-flows/message.received.md +14 -0
- package/agent-assets/task-flows/routine.custom.md +38 -0
- package/agent-assets/task-flows/routine.evening_review.md +323 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.claude.md +405 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.codex.md +400 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.gemini.md +404 -0
- package/agent-assets/task-flows/routine.hourly_check.md +184 -0
- package/agent-assets/task-flows/routine.hourly_check.triage.md +93 -0
- package/agent-assets/task-flows/routine.monthly_review.md +250 -0
- package/agent-assets/task-flows/routine.morning_routine.md +300 -0
- package/agent-assets/task-flows/routine.morning_routine_initial.md +184 -0
- package/agent-assets/task-flows/routine.roadmap_refresh.md +275 -0
- package/agent-assets/task-flows/routine.today_refresh.md +172 -0
- package/agent-assets/task-flows/routine.user_profile_sweep.md +242 -0
- package/agent-assets/task-flows/routine.weekly_review.md +247 -0
- package/agent-assets/task-flows/schedule.approaching.md +124 -0
- package/agent-assets/task-flows/scheduled.dm.md +391 -0
- package/agent-assets/task-flows/scheduled.task.md +141 -0
- package/agent-assets/task-flows/setup.initial.md +277 -0
- package/agent-assets/task-flows/setup.update.md +53 -0
- package/agent-assets/templates/README.md +85 -0
- package/agent-assets/templates/_index.md +39 -0
- package/agent-assets/templates/_manifest.json +103 -0
- package/agent-assets/templates/agent/journal.md +10 -0
- package/agent-assets/templates/agent/profile-questions.md +74 -0
- package/agent-assets/templates/context-index.md +42 -0
- package/agent-assets/templates/dossiers/_index.md +22 -0
- package/agent-assets/templates/dossiers/evening.md +23 -0
- package/agent-assets/templates/dossiers/hourly.md +23 -0
- package/agent-assets/templates/dossiers/monthly.md +23 -0
- package/agent-assets/templates/dossiers/morning.md +23 -0
- package/agent-assets/templates/dossiers/roadmap.md +23 -0
- package/agent-assets/templates/dossiers/weekly.md +23 -0
- package/agent-assets/templates/projects/_active.base +14 -0
- package/agent-assets/templates/projects/_index.md +29 -0
- package/agent-assets/templates/roadmap.md +15 -0
- package/agent-assets/templates/routines/_index.md +20 -0
- package/agent-assets/templates/routines/evening.md +22 -0
- package/agent-assets/templates/routines/hourly.md +30 -0
- package/agent-assets/templates/routines/monthly.md +25 -0
- package/agent-assets/templates/routines/morning.md +26 -0
- package/agent-assets/templates/routines/weekly.md +23 -0
- package/agent-assets/templates/rules/_index.md +19 -0
- package/agent-assets/templates/rules/journal-export.md +41 -0
- package/agent-assets/templates/rules/journal-format.md +61 -0
- package/agent-assets/templates/rules/management.md +48 -0
- package/agent-assets/templates/rules/mcp.md +40 -0
- package/agent-assets/templates/rules/policies/_index.md +22 -0
- package/agent-assets/templates/rules/redaction.md +30 -0
- package/agent-assets/templates/today.md +13 -0
- package/agent-assets/templates/user/_index.md +16 -0
- package/agent-assets/templates/user/expertise.md +7 -0
- package/agent-assets/templates/user/goals.md +7 -0
- package/agent-assets/templates/user/people.md +7 -0
- package/agent-assets/templates/user/personal.md +7 -0
- package/agent-assets/templates/user/profile.md +28 -0
- package/agent-assets/templates/user/work.md +7 -0
- package/bin/aitne.mjs +1096 -0
- package/package.json +78 -0
- package/personal-agent.mjs +39 -0
- package/scripts/browser.mjs +99 -0
- package/scripts/check-redaction-coverage.mjs +109 -0
- package/scripts/commands/audit.mjs +309 -0
- package/scripts/commands/doctor.mjs +437 -0
- package/scripts/commands/open.mjs +40 -0
- package/scripts/commands/setup.mjs +21 -0
- package/scripts/commands/uninstall.mjs +114 -0
- package/scripts/commands/update.mjs +96 -0
- package/scripts/commands/version.mjs +62 -0
- package/scripts/commands.md +0 -0
- package/scripts/lib/sqlite-loader.mjs +49 -0
- package/scripts/message-discipline-digest.mjs +535 -0
- package/scripts/poc/google-connector-inheritance/REPORT.md +197 -0
- package/scripts/poc/google-connector-inheritance/claude-sdk-probe.mjs +79 -0
- package/scripts/remint-roadmap-ids.mjs +257 -0
- package/scripts/rm-paths.mjs +22 -0
- package/scripts/run-node.mjs +223 -0
- package/scripts/smoke-obsidian-api.mjs +166 -0
- package/scripts/start.mjs +160 -0
package/bin/aitne.mjs
ADDED
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { ensureBuild } from "../scripts/run-node.mjs";
|
|
10
|
+
import { fetchHttpOk, openBrowser } from "../scripts/browser.mjs";
|
|
11
|
+
|
|
12
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* aitne — CLI for managing the Aitne local-first personal agent.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle:
|
|
18
|
+
* aitne start [--no-open] Build & launch daemon + dashboard
|
|
19
|
+
* aitne stop Graceful shutdown
|
|
20
|
+
* aitne restart [--clean-context] Stop then start
|
|
21
|
+
* aitne status PIDs, uptime, integrations, today spend
|
|
22
|
+
* aitne logs [-f] [-n N] [-d] View daemon (or dashboard) logs
|
|
23
|
+
* aitne dev Foreground mode (development)
|
|
24
|
+
* aitne build Explicit build
|
|
25
|
+
*
|
|
26
|
+
* Operations:
|
|
27
|
+
* aitne setup Open dashboard /setup wizard
|
|
28
|
+
* aitne open Open dashboard root
|
|
29
|
+
* aitne doctor Diagnose install issues
|
|
30
|
+
* aitne audit [--since 24h] [--type X] Show agent action log
|
|
31
|
+
* aitne version Print version + environment
|
|
32
|
+
* aitne update Print npm upgrade command
|
|
33
|
+
* aitne uninstall Stop, then offer to wipe data dir
|
|
34
|
+
* aitne help [cmd] Show help (or per-command help)
|
|
35
|
+
*
|
|
36
|
+
* APP_NAME is hardcoded inline here, not imported from packages/shared, because
|
|
37
|
+
* this bin runs *before* `pnpm build` completes (the whole point of `aitne
|
|
38
|
+
* start` is to trigger that build), so importing from `shared/dist/` would fail
|
|
39
|
+
* on a fresh checkout. Keep this in sync with packages/shared/src/branding.ts.
|
|
40
|
+
*/
|
|
41
|
+
const APP_NAME = "Aitne";
|
|
42
|
+
// MUST stay in sync with packages/shared/src/branding.ts:APP_NAME
|
|
43
|
+
|
|
44
|
+
// ── Resolve project root (works from bin symlink, direct invocation, or
|
|
45
|
+
// node_modules/aitne/bin/ once published — `..` resolves to package root in
|
|
46
|
+
// every case). ──
|
|
47
|
+
const BIN_FILE = fileURLToPath(import.meta.url);
|
|
48
|
+
const __dirname = path.dirname(BIN_FILE);
|
|
49
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
50
|
+
const requireFromBin = createRequire(import.meta.url);
|
|
51
|
+
|
|
52
|
+
const DATA_DIR = process.env.PA_DATA_DIR || path.join(os.homedir(), ".personal-agent");
|
|
53
|
+
const PIDS_DIR = path.join(DATA_DIR, "run");
|
|
54
|
+
const DAEMON_PID_FILE = path.join(PIDS_DIR, "daemon.pid");
|
|
55
|
+
const DASHBOARD_PID_FILE = path.join(PIDS_DIR, "dashboard.pid");
|
|
56
|
+
const DAEMON_LOG_FILE = path.join(DATA_DIR, "logs", "daemon.log");
|
|
57
|
+
const DASHBOARD_LOG_FILE = path.join(DATA_DIR, "logs", "dashboard.log");
|
|
58
|
+
const DAEMON_PORT = parseInt(process.env.PA_API_PORT || "8321", 10);
|
|
59
|
+
const DASHBOARD_PORT = parseInt(process.env.PA_DASHBOARD_PORT || "3000", 10);
|
|
60
|
+
|
|
61
|
+
const VERSION = JSON.parse(
|
|
62
|
+
fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf8"),
|
|
63
|
+
).version || "0.1.0";
|
|
64
|
+
|
|
65
|
+
// ── PID helpers ──
|
|
66
|
+
|
|
67
|
+
const parsedLogMaxBytes = parseInt(process.env.PA_LOG_MAX_BYTES || "", 10);
|
|
68
|
+
const LOG_MAX_BYTES =
|
|
69
|
+
Number.isFinite(parsedLogMaxBytes) && parsedLogMaxBytes > 0
|
|
70
|
+
? parsedLogMaxBytes
|
|
71
|
+
: 10 * 1024 * 1024;
|
|
72
|
+
|
|
73
|
+
function ensureDirs() {
|
|
74
|
+
fs.mkdirSync(PIDS_DIR, { recursive: true });
|
|
75
|
+
fs.mkdirSync(path.dirname(DAEMON_LOG_FILE), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Rotate log if over threshold. Keeps one .1 backup. */
|
|
79
|
+
function rotateLogIfNeeded(logFile, maxBytes = LOG_MAX_BYTES) {
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.statSync(logFile);
|
|
82
|
+
if (stat.size > maxBytes) rotateLogFile(logFile);
|
|
83
|
+
} catch { /* file doesn't exist yet — fine */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function rotateLogFile(logFile) {
|
|
87
|
+
const rotated = logFile + ".1";
|
|
88
|
+
try { fs.unlinkSync(rotated); } catch { /* ignore */ }
|
|
89
|
+
try { fs.renameSync(logFile, rotated); } catch { /* ignore */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readPid(pidFile) {
|
|
93
|
+
try {
|
|
94
|
+
const content = fs.readFileSync(pidFile, "utf8").trim();
|
|
95
|
+
const pid = parseInt(content, 10);
|
|
96
|
+
return Number.isFinite(pid) ? pid : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function writePid(pidFile, pid) {
|
|
103
|
+
fs.writeFileSync(pidFile, String(pid) + "\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function removePid(pidFile) {
|
|
107
|
+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isAlive(pid) {
|
|
111
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the dashboard package directory.
|
|
116
|
+
*
|
|
117
|
+
* Workspace dev: pnpm symlinks node_modules/@aitne/dashboard → packages/dashboard
|
|
118
|
+
* Published install: @aitne/dashboard is installed as a sibling node_modules entry
|
|
119
|
+
*
|
|
120
|
+
* Falls back to PROJECT_ROOT/packages/dashboard for fresh checkouts before
|
|
121
|
+
* `pnpm install` has created the symlink.
|
|
122
|
+
*/
|
|
123
|
+
function resolveDashboardDir() {
|
|
124
|
+
try {
|
|
125
|
+
return path.dirname(requireFromBin.resolve("@aitne/dashboard/package.json"));
|
|
126
|
+
} catch {
|
|
127
|
+
return path.join(PROJECT_ROOT, "packages/dashboard");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the dashboard `next` binary for the current OS.
|
|
133
|
+
*
|
|
134
|
+
* Prefer Node's package resolution for Next's real CLI entrypoint. This works
|
|
135
|
+
* in the pnpm workspace, in npm global installs, and in local npm installs
|
|
136
|
+
* where dependencies are hoisted to the parent app's node_modules instead of
|
|
137
|
+
* living under node_modules/aitne/node_modules.
|
|
138
|
+
*
|
|
139
|
+
* The fallback .bin lookup is kept for unusual layouts and older installs.
|
|
140
|
+
* Batch shims need `cmd.exe` on Windows, so the resolved Node entrypoint is
|
|
141
|
+
* preferred there too.
|
|
142
|
+
*/
|
|
143
|
+
function resolveNextBin(dashboardDir) {
|
|
144
|
+
try {
|
|
145
|
+
requireFromBin.resolve("next/dist/bin/next");
|
|
146
|
+
return process.execPath;
|
|
147
|
+
} catch {
|
|
148
|
+
// Fall through to legacy .bin probing.
|
|
149
|
+
}
|
|
150
|
+
const binDir = path.join(dashboardDir, "node_modules", ".bin");
|
|
151
|
+
if (IS_WINDOWS) {
|
|
152
|
+
const direct = path.join(dashboardDir, "node_modules", "next", "dist", "bin", "next");
|
|
153
|
+
if (fs.existsSync(direct)) return process.execPath;
|
|
154
|
+
const cmd = path.join(binDir, "next.cmd");
|
|
155
|
+
if (fs.existsSync(cmd)) return cmd;
|
|
156
|
+
}
|
|
157
|
+
return path.join(binDir, "next");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the spawn args for the dashboard. When resolveNextBin returns the
|
|
162
|
+
* current Node executable, inject Next's CLI entrypoint before the user's args.
|
|
163
|
+
* Mirrors the candidate-dirs fallback lookup in resolveNextBin.
|
|
164
|
+
*/
|
|
165
|
+
function nextSpawnArgs(dashboardDir, nextBin, userArgs) {
|
|
166
|
+
if (nextBin === process.execPath) {
|
|
167
|
+
try {
|
|
168
|
+
return [requireFromBin.resolve("next/dist/bin/next"), ...userArgs];
|
|
169
|
+
} catch {
|
|
170
|
+
// Fall through to direct-path probing.
|
|
171
|
+
}
|
|
172
|
+
for (const dir of [dashboardDir, PROJECT_ROOT]) {
|
|
173
|
+
const direct = path.join(dir, "node_modules", "next", "dist", "bin", "next");
|
|
174
|
+
if (fs.existsSync(direct)) return [direct, ...userArgs];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return userArgs;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getRunningPid(pidFile) {
|
|
181
|
+
const pid = readPid(pidFile);
|
|
182
|
+
if (pid == null) return null;
|
|
183
|
+
if (!isAlive(pid)) { removePid(pidFile); return null; }
|
|
184
|
+
return pid;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Kill `pid` (and its descendants where supported) and wait for it to exit.
|
|
189
|
+
*
|
|
190
|
+
* - POSIX: signal the process *group* via `process.kill(-pid, ...)` so any
|
|
191
|
+
* child processes the daemon spawned (Claude/Codex/Gemini CLIs, the
|
|
192
|
+
* bundled Next dashboard) also receive the signal. Falls back to a
|
|
193
|
+
* single-process kill if the group call fails.
|
|
194
|
+
* - Windows: no process groups; use `taskkill /T /F /PID <pid>` to walk the
|
|
195
|
+
* parent-pid chain and terminate descendants. `/F` is required because
|
|
196
|
+
* Windows console apps don't honor a graceful close — Node simulates
|
|
197
|
+
* SIGTERM as TerminateProcess in this scenario, so a graceful first pass
|
|
198
|
+
* would no-op.
|
|
199
|
+
*/
|
|
200
|
+
function killTree(pid, signal) {
|
|
201
|
+
if (IS_WINDOWS) {
|
|
202
|
+
try {
|
|
203
|
+
execFileSync("taskkill", ["/T", "/F", "/PID", String(pid)], {
|
|
204
|
+
stdio: "pipe",
|
|
205
|
+
windowsHide: true,
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
// Fall through to per-process kill below.
|
|
210
|
+
}
|
|
211
|
+
try { process.kill(pid, signal); return true; } catch { return false; }
|
|
212
|
+
}
|
|
213
|
+
try { process.kill(-pid, signal); return true; } catch {
|
|
214
|
+
try { process.kill(pid, signal); return true; } catch { return false; }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function killAndWait(pid, pidFile, label, timeoutMs = 10_000) {
|
|
219
|
+
killTree(pid, "SIGTERM");
|
|
220
|
+
const deadline = Date.now() + timeoutMs;
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
if (!isAlive(pid)) { removePid(pidFile); return true; }
|
|
223
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
224
|
+
}
|
|
225
|
+
// Force kill if still alive. On Windows the first taskkill /F already
|
|
226
|
+
// hard-kills, so this branch is mostly a POSIX escalation.
|
|
227
|
+
killTree(pid, "SIGKILL");
|
|
228
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
229
|
+
if (!isAlive(pid)) { removePid(pidFile); return true; }
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Print the last N lines of a log file to stderr, framed so it's visible.
|
|
235
|
+
* Used on startup failure so the user gets immediate signal instead of
|
|
236
|
+
* having to chase `pa logs`. Falls back silently if the file is missing.
|
|
237
|
+
*/
|
|
238
|
+
function printLogTail(logFile, n) {
|
|
239
|
+
try {
|
|
240
|
+
const content = fs.readFileSync(logFile, "utf8");
|
|
241
|
+
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
242
|
+
const tail = lines.slice(-n);
|
|
243
|
+
if (tail.length === 0) return;
|
|
244
|
+
console.log("");
|
|
245
|
+
console.log(` --- last ${tail.length} log line(s) ---`);
|
|
246
|
+
for (const line of tail) console.log(` ${line}`);
|
|
247
|
+
console.log(` --- end of log ---`);
|
|
248
|
+
} catch {
|
|
249
|
+
/* log not present — caller already told the user where to look */
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function encodeLogRunnerSpec(spec) {
|
|
254
|
+
return Buffer.from(JSON.stringify(spec), "utf8").toString("base64url");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function decodeLogRunnerSpec(encoded) {
|
|
258
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function spawnLoggedService({ command, args, cwd, logFile, env, shell = false }) {
|
|
262
|
+
return spawn(
|
|
263
|
+
process.execPath,
|
|
264
|
+
[BIN_FILE, "_log-runner", encodeLogRunnerSpec({ command, args, cwd, logFile, shell })],
|
|
265
|
+
{
|
|
266
|
+
cwd: PROJECT_ROOT,
|
|
267
|
+
detached: true,
|
|
268
|
+
stdio: "ignore",
|
|
269
|
+
env,
|
|
270
|
+
windowsHide: true,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function cmdLogRunner(args) {
|
|
276
|
+
const spec = decodeLogRunnerSpec(args[0] || "");
|
|
277
|
+
fs.mkdirSync(path.dirname(spec.logFile), { recursive: true });
|
|
278
|
+
rotateLogIfNeeded(spec.logFile);
|
|
279
|
+
|
|
280
|
+
let fd = fs.openSync(spec.logFile, "a");
|
|
281
|
+
let size = 0;
|
|
282
|
+
try {
|
|
283
|
+
size = fs.fstatSync(fd).size;
|
|
284
|
+
} catch {
|
|
285
|
+
size = 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const reopen = () => {
|
|
289
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
290
|
+
rotateLogFile(spec.logFile);
|
|
291
|
+
fd = fs.openSync(spec.logFile, "a");
|
|
292
|
+
size = 0;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const writeChunk = (chunk) => {
|
|
296
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
297
|
+
let offset = 0;
|
|
298
|
+
while (offset < buf.length) {
|
|
299
|
+
if (size >= LOG_MAX_BYTES) reopen();
|
|
300
|
+
const room = Math.max(1, LOG_MAX_BYTES - size);
|
|
301
|
+
const length = Math.min(room, buf.length - offset);
|
|
302
|
+
fs.writeSync(fd, buf, offset, length);
|
|
303
|
+
offset += length;
|
|
304
|
+
size += length;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const child = spawn(spec.command, spec.args, {
|
|
309
|
+
cwd: spec.cwd,
|
|
310
|
+
env: process.env,
|
|
311
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
312
|
+
windowsHide: true,
|
|
313
|
+
shell: spec.shell === true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
child.stdout.on("data", writeChunk);
|
|
317
|
+
child.stderr.on("data", writeChunk);
|
|
318
|
+
child.on("error", (err) => {
|
|
319
|
+
writeChunk(`[log-runner] failed to start child: ${err.message}\n`);
|
|
320
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const stopChild = (signal) => {
|
|
325
|
+
try { child.kill(signal); } catch { /* ignore */ }
|
|
326
|
+
};
|
|
327
|
+
process.on("SIGTERM", () => stopChild("SIGTERM"));
|
|
328
|
+
process.on("SIGINT", () => stopChild("SIGINT"));
|
|
329
|
+
|
|
330
|
+
child.on("exit", (code, signal) => {
|
|
331
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
332
|
+
if (signal) process.exit(128);
|
|
333
|
+
process.exit(code ?? 0);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function fetchHealth() {
|
|
338
|
+
try {
|
|
339
|
+
const controller = new AbortController();
|
|
340
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
341
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT}/api/health`, {
|
|
342
|
+
signal: controller.signal,
|
|
343
|
+
});
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
if (res.ok) return await res.json();
|
|
346
|
+
} catch { /* not responding */ }
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function formatUptime(totalSeconds) {
|
|
351
|
+
const s = Math.floor(totalSeconds);
|
|
352
|
+
const m = Math.floor(s / 60);
|
|
353
|
+
const h = Math.floor(m / 60);
|
|
354
|
+
const d = Math.floor(h / 24);
|
|
355
|
+
if (d > 0) return `${d}d ${h % 24}h ${m % 60}m`;
|
|
356
|
+
if (h > 0) return `${h}h ${m % 60}m`;
|
|
357
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
358
|
+
return `${s}s`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Commands ──
|
|
362
|
+
|
|
363
|
+
async function cmdStart(args = []) {
|
|
364
|
+
const noOpen = args.includes("--no-open");
|
|
365
|
+
const daemonPid = getRunningPid(DAEMON_PID_FILE);
|
|
366
|
+
const dashPid = getRunningPid(DASHBOARD_PID_FILE);
|
|
367
|
+
if (daemonPid && dashPid) {
|
|
368
|
+
console.log(`Already running (daemon: ${daemonPid}, dashboard: ${dashPid}).`);
|
|
369
|
+
if (!noOpen) {
|
|
370
|
+
const url = `http://localhost:${DASHBOARD_PORT}`;
|
|
371
|
+
if (await openBrowser(url)) console.log(` Opened ${url} in browser.`);
|
|
372
|
+
}
|
|
373
|
+
// `pa start` on an already-running instance is idempotent, not an error.
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
if (daemonPid || dashPid) {
|
|
377
|
+
// Partial state — stop first
|
|
378
|
+
console.log("Partial state detected, cleaning up...");
|
|
379
|
+
await cmdStop();
|
|
380
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Build if needed (quiet — only show output on failure)
|
|
384
|
+
const { shouldBuild } = await import("../scripts/run-node.mjs");
|
|
385
|
+
if (shouldBuild(PROJECT_ROOT)) {
|
|
386
|
+
process.stdout.write("Building...");
|
|
387
|
+
const buildCode = await ensureBuild(PROJECT_ROOT, { quiet: true });
|
|
388
|
+
if (buildCode !== 0) {
|
|
389
|
+
console.log(" failed.");
|
|
390
|
+
process.exit(buildCode);
|
|
391
|
+
}
|
|
392
|
+
console.log(" done.");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
ensureDirs();
|
|
396
|
+
rotateLogIfNeeded(DAEMON_LOG_FILE);
|
|
397
|
+
rotateLogIfNeeded(DASHBOARD_LOG_FILE);
|
|
398
|
+
|
|
399
|
+
// Start daemon under a tiny log runner. The runner owns the file handle and
|
|
400
|
+
// rotates while the service is running; direct child fd redirection can only
|
|
401
|
+
// rotate on the next `aitne start`.
|
|
402
|
+
const daemon = spawnLoggedService({
|
|
403
|
+
command: process.execPath,
|
|
404
|
+
args: ["personal-agent.mjs"],
|
|
405
|
+
cwd: PROJECT_ROOT,
|
|
406
|
+
logFile: DAEMON_LOG_FILE,
|
|
407
|
+
env: { ...process.env, PA_DAEMONIZED: "1" },
|
|
408
|
+
});
|
|
409
|
+
writePid(DAEMON_PID_FILE, daemon.pid);
|
|
410
|
+
daemon.unref();
|
|
411
|
+
|
|
412
|
+
// Start dashboard (next start — production mode).
|
|
413
|
+
// Resolve via package.json so we work in both workspace dev (pnpm symlinks
|
|
414
|
+
// node_modules/@aitne/dashboard → packages/dashboard) and global installs
|
|
415
|
+
// (where @aitne/dashboard is a sibling node_modules entry).
|
|
416
|
+
const dashboardDir = resolveDashboardDir();
|
|
417
|
+
// Prefer Next's direct Node entrypoint on Windows; `.cmd` shims require
|
|
418
|
+
// cmd.exe and are kept only as a last fallback.
|
|
419
|
+
const nextBin = resolveNextBin(dashboardDir);
|
|
420
|
+
const dashArgs = nextSpawnArgs(dashboardDir, nextBin, [
|
|
421
|
+
"start", "--port", String(DASHBOARD_PORT),
|
|
422
|
+
]);
|
|
423
|
+
const dashboard = spawnLoggedService({
|
|
424
|
+
command: nextBin,
|
|
425
|
+
args: dashArgs,
|
|
426
|
+
cwd: dashboardDir,
|
|
427
|
+
logFile: DASHBOARD_LOG_FILE,
|
|
428
|
+
env: { ...process.env, PA_DAEMONIZED: "1" },
|
|
429
|
+
shell: IS_WINDOWS && nextBin.toLowerCase().endsWith(".cmd"),
|
|
430
|
+
});
|
|
431
|
+
writePid(DASHBOARD_PID_FILE, dashboard.pid);
|
|
432
|
+
dashboard.unref();
|
|
433
|
+
|
|
434
|
+
// Verify startup — wait for daemon health AND dashboard HTTP in one phase,
|
|
435
|
+
// so the "ok." line is only printed once everything is actually ready.
|
|
436
|
+
process.stdout.write("Starting...");
|
|
437
|
+
const status = await verifyStartup(daemon.pid, dashboard.pid);
|
|
438
|
+
if (!status.daemon) {
|
|
439
|
+
console.log(" failed.");
|
|
440
|
+
const daemonAlive = isAlive(daemon.pid);
|
|
441
|
+
if (daemonAlive) {
|
|
442
|
+
console.log(` Daemon process ${daemon.pid} is alive but /api/health did not respond — likely hung during startup.`);
|
|
443
|
+
} else {
|
|
444
|
+
console.log(` Daemon process ${daemon.pid} exited before becoming ready.`);
|
|
445
|
+
}
|
|
446
|
+
console.log(` Check logs: ${DAEMON_LOG_FILE}`);
|
|
447
|
+
printLogTail(DAEMON_LOG_FILE, 30);
|
|
448
|
+
// Leave the hung daemon cleaned up so the next `pa start` is not
|
|
449
|
+
// short-circuited by the "already running" check — otherwise the user
|
|
450
|
+
// would have to `pa stop` first.
|
|
451
|
+
if (daemonAlive) {
|
|
452
|
+
await killAndWait(daemon.pid, DAEMON_PID_FILE, "daemon", 5_000);
|
|
453
|
+
} else {
|
|
454
|
+
removePid(DAEMON_PID_FILE);
|
|
455
|
+
}
|
|
456
|
+
if (isAlive(dashboard.pid)) {
|
|
457
|
+
await killAndWait(dashboard.pid, DASHBOARD_PID_FILE, "dashboard", 5_000);
|
|
458
|
+
} else {
|
|
459
|
+
removePid(DASHBOARD_PID_FILE);
|
|
460
|
+
}
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
console.log(" ok.");
|
|
464
|
+
|
|
465
|
+
let dashSuffix = "";
|
|
466
|
+
if (!status.dashboard) {
|
|
467
|
+
dashSuffix = " (not yet responding — open manually)";
|
|
468
|
+
} else if (!noOpen) {
|
|
469
|
+
const url = `http://localhost:${DASHBOARD_PORT}`;
|
|
470
|
+
if (await openBrowser(url)) dashSuffix = " (opened in browser)";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log(` Daemon: PID ${daemon.pid} → http://127.0.0.1:${DAEMON_PORT}`);
|
|
474
|
+
console.log(` Dashboard: PID ${dashboard.pid} → http://localhost:${DASHBOARD_PORT}${dashSuffix}`);
|
|
475
|
+
|
|
476
|
+
// Post-startup summary — best-effort. /api/health is the daemon's own
|
|
477
|
+
// synthesis of ready integrations + auth state, so we lean on it instead
|
|
478
|
+
// of re-querying multiple endpoints. Skip silently if anything is shaped
|
|
479
|
+
// unexpectedly — a successful daemon start is the signal here, not this
|
|
480
|
+
// line.
|
|
481
|
+
try {
|
|
482
|
+
const finalHealth = await fetchHealth();
|
|
483
|
+
if (finalHealth) {
|
|
484
|
+
const platforms = Array.isArray(finalHealth.connectedPlatforms)
|
|
485
|
+
? finalHealth.connectedPlatforms.length
|
|
486
|
+
: 0;
|
|
487
|
+
const backends = Array.isArray(finalHealth.backends)
|
|
488
|
+
? finalHealth.backends.filter((b) => b.authOk !== false).length
|
|
489
|
+
: 0;
|
|
490
|
+
const totalBackends = Array.isArray(finalHealth.backends) ? finalHealth.backends.length : 0;
|
|
491
|
+
const integrations = finalHealth.integrationModes
|
|
492
|
+
? Object.values(finalHealth.integrationModes).filter((m) => m && m !== "off").length
|
|
493
|
+
: 0;
|
|
494
|
+
console.log("");
|
|
495
|
+
console.log(` ${platforms} platform(s) · ${backends}/${totalBackends} backend(s) ready · ${integrations} integration(s) active`);
|
|
496
|
+
}
|
|
497
|
+
} catch { /* health query racy at boot — ignore */ }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Wait for both daemon (/api/health) and dashboard (HTTP root) to become
|
|
502
|
+
* reachable. Exits early if either process dies. Returns a structured result
|
|
503
|
+
* so the caller can differentiate "daemon crashed" (hard failure) from
|
|
504
|
+
* "dashboard is just slow" (soft degradation).
|
|
505
|
+
*/
|
|
506
|
+
async function verifyStartup(daemonPid, dashPid, timeoutMs = 30_000) {
|
|
507
|
+
const dashUrl = `http://127.0.0.1:${DASHBOARD_PORT}/`;
|
|
508
|
+
const deadline = Date.now() + timeoutMs;
|
|
509
|
+
let daemonHealthy = false;
|
|
510
|
+
let dashboardReady = false;
|
|
511
|
+
while (Date.now() < deadline) {
|
|
512
|
+
if (!isAlive(daemonPid)) return { daemon: false, dashboard: false };
|
|
513
|
+
if (!isAlive(dashPid)) return { daemon: daemonHealthy, dashboard: false };
|
|
514
|
+
if (!daemonHealthy) {
|
|
515
|
+
if (await fetchHealth()) daemonHealthy = true;
|
|
516
|
+
}
|
|
517
|
+
if (!dashboardReady) {
|
|
518
|
+
if (await fetchHttpOk(dashUrl, 1_500)) dashboardReady = true;
|
|
519
|
+
}
|
|
520
|
+
if (daemonHealthy && dashboardReady) return { daemon: true, dashboard: true };
|
|
521
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
522
|
+
}
|
|
523
|
+
return { daemon: daemonHealthy, dashboard: dashboardReady };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function cmdStop() {
|
|
527
|
+
const daemonPid = getRunningPid(DAEMON_PID_FILE);
|
|
528
|
+
const dashPid = getRunningPid(DASHBOARD_PID_FILE);
|
|
529
|
+
|
|
530
|
+
if (!daemonPid && !dashPid) {
|
|
531
|
+
console.log("Not running.");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Kill both in parallel
|
|
536
|
+
const kills = [];
|
|
537
|
+
if (daemonPid) kills.push(killAndWait(daemonPid, DAEMON_PID_FILE, "daemon"));
|
|
538
|
+
if (dashPid) kills.push(killAndWait(dashPid, DASHBOARD_PID_FILE, "dashboard"));
|
|
539
|
+
const results = await Promise.all(kills);
|
|
540
|
+
|
|
541
|
+
const labels = [];
|
|
542
|
+
if (daemonPid) labels.push(results.shift() ? "daemon stopped" : `daemon (PID ${daemonPid}) did not exit`);
|
|
543
|
+
if (dashPid) labels.push(results.shift() ? "dashboard stopped" : `dashboard (PID ${dashPid}) did not exit`);
|
|
544
|
+
|
|
545
|
+
for (const l of labels) console.log(` ${l}`);
|
|
546
|
+
console.log("Stopped.");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function cmdRestart(args = []) {
|
|
550
|
+
const cleanContext = args.includes("--clean-context");
|
|
551
|
+
await cmdStop();
|
|
552
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
553
|
+
if (cleanContext) {
|
|
554
|
+
await cleanContextDirectory();
|
|
555
|
+
}
|
|
556
|
+
await cmdStart(args.filter((a) => a !== "--clean-context"));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// B-007 §7 — wipe context/ and md_file_snapshots rows after stopping the
|
|
560
|
+
// daemon, with a tarball safety backup. Mirrors
|
|
561
|
+
// packages/daemon/src/core/reinstall.ts but runs from the CLI so the
|
|
562
|
+
// daemon does not have to serve API calls through its own deletion.
|
|
563
|
+
async function cleanContextDirectory() {
|
|
564
|
+
const contextDir = path.join(DATA_DIR, "context");
|
|
565
|
+
const backupDir = path.join(DATA_DIR, "backup");
|
|
566
|
+
const dbPath = path.join(DATA_DIR, "data", "personal_agent.db");
|
|
567
|
+
|
|
568
|
+
if (!fs.existsSync(contextDir) && !fs.existsSync(dbPath)) {
|
|
569
|
+
console.log("Nothing to wipe — no context/ or personal_agent.db present.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const plan = await enumerateContextPlan(contextDir, dbPath);
|
|
574
|
+
const sizeMb = (plan.totalBytes / (1024 * 1024)).toFixed(2);
|
|
575
|
+
console.log("");
|
|
576
|
+
console.log("About to WIPE the following:");
|
|
577
|
+
console.log(` context/ (${plan.fileCount} files, ${sizeMb} MB)`);
|
|
578
|
+
console.log(` md_file_snapshots (${plan.snapshotRowCount} rows)`);
|
|
579
|
+
console.log("");
|
|
580
|
+
console.log("Other SQLite tables, the OS keychain, and cache/ stay intact.");
|
|
581
|
+
console.log("Type CLEAN to proceed, anything else to abort:");
|
|
582
|
+
const confirm = await readLineFromStdin();
|
|
583
|
+
if (confirm.trim() !== "CLEAN") {
|
|
584
|
+
console.log("Aborted.");
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (plan.fileCount > 0) {
|
|
589
|
+
const backupPath = path.join(
|
|
590
|
+
backupDir,
|
|
591
|
+
`context-pre-reinstall-${new Date().toISOString().replace(/[:.]/g, "-")}.tar.gz`,
|
|
592
|
+
);
|
|
593
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
594
|
+
const parent = path.dirname(contextDir);
|
|
595
|
+
const leaf = path.basename(contextDir);
|
|
596
|
+
// `tar` ships with macOS, every modern Linux, and Windows 10 1803+
|
|
597
|
+
// (bsdtar in System32). The flags below (`-czf -C`) work on all three.
|
|
598
|
+
// If `tar` is missing (older Windows), fall back to a Node tarball
|
|
599
|
+
// implementation lazy-loaded only on that path.
|
|
600
|
+
const tarOk = await runBackupTar(parent, leaf, backupPath);
|
|
601
|
+
if (!tarOk) {
|
|
602
|
+
console.error("Backup failed — aborting.");
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
console.log(` backup → ${backupPath}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (fs.existsSync(contextDir)) {
|
|
609
|
+
fs.rmSync(contextDir, { recursive: true, force: true });
|
|
610
|
+
console.log(` wiped → ${contextDir}`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// B-007 §7.1 — ancillary caches: prompts/ (regenerable), agent-sessions/
|
|
614
|
+
// (bound to the old layout). Missing dirs are skipped silently.
|
|
615
|
+
for (const sub of ["prompts", "agent-sessions"]) {
|
|
616
|
+
const full = path.join(DATA_DIR, sub);
|
|
617
|
+
if (fs.existsSync(full)) {
|
|
618
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
619
|
+
console.log(` wiped → ${full}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (plan.snapshotRowCount > 0 && fs.existsSync(dbPath)) {
|
|
624
|
+
// The sqlite3 CLI isn't on Windows by default and isn't required on
|
|
625
|
+
// POSIX either. Use the workspace's better-sqlite3 (already a daemon
|
|
626
|
+
// dep and prebuilt for win32-x64 / linux-{x64,arm64} / darwin) so the
|
|
627
|
+
// CLI cleanup path is identical across platforms.
|
|
628
|
+
try {
|
|
629
|
+
const { loadBetterSqlite3 } = await import("../scripts/lib/sqlite-loader.mjs");
|
|
630
|
+
const Database = await loadBetterSqlite3(PROJECT_ROOT);
|
|
631
|
+
const db = new Database(dbPath);
|
|
632
|
+
try {
|
|
633
|
+
db.exec("DELETE FROM md_file_snapshots;");
|
|
634
|
+
} finally {
|
|
635
|
+
db.close();
|
|
636
|
+
}
|
|
637
|
+
console.log(` cleared → md_file_snapshots (${plan.snapshotRowCount} rows)`);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.error(`Failed to clear md_file_snapshots: ${err?.message ?? err}`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
console.log("");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Run `tar -czf <out> -C <cwd> <leaf>`. Returns true on success. Spawns
|
|
648
|
+
* the system `tar` (bsdtar on macOS / Windows 10 1803+, GNU tar on Linux);
|
|
649
|
+
* if the binary itself is missing (ENOENT) or the spawn fails, returns
|
|
650
|
+
* false so the caller can decide what to do. We don't ship a Node-side
|
|
651
|
+
* tarball implementation: the failure case (no tar binary) is a stale
|
|
652
|
+
* Windows 7/8 install we don't claim to support.
|
|
653
|
+
*/
|
|
654
|
+
function runBackupTar(cwd, leaf, outPath) {
|
|
655
|
+
return new Promise((resolve) => {
|
|
656
|
+
let child;
|
|
657
|
+
try {
|
|
658
|
+
child = spawn("tar", ["-czf", outPath, "-C", cwd, leaf], {
|
|
659
|
+
stdio: "inherit",
|
|
660
|
+
windowsHide: true,
|
|
661
|
+
});
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`tar spawn failed: ${err?.message ?? err}`);
|
|
664
|
+
resolve(false);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
child.on("error", (err) => {
|
|
668
|
+
// ENOENT — `tar` not on PATH (e.g. very old Windows).
|
|
669
|
+
console.error(`tar not available: ${err?.message ?? err}`);
|
|
670
|
+
resolve(false);
|
|
671
|
+
});
|
|
672
|
+
child.on("close", (code) => resolve(code === 0));
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function enumerateContextPlan(contextDir, dbPath) {
|
|
677
|
+
let fileCount = 0;
|
|
678
|
+
let totalBytes = 0;
|
|
679
|
+
if (fs.existsSync(contextDir)) {
|
|
680
|
+
const walk = (dir) => {
|
|
681
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
682
|
+
const full = path.join(dir, entry.name);
|
|
683
|
+
if (entry.isDirectory()) walk(full);
|
|
684
|
+
else if (entry.isFile()) {
|
|
685
|
+
fileCount++;
|
|
686
|
+
totalBytes += fs.statSync(full).size;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
walk(contextDir);
|
|
691
|
+
}
|
|
692
|
+
let snapshotRowCount = 0;
|
|
693
|
+
if (fs.existsSync(dbPath)) {
|
|
694
|
+
// Use better-sqlite3 (workspace dep, prebuilds for all three OSes)
|
|
695
|
+
// instead of the sqlite3 CLI. The CLI isn't on Windows by default and
|
|
696
|
+
// its absence on POSIX would silently undercount snapshots in the
|
|
697
|
+
// confirm prompt — better to fail loud.
|
|
698
|
+
try {
|
|
699
|
+
const { loadBetterSqlite3 } = await import("../scripts/lib/sqlite-loader.mjs");
|
|
700
|
+
const Database = await loadBetterSqlite3(PROJECT_ROOT);
|
|
701
|
+
const db = new Database(dbPath, { readonly: true });
|
|
702
|
+
try {
|
|
703
|
+
const row = db
|
|
704
|
+
.prepare("SELECT COUNT(*) AS n FROM md_file_snapshots")
|
|
705
|
+
.get();
|
|
706
|
+
snapshotRowCount = Number(row?.n ?? 0) || 0;
|
|
707
|
+
} finally {
|
|
708
|
+
db.close();
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
// Table missing on a half-migrated DB, etc. — leave at 0; the daemon
|
|
712
|
+
// will finish cleanup on next boot.
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { fileCount, totalBytes, snapshotRowCount };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function readLineFromStdin() {
|
|
719
|
+
return new Promise((resolve) => {
|
|
720
|
+
process.stdin.resume();
|
|
721
|
+
process.stdin.setEncoding("utf-8");
|
|
722
|
+
const onData = (chunk) => {
|
|
723
|
+
process.stdin.pause();
|
|
724
|
+
process.stdin.removeListener("data", onData);
|
|
725
|
+
resolve(chunk);
|
|
726
|
+
};
|
|
727
|
+
process.stdin.on("data", onData);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function cmdStatus() {
|
|
732
|
+
const daemonPid = getRunningPid(DAEMON_PID_FILE);
|
|
733
|
+
const dashPid = getRunningPid(DASHBOARD_PID_FILE);
|
|
734
|
+
|
|
735
|
+
if (!daemonPid && !dashPid) {
|
|
736
|
+
console.log("Not running.");
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const health = daemonPid ? await fetchHealth() : null;
|
|
741
|
+
|
|
742
|
+
console.log(`${APP_NAME} status:`);
|
|
743
|
+
console.log("");
|
|
744
|
+
|
|
745
|
+
// Daemon
|
|
746
|
+
if (daemonPid) {
|
|
747
|
+
const uptime = health?.uptime != null ? formatUptime(health.uptime) : "—";
|
|
748
|
+
console.log(` Daemon: running (PID ${daemonPid})`);
|
|
749
|
+
console.log(` Uptime: ${uptime}`);
|
|
750
|
+
console.log(` API: http://127.0.0.1:${DAEMON_PORT}`);
|
|
751
|
+
if (health?.connectedPlatforms?.length > 0) {
|
|
752
|
+
console.log(` Platforms: ${health.connectedPlatforms.join(", ")}`);
|
|
753
|
+
}
|
|
754
|
+
if (Array.isArray(health?.backends) && health.backends.length > 0) {
|
|
755
|
+
const summary = health.backends
|
|
756
|
+
.map((b) => `${b.id}${b.authOk === false ? "(auth!)" : ""}`)
|
|
757
|
+
.join(", ");
|
|
758
|
+
console.log(` Backends: ${summary}`);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
console.log(` Daemon: not running`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Dashboard
|
|
765
|
+
if (dashPid) {
|
|
766
|
+
console.log(` Dashboard: running (PID ${dashPid})`);
|
|
767
|
+
console.log(` URL: http://localhost:${DASHBOARD_PORT}`);
|
|
768
|
+
} else {
|
|
769
|
+
console.log(` Dashboard: not running`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Activity / cost summary — best-effort, read-only via SQLite. Skip silently
|
|
773
|
+
// if the DB doesn't exist yet or the schema is unexpected.
|
|
774
|
+
try {
|
|
775
|
+
const summary = await readActivitySummary();
|
|
776
|
+
if (summary) {
|
|
777
|
+
console.log("");
|
|
778
|
+
console.log(` Last action: ${summary.lastActionAt ?? "—"}${summary.lastActionType ? ` (${summary.lastActionType})` : ""}`);
|
|
779
|
+
console.log(` Today: ${summary.actionsToday} action(s) · $${summary.costTodayUsd.toFixed(3)} spent`);
|
|
780
|
+
if (summary.nextScheduled) {
|
|
781
|
+
console.log(` Next: ${summary.nextScheduled.at} ${summary.nextScheduled.label}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
/* DB missing or pre-init — silent. */
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Read a tiny activity summary directly from SQLite for `aitne status`.
|
|
791
|
+
*
|
|
792
|
+
* Safe to do while the daemon runs because the daemon enables WAL
|
|
793
|
+
* (`packages/daemon/src/db/client.ts`) — concurrent readers are fine.
|
|
794
|
+
* Returns `null` if the DB file doesn't exist yet (fresh install before
|
|
795
|
+
* the daemon's first boot) so the caller can skip the section gracefully.
|
|
796
|
+
*/
|
|
797
|
+
async function readActivitySummary() {
|
|
798
|
+
const dbPath = path.join(DATA_DIR, "data", "personal_agent.db");
|
|
799
|
+
if (!fs.existsSync(dbPath)) return null;
|
|
800
|
+
const { loadBetterSqlite3 } = await import("../scripts/lib/sqlite-loader.mjs");
|
|
801
|
+
const Database = await loadBetterSqlite3(PROJECT_ROOT);
|
|
802
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
803
|
+
try {
|
|
804
|
+
const lastRow = db
|
|
805
|
+
.prepare("SELECT action_type, started_at FROM agent_actions ORDER BY started_at DESC LIMIT 1")
|
|
806
|
+
.get();
|
|
807
|
+
const todayRow = db
|
|
808
|
+
.prepare(`SELECT COUNT(*) AS n, COALESCE(SUM(cost_usd), 0) AS cost
|
|
809
|
+
FROM agent_actions
|
|
810
|
+
WHERE date(started_at) = date('now', 'localtime')`)
|
|
811
|
+
.get();
|
|
812
|
+
// Schema: agent_schedule(scheduled_for, task_type, status, …) — see
|
|
813
|
+
// packages/daemon/src/db/schema.ts. Status enum: pending|running|completed|skipped|failed.
|
|
814
|
+
let nextScheduled = null;
|
|
815
|
+
try {
|
|
816
|
+
const nextRow = db
|
|
817
|
+
.prepare(`SELECT scheduled_for, task_type FROM agent_schedule
|
|
818
|
+
WHERE status = 'pending' AND scheduled_for >= datetime('now')
|
|
819
|
+
ORDER BY scheduled_for ASC LIMIT 1`)
|
|
820
|
+
.get();
|
|
821
|
+
if (nextRow) {
|
|
822
|
+
nextScheduled = { at: nextRow.scheduled_for, label: nextRow.task_type ?? "" };
|
|
823
|
+
}
|
|
824
|
+
} catch { /* table absent on a much-older DB — skip silently */ }
|
|
825
|
+
return {
|
|
826
|
+
lastActionAt: lastRow?.started_at ?? null,
|
|
827
|
+
lastActionType: lastRow?.action_type ?? null,
|
|
828
|
+
actionsToday: Number(todayRow?.n ?? 0),
|
|
829
|
+
costTodayUsd: Number(todayRow?.cost ?? 0),
|
|
830
|
+
nextScheduled,
|
|
831
|
+
};
|
|
832
|
+
} finally {
|
|
833
|
+
db.close();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function cmdLogs(args) {
|
|
838
|
+
const follow = args.includes("-f") || args.includes("--follow");
|
|
839
|
+
const isDash = args.includes("--dashboard") || args.includes("-d");
|
|
840
|
+
const logFile = isDash ? DASHBOARD_LOG_FILE : DAEMON_LOG_FILE;
|
|
841
|
+
const linesArg = (() => {
|
|
842
|
+
const nIdx = args.indexOf("-n");
|
|
843
|
+
if (nIdx !== -1 && args[nIdx + 1]) return parseInt(args[nIdx + 1], 10);
|
|
844
|
+
return 50;
|
|
845
|
+
})();
|
|
846
|
+
const lineCount = Number.isFinite(linesArg) && linesArg > 0 ? linesArg : 50;
|
|
847
|
+
|
|
848
|
+
if (!fs.existsSync(logFile)) {
|
|
849
|
+
console.log(`No log file found at ${logFile}`);
|
|
850
|
+
console.log("Has the service been started?");
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
await tailFile(logFile, { follow, lines: lineCount });
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Pure-Node `tail` / `tail -f` so the CLI works on Windows (no `tail` binary)
|
|
859
|
+
* as well as POSIX. Trade-offs:
|
|
860
|
+
* - `lines` is honored by reading the whole file and slicing the last N lines.
|
|
861
|
+
* Daemon logs rotate at 10 MB (see `rotateLogIfNeeded` above), so reading
|
|
862
|
+
* them entirely is bounded.
|
|
863
|
+
* - `follow` polls via `fs.watchFile` (1 s interval). `fs.watch` would be
|
|
864
|
+
* pushier but is unreliable across Windows network drives and various
|
|
865
|
+
* editor write patterns; polling is dull but trustworthy.
|
|
866
|
+
* - On rotate or truncate the read offset is reset so we don't print stale
|
|
867
|
+
* bytes.
|
|
868
|
+
*/
|
|
869
|
+
function tailFile(logFile, { follow, lines }) {
|
|
870
|
+
return new Promise((resolve) => {
|
|
871
|
+
let position = 0;
|
|
872
|
+
try {
|
|
873
|
+
const initial = fs.readFileSync(logFile, "utf8");
|
|
874
|
+
const tail = initial.split("\n");
|
|
875
|
+
// Trailing empty element when file ends with newline — drop so we
|
|
876
|
+
// don't print a blank line.
|
|
877
|
+
if (tail.length > 0 && tail[tail.length - 1] === "") tail.pop();
|
|
878
|
+
const slice = tail.slice(-lines);
|
|
879
|
+
if (slice.length > 0) process.stdout.write(slice.join("\n") + "\n");
|
|
880
|
+
position = Buffer.byteLength(initial, "utf8");
|
|
881
|
+
} catch (err) {
|
|
882
|
+
console.error(`Failed to read ${logFile}: ${err.message}`);
|
|
883
|
+
resolve(1);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (!follow) {
|
|
888
|
+
resolve(0);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
let watching = true;
|
|
893
|
+
const onChange = (curr, prev) => {
|
|
894
|
+
if (!watching) return;
|
|
895
|
+
// File was truncated or rotated — reset to the start so we don't
|
|
896
|
+
// emit garbage from a lost offset.
|
|
897
|
+
if (curr.size < position) position = 0;
|
|
898
|
+
if (curr.size === position) return;
|
|
899
|
+
try {
|
|
900
|
+
const fd = fs.openSync(logFile, "r");
|
|
901
|
+
const length = curr.size - position;
|
|
902
|
+
const buf = Buffer.alloc(length);
|
|
903
|
+
fs.readSync(fd, buf, 0, length, position);
|
|
904
|
+
fs.closeSync(fd);
|
|
905
|
+
position = curr.size;
|
|
906
|
+
process.stdout.write(buf.toString("utf8"));
|
|
907
|
+
} catch {
|
|
908
|
+
// Log gone (rotation race) — fs.watchFile will fire again when it
|
|
909
|
+
// reappears; resetting position handles the new file.
|
|
910
|
+
position = 0;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
fs.watchFile(logFile, { interval: 1000 }, onChange);
|
|
914
|
+
|
|
915
|
+
const stop = () => {
|
|
916
|
+
if (!watching) return;
|
|
917
|
+
watching = false;
|
|
918
|
+
fs.unwatchFile(logFile, onChange);
|
|
919
|
+
resolve(0);
|
|
920
|
+
process.exit(0);
|
|
921
|
+
};
|
|
922
|
+
process.on("SIGINT", stop);
|
|
923
|
+
process.on("SIGTERM", stop);
|
|
924
|
+
if (IS_WINDOWS) process.on("SIGBREAK", stop);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async function cmdDev(args = []) {
|
|
929
|
+
// Foreground mode: daemon + dashboard with full stdio
|
|
930
|
+
const child = spawn(
|
|
931
|
+
process.execPath,
|
|
932
|
+
[path.join(PROJECT_ROOT, "scripts/start.mjs"), ...args],
|
|
933
|
+
{
|
|
934
|
+
cwd: PROJECT_ROOT,
|
|
935
|
+
stdio: "inherit",
|
|
936
|
+
env: process.env,
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
940
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
941
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function cmdBuild() {
|
|
945
|
+
const { runBuild, writeBuildStamp } = await import("../scripts/run-node.mjs");
|
|
946
|
+
const code = await runBuild(PROJECT_ROOT);
|
|
947
|
+
if (code === 0) {
|
|
948
|
+
writeBuildStamp(PROJECT_ROOT);
|
|
949
|
+
console.log("Build complete.");
|
|
950
|
+
} else {
|
|
951
|
+
console.error("Build failed.");
|
|
952
|
+
process.exit(code);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* `aitne help` and `aitne help <command>`. Async so per-command help
|
|
958
|
+
* dispatch into a dynamically-imported module is awaited — without await,
|
|
959
|
+
* the runExternalCommand promise can race against process exit. In practice
|
|
960
|
+
* Node's I/O wait usually keeps it alive long enough, but relying on that
|
|
961
|
+
* is brittle.
|
|
962
|
+
*/
|
|
963
|
+
async function cmdHelp(args = []) {
|
|
964
|
+
// Per-command help: `aitne help <cmd>` or `aitne <cmd> --help`. Defers to
|
|
965
|
+
// each command module's run() with --help so the long-form usage lives
|
|
966
|
+
// next to the command's logic, not duplicated in this dispatcher.
|
|
967
|
+
const target = args[0];
|
|
968
|
+
if (target && PER_COMMAND_HELP.has(target)) {
|
|
969
|
+
await runExternalCommand(target, ["--help"]);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.log(`${APP_NAME} v${VERSION} — local-first personal AI agent
|
|
973
|
+
|
|
974
|
+
Usage: aitne <command> [options]
|
|
975
|
+
|
|
976
|
+
Lifecycle:
|
|
977
|
+
start [--no-open] Build (if stale) & launch daemon + dashboard
|
|
978
|
+
stop Graceful shutdown (SIGTERM → SIGKILL after 10s)
|
|
979
|
+
restart [--no-open] [--clean-context]
|
|
980
|
+
Restart; --clean-context wipes context/ &
|
|
981
|
+
md_file_snapshots after tarball backup (B-007)
|
|
982
|
+
status PIDs, uptime, integrations, today's spend
|
|
983
|
+
logs [-f] [-n N] [-d] Tail daemon log (-d for dashboard log)
|
|
984
|
+
dev [--no-open] Foreground mode (development, full stdio)
|
|
985
|
+
build Build TypeScript explicitly
|
|
986
|
+
|
|
987
|
+
Operations:
|
|
988
|
+
setup Open dashboard /setup wizard
|
|
989
|
+
open Open dashboard root in browser
|
|
990
|
+
doctor Diagnose install (Node, ports, keychain, CLIs, …)
|
|
991
|
+
audit [--since <dur>] [--type X] Show agent action log (filterable)
|
|
992
|
+
version Print version + Node + install path
|
|
993
|
+
update Print npm command to upgrade
|
|
994
|
+
uninstall Stop, then offer to wipe ${path.basename(DATA_DIR) || "data dir"}
|
|
995
|
+
|
|
996
|
+
Options:
|
|
997
|
+
--version, -v Print version
|
|
998
|
+
--help, -h Show this help (or per-command help)
|
|
999
|
+
|
|
1000
|
+
Environment:
|
|
1001
|
+
PA_DATA_DIR Data directory (default: ~/.personal-agent)
|
|
1002
|
+
PA_API_PORT Daemon port (default: 8321)
|
|
1003
|
+
PA_DASHBOARD_PORT Dashboard port (default: 3000)
|
|
1004
|
+
|
|
1005
|
+
Examples:
|
|
1006
|
+
aitne start Launch in background
|
|
1007
|
+
aitne status Check what's running
|
|
1008
|
+
aitne audit --since 7d Last week's agent actions
|
|
1009
|
+
aitne doctor Diagnose first-install issues
|
|
1010
|
+
aitne logs -f Follow daemon log
|
|
1011
|
+
|
|
1012
|
+
Run 'aitne help <command>' for detailed usage of a single command.`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Commands that own their own --help output (i.e. live in scripts/commands/).
|
|
1016
|
+
// Used by `aitne help <cmd>` to dispatch into the module rather than duplicating
|
|
1017
|
+
// usage strings in cmdHelp() above.
|
|
1018
|
+
const PER_COMMAND_HELP = new Set([
|
|
1019
|
+
"doctor", "audit", "setup", "open", "version", "update", "uninstall",
|
|
1020
|
+
]);
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Dispatch into a command module under scripts/commands/. The dispatcher hands
|
|
1024
|
+
* the module a precomputed context (paths, ports, brand) so individual
|
|
1025
|
+
* commands don't re-derive shared state.
|
|
1026
|
+
*/
|
|
1027
|
+
async function runExternalCommand(name, args) {
|
|
1028
|
+
const mod = await import(`../scripts/commands/${name}.mjs`);
|
|
1029
|
+
const ctx = {
|
|
1030
|
+
APP_NAME,
|
|
1031
|
+
VERSION,
|
|
1032
|
+
DATA_DIR,
|
|
1033
|
+
DAEMON_PORT,
|
|
1034
|
+
DASHBOARD_PORT,
|
|
1035
|
+
PROJECT_ROOT,
|
|
1036
|
+
DAEMON_PID_FILE,
|
|
1037
|
+
DASHBOARD_PID_FILE,
|
|
1038
|
+
DAEMON_LOG_FILE,
|
|
1039
|
+
DASHBOARD_LOG_FILE,
|
|
1040
|
+
IS_WINDOWS,
|
|
1041
|
+
helpers: {
|
|
1042
|
+
getRunningPid,
|
|
1043
|
+
formatUptime,
|
|
1044
|
+
fetchHealth,
|
|
1045
|
+
openBrowser,
|
|
1046
|
+
cmdStart,
|
|
1047
|
+
cmdStop,
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
return mod.run(args, ctx);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ── Main ──
|
|
1054
|
+
|
|
1055
|
+
const subcommand = process.argv[2];
|
|
1056
|
+
const subArgs = process.argv.slice(3);
|
|
1057
|
+
|
|
1058
|
+
switch (subcommand) {
|
|
1059
|
+
case "_log-runner": await cmdLogRunner(subArgs); break;
|
|
1060
|
+
case "start": await cmdStart(subArgs); break;
|
|
1061
|
+
case "stop": await cmdStop(); break;
|
|
1062
|
+
case "restart": await cmdRestart(subArgs); break;
|
|
1063
|
+
case "status": await cmdStatus(); break;
|
|
1064
|
+
case "logs": await cmdLogs(subArgs); break;
|
|
1065
|
+
case "dev": await cmdDev(subArgs); break;
|
|
1066
|
+
case "build": await cmdBuild(); break;
|
|
1067
|
+
|
|
1068
|
+
case "setup": await runExternalCommand("setup", subArgs); break;
|
|
1069
|
+
case "open": await runExternalCommand("open", subArgs); break;
|
|
1070
|
+
case "doctor": await runExternalCommand("doctor", subArgs); break;
|
|
1071
|
+
case "audit": await runExternalCommand("audit", subArgs); break;
|
|
1072
|
+
|
|
1073
|
+
// version / -v / --version all dispatch identically — passing subArgs
|
|
1074
|
+
// through so `aitne --version --json` yields the same output as
|
|
1075
|
+
// `aitne version --json`. (Earlier code dropped subArgs in the
|
|
1076
|
+
// short-circuit path, producing inconsistent behavior.)
|
|
1077
|
+
case "version":
|
|
1078
|
+
case "--version":
|
|
1079
|
+
case "-v":
|
|
1080
|
+
await runExternalCommand("version", subArgs); break;
|
|
1081
|
+
|
|
1082
|
+
case "update": await runExternalCommand("update", subArgs); break;
|
|
1083
|
+
case "uninstall": await runExternalCommand("uninstall", subArgs); break;
|
|
1084
|
+
|
|
1085
|
+
case "help":
|
|
1086
|
+
case "--help":
|
|
1087
|
+
case "-h":
|
|
1088
|
+
await cmdHelp(subArgs); break;
|
|
1089
|
+
default:
|
|
1090
|
+
if (subcommand) {
|
|
1091
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
1092
|
+
console.error(`Run 'aitne help' for the list of commands.`);
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
await cmdHelp();
|
|
1096
|
+
}
|