arkaos 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +318 -107
  2. package/VERSION +1 -1
  3. package/config/hooks/cwd-changed.ps1 +144 -0
  4. package/config/hooks/post-tool-use.ps1 +347 -0
  5. package/config/hooks/post-tool-use.sh +6 -6
  6. package/config/hooks/pre-compact.ps1 +238 -0
  7. package/config/hooks/pre-compact.sh +10 -6
  8. package/config/hooks/session-start.ps1 +109 -0
  9. package/config/hooks/session-start.sh +1 -1
  10. package/config/hooks/user-prompt-submit.ps1 +287 -0
  11. package/config/hooks/user-prompt-submit.sh +5 -2
  12. package/config/statusline.ps1 +160 -0
  13. package/core/cognition/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/core/cognition/capture/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
  16. package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
  17. package/core/cognition/insights/__pycache__/__init__.cpython-313.pyc +0 -0
  18. package/core/cognition/insights/__pycache__/store.cpython-313.pyc +0 -0
  19. package/core/cognition/memory/__pycache__/__init__.cpython-313.pyc +0 -0
  20. package/core/cognition/memory/__pycache__/obsidian.cpython-313.pyc +0 -0
  21. package/core/cognition/memory/__pycache__/schemas.cpython-313.pyc +0 -0
  22. package/core/cognition/memory/__pycache__/vector.cpython-313.pyc +0 -0
  23. package/core/cognition/memory/__pycache__/writer.cpython-313.pyc +0 -0
  24. package/core/cognition/research/__pycache__/__init__.cpython-313.pyc +0 -0
  25. package/core/cognition/research/__pycache__/profiler.cpython-313.pyc +0 -0
  26. package/core/cognition/scheduler/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/core/cognition/scheduler/__pycache__/cli.cpython-313.pyc +0 -0
  28. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  29. package/core/cognition/scheduler/__pycache__/platform.cpython-313.pyc +0 -0
  30. package/core/cognition/scheduler/daemon.py +77 -21
  31. package/core/cognition/scheduler/platform.py +43 -12
  32. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  33. package/core/knowledge/vector_store.py +50 -25
  34. package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
  35. package/core/synapse/layers.py +2 -2
  36. package/installer/adapters/claude-code.js +72 -45
  37. package/installer/cli.js +19 -6
  38. package/installer/doctor.js +130 -18
  39. package/installer/index.js +592 -149
  40. package/installer/platform.js +20 -0
  41. package/installer/prompts.js +109 -5
  42. package/installer/python-resolver.js +251 -0
  43. package/installer/update.js +497 -62
  44. package/package.json +1 -1
  45. package/pyproject.toml +2 -2
  46. package/scripts/start-dashboard.ps1 +271 -0
@@ -1,9 +1,11 @@
1
- import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, chmodSync, cpSync } from "node:fs";
1
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, chmodSync, cpSync, statSync } from "node:fs";
2
2
  import { join, resolve, dirname } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { execSync } from "node:child_process";
4
+ import { execSync, execFileSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getRuntimeConfig } from "./detect-runtime.js";
7
+ import { findSystemPython, ensureVenv, getArkaosPython, getArkaosPip, pipInstall } from "./python-resolver.js";
8
+ import { IS_WINDOWS, HOOK_EXT } from "./platform.js";
7
9
 
8
10
  const __filename = fileURLToPath(import.meta.url);
9
11
  const __dirname = dirname(__filename);
@@ -30,14 +32,14 @@ export async function install({ runtime, path, force }) {
30
32
  const installDir = userConfig.installDir;
31
33
 
32
34
  // ═══ Step 1: Create directories ═══
33
- step(1, 13, "Creating directories...");
35
+ step(1, 14, "Creating directories...");
34
36
  ensureDir(installDir);
35
37
  const dirs = ["config", "config/hooks", "agents", "media", "session-digests", "vault"];
36
38
  for (const d of dirs) ensureDir(join(installDir, d));
37
39
  ok(`${dirs.length + 1} directories ready`);
38
40
 
39
41
  // ═══ Step 2: Detect v1 installation ═══
40
- step(2, 13, "Checking for v1 installation...");
42
+ step(2, 14, "Checking for v1 installation...");
41
43
  const v1Paths = [
42
44
  join(homedir(), ".claude", "skills", "arka-os"),
43
45
  join(homedir(), ".claude", "skills", "arkaos"),
@@ -50,45 +52,85 @@ export async function install({ runtime, path, force }) {
50
52
  ok("No v1 installation found");
51
53
  }
52
54
 
53
- // ═══ Step 3: Check Python ═══
54
- step(3, 13, "Checking Python 3.11+...");
55
- const pythonCmd = checkPython();
56
- ok(`Found: ${pythonCmd}`);
55
+ // ═══ Step 3: Check Python + create venv ═══
56
+ step(3, 14, "Checking Python 3.11+ and creating virtual environment...");
57
+ const systemPython = findSystemPython();
58
+ if (!systemPython) {
59
+ console.error(`
60
+ ✗ Python 3.11+ is required but not found.
61
+
62
+ Install Python:
63
+ macOS: brew install python@3.13
64
+ Linux: sudo apt install python3.13
65
+ Windows: https://python.org/downloads/
66
+ `);
67
+ process.exit(1);
68
+ }
69
+ ok(`System Python found: ${systemPython}`);
70
+ const venvCreated = ensureVenv((msg) => console.log(msg));
71
+ if (!venvCreated) {
72
+ warn("Venv creation failed — falling back to system Python (PEP 668 may apply)");
73
+ }
74
+ const pythonCmd = getArkaosPython();
75
+ ok(`ArkaOS Python: ${pythonCmd}`);
57
76
 
58
77
  // ═══ Step 4: Install Python core + dependencies based on user choices ═══
59
- step(4, 13, "Installing Python dependencies (this may take a minute)...");
60
- installAllPythonDeps(pythonCmd, userConfig);
78
+ step(4, 14, "Installing Python dependencies (this may take a minute)...");
79
+ installAllPythonDeps(userConfig);
61
80
 
62
81
  // ═══ Step 5: Copy configuration files ═══
63
- step(5, 13, "Copying configuration files...");
82
+ step(5, 14, "Copying configuration files...");
64
83
  copyConfigFiles(installDir);
65
84
  ok("Constitution, standards, and config copied");
66
85
 
67
86
  // ═══ Step 6: Install hooks with real paths ═══
68
- step(6, 13, "Installing hooks...");
87
+ step(6, 14, "Installing hooks...");
69
88
  installHooks(installDir);
70
89
 
71
90
  // ═══ Step 7: Configure runtime ═══
72
- step(7, 13, "Configuring runtime...");
91
+ step(7, 14, "Configuring runtime...");
73
92
  const adapter = await loadAdapter(runtime);
74
93
  adapter.configureHooks(config, installDir);
75
94
  ok(`${config.name} configured`);
76
95
 
77
96
  // ═══ Step 8: Install ArkaOS skill to Claude Code ═══
78
- step(8, 13, "Installing /arka skill...");
97
+ step(8, 14, "Installing /arka skill...");
79
98
  installSkill(config, installDir);
80
99
 
81
100
  // ═══ Step 9: Install CLI wrapper and user instructions ═══
82
- step(9, 13, "Installing CLI wrapper...");
101
+ step(9, 14, "Installing CLI wrapper...");
83
102
  const binDir = join(installDir, "bin");
84
103
  ensureDir(binDir);
85
- const wrapperSrc = join(ARKAOS_ROOT, "bin", "arka-claude");
86
- if (existsSync(wrapperSrc)) {
87
- copyFileSync(wrapperSrc, join(binDir, "arka-claude"));
88
- try { chmodSync(join(binDir, "arka-claude"), 0o755); } catch {}
89
- ok("arka-claude wrapper installed");
90
- console.log(` Add to PATH: export PATH="$HOME/.arkaos/bin:$PATH"`);
91
- console.log(` Optional alias: alias claude="arka-claude"`);
104
+
105
+ // On Unix we deploy the bash wrapper. On Windows we deploy the PowerShell
106
+ // port plus a .cmd shim so `arka-claude ...` works from cmd.exe,
107
+ // PowerShell, and Windows Terminal alike.
108
+ if (IS_WINDOWS) {
109
+ const psSrc = join(ARKAOS_ROOT, "bin", "arka-claude.ps1");
110
+ const cmdSrc = join(ARKAOS_ROOT, "bin", "arka-claude.cmd");
111
+ let installed = false;
112
+ if (existsSync(psSrc)) {
113
+ copyFileSync(psSrc, join(binDir, "arka-claude.ps1"));
114
+ installed = true;
115
+ }
116
+ if (existsSync(cmdSrc)) {
117
+ copyFileSync(cmdSrc, join(binDir, "arka-claude.cmd"));
118
+ installed = true;
119
+ }
120
+ if (installed) {
121
+ ok("arka-claude wrapper installed (.cmd + .ps1)");
122
+ console.log(` Add to PATH: setx PATH "%PATH%;%USERPROFILE%\\.arkaos\\bin"`);
123
+ console.log(` Then reopen any shell and run: arka-claude`);
124
+ }
125
+ } else {
126
+ const wrapperSrc = join(ARKAOS_ROOT, "bin", "arka-claude");
127
+ if (existsSync(wrapperSrc)) {
128
+ copyFileSync(wrapperSrc, join(binDir, "arka-claude"));
129
+ try { chmodSync(join(binDir, "arka-claude"), 0o755); } catch {}
130
+ ok("arka-claude wrapper installed");
131
+ console.log(` Add to PATH: export PATH="$HOME/.arkaos/bin:$PATH"`);
132
+ console.log(` Optional alias: alias claude="arka-claude"`);
133
+ }
92
134
  }
93
135
  const claudeMdSrc = join(ARKAOS_ROOT, "config", "user-claude.md");
94
136
  const userClaudeMd = join(homedir(), ".claude", "CLAUDE.md");
@@ -99,8 +141,12 @@ export async function install({ runtime, path, force }) {
99
141
  ok("~/.claude/CLAUDE.md already exists (preserved)");
100
142
  }
101
143
 
102
- // ═══ Step 10: Create references and profile ═══
103
- step(10, 13, "Creating references...");
144
+ // ═══ Step 10: Deploy Cognitive Scheduler ═══
145
+ step(10, 14, "Deploying cognitive scheduler...");
146
+ deployCognitiveScheduler(installDir, ARKAOS_ROOT);
147
+
148
+ // ═══ Step 11: Create references and profile ═══
149
+ step(11, 14, "Creating references...");
104
150
  writeFileSync(join(installDir, ".repo-path"), ARKAOS_ROOT);
105
151
  const skillsDir = join(config.skillsDir || join(homedir(), ".claude", "skills"), "arkaos");
106
152
  ensureDir(skillsDir);
@@ -135,8 +181,8 @@ export async function install({ runtime, path, force }) {
135
181
  ok("API keys saved");
136
182
  }
137
183
 
138
- // ═══ Step 10: Index knowledge base ═══
139
- step(11, 13, "Setting up knowledge base...");
184
+ // ═══ Step 12: Index knowledge base ═══
185
+ step(12, 14, "Setting up knowledge base...");
140
186
  if (userConfig.installKnowledge) {
141
187
  const kbDb = join(installDir, "knowledge.db");
142
188
  // Index ArkaOS skills first
@@ -163,11 +209,12 @@ export async function install({ runtime, path, force }) {
163
209
  ok("Knowledge base skipped (install later with 'npx arkaos index')");
164
210
  }
165
211
 
166
- // ═══ Step 11: Verify installation ═══
167
- step(12, 13, "Verifying installation...");
212
+ // ═══ Step 13: Verify installation ═══
213
+ step(13, 14, "Verifying installation...");
168
214
  let checks = 0;
169
215
  if (existsSync(join(installDir, "config", "constitution.yaml"))) checks++;
170
- if (existsSync(join(installDir, "config", "hooks", "user-prompt-submit.sh"))) checks++;
216
+ const verifyHookExt = HOOK_EXT;
217
+ if (existsSync(join(installDir, "config", "hooks", `user-prompt-submit${verifyHookExt}`))) checks++;
171
218
  if (existsSync(join(installDir, ".repo-path"))) checks++;
172
219
  if (existsSync(profilePath)) checks++;
173
220
  try {
@@ -178,8 +225,8 @@ export async function install({ runtime, path, force }) {
178
225
  } catch {}
179
226
  ok(`${checks}/5 checks passed`);
180
227
 
181
- // ═══ Step 12: Finalize ═══
182
- step(13, 13, "Finalizing...");
228
+ // ═══ Step 14: Finalize ═══
229
+ step(14, 14, "Finalizing...");
183
230
  const manifest = {
184
231
  version: VERSION,
185
232
  runtime,
@@ -241,35 +288,13 @@ function ensureDir(dir) {
241
288
  }
242
289
  }
243
290
 
244
- function checkPython() {
245
- const candidates = ["python3", "python"];
246
- for (const cmd of candidates) {
247
- try {
248
- const version = execSync(`${cmd} --version 2>&1`, { stdio: "pipe" }).toString().trim();
249
- const match = version.match(/(\d+)\.(\d+)/);
250
- if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 11) {
251
- return cmd;
252
- }
253
- } catch {
254
- continue;
255
- }
256
- }
257
- console.error(`
258
- ✗ Python 3.11+ is required but not found.
291
+ function installAllPythonDeps(userConfig = {}) {
292
+ const log = (msg) => console.log(msg);
259
293
 
260
- Install Python:
261
- macOS: brew install python@3.13
262
- Linux: sudo apt install python3.13
263
- Windows: https://python.org/downloads/
264
- `);
265
- process.exit(1);
266
- }
267
-
268
- function installAllPythonDeps(pythonCmd, userConfig = {}) {
269
294
  // Core dependencies
270
295
  const coreDeps = "pyyaml pydantic rich click jinja2";
271
- // Knowledge + Vector DB
272
- const knowledgeDeps = "fastembed sqlite-vss";
296
+ // Knowledge + Vector DB - installed individually (see below) so that
297
+ // a failure of one does not block the other.
273
298
  // Ingest (YouTube, PDF, web, audio)
274
299
  const ingestDeps = "yt-dlp pdfplumber beautifulsoup4 requests";
275
300
  // Dashboard API
@@ -277,75 +302,65 @@ function installAllPythonDeps(pythonCmd, userConfig = {}) {
277
302
  // Transcription
278
303
  const transcriptionDeps = "faster-whisper";
279
304
 
280
- // Build deps list based on user choices
281
- let allDeps = coreDeps;
282
- if (userConfig.installKnowledge !== false) allDeps += ` ${knowledgeDeps}`;
283
- allDeps += ` ${ingestDeps}`;
284
- if (userConfig.installDashboard !== false) allDeps += ` ${dashboardDeps}`;
285
-
286
- try {
287
- // Try uv first (faster)
288
- try {
289
- execSync("uv --version", { stdio: "pipe" });
290
- console.log(" Using uv (fast installer)...");
291
- execSync(`uv pip install ${allDeps}`, { stdio: "pipe", timeout: 300000 });
292
- ok("Core + Knowledge + Ingest + Dashboard deps installed");
293
- // Transcription optional (heavy)
294
- try {
295
- execSync(`uv pip install ${transcriptionDeps}`, { stdio: "pipe", timeout: 300000 });
296
- ok("Whisper transcription installed");
297
- } catch {
298
- warn("Whisper not installed (optional — needed for YouTube/audio transcription)");
299
- }
300
- return;
301
- } catch {
302
- // uv not available, use pip
303
- }
304
-
305
- console.log(" Installing core dependencies...");
306
- execSync(`${pythonCmd} -m pip install ${coreDeps} --quiet`, { stdio: "pipe", timeout: 120000 });
305
+ // Core deps (required)
306
+ console.log(" Installing core dependencies...");
307
+ if (pipInstall(coreDeps, { log })) {
307
308
  ok("Core deps installed (pyyaml, pydantic, rich, click, jinja2)");
309
+ } else {
310
+ warn("Core deps failed — some features may not work");
311
+ }
308
312
 
313
+ // Knowledge deps (optional). Installed one package at a time so a
314
+ // partial success (e.g. fastembed OK, sqlite-vec fails on Windows)
315
+ // is reported accurately and the user knows which capability they
316
+ // actually have.
317
+ if (userConfig.installKnowledge !== false) {
309
318
  console.log(" Installing knowledge base dependencies...");
310
- try {
311
- execSync(`${pythonCmd} -m pip install ${knowledgeDeps} --quiet`, { stdio: "pipe", timeout: 180000 });
312
- ok("Vector DB installed (fastembed, sqlite-vss)");
313
- } catch {
314
- warn("Vector DB not installed (run: pip install fastembed sqlite-vss)");
319
+ const fastembedOk = pipInstall("fastembed", { log, timeout: 180000 });
320
+ const sqliteVssOk = pipInstall("sqlite-vec", { log, timeout: 180000 });
321
+ if (fastembedOk && sqliteVssOk) {
322
+ ok("Vector DB installed (fastembed, sqlite-vec)");
323
+ } else if (fastembedOk) {
324
+ warn("fastembed installed but sqlite-vec failed — semantic search degraded");
325
+ } else if (sqliteVssOk) {
326
+ warn("sqlite-vec installed but fastembed failed — embedding pipeline degraded");
327
+ } else {
328
+ warn("Vector DB not installed (run later: npx arkaos doctor for fix)");
315
329
  }
330
+ }
316
331
 
317
- console.log(" Installing content ingest dependencies...");
318
- try {
319
- execSync(`${pythonCmd} -m pip install ${ingestDeps} --quiet`, { stdio: "pipe", timeout: 120000 });
320
- ok("Ingest deps installed (yt-dlp, pdfplumber, beautifulsoup4)");
321
- } catch {
322
- warn("Ingest deps not fully installed (some content types may not work)");
323
- }
332
+ // Ingest deps
333
+ console.log(" Installing content ingest dependencies...");
334
+ if (pipInstall(ingestDeps, { log })) {
335
+ ok("Ingest deps installed (yt-dlp, pdfplumber, beautifulsoup4)");
336
+ } else {
337
+ warn("Ingest deps not fully installed (some content types may not work)");
338
+ }
324
339
 
340
+ // Dashboard deps (optional)
341
+ if (userConfig.installDashboard !== false) {
325
342
  console.log(" Installing dashboard dependencies...");
326
- try {
327
- execSync(`${pythonCmd} -m pip install ${dashboardDeps} --quiet`, { stdio: "pipe", timeout: 60000 });
343
+ if (pipInstall(dashboardDeps, { log, timeout: 60000 })) {
328
344
  ok("Dashboard API installed (fastapi, uvicorn)");
329
- } catch {
330
- warn("Dashboard API not installed (run: pip install fastapi uvicorn)");
331
- }
332
-
333
- console.log(" Installing transcription engine...");
334
- try {
335
- execSync(`${pythonCmd} -m pip install ${transcriptionDeps} --quiet`, { stdio: "pipe", timeout: 300000 });
336
- ok("Whisper transcription installed");
337
- } catch {
338
- warn("Whisper not installed (optional — needed for YouTube/audio)");
345
+ } else {
346
+ warn("Dashboard API not installed (optional)");
339
347
  }
348
+ }
340
349
 
341
- // Install ArkaOS itself as editable
342
- try {
343
- execSync(`${pythonCmd} -m pip install -e "${ARKAOS_ROOT}" --quiet`, { stdio: "pipe", timeout: 60000 });
344
- } catch {}
350
+ // Transcription (optional, heavy)
351
+ console.log(" Installing transcription engine...");
352
+ if (pipInstall(transcriptionDeps, { log, timeout: 300000 })) {
353
+ ok("Whisper transcription installed");
354
+ } else {
355
+ warn("Whisper not installed (optional — needed for YouTube/audio)");
356
+ }
345
357
 
346
- } catch (err) {
347
- warn(`Some Python deps failed: ${err.message.slice(0, 100)}`);
348
- console.log(" You can install manually: pip install pyyaml pydantic rich click fastembed sqlite-vss");
358
+ // Install ArkaOS core package as editable
359
+ console.log(" Installing ArkaOS core engine...");
360
+ if (pipInstall("", { editable: ARKAOS_ROOT, log, timeout: 60000 })) {
361
+ ok("ArkaOS core engine installed");
362
+ } else {
363
+ warn("Core engine install failed — run: npx arkaos doctor for fix");
349
364
  }
350
365
  }
351
366
 
@@ -364,6 +379,12 @@ function copyConfigFiles(installDir) {
364
379
  }
365
380
  }
366
381
 
382
+ // Statusline script (platform-aware: .ps1 on Windows, .sh elsewhere)
383
+ const statuslineFile = IS_WINDOWS ? "config/statusline.ps1" : "config/statusline.sh";
384
+ if (existsSync(join(ARKAOS_ROOT, statuslineFile))) {
385
+ files.push([statuslineFile, statuslineFile]);
386
+ }
387
+
367
388
  for (const [src, dest] of files) {
368
389
  const srcPath = join(ARKAOS_ROOT, src);
369
390
  const destPath = join(installDir, dest);
@@ -378,39 +399,96 @@ function installHooks(installDir) {
378
399
  const hooksDir = join(installDir, "config", "hooks");
379
400
  ensureDir(hooksDir);
380
401
 
381
- const hookMap = {
382
- "session-start.sh": "session-start.sh",
383
- "user-prompt-submit.sh": "user-prompt-submit.sh",
384
- "post-tool-use.sh": "post-tool-use.sh",
385
- "pre-compact.sh": "pre-compact.sh",
386
- "cwd-changed.sh": "cwd-changed.sh",
387
- };
402
+ // On Windows we deploy the PowerShell ports of the hooks; on Unix-like
403
+ // systems we deploy the Bash originals. The adapter in
404
+ // installer/adapters/claude-code.js registers whichever extension was
405
+ // deployed here, so the two lists must stay in sync.
406
+ const hookNames = [
407
+ "session-start",
408
+ "user-prompt-submit",
409
+ "post-tool-use",
410
+ "pre-compact",
411
+ "cwd-changed",
412
+ ];
413
+ const hookExt = HOOK_EXT;
388
414
 
389
415
  const srcHooksDir = join(ARKAOS_ROOT, "config", "hooks");
390
416
 
391
- for (const [src, dest] of Object.entries(hookMap)) {
392
- const srcPath = join(srcHooksDir, src);
393
- const destPath = join(hooksDir, dest);
417
+ for (const name of hookNames) {
418
+ const filename = `${name}${hookExt}`;
419
+ const srcPath = join(srcHooksDir, filename);
420
+ const destPath = join(hooksDir, filename);
394
421
  if (existsSync(srcPath)) {
395
422
  let content = readFileSync(srcPath, "utf-8");
396
- // Set ARKAOS_ROOT to the npm package location (persistent)
397
- content = content.replace(
398
- /ARKAOS_ROOT="\$\{ARKA_OS:-\$HOME\/\.claude\/skills\/arkaos\}"/g,
399
- `ARKAOS_ROOT="${ARKAOS_ROOT}"`
400
- );
401
- content = content.replace(
402
- /ARKAOS_HOME="\$\{HOME\}\/\.arkaos"/g,
403
- `ARKAOS_HOME="${installDir}"`
404
- );
423
+ // The text-replacement pattern below only exists in the legacy
424
+ // bash hooks. Skip it on Windows (.ps1 files resolve ARKAOS_ROOT
425
+ // from the install manifest at runtime instead).
426
+ if (hookExt === ".sh") {
427
+ content = content.replace(
428
+ /ARKAOS_ROOT="\$\{ARKA_OS:-\$HOME\/\.claude\/skills\/arkaos\}"/g,
429
+ `ARKAOS_ROOT="${ARKAOS_ROOT}"`
430
+ );
431
+ content = content.replace(
432
+ /ARKAOS_HOME="\$\{HOME\}\/\.arkaos"/g,
433
+ `ARKAOS_HOME="${installDir}"`
434
+ );
435
+ }
405
436
  writeFileSync(destPath, content);
437
+ // chmod is a no-op on Windows (NTFS ACLs aren't POSIX), but the
438
+ // try/catch kept it safe already. Leaving the call in place.
406
439
  try { chmodSync(destPath, 0o755); } catch {}
407
- ok(`Hook: ${dest}`);
440
+ ok(`Hook: ${filename}`);
441
+ }
442
+ }
443
+ }
444
+
445
+ // Safe directory iteration. Returns an empty array instead of throwing
446
+ // when the target does not exist, which lets us skip missing sources
447
+ // (fresh repo checkouts may omit optional department resource dirs).
448
+ function listSubdirs(parent) {
449
+ if (!existsSync(parent)) return [];
450
+ try {
451
+ return readdirSync(parent, { withFileTypes: true })
452
+ .filter((e) => e.isDirectory())
453
+ .map((e) => e.name);
454
+ } catch {
455
+ return [];
456
+ }
457
+ }
458
+
459
+ // Port of the "Resources" copy loop from install.sh:399-403. Copies any
460
+ // of scripts/references/assets that exist next to a skill's SKILL.md
461
+ // into the deployed skill directory. Silent no-op when a resource dir
462
+ // does not exist. Each copy is wrapped in a try/catch so a partial
463
+ // failure on one resource doesn't block the rest of the deployment.
464
+ function copySkillResources(skillSrcDir, skillDestDir) {
465
+ const resources = ["scripts", "references", "assets"];
466
+ for (const res of resources) {
467
+ const src = join(skillSrcDir, res);
468
+ if (!existsSync(src)) continue;
469
+ try {
470
+ cpSync(src, join(skillDestDir, res), { recursive: true });
471
+ } catch {
472
+ // Best-effort — a single missing resource dir shouldn't break install.
408
473
  }
409
474
  }
410
475
  }
411
476
 
477
+ // Deploy one skill directory as a top-level `arka-<name>/` under the
478
+ // Claude Code skills base. Mirrors install.sh lines 396-405 and 414-425.
479
+ // Returns true if SKILL.md was deployed, false otherwise.
480
+ function deployTopLevelSkill(skillSrcDir, arkaName, skillsBase) {
481
+ const skillMd = join(skillSrcDir, "SKILL.md");
482
+ if (!existsSync(skillMd)) return false;
483
+ const dest = join(skillsBase, arkaName);
484
+ ensureDir(dest);
485
+ copyFileSync(skillMd, join(dest, "SKILL.md"));
486
+ copySkillResources(skillSrcDir, dest);
487
+ return true;
488
+ }
489
+
412
490
  function installSkill(config, installDir) {
413
- // Copy arka/SKILL.md to Claude Code skills directory
491
+ // ── Main /arka skill ────────────────────────────────────────────────
414
492
  const skillSrc = join(ARKAOS_ROOT, "arka", "SKILL.md");
415
493
  const skillsBase = config.skillsDir || join(homedir(), ".claude", "skills");
416
494
  const skillDest = join(skillsBase, "arka");
@@ -419,30 +497,395 @@ function installSkill(config, installDir) {
419
497
 
420
498
  if (existsSync(skillSrc)) {
421
499
  copyFileSync(skillSrc, join(skillDest, "SKILL.md"));
422
- // Write repo path reference
423
500
  writeFileSync(join(skillDest, ".repo-path"), ARKAOS_ROOT);
424
501
  writeFileSync(join(skillDest, "VERSION"), VERSION);
425
- ok("/arka skill installed Claude Code can now use ArkaOS");
502
+ // Nested arka/skills/ (conclave, human-writing) is copied as a
503
+ // reference bundle under the main /arka skill, preserving the
504
+ // old behaviour for anything that reads from that path.
505
+ const nestedSkillsSrc = join(ARKAOS_ROOT, "arka", "skills");
506
+ if (existsSync(nestedSkillsSrc)) {
507
+ const nestedSkillsOut = join(skillDest, "skills");
508
+ ensureDir(nestedSkillsOut);
509
+ try {
510
+ cpSync(nestedSkillsSrc, nestedSkillsOut, { recursive: true });
511
+ } catch {
512
+ // Silent — these are bundled references, not user-facing skills.
513
+ }
514
+ }
515
+ ok("/arka skill installed");
426
516
  } else {
427
- warn("SKILL.md not found in package");
517
+ warn("arka/SKILL.md not found in package");
428
518
  }
429
519
 
430
- // Also copy department skills as references
431
- const deptSkillSrc = join(ARKAOS_ROOT, "arka", "skills");
432
- if (existsSync(deptSkillSrc)) {
433
- const skillsOut = join(skillDest, "skills");
434
- ensureDir(skillsOut);
520
+ // ── Department skills ───────────────────────────────────────────────
521
+ // For each `departments/<dept>/SKILL.md`, deploy as top-level
522
+ // `~/.claude/skills/arka-<dept>/`. Mirrors install.sh:391-406, but
523
+ // iterates the source directory dynamically instead of hardcoding a
524
+ // department list (the bash list is stale — ships 8 of the 18 real
525
+ // departments on disk, so half are silently skipped on macOS/Linux
526
+ // too until a user edits install.sh).
527
+ const deptRoot = join(ARKAOS_ROOT, "departments");
528
+ let deptDeployed = 0;
529
+ for (const dept of listSubdirs(deptRoot)) {
530
+ if (deployTopLevelSkill(join(deptRoot, dept), `arka-${dept}`, skillsBase)) {
531
+ deptDeployed++;
532
+ }
533
+ }
534
+ if (deptDeployed > 0) {
535
+ ok(`${deptDeployed} department skills installed (/arka-<dept>)`);
536
+ }
537
+
538
+ // ── Sub-skills (departments/*/skills/*) ─────────────────────────────
539
+ // For each `departments/<dept>/skills/<skill>/SKILL.md`, deploy as
540
+ // top-level `~/.claude/skills/arka-<skill>/`. Mirrors install.sh:411-425.
541
+ // Collisions between departments (two depts defining the same
542
+ // sub-skill name) are resolved by "later wins", same semantics as
543
+ // the bash loop — in practice the repo has no collisions today.
544
+ let subSkillDeployed = 0;
545
+ for (const dept of listSubdirs(deptRoot)) {
546
+ const deptSkillsDir = join(deptRoot, dept, "skills");
547
+ for (const subSkill of listSubdirs(deptSkillsDir)) {
548
+ const subSkillSrc = join(deptSkillsDir, subSkill);
549
+ if (deployTopLevelSkill(subSkillSrc, `arka-${subSkill}`, skillsBase)) {
550
+ subSkillDeployed++;
551
+ }
552
+ }
553
+ }
554
+ if (subSkillDeployed > 0) {
555
+ ok(`${subSkillDeployed} sub-skills installed (/arka-<skill>)`);
556
+ }
557
+
558
+ // ── Agent personas ──────────────────────────────────────────────────
559
+ // For each `departments/<dept>/agents/<name>.md`, deploy to
560
+ // `~/.claude/agents/arka-<name>.md`. Mirrors install.sh:436-443.
561
+ // The agents dir parallels skillsBase — Claude Code reads it
562
+ // separately from the skills tree.
563
+ const agentsBase = join(homedir(), ".claude", "agents");
564
+ ensureDir(agentsBase);
565
+ let agentDeployed = 0;
566
+ for (const dept of listSubdirs(deptRoot)) {
567
+ const agentsSrc = join(deptRoot, dept, "agents");
568
+ if (!existsSync(agentsSrc)) continue;
569
+ try {
570
+ for (const agentFile of readdirSync(agentsSrc)) {
571
+ if (!agentFile.endsWith(".md")) continue;
572
+ const srcFile = join(agentsSrc, agentFile);
573
+ // Safety: don't deploy anything that's not a regular file.
574
+ try {
575
+ if (!statSync(srcFile).isFile()) continue;
576
+ } catch {
577
+ continue;
578
+ }
579
+ const baseName = agentFile.replace(/\.md$/, "");
580
+ copyFileSync(srcFile, join(agentsBase, `arka-${baseName}.md`));
581
+ agentDeployed++;
582
+ }
583
+ } catch {
584
+ // Silently skip departments with unreadable agents dirs.
585
+ }
586
+ }
587
+ if (agentDeployed > 0) {
588
+ ok(`${agentDeployed} agent personas installed (~/.claude/agents/arka-*.md)`);
589
+ }
590
+ }
591
+
592
+ function deployCognitiveScheduler(installDir, arkaosRoot) {
593
+ const platform = process.platform;
594
+
595
+ // 1. Copy schedule config
596
+ const schedSrc = join(arkaosRoot, "config", "cognition", "schedules.yaml");
597
+ const schedDest = join(installDir, "schedules.yaml");
598
+ if (existsSync(schedSrc)) {
599
+ copyFileSync(schedSrc, schedDest);
600
+ ok("Schedule config deployed");
601
+ } else {
602
+ warn("schedules.yaml not found in package");
603
+ return;
604
+ }
605
+
606
+ // 2. Copy prompt files
607
+ const promptsDir = join(installDir, "cognition", "prompts");
608
+ ensureDir(promptsDir);
609
+ const promptsSrc = join(arkaosRoot, "config", "cognition", "prompts");
610
+ if (existsSync(promptsSrc)) {
611
+ for (const f of readdirSync(promptsSrc)) {
612
+ copyFileSync(join(promptsSrc, f), join(promptsDir, f));
613
+ }
614
+ ok("Cognitive prompts deployed (dreaming, research)");
615
+ }
616
+
617
+ // 3. Copy daemon script and core modules it imports
618
+ const daemonSrc = join(arkaosRoot, "bin", "scheduler-daemon.py");
619
+ const binDir = join(installDir, "bin");
620
+ ensureDir(binDir);
621
+ if (existsSync(daemonSrc)) {
622
+ copyFileSync(daemonSrc, join(binDir, "scheduler-daemon.py"));
623
+ try { chmodSync(join(binDir, "scheduler-daemon.py"), 0o755); } catch {}
624
+ ok("Scheduler daemon deployed");
625
+ }
626
+
627
+ // 3b. Copy scheduler core modules so the daemon can import them
628
+ const schedulerModules = [
629
+ "core/cognition/scheduler/__init__.py",
630
+ "core/cognition/scheduler/daemon.py",
631
+ "core/cognition/scheduler/platform.py",
632
+ "core/cognition/scheduler/cli.py",
633
+ ];
634
+ for (const mod of schedulerModules) {
635
+ const src = join(arkaosRoot, mod);
636
+ const dest = join(installDir, mod);
637
+ if (existsSync(src)) {
638
+ ensureDir(dirname(dest));
639
+ copyFileSync(src, dest);
640
+ }
641
+ }
642
+ // Write minimal __init__.py files (don't copy full cognition init — it
643
+ // imports modules not deployed here like capture, insights, memory)
644
+ for (const init of ["core/__init__.py", "core/cognition/__init__.py"]) {
645
+ const dest = join(installDir, init);
646
+ ensureDir(dirname(dest));
647
+ writeFileSync(dest, '"""ArkaOS — deployed subset for scheduler."""\n');
648
+ }
649
+ ok("Scheduler core modules deployed");
650
+
651
+ // 4. Create log directories
652
+ ensureDir(join(installDir, "logs", "dreaming"));
653
+ ensureDir(join(installDir, "logs", "research"));
654
+
655
+ // 5. Install platform service
656
+ const daemonPath = join(binDir, "scheduler-daemon.py");
657
+ if (platform === "darwin") {
658
+ installLaunchdService(installDir, daemonPath);
659
+ } else if (platform === "linux") {
660
+ installSystemdService(installDir, daemonPath);
661
+ } else if (platform === "win32") {
662
+ installSchtasksService(daemonPath);
663
+ } else {
664
+ warn(`Scheduler auto-start not supported on ${platform} — run manually`);
665
+ }
666
+ }
667
+
668
+ function installLaunchdService(installDir, daemonPath) {
669
+ const label = "com.arkaos.scheduler";
670
+ const plistDir = join(homedir(), "Library", "LaunchAgents");
671
+ const plistPath = join(plistDir, `${label}.plist`);
672
+ const logDir = join(installDir, "logs");
673
+ const home = homedir();
674
+
675
+ // Use the ArkaOS venv Python, not whatever `which python3` finds
676
+ let pythonPath;
677
+ try {
678
+ pythonPath = getArkaosPython();
679
+ } catch {
680
+ try {
681
+ pythonPath = execSync("which python3", { stdio: "pipe" }).toString().trim();
682
+ } catch {
683
+ pythonPath = "python3";
684
+ }
685
+ }
686
+
687
+ // PATH must include known Claude CLI locations — launchd inherits a minimal PATH
688
+ const pathValue = `${home}/.local/bin:${home}/.arkaos/bin:/usr/local/bin:/usr/bin:/bin`;
689
+
690
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
691
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
692
+ <plist version="1.0">
693
+ <dict>
694
+ \t<key>Label</key>
695
+ \t<string>${label}</string>
696
+ \t<key>ProgramArguments</key>
697
+ \t<array>
698
+ \t\t<string>${pythonPath}</string>
699
+ \t\t<string>${daemonPath}</string>
700
+ \t</array>
701
+ \t<key>EnvironmentVariables</key>
702
+ \t<dict>
703
+ \t\t<key>PATH</key>
704
+ \t\t<string>${pathValue}</string>
705
+ \t\t<key>HOME</key>
706
+ \t\t<string>${home}</string>
707
+ \t</dict>
708
+ \t<key>RunAtLoad</key>
709
+ \t<true/>
710
+ \t<key>KeepAlive</key>
711
+ \t<true/>
712
+ \t<key>StandardOutPath</key>
713
+ \t<string>${join(logDir, "scheduler-stdout.log")}</string>
714
+ \t<key>StandardErrorPath</key>
715
+ \t<string>${join(logDir, "scheduler-stderr.log")}</string>
716
+ </dict>
717
+ </plist>`;
718
+
719
+ ensureDir(plistDir);
720
+ // Unload existing if present
721
+ try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "pipe" }); } catch {}
722
+ writeFileSync(plistPath, plist);
723
+ try {
724
+ execSync(`launchctl load "${plistPath}"`, { stdio: "pipe" });
725
+ ok("Scheduler service installed and started (launchd)");
726
+ } catch {
727
+ warn("Scheduler plist written but launchctl load failed — start manually");
728
+ }
729
+ }
730
+
731
+ function installSystemdService(installDir, daemonPath) {
732
+ const home = homedir();
733
+ let pythonPath;
734
+ try {
735
+ pythonPath = getArkaosPython();
736
+ } catch {
435
737
  try {
436
- cpSync(deptSkillSrc, skillsOut, { recursive: true });
437
- ok("Department skills copied");
738
+ pythonPath = execSync("which python3", { stdio: "pipe" }).toString().trim();
438
739
  } catch {
439
- // cpSync may not be available in older Node
440
- warn("Department skills copy skipped");
740
+ pythonPath = "python3";
741
+ }
742
+ }
743
+
744
+ // systemd inherits a minimal PATH — inject known Claude CLI locations
745
+ const pathValue = `${home}/.local/bin:${home}/.arkaos/bin:/usr/local/bin:/usr/bin:/bin`;
746
+
747
+ const serviceDir = join(home, ".config", "systemd", "user");
748
+ const servicePath = join(serviceDir, "arkaos-scheduler.service");
749
+ const unit = `[Unit]
750
+ Description=ArkaOS Cognitive Scheduler
751
+ After=network.target
752
+
753
+ [Service]
754
+ Type=simple
755
+ Environment=PATH=${pathValue}
756
+ ExecStart=${pythonPath} ${daemonPath}
757
+ Restart=on-failure
758
+ RestartSec=60
759
+
760
+ [Install]
761
+ WantedBy=default.target
762
+ `;
763
+
764
+ ensureDir(serviceDir);
765
+ writeFileSync(servicePath, unit);
766
+ try {
767
+ execSync("systemctl --user enable --now arkaos-scheduler.service", { stdio: "pipe" });
768
+ ok("Scheduler service installed and started (systemd)");
769
+ } catch {
770
+ warn("Scheduler unit written but systemctl enable failed — start manually");
771
+ }
772
+ }
773
+
774
+ // Recognize the common schtasks failure modes so we can print actionable
775
+ // guidance instead of a cryptic "ERROR: Access is denied." Windows
776
+ // localizes these strings, so match against both English text and the
777
+ // canonical HRESULT code 0x80070005 (E_ACCESSDENIED).
778
+ function classifySchtasksError(stderr) {
779
+ const s = (stderr || "").toLowerCase();
780
+ if (s.includes("access is denied") || s.includes("0x80070005") || s.includes("access denied")) {
781
+ return "access-denied";
782
+ }
783
+ if (s.includes("the system cannot find the file") || s.includes("cannot find the path")) {
784
+ return "not-found";
785
+ }
786
+ if (s.includes("already exists")) {
787
+ return "already-exists";
788
+ }
789
+ return "other";
790
+ }
791
+
792
+ function printSchtasksAccessDeniedHelp(pythonPath, daemonPath) {
793
+ // Two realistic root causes on Windows 10/11:
794
+ // 1. Current shell is not elevated AND the local security policy
795
+ // denies "Log on as a batch job" for the standard Users group
796
+ // (common on domain-joined or org-managed machines).
797
+ // 2. A previous admin-owned ArkaOS-Scheduler task exists and our
798
+ // /F overwrite is blocked because the current user can't modify
799
+ // tasks owned by another principal.
800
+ //
801
+ // Neither case is fatal to the install — the scheduler daemon is a
802
+ // nice-to-have that keeps background tasks ticking. We print the
803
+ // manual registration command so power users can elevate once and
804
+ // run it, and move on.
805
+ console.log(" Access denied. Options:");
806
+ console.log(" 1) Re-run the installer from an elevated PowerShell (Run as Administrator)");
807
+ console.log(" 2) Or register the task manually from an elevated prompt:");
808
+ console.log(` schtasks /Create /F /TN "ArkaOS-Scheduler" /SC ONLOGON /TR "\"${pythonPath}\" \"${daemonPath}\""`);
809
+ console.log(" 3) Or skip the scheduler and launch the daemon manually when needed:");
810
+ console.log(` "${pythonPath}" "${daemonPath}"`);
811
+ console.log(" The rest of ArkaOS is installed and functional — this only affects background task automation.");
812
+ }
813
+
814
+ function installSchtasksService(daemonPath) {
815
+ // Prefer the ArkaOS venv python so the scheduled task runs against the
816
+ // interpreter that actually has the ArkaOS core dependencies installed.
817
+ // Falls back to system Python via findSystemPython -> "python" when no
818
+ // venv exists. This replaces `where python3`, which is broken on
819
+ // Windows because the binary is `python.exe`, not `python3.exe`.
820
+ const pythonPath = getArkaosPython() || findSystemPython() || "python";
821
+
822
+ // Build the /TR value as a single command line with both paths
823
+ // individually quoted. When schtasks runs the task later, Windows
824
+ // parses this line with CommandLineToArgvW and lifts argv[0] as the
825
+ // executable and argv[1..] as the arguments. Without inner quotes a
826
+ // pythonPath or daemonPath containing spaces would break the parse.
827
+ const trValue = `\"${pythonPath}\" \"${daemonPath}\"`;
828
+
829
+ // Use execFileSync (no shell) with an argv array so we bypass cmd.exe
830
+ // quoting rules entirely. Node on Windows handles the argv -> command
831
+ // line encoding, escaping internal quotes with the `\"` convention
832
+ // that CommandLineToArgvW (used by schtasks) understands.
833
+ //
834
+ // /RU with the current username is redundant for a local on-logon
835
+ // task (schtasks defaults to the invoking user), but passing it
836
+ // explicitly makes our intent visible in the XML schtasks writes and
837
+ // surfaces "this task runs as <name>" when a sysadmin audits tasks.
838
+ const currentUser = process.env.USERNAME || process.env.USER || "";
839
+ const createArgs = [
840
+ "/Create", "/F",
841
+ "/TN", "ArkaOS-Scheduler",
842
+ "/SC", "ONLOGON",
843
+ "/TR", trValue,
844
+ ];
845
+ if (currentUser) {
846
+ createArgs.push("/RU", currentUser);
847
+ }
848
+ const runArgs = ["/Run", "/TN", "ArkaOS-Scheduler"];
849
+
850
+ const tryRun = (cmd, args) => {
851
+ try {
852
+ execFileSync(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
853
+ return { ok: true, stderr: "" };
854
+ } catch (err) {
855
+ const raw = err.stderr ? err.stderr.toString() : (err.message || "");
856
+ const stderr = raw.replace(/\r?\n/g, " ").trim();
857
+ return { ok: false, stderr };
858
+ }
859
+ };
860
+
861
+ const createResult = tryRun("schtasks", createArgs);
862
+ if (!createResult.ok) {
863
+ const kind = classifySchtasksError(createResult.stderr);
864
+ warn(`Scheduler: schtasks /Create failed (${kind}): ${createResult.stderr.slice(0, 220)}`);
865
+ if (kind === "access-denied") {
866
+ printSchtasksAccessDeniedHelp(pythonPath, daemonPath);
867
+ } else {
868
+ console.log(" Try manually: schtasks /Create /F /TN \"ArkaOS-Scheduler\" /SC ONLOGON /TR \"" + pythonPath + " " + daemonPath + "\"");
441
869
  }
870
+ return;
442
871
  }
872
+
873
+ const runResult = tryRun("schtasks", runArgs);
874
+ if (!runResult.ok) {
875
+ const kind = classifySchtasksError(runResult.stderr);
876
+ warn(`Scheduler: task created but /Run failed (${kind}): ${runResult.stderr.slice(0, 220)}`);
877
+ if (kind === "access-denied") {
878
+ console.log(" Task is registered but couldn't be started now. It will run automatically at next logon.");
879
+ } else {
880
+ console.log(" Task will start at next logon");
881
+ }
882
+ return;
883
+ }
884
+
885
+ ok("Scheduler task installed and started (schtasks)");
443
886
  }
444
887
 
445
- async function loadAdapter(runtime) {
888
+ export async function loadAdapter(runtime) {
446
889
  try {
447
890
  const mod = await import(`./adapters/${runtime}.js`);
448
891
  return mod.default;