arkaos 3.71.0 → 3.72.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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Cross-OS autostart for the ArkaOS dashboard (v3.72.0, feature #2).
3
+ *
4
+ * Opt-in: `npx arkaos autostart enable|disable|status`. Installs a per-OS
5
+ * boot item that runs `scripts/start-dashboard.sh` (macOS/Linux) or
6
+ * `scripts/start-dashboard.ps1` (Windows) — the same launcher the
7
+ * `dashboard` command uses, which already starts the Python API + the
8
+ * dashboard UI (preferring the production build, falling back to dev).
9
+ *
10
+ * ESM, Node + Bun, no interactive prompts. The unit-generation (`unitFor`)
11
+ * is pure so it can be unit-tested without touching the OS.
12
+ */
13
+
14
+ import { execSync } from "node:child_process";
15
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ export const AUTOSTART_LABEL = "io.wizardingcode.arkaos.dashboard";
21
+ const SERVICE_NAME = "arkaos-dashboard.service";
22
+
23
+ function defaultRepoRoot() {
24
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
25
+ }
26
+
27
+ /** Pure: the boot unit for a given platform. Throws on unsupported OS. */
28
+ export function unitFor(os, { repoRoot, home }) {
29
+ if (os === "darwin") {
30
+ const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
31
+ const log = `${home}/.arkaos/logs/dashboard-autostart.log`;
32
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
33
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
34
+ <plist version="1.0">
35
+ <dict>
36
+ <key>Label</key>
37
+ <string>${AUTOSTART_LABEL}</string>
38
+ <key>ProgramArguments</key>
39
+ <array>
40
+ <string>/bin/bash</string>
41
+ <string>${startScript}</string>
42
+ </array>
43
+ <key>EnvironmentVariables</key>
44
+ <dict>
45
+ <key>ARKAOS_ROOT</key>
46
+ <string>${repoRoot}</string>
47
+ </dict>
48
+ <key>RunAtLoad</key>
49
+ <true/>
50
+ <key>StandardOutPath</key>
51
+ <string>${log}</string>
52
+ <key>StandardErrorPath</key>
53
+ <string>${log}</string>
54
+ </dict>
55
+ </plist>
56
+ `;
57
+ return {
58
+ kind: "launchd",
59
+ path: join(home, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`),
60
+ content,
61
+ };
62
+ }
63
+
64
+ if (os === "linux") {
65
+ const startScript = `${repoRoot}/scripts/start-dashboard.sh`;
66
+ const content = `[Unit]
67
+ Description=ArkaOS Dashboard (Python API + UI)
68
+ After=network.target
69
+
70
+ [Service]
71
+ Type=simple
72
+ Environment=ARKAOS_ROOT=${repoRoot}
73
+ ExecStart=/bin/bash ${startScript}
74
+ Restart=on-failure
75
+
76
+ [Install]
77
+ WantedBy=default.target
78
+ `;
79
+ return {
80
+ kind: "systemd",
81
+ path: join(home, ".config", "systemd", "user", SERVICE_NAME),
82
+ content,
83
+ };
84
+ }
85
+
86
+ if (os === "win32") {
87
+ const ps1 = join(repoRoot, "scripts", "start-dashboard.ps1");
88
+ const content = `@echo off
89
+ rem ArkaOS dashboard autostart (login).
90
+ powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${ps1}"
91
+ `;
92
+ return {
93
+ kind: "startup",
94
+ path: join(
95
+ home,
96
+ "AppData", "Roaming", "Microsoft", "Windows",
97
+ "Start Menu", "Programs", "Startup",
98
+ "arkaos-dashboard.cmd",
99
+ ),
100
+ content,
101
+ };
102
+ }
103
+
104
+ throw new Error(`unsupported platform for autostart: ${os}`);
105
+ }
106
+
107
+ function _silent(cmd) {
108
+ try {
109
+ execSync(cmd, { stdio: "pipe" });
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ export function enable({ repoRoot = defaultRepoRoot() } = {}) {
117
+ const unit = unitFor(process.platform, { repoRoot, home: homedir() });
118
+ mkdirSync(dirname(unit.path), { recursive: true });
119
+ mkdirSync(join(homedir(), ".arkaos", "logs"), { recursive: true });
120
+ writeFileSync(unit.path, unit.content, "utf8");
121
+ if (unit.kind === "launchd") {
122
+ _silent(`launchctl unload "${unit.path}"`);
123
+ if (!_silent(`launchctl load -w "${unit.path}"`)) {
124
+ return { ok: false, path: unit.path,
125
+ message: `Plist written but launchctl load failed — it will still run at next login.` };
126
+ }
127
+ } else if (unit.kind === "systemd") {
128
+ _silent("systemctl --user daemon-reload");
129
+ if (!_silent(`systemctl --user enable --now ${SERVICE_NAME}`)) {
130
+ return { ok: false, path: unit.path,
131
+ message: `Service written but systemctl enable failed — check 'systemctl --user' availability.` };
132
+ }
133
+ }
134
+ // startup: writing the .cmd is sufficient; it runs on next login.
135
+ return { ok: true, path: unit.path,
136
+ message: `Autostart enabled (${unit.kind}). The dashboard will start on login.` };
137
+ }
138
+
139
+ export function disable() {
140
+ const unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
141
+ if (unit.kind === "launchd") _silent(`launchctl unload "${unit.path}"`);
142
+ else if (unit.kind === "systemd") _silent(`systemctl --user disable --now ${SERVICE_NAME}`);
143
+ if (existsSync(unit.path)) rmSync(unit.path);
144
+ return { ok: true, path: unit.path, message: "Autostart disabled." };
145
+ }
146
+
147
+ export function status() {
148
+ let unit;
149
+ try {
150
+ unit = unitFor(process.platform, { repoRoot: defaultRepoRoot(), home: homedir() });
151
+ } catch (err) {
152
+ return { installed: false, supported: false, message: err.message };
153
+ }
154
+ return {
155
+ installed: existsSync(unit.path),
156
+ supported: true,
157
+ kind: unit.kind,
158
+ path: unit.path,
159
+ };
160
+ }
161
+
162
+ export async function autostart(args = []) {
163
+ const action = (args[0] || "status").toLowerCase();
164
+ if (action === "enable") {
165
+ const r = enable();
166
+ console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}\n ${r.path}`);
167
+ } else if (action === "disable") {
168
+ const r = disable();
169
+ console.log(` ✓ ${r.message}`);
170
+ } else if (action === "status") {
171
+ const s = status();
172
+ if (!s.supported) console.log(` Autostart not supported here: ${s.message}`);
173
+ else console.log(` Autostart: ${s.installed ? "ENABLED" : "disabled"} (${s.kind})\n ${s.path}`);
174
+ } else {
175
+ console.error(` Unknown autostart action: ${action}. Use enable | disable | status.`);
176
+ process.exitCode = 1;
177
+ }
178
+ }
package/installer/cli.js CHANGED
@@ -45,6 +45,7 @@ Usage:
45
45
  npx arkaos migrate Migrate from v1 to v2
46
46
  npx arkaos migrate-user-data Move user data (~/.claude/skills/arka/ → ~/.arkaos/)
47
47
  npx arkaos dashboard Start monitoring dashboard
48
+ npx arkaos autostart <enable|disable|status> Start dashboard on boot
48
49
  npx arkaos keys Manage API keys (OpenAI, fal.ai, etc.)
49
50
  npx arkaos doctor Run health checks
50
51
  npx arkaos uninstall Remove ArkaOS
@@ -101,6 +102,12 @@ async function main() {
101
102
  await update();
102
103
  break;
103
104
 
105
+ case "autostart": {
106
+ const { autostart } = await import("./autostart.js");
107
+ await autostart(positionals.slice(1));
108
+ break;
109
+ }
110
+
104
111
  case "uninstall":
105
112
  const { uninstall } = await import("./uninstall.js");
106
113
  await uninstall();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.71.0",
3
+ "version": "3.72.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.71.0"
3
+ version = "3.72.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -20,7 +20,7 @@ from typing import Optional
20
20
  ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
21
21
  sys.path.insert(0, str(ARKAOS_ROOT))
22
22
 
23
- from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
23
+ from fastapi import FastAPI, Query, Request, WebSocket, WebSocketDisconnect
24
24
  from fastapi.middleware.cors import CORSMiddleware
25
25
 
26
26
  app = FastAPI(title="ArkaOS Dashboard API", version="2.2.0")
@@ -2259,6 +2259,160 @@ def workflow_update_yaml(workflow_id: str, body: dict):
2259
2259
  return {"updated": True, "id": workflow_id, "file": str(target)}
2260
2260
 
2261
2261
 
2262
+ # ============================================================================
2263
+ # v3.72.0 — Cognition: surface Dreaming insights (read-only) to the dashboard.
2264
+ # Reads existing markdown insights from <vault>/Projects/ArkaOS/Dreams via
2265
+ # core.cognition.dreams_reader. No new engine — pure read.
2266
+ # ============================================================================
2267
+
2268
+
2269
+ def _dreams_dir():
2270
+ """Resolve the Dreams folder from the user's vault, or None."""
2271
+ from core.runtime.path_resolver import load_profile, ProfileMissingError
2272
+ try:
2273
+ profile = load_profile()
2274
+ except ProfileMissingError:
2275
+ return None
2276
+ except Exception: # noqa: BLE001 — never break the endpoint on profile errors
2277
+ return None
2278
+ vault = getattr(profile, "vault_path", None)
2279
+ return (Path(vault) / "Projects" / "ArkaOS" / "Dreams") if vault else None
2280
+
2281
+
2282
+ def _insight_to_dict(ins) -> dict:
2283
+ return {
2284
+ "date": ins.date,
2285
+ "title": ins.title,
2286
+ "confidence": ins.confidence,
2287
+ "sources": list(ins.sources),
2288
+ "tags": list(ins.tags),
2289
+ "body": ins.body,
2290
+ }
2291
+
2292
+
2293
+ @app.get("/api/cognition/insights")
2294
+ def cognition_insights(days: int = 7):
2295
+ """Recent Dreaming insights within the given window. Never 500s."""
2296
+ dreams = _dreams_dir()
2297
+ if dreams is None:
2298
+ return {"insights": [], "available": False}
2299
+ from core.cognition.dreams_reader import list_insights
2300
+ items = list_insights(dreams, since_days=max(1, int(days)))
2301
+ return {"insights": [_insight_to_dict(i) for i in items], "available": True}
2302
+
2303
+
2304
+ @app.get("/api/cognition/status")
2305
+ def cognition_status():
2306
+ """Insight counts + confidence breakdown + last activity date."""
2307
+ dreams = _dreams_dir()
2308
+ if dreams is None:
2309
+ return {
2310
+ "today": 0, "week": 0, "total": 0,
2311
+ "by_confidence": {"high": 0, "medium": 0, "low": 0},
2312
+ "vault_configured": False, "last_date": None,
2313
+ }
2314
+ from core.cognition.dreams_reader import list_insights
2315
+ all_items = list_insights(dreams, since_days=36500)
2316
+ by_conf = {"high": 0, "medium": 0, "low": 0}
2317
+ for i in all_items:
2318
+ by_conf[i.confidence if i.confidence in by_conf else "medium"] += 1
2319
+ return {
2320
+ "today": len(list_insights(dreams, since_days=1)),
2321
+ "week": len(list_insights(dreams, since_days=7)),
2322
+ "total": len(all_items),
2323
+ "by_confidence": by_conf,
2324
+ "vault_configured": True,
2325
+ "last_date": all_items[0].date if all_items else None,
2326
+ }
2327
+
2328
+
2329
+ # ============================================================================
2330
+ # v3.72.0 — System: version check + one-click core update (feature #3).
2331
+ # ============================================================================
2332
+
2333
+ _npm_latest_cache = {"version": None, "ts": 0.0}
2334
+
2335
+
2336
+ def _current_version() -> str:
2337
+ root = os.environ.get("ARKAOS_ROOT") or str(Path(__file__).resolve().parent.parent)
2338
+ try:
2339
+ return (Path(root) / "VERSION").read_text(encoding="utf-8").strip()
2340
+ except OSError:
2341
+ return "0.0.0"
2342
+
2343
+
2344
+ def _npm_latest_version():
2345
+ import subprocess
2346
+ import time
2347
+ now = time.time()
2348
+ if _npm_latest_cache["version"] and now - _npm_latest_cache["ts"] < 600:
2349
+ return _npm_latest_cache["version"]
2350
+ try:
2351
+ out = subprocess.run(
2352
+ ["npm", "view", "arkaos", "version"],
2353
+ capture_output=True, text=True, timeout=20,
2354
+ )
2355
+ latest = (out.stdout or "").strip() or None
2356
+ except Exception: # noqa: BLE001
2357
+ latest = None
2358
+ if latest:
2359
+ _npm_latest_cache["version"] = latest
2360
+ _npm_latest_cache["ts"] = now
2361
+ return latest
2362
+
2363
+
2364
+ def _is_newer(latest: str, current: str) -> bool:
2365
+ import re
2366
+ def _parts(v):
2367
+ nums = [int(n) for n in re.findall(r"\d+", v)[:3]]
2368
+ return nums or [0]
2369
+ try:
2370
+ return _parts(latest) > _parts(current)
2371
+ except Exception: # noqa: BLE001
2372
+ return False
2373
+
2374
+
2375
+ def _run_core_update() -> dict:
2376
+ import subprocess
2377
+ try:
2378
+ out = subprocess.run(
2379
+ ["npx", "arkaos@latest", "update"],
2380
+ capture_output=True, text=True, timeout=600,
2381
+ )
2382
+ tail = ((out.stdout or "") + (out.stderr or ""))[-2000:]
2383
+ return {"ok": out.returncode == 0, "output": tail}
2384
+ except Exception as exc: # noqa: BLE001
2385
+ return {"ok": False, "output": f"update failed: {exc}"}
2386
+
2387
+
2388
+ @app.get("/api/system/version")
2389
+ def system_version():
2390
+ """Current installed version vs the latest on npm."""
2391
+ current = _current_version()
2392
+ latest = _npm_latest_version()
2393
+ return {
2394
+ "current": current,
2395
+ "latest": latest,
2396
+ "update_available": bool(latest) and _is_newer(latest, current),
2397
+ }
2398
+
2399
+
2400
+ @app.post("/api/system/update")
2401
+ def system_update(request: Request):
2402
+ """Run `npx arkaos@latest update` (update step 1). Step 2 (project
2403
+ sync via /arka update) is a Claude Code action the UI prompts for.
2404
+
2405
+ A regular POST is CORS-protected; as defense-in-depth for an endpoint
2406
+ that runs an update, reject any explicitly non-localhost origin (an
2407
+ empty origin — local CLI / same-origin — is allowed).
2408
+ """
2409
+ origin = request.headers.get("origin", "")
2410
+ if origin and not _terminal_origin_ok(origin):
2411
+ from fastapi import HTTPException
2412
+ raise HTTPException(status_code=403, detail="origin not allowed")
2413
+ return _run_core_update()
2414
+
2415
+
2262
2416
  # ============================================================================
2263
2417
  # PR99a v3.67.0 — Terminal PTY WebSocket + REST.
2264
2418
  #
@@ -2282,6 +2436,12 @@ def _terminal_origin_ok(origin: str) -> bool:
2282
2436
  )
2283
2437
 
2284
2438
 
2439
+ # v3.71.1 — enforce a single live WebSocket per session (latest wins), so a
2440
+ # reload or a second tab can't fight over the one PTY fd reader.
2441
+ from core.terminal.connections import ConnectionRegistry as _TerminalConnRegistry
2442
+ _terminal_conns = _TerminalConnRegistry()
2443
+
2444
+
2285
2445
  @app.get("/api/terminal/token")
2286
2446
  def terminal_token_endpoint():
2287
2447
  """Return the per-process bearer token used in WS handshakes.
@@ -2366,6 +2526,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2366
2526
 
2367
2527
  await ws.accept()
2368
2528
 
2529
+ # v3.71.1 — single live connection per session: a newer connection
2530
+ # (a reload, or a second tab) supersedes the previous one, which we
2531
+ # close so it stops pumping the shared PTY fd.
2532
+ superseded = _terminal_conns.acquire(session_id, ws)
2533
+ if superseded is not None:
2534
+ try:
2535
+ await superseded.close(code=4409, reason="superseded by a newer connection")
2536
+ except Exception:
2537
+ pass
2538
+
2369
2539
  # v3.71.0 — replay recent scrollback so a client reconnecting after
2370
2540
  # the operator navigated away / reloaded restores its session as it
2371
2541
  # left it. Sent before the live reader is attached, so the historical
@@ -2437,10 +2607,13 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2437
2607
  pass
2438
2608
  finally:
2439
2609
  pump_task.cancel()
2440
- try:
2441
- loop.remove_reader(session.master_fd)
2442
- except (ValueError, OSError):
2443
- pass
2610
+ # Only the still-active connection owns the fd reader — a superseded
2611
+ # connection tearing down must not remove its replacement's reader.
2612
+ if _terminal_conns.release(session_id, ws):
2613
+ try:
2614
+ loop.remove_reader(session.master_fd)
2615
+ except (ValueError, OSError):
2616
+ pass
2444
2617
 
2445
2618
 
2446
2619
  @app.on_event("startup")