arkaos 4.3.5 → 4.4.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 (113) hide show
  1. package/VERSION +1 -1
  2. package/arka/SKILL.md +1 -1
  3. package/bin/arka-claude +35 -4
  4. package/bin/arka-py +22 -7
  5. package/config/hooks/_lib/arka_python.ps1 +31 -0
  6. package/config/hooks/_lib/arka_python.sh +38 -0
  7. package/config/hooks/post-tool-use.ps1 +8 -0
  8. package/config/hooks/post-tool-use.sh +11 -5
  9. package/config/hooks/pre-tool-use.ps1 +11 -6
  10. package/config/hooks/pre-tool-use.sh +11 -6
  11. package/config/hooks/session-start.ps1 +10 -0
  12. package/config/hooks/session-start.sh +5 -0
  13. package/config/hooks/stop.ps1 +9 -1
  14. package/config/hooks/stop.sh +11 -5
  15. package/config/hooks/user-prompt-submit.ps1 +18 -0
  16. package/config/hooks/user-prompt-submit.sh +11 -5
  17. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  18. package/core/cognition/scheduler/daemon.py +7 -3
  19. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  20. package/core/forge/orchestrator.py +2 -1
  21. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  22. package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
  23. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  24. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  25. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  26. package/core/hooks/_shared.py +29 -9
  27. package/core/hooks/stop.py +4 -3
  28. package/core/hooks/user_prompt_submit.py +5 -4
  29. package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
  30. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  31. package/core/knowledge/indexer.py +1 -1
  32. package/core/knowledge/vector_store.py +10 -3
  33. package/core/runtime/__pycache__/model_router.cpython-312.pyc +0 -0
  34. package/core/runtime/__pycache__/model_router.cpython-314.pyc +0 -0
  35. package/core/runtime/__pycache__/model_routing_check.cpython-313.pyc +0 -0
  36. package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
  37. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  38. package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
  39. package/core/runtime/gateway/__init__.py +28 -0
  40. package/core/runtime/gateway/__main__.py +30 -0
  41. package/core/runtime/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
  42. package/core/runtime/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/core/runtime/gateway/__pycache__/__init__.cpython-314.pyc +0 -0
  44. package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
  45. package/core/runtime/gateway/__pycache__/litellm_config.cpython-312.pyc +0 -0
  46. package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
  47. package/core/runtime/gateway/__pycache__/litellm_config.cpython-314.pyc +0 -0
  48. package/core/runtime/gateway/litellm_config.py +205 -0
  49. package/core/runtime/model_routing_check.py +71 -0
  50. package/core/runtime/model_routing_context.py +22 -1
  51. package/core/runtime/native_usage.py +2 -1
  52. package/core/shared/__pycache__/temp_paths.cpython-313.pyc +0 -0
  53. package/core/shared/__pycache__/temp_paths.cpython-314.pyc +0 -0
  54. package/core/shared/temp_paths.py +40 -0
  55. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  56. package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
  57. package/core/synapse/kb_cache.py +3 -2
  58. package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
  59. package/core/sync/__pycache__/ai_mcp_decider.cpython-313.pyc +0 -0
  60. package/core/sync/__pycache__/content_syncer.cpython-313.pyc +0 -0
  61. package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
  62. package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
  63. package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
  64. package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
  65. package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
  66. package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
  67. package/core/sync/__pycache__/policy_loader.cpython-313.pyc +0 -0
  68. package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
  69. package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
  70. package/core/sync/agent_provisioner.py +5 -5
  71. package/core/sync/ai_mcp_decider.py +2 -2
  72. package/core/sync/content_syncer.py +14 -14
  73. package/core/sync/descriptor_syncer.py +2 -2
  74. package/core/sync/discovery.py +4 -4
  75. package/core/sync/engine.py +4 -4
  76. package/core/sync/manifest.py +1 -1
  77. package/core/sync/mcp_optimizer.py +5 -5
  78. package/core/sync/mcp_syncer.py +3 -3
  79. package/core/sync/policy_loader.py +1 -1
  80. package/core/sync/reporter.py +1 -1
  81. package/core/sync/settings_syncer.py +2 -2
  82. package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
  83. package/core/terminal/__pycache__/session_windows.cpython-313.pyc +0 -0
  84. package/core/terminal/session.py +66 -11
  85. package/core/terminal/session_windows.py +211 -0
  86. package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
  87. package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
  88. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  89. package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
  90. package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
  91. package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
  92. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  93. package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
  94. package/core/workflow/flow_authorization.py +2 -1
  95. package/core/workflow/flow_enforcer.py +2 -1
  96. package/core/workflow/marker_cache.py +2 -1
  97. package/core/workflow/research_gate.py +2 -1
  98. package/dashboard/app/pages/index.vue +6 -1
  99. package/installer/cli.js +5 -4
  100. package/installer/core-snapshot.js +53 -0
  101. package/installer/doctor.js +8 -3
  102. package/installer/index.js +29 -6
  103. package/installer/migrate.js +7 -4
  104. package/installer/update.js +13 -0
  105. package/package.json +1 -1
  106. package/pyproject.toml +8 -1
  107. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  108. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  109. package/scripts/dashboard-api.py +69 -33
  110. package/scripts/gateway-ensure.sh +93 -0
  111. package/scripts/knowledge-index.py +35 -2
  112. package/scripts/start-dashboard.ps1 +34 -14
  113. package/scripts/synapse-bridge.py +3 -1
@@ -20,13 +20,14 @@ from dataclasses import dataclass
20
20
  from pathlib import Path
21
21
 
22
22
  from core.shared import safe_session_id as _safe_session_id_module
23
+ from core.shared.temp_paths import arkaos_temp_dir
23
24
 
24
25
 
25
26
  def _resolve_cache_dir() -> Path:
26
27
  override = os.environ.get("ARKA_MARKER_CACHE_DIR", "").strip()
27
28
  if override:
28
29
  return Path(override)
29
- return Path("/tmp/arkaos-flow-marker")
30
+ return arkaos_temp_dir("arkaos-flow-marker")
30
31
 
31
32
 
32
33
  MARKER_CACHE_DIR = _resolve_cache_dir()
@@ -29,6 +29,7 @@ from datetime import datetime, timezone
29
29
  from pathlib import Path
30
30
 
31
31
  from core.shared import safe_session_id as _safe_session_id_module
32
+ from core.shared.temp_paths import arkaos_temp_dir
32
33
  from core.synapse import kb_cache
33
34
 
34
35
  try:
@@ -55,7 +56,7 @@ SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
55
56
  CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
56
57
  BYPASS_AUDIT_PATH = Path.home() / ".arkaos" / "audit" / "kb_first_bypass.log"
57
58
  TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "kb_first.jsonl"
58
- VIOLATION_DIR = Path("/tmp/arkaos-kb-violation")
59
+ VIOLATION_DIR = arkaos_temp_dir("arkaos-kb-violation")
59
60
 
60
61
 
61
62
  def _violation_dir() -> Path:
@@ -131,9 +131,14 @@ function formatIncidentTs(iso: string): string {
131
131
  }
132
132
  }
133
133
 
134
+ const toast = useToast()
134
135
  function copyCommand(cmd: string) {
135
136
  if (typeof navigator !== 'undefined' && navigator.clipboard) {
136
- navigator.clipboard.writeText(cmd).catch(() => { /* ignore */ })
137
+ navigator.clipboard.writeText(cmd)
138
+ .then(() => toast.add({ title: 'Copied to clipboard', description: cmd, icon: 'i-lucide-clipboard-check' }))
139
+ .catch(() => toast.add({ title: 'Copy failed', description: cmd, color: 'error', icon: 'i-lucide-clipboard-x' }))
140
+ } else {
141
+ toast.add({ title: 'Clipboard unavailable', description: cmd, color: 'warning' })
137
142
  }
138
143
  }
139
144
  </script>
package/installer/cli.js CHANGED
@@ -152,7 +152,7 @@ async function main() {
152
152
  const repoRootDash = join(__dirname, "..");
153
153
  const dashCmd = IS_WINDOWS
154
154
  ? `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${join(repoRootDash, "scripts", "start-dashboard.ps1")}"`
155
- : `bash "${repoRootDash}/scripts/start-dashboard.sh"`;
155
+ : `bash "${join(repoRootDash, "scripts", "start-dashboard.sh")}"`;
156
156
  try {
157
157
  execDash(dashCmd, {
158
158
  stdio: "inherit",
@@ -199,13 +199,14 @@ async function main() {
199
199
  }
200
200
 
201
201
  case "index": {
202
- const { execSync } = await import("node:child_process");
203
- const indexArgs = positionals.slice(1).join(" ");
202
+ const { execFileSync } = await import("node:child_process");
204
203
  const repoRoot = join(__dirname, "..");
205
204
  const pyIndex = getArkaosPython();
206
205
  if (!pyIndex) { console.error("No Python found. Run: npx arkaos install"); process.exit(1); }
207
206
  try {
208
- execSync(`"${pyIndex}" "${join(repoRoot, "scripts", "knowledge-index.py")}" ${indexArgs || ""}`, {
207
+ // argv array, not a joined string: vault paths with spaces
208
+ // ("C:\Users\Ana Maria\vault") broke the quoted-string form.
209
+ execFileSync(pyIndex, [join(repoRoot, "scripts", "knowledge-index.py"), ...positionals.slice(1)], {
209
210
  stdio: "inherit",
210
211
  env: { ...process.env, ARKAOS_ROOT: repoRoot },
211
212
  });
@@ -0,0 +1,53 @@
1
+ import {
2
+ existsSync,
3
+ cpSync,
4
+ mkdirSync,
5
+ rmSync,
6
+ renameSync,
7
+ copyFileSync,
8
+ } from "node:fs";
9
+ import { join, basename } from "node:path";
10
+
11
+ // Deploys a stable snapshot of the Python core package (+ VERSION) into
12
+ // ~/.arkaos/lib. `.repo-path` points at whichever npx cache last ran an
13
+ // install/update — a location `npm cache clean` can purge at any time.
14
+ // When that happens, every `arka-py -m core.*` entrypoint (hooks,
15
+ // /arka update, telemetry CLIs) loses the core package unless it runs
16
+ // from a dev checkout. The snapshot is the always-present fallback that
17
+ // bin/arka-py and core/hooks/_shared.py validate against.
18
+ //
19
+ // core/sync/__init__.py is the validation marker: it distinguishes the
20
+ // full package from the cognitive scheduler's minimal core/ copy in
21
+ // ~/.arkaos/core (cognition + workflow only).
22
+ //
23
+ // Shared by installer/index.js (fresh install) and installer/update.js —
24
+ // same single-implementation rationale as hook-lib.js: the v4.3.2
25
+ // regression existed because two deploy loops drifted.
26
+ export function deployCoreSnapshot(arkaosRoot, installDir) {
27
+ const srcCore = join(arkaosRoot, "core");
28
+ if (!existsSync(join(srcCore, "sync", "__init__.py"))) return false;
29
+
30
+ const libDir = join(installDir, "lib");
31
+ const destCore = join(libDir, "core");
32
+ const staging = join(libDir, ".core.staging");
33
+ const previous = join(libDir, ".core.previous");
34
+
35
+ // Stage + swap so a crash at any point never destroys the last good
36
+ // snapshot: the new tree is fully written to staging first, the old
37
+ // snapshot is only moved aside (not deleted) before the swap, and its
38
+ // removal is the final step.
39
+ rmSync(staging, { recursive: true, force: true });
40
+ rmSync(previous, { recursive: true, force: true });
41
+ mkdirSync(staging, { recursive: true });
42
+ cpSync(srcCore, staging, {
43
+ recursive: true,
44
+ filter: (src) => basename(src) !== "__pycache__",
45
+ });
46
+ if (existsSync(destCore)) renameSync(destCore, previous);
47
+ renameSync(staging, destCore);
48
+ rmSync(previous, { recursive: true, force: true });
49
+
50
+ const versionFile = join(arkaosRoot, "VERSION");
51
+ if (existsSync(versionFile)) copyFileSync(versionFile, join(libDir, "VERSION"));
52
+ return true;
53
+ }
@@ -99,15 +99,20 @@ const checks = [
99
99
  },
100
100
  {
101
101
  name: "repo-path",
102
- description: "Repo path reference exists",
102
+ description: "Python core reachable (.repo-path or ~/.arkaos/lib snapshot)",
103
103
  severity: "warn",
104
104
  check: () => {
105
+ // The stable snapshot keeps arka-py working even after
106
+ // `npm cache clean` purges the npx dir .repo-path points at.
107
+ if (existsSync(join(INSTALL_DIR, "lib", "core", "sync", "__init__.py"))) {
108
+ return true;
109
+ }
105
110
  const p = join(INSTALL_DIR, ".repo-path");
106
111
  if (!existsSync(p)) return false;
107
112
  const root = readFileSync(p, "utf-8").trim();
108
- return existsSync(root);
113
+ return existsSync(join(root, "core", "sync", "__init__.py"));
109
114
  },
110
- fix: () => "Run: npx arkaos install --force",
115
+ fix: () => "Run: npx arkaos@latest update (recreates the ~/.arkaos/lib core snapshot)",
111
116
  },
112
117
  {
113
118
  name: "profile",
@@ -7,6 +7,7 @@ import { getRuntimeConfig } from "./detect-runtime.js";
7
7
  import { findSystemPython, ensureVenv, getArkaosPython, getArkaosPip, pipInstall } from "./python-resolver.js";
8
8
  import { IS_WINDOWS, HOOK_EXT } from "./platform.js";
9
9
  import { copyHookLib } from "./hook-lib.js";
10
+ import { deployCoreSnapshot } from "./core-snapshot.js";
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
@@ -211,6 +212,17 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
211
212
  // ═══ Step 11: Create references and profile ═══
212
213
  step(11, 14, "Creating references...");
213
214
  writeFileSync(join(installDir, ".repo-path"), ARKAOS_ROOT);
215
+ // .repo-path points at the npx cache, which `npm cache clean` can purge;
216
+ // the ~/.arkaos/lib snapshot is the validated fallback for arka-py and
217
+ // the Python hooks (see installer/core-snapshot.js). A failed snapshot
218
+ // must never fail the install — the resolvers degrade to .repo-path.
219
+ try {
220
+ if (deployCoreSnapshot(ARKAOS_ROOT, installDir)) {
221
+ ok("Core snapshot deployed to ~/.arkaos/lib (survives npx cache purges)");
222
+ }
223
+ } catch (err) {
224
+ console.log(` ⚠ Core snapshot skipped (${err.message}) — arka-py falls back to .repo-path`);
225
+ }
214
226
  const skillsDir = join(config.skillsDir || join(homedir(), ".claude", "skills"), "arkaos");
215
227
  ensureDir(skillsDir);
216
228
  writeFileSync(join(skillsDir, ".arkaos-root"), ARKAOS_ROOT);
@@ -520,8 +532,12 @@ function installAllPythonDeps(userConfig = {}) {
520
532
  // a failure of one does not block the other.
521
533
  // Ingest (YouTube, PDF, web, audio)
522
534
  const ingestDeps = "yt-dlp pdfplumber beautifulsoup4 requests";
523
- // Dashboard API
524
- const dashboardDeps = "fastapi uvicorn";
535
+ // Dashboard API. python-multipart is required by FastAPI for the
536
+ // knowledge upload endpoint (its absence fails the API import on every
537
+ // platform). pywinpty backs the dashboard terminal on Windows (ConPTY).
538
+ const dashboardDeps = process.platform === "win32"
539
+ ? "fastapi uvicorn python-multipart pywinpty"
540
+ : "fastapi uvicorn python-multipart";
525
541
  // Transcription
526
542
  const transcriptionDeps = "faster-whisper";
527
543
 
@@ -563,8 +579,8 @@ function installAllPythonDeps(userConfig = {}) {
563
579
  // Dashboard deps (optional)
564
580
  if (userConfig.installDashboard !== false) {
565
581
  console.log(" Installing dashboard dependencies...");
566
- if (pipInstall(dashboardDeps, { log, timeout: 60000 })) {
567
- ok("Dashboard API installed (fastapi, uvicorn)");
582
+ if (pipInstall(dashboardDeps, { log, timeout: 120000 })) {
583
+ ok("Dashboard API installed (fastapi, uvicorn, python-multipart)");
568
584
  } else {
569
585
  warn("Dashboard API not installed (optional)");
570
586
  }
@@ -593,12 +609,19 @@ function copyConfigFiles(installDir) {
593
609
  ["config/constitution.yaml", "config/constitution.yaml"],
594
610
  ];
595
611
 
596
- // Standards
612
+ // Standards — files copied individually; subdirectories (e.g. claude-md-overlays)
613
+ // use cpSync recursively because copyFileSync fails with EPERM on directories.
597
614
  const standardsDir = join(ARKAOS_ROOT, "config", "standards");
598
615
  if (existsSync(standardsDir)) {
599
616
  ensureDir(join(installDir, "config", "standards"));
600
617
  for (const f of readdirSync(standardsDir)) {
601
- files.push([`config/standards/${f}`, `config/standards/${f}`]);
618
+ const srcEntry = join(standardsDir, f);
619
+ const destEntry = join(installDir, "config", "standards", f);
620
+ if (statSync(srcEntry).isDirectory()) {
621
+ cpSync(srcEntry, destEntry, { recursive: true });
622
+ } else {
623
+ files.push([`config/standards/${f}`, `config/standards/${f}`]);
624
+ }
602
625
  }
603
626
  }
604
627
 
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, renameSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, renameSync, mkdirSync, writeFileSync, cpSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
@@ -75,7 +75,7 @@ export async function migrate() {
75
75
  const v2Digests = join(V2_PATH, "session-digests");
76
76
  if (existsSync(v1Digests) && !existsSync(v2Digests)) {
77
77
  try {
78
- execSync(`cp -r "${v1Digests}" "${v2Digests}"`, { stdio: "pipe" });
78
+ cpSync(v1Digests, v2Digests, { recursive: true });
79
79
  console.log(" Preserved session digests.");
80
80
  } catch {
81
81
  console.log(" Could not copy session digests.");
@@ -87,7 +87,7 @@ export async function migrate() {
87
87
  const v2Media = join(V2_PATH, "media");
88
88
  if (existsSync(v1Media) && !existsSync(v2Media)) {
89
89
  try {
90
- execSync(`cp -r "${v1Media}" "${v2Media}"`, { stdio: "pipe" });
90
+ cpSync(v1Media, v2Media, { recursive: true });
91
91
  console.log(" Preserved media files.");
92
92
  } catch {
93
93
  console.log(" Could not copy media files.");
@@ -101,7 +101,10 @@ export async function migrate() {
101
101
  } catch (err) {
102
102
  console.error(`\n Migration failed during install: ${err.message}`);
103
103
  console.error(` Your v1 backup is at: ${backupDir}`);
104
- console.error(` To restore: mv "${backupDir}" "${v1Dir}"\n`);
104
+ const restoreHint = process.platform === "win32"
105
+ ? `Move-Item "${backupDir}" "${v1Dir}"`
106
+ : `mv "${backupDir}" "${v1Dir}"`;
107
+ console.error(` To restore: ${restoreHint}\n`);
105
108
  process.exit(1);
106
109
  }
107
110
 
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
  import { ensureVenv, ensureVenvHealthy, getArkaosPython, pipInstall } from "./python-resolver.js";
6
6
  import { copyHookLib } from "./hook-lib.js";
7
+ import { deployCoreSnapshot } from "./core-snapshot.js";
7
8
  import { getRuntimeConfig } from "./detect-runtime.js";
8
9
  import { loadAdapter } from "./index.js";
9
10
  import { migrateUserData, printMigrationReport } from "./migrate-user-data.js";
@@ -455,6 +456,18 @@ export async function update() {
455
456
  // different clone than the original install.
456
457
  console.log(" [7/8] Updating references...");
457
458
  writeFileSync(join(installDir, ".repo-path"), ARKAOS_ROOT);
459
+ // .repo-path points at the npx cache, which `npm cache clean` can purge;
460
+ // refresh the ~/.arkaos/lib snapshot so arka-py and the Python hooks
461
+ // always keep a validated fallback (see installer/core-snapshot.js).
462
+ // A failed snapshot must never fail the update — resolvers degrade to
463
+ // .repo-path (and any previous snapshot is preserved by the safe swap).
464
+ try {
465
+ if (deployCoreSnapshot(ARKAOS_ROOT, installDir)) {
466
+ console.log(" ✓ Core snapshot refreshed in ~/.arkaos/lib");
467
+ }
468
+ } catch (err) {
469
+ console.log(` ⚠ Core snapshot skipped (${err.message}) — arka-py falls back to .repo-path`);
470
+ }
458
471
  const skillsArkaosDir = join(homedir(), ".claude", "skills", "arkaos");
459
472
  if (existsSync(skillsArkaosDir)) {
460
473
  writeFileSync(join(skillsArkaosDir, ".arkaos-root"), ARKAOS_ROOT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "4.3.5",
3
+ "version": "4.4.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 = "4.3.5"
3
+ version = "4.4.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"}
@@ -37,6 +37,7 @@ dashboard = [
37
37
  "uvicorn>=0.32.0",
38
38
  "python-multipart>=0.0.9",
39
39
  "websockets>=12.0",
40
+ "pywinpty>=2.0.0; sys_platform == 'win32'",
40
41
  ]
41
42
  ingest = [
42
43
  "yt-dlp>=2024.0",
@@ -45,6 +46,12 @@ ingest = [
45
46
  "beautifulsoup4>=4.12.0",
46
47
  "requests>=2.31.0",
47
48
  ]
49
+ gateway = [
50
+ # LiteLLM proxy: the Anthropic-compatible fan-out endpoint that makes
51
+ # per-role mixed-provider routing (execution→Ollama, quality→Anthropic)
52
+ # take effect. Only needed when launching with ARKA_GATEWAY=1.
53
+ "litellm[proxy]>=1.50.0",
54
+ ]
48
55
  dev = [
49
56
  "pytest>=8.0",
50
57
  "pytest-cov>=5.0",
@@ -20,6 +20,8 @@ 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 core.shared.temp_paths import arkaos_temp_dir
24
+
23
25
  from fastapi import FastAPI, Query, Request, WebSocket, WebSocketDisconnect
24
26
  from fastapi.middleware.cors import CORSMiddleware
25
27
  from fastapi.responses import FileResponse, JSONResponse
@@ -158,10 +160,10 @@ def overview():
158
160
 
159
161
  skills_count = 0
160
162
  try:
161
- skills_count = int(subprocess.run(
162
- ["find", str(ARKAOS_ROOT / "departments"), "-name", "SKILL.md", "-path", "*/skills/*/SKILL.md"],
163
- capture_output=True, text=True, timeout=5,
164
- ).stdout.strip().count("\n")) + 1
163
+ skills_count = sum(
164
+ 1 for p in (ARKAOS_ROOT / "departments").rglob("SKILL.md")
165
+ if "skills" in p.parts
166
+ )
165
167
  except Exception:
166
168
  skills_count = 250
167
169
 
@@ -1733,7 +1735,7 @@ def health():
1733
1735
  "npx arkaos install")
1734
1736
 
1735
1737
  try:
1736
- subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
1738
+ subprocess.run([sys.executable, "--version"], capture_output=True, timeout=2)
1737
1739
  check("python", True)
1738
1740
  except Exception:
1739
1741
  check("python", False, "Install Python 3.11+")
@@ -2794,6 +2796,18 @@ def _current_version() -> str:
2794
2796
  return "0.0.0"
2795
2797
 
2796
2798
 
2799
+ def _win_shim(cmd: list) -> list:
2800
+ """Wrap npm/npx-style commands so Windows can execute them.
2801
+
2802
+ npm and npx are ``.cmd`` shims; ``subprocess`` (CreateProcess) cannot
2803
+ run them directly ("%1 is not a valid Win32 application"), so route
2804
+ through ``cmd /c`` on Windows. No-op on POSIX.
2805
+ """
2806
+ if os.name == "nt":
2807
+ return ["cmd", "/c", *cmd]
2808
+ return cmd
2809
+
2810
+
2797
2811
  def _npm_latest_version():
2798
2812
  import subprocess
2799
2813
  import time
@@ -2802,7 +2816,7 @@ def _npm_latest_version():
2802
2816
  return _npm_latest_cache["version"]
2803
2817
  try:
2804
2818
  out = subprocess.run(
2805
- ["npm", "view", "arkaos", "version"],
2819
+ _win_shim(["npm", "view", "arkaos", "version"]),
2806
2820
  capture_output=True, text=True, timeout=20,
2807
2821
  )
2808
2822
  latest = (out.stdout or "").strip() or None
@@ -2829,7 +2843,7 @@ def _run_core_update() -> dict:
2829
2843
  import subprocess
2830
2844
  try:
2831
2845
  out = subprocess.run(
2832
- ["npx", "arkaos@latest", "update"],
2846
+ _win_shim(["npx", "arkaos@latest", "update"]),
2833
2847
  capture_output=True, text=True, timeout=600,
2834
2848
  )
2835
2849
  tail = ((out.stdout or "") + (out.stderr or ""))[-2000:]
@@ -2994,30 +3008,49 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2994
3008
  # left it. Sent before the live reader is attached, so the historical
2995
3009
  # prefix always precedes any new output (no interleave, no dup — these
2996
3010
  # bytes were already consumed from the kernel buffer when first read).
2997
- replay = session.scrollback()
2998
- if replay:
2999
- try:
3000
- await ws.send_bytes(replay)
3001
- except Exception:
3002
- await ws.close(code=1011, reason="replay failed")
3003
- return
3004
-
3005
3011
  loop = asyncio.get_event_loop()
3006
3012
  output_queue: asyncio.Queue = asyncio.Queue()
3007
3013
 
3008
- def _on_readable():
3014
+ # POSIX exposes a pollable master fd (add_reader); the Windows ConPTY
3015
+ # backend has none (master_fd == -1) and instead pushes output from its
3016
+ # own reader thread into a registered listener.
3017
+ use_fd_reader = getattr(session, "master_fd", -1) >= 0
3018
+
3019
+ if use_fd_reader:
3020
+ replay = session.scrollback()
3021
+ if replay:
3022
+ try:
3023
+ await ws.send_bytes(replay)
3024
+ except Exception:
3025
+ await ws.close(code=1011, reason="replay failed")
3026
+ return
3027
+
3028
+ def _on_readable():
3029
+ try:
3030
+ data = session.read(8192)
3031
+ except OSError:
3032
+ data = b""
3033
+ if data:
3034
+ output_queue.put_nowait(data)
3035
+
3009
3036
  try:
3010
- data = session.read(8192)
3011
- except OSError:
3012
- data = b""
3013
- if data:
3014
- output_queue.put_nowait(data)
3037
+ loop.add_reader(session.master_fd, _on_readable)
3038
+ except (ValueError, OSError):
3039
+ await ws.close(code=1011, reason="pty unavailable")
3040
+ return
3041
+ else:
3042
+ def _emit(data: bytes):
3043
+ loop.call_soon_threadsafe(output_queue.put_nowait, data)
3015
3044
 
3016
- try:
3017
- loop.add_reader(session.master_fd, _on_readable)
3018
- except (ValueError, OSError):
3019
- await ws.close(code=1011, reason="pty unavailable")
3020
- return
3045
+ # Atomic: snapshot scrollback and start the live feed with no gap.
3046
+ replay = session.attach(_emit)
3047
+ if replay:
3048
+ try:
3049
+ await ws.send_bytes(replay)
3050
+ except Exception:
3051
+ session.set_listener(None)
3052
+ await ws.close(code=1011, reason="replay failed")
3053
+ return
3021
3054
 
3022
3055
  async def pump_to_client():
3023
3056
  while True:
@@ -3060,13 +3093,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
3060
3093
  pass
3061
3094
  finally:
3062
3095
  pump_task.cancel()
3063
- # Only the still-active connection owns the fd reader — a superseded
3064
- # connection tearing down must not remove its replacement's reader.
3096
+ # Only the still-active connection owns the reader — a superseded
3097
+ # connection tearing down must not detach its replacement's reader.
3065
3098
  if _terminal_conns.release(session_id, ws):
3066
- try:
3067
- loop.remove_reader(session.master_fd)
3068
- except (ValueError, OSError):
3069
- pass
3099
+ if use_fd_reader:
3100
+ try:
3101
+ loop.remove_reader(session.master_fd)
3102
+ except (ValueError, OSError):
3103
+ pass
3104
+ else:
3105
+ session.set_listener(None)
3070
3106
 
3071
3107
 
3072
3108
  @app.on_event("startup")
@@ -4233,7 +4269,7 @@ def keys_delete(key_name: str):
4233
4269
 
4234
4270
  @app.get("/api/metrics")
4235
4271
  def metrics():
4236
- metrics_file = Path("/tmp/arkaos-context-cache/hook-metrics.jsonl")
4272
+ metrics_file = arkaos_temp_dir("arkaos-context-cache", "hook-metrics.jsonl")
4237
4273
  if not metrics_file.exists():
4238
4274
  return {"entries": [], "avg_ms": 0}
4239
4275
  entries = []
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ArkaOS — Model-routing gateway ensure
4
+ #
5
+ # Renders the LiteLLM proxy config from ~/.arkaos/models.yaml, ensures the
6
+ # proxy is running, and writes the Claude Code launch env to a file the
7
+ # caller sources. Best-effort: any failure returns non-zero so arka-claude
8
+ # degrades to a plain `claude` launch — a broken gateway never blocks work.
9
+ #
10
+ # Contract:
11
+ # in : $ARKA_PY (interpreter), $ARKAOS_ROOT (repo), env ARKA_GATEWAY_PORT
12
+ # out: writes $ARKAOS_HOME/gateway/launch.env on success; exit 0
13
+ # exit non-zero on any failure (caller must degrade)
14
+ # ============================================================================
15
+ set -uo pipefail
16
+
17
+ ARKAOS_HOME="${ARKAOS_HOME:-$HOME/.arkaos}"
18
+ PORT="${ARKA_GATEWAY_PORT:-4000}"
19
+ GW_DIR="$ARKAOS_HOME/gateway"
20
+ CONFIG="$GW_DIR/config.yaml"
21
+ ENV_FILE="$GW_DIR/launch.env"
22
+ LOG="$GW_DIR/litellm.log"
23
+ LITELLM="$ARKAOS_HOME/venv/bin/litellm"
24
+
25
+ warn() { printf ' \033[1;33m⚠ gateway: %s\033[0m\n' "$1" >&2; }
26
+
27
+ # ─── Preconditions ────────────────────────────────────────────────────────
28
+ [ -n "${ARKA_PY:-}" ] || { warn "ARKA_PY unset"; exit 1; }
29
+ [ -n "${ARKAOS_ROOT:-}" ] || { warn "ARKAOS_ROOT unset"; exit 1; }
30
+ if [ ! -x "$LITELLM" ]; then
31
+ warn "LiteLLM not installed in the ArkaOS venv — run: ~/.arkaos/venv/bin/pip install 'litellm[proxy]'"
32
+ exit 1
33
+ fi
34
+
35
+ # Mode: with an API key -> mixed (quality→Anthropic, execution→Ollama).
36
+ # Without one (subscription users) -> local-only: every route runs on the
37
+ # local Ollama model, keyless. The main arka-claude keeps the subscription.
38
+ LOCAL_FLAG=""
39
+ if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
40
+ LOCAL_FLAG="--local"
41
+ warn "no ANTHROPIC_API_KEY — local-only mode: the whole session runs on the local Ollama model (use plain arka-claude for subscription/quality work)"
42
+ fi
43
+
44
+ mkdir -p "$GW_DIR"
45
+ health() { curl -fsS -m 2 "http://127.0.0.1:$PORT/health/liveliness" >/dev/null 2>&1; }
46
+
47
+ # ─── Reuse a healthy proxy WITHOUT rotating its key ───────────────────────
48
+ # The running proxy validates the ARKA_GATEWAY_KEY it started with; the
49
+ # client reads that same value from the existing launch.env. Minting a new
50
+ # key on reuse would 401 every request, so the reuse path touches neither
51
+ # the key nor launch.env. Changing models.yaml needs ARKA_GATEWAY_RESTART=1.
52
+ if [ "${ARKA_GATEWAY_RESTART:-0}" != "1" ] && health && [ -f "$ENV_FILE" ]; then
53
+ exit 0
54
+ fi
55
+ if health; then # RESTART path — old proxy must go
56
+ pkill -f "litellm .*--port $PORT" 2>/dev/null || true
57
+ sleep 1
58
+ fi
59
+
60
+ # ─── Render config from models.yaml ───────────────────────────────────────
61
+ # shellcheck disable=SC2086 # LOCAL_FLAG is a single controlled token
62
+ if ! PYTHONPATH="$ARKAOS_ROOT" "$ARKA_PY" -m core.runtime.gateway $LOCAL_FLAG > "$CONFIG" 2>/dev/null; then
63
+ if [ -n "$LOCAL_FLAG" ]; then
64
+ warn "could not render gateway config — local-only needs at least one ollama route in models.yaml"
65
+ else
66
+ warn "could not render gateway config from models.yaml"
67
+ fi
68
+ exit 1
69
+ fi
70
+
71
+ # ─── Fresh master key + client launch env (created for THIS proxy) ────────
72
+ MASTER_KEY="$("$ARKA_PY" -c 'import secrets; print("sk-arka-" + secrets.token_hex(16))')"
73
+ export ARKA_GATEWAY_KEY="$MASTER_KEY"
74
+
75
+ if ! PYTHONPATH="$ARKAOS_ROOT" "$ARKA_PY" -m core.runtime.gateway --env "$MASTER_KEY" > "$ENV_FILE" 2>/dev/null; then
76
+ warn "could not render launch env"
77
+ exit 1
78
+ fi
79
+ # The proxy reads ANTHROPIC_API_KEY + ARKA_GATEWAY_KEY from its own env.
80
+ printf 'ARKA_GATEWAY_KEY=%s\n' "$MASTER_KEY" >> "$ENV_FILE"
81
+ chmod 600 "$ENV_FILE" 2>/dev/null || true # bearer token — owner-only
82
+
83
+ # Start in the background; ARKA_GATEWAY_KEY + ANTHROPIC_API_KEY inherited.
84
+ ( ARKA_GATEWAY_KEY="$MASTER_KEY" nohup "$LITELLM" --config "$CONFIG" --port "$PORT" \
85
+ >> "$LOG" 2>&1 & disown 2>/dev/null || true )
86
+
87
+ # ─── Health-wait (bounded) ────────────────────────────────────────────────
88
+ for _ in $(seq 1 30); do
89
+ if health; then exit 0; fi
90
+ sleep 1
91
+ done
92
+ warn "gateway did not become healthy within 30s (see $LOG)"
93
+ exit 1
@@ -14,6 +14,12 @@ import os
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
+ # Windows consoles default to cp1252; progress lines with non-ASCII vault
18
+ # names would raise UnicodeEncodeError and kill the index run silently.
19
+ if hasattr(sys.stdout, "reconfigure"):
20
+ sys.stdout.reconfigure(encoding="utf-8")
21
+ sys.stderr.reconfigure(encoding="utf-8")
22
+
17
23
  ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
18
24
  sys.path.insert(0, str(ARKAOS_ROOT))
19
25
 
@@ -77,13 +83,40 @@ def main() -> int:
77
83
  # Index mode
78
84
  directory = args.vault or args.dir
79
85
  if not directory:
80
- # Auto-detect vault from config
86
+ # Auto-detect vault from config. vault_path may be a template like
87
+ # "${VAULT_PATH}" — resolve it through the canonical path resolver
88
+ # (ARKAOS_VAULT_PATH env / profile.json vaultPath) before testing.
81
89
  config_path = ARKAOS_ROOT / "knowledge" / "obsidian-config.json"
82
90
  if config_path.exists():
83
- config = json.loads(config_path.read_text())
91
+ config = json.loads(config_path.read_text(encoding="utf-8"))
84
92
  vault = config.get("vault_path", "")
93
+ if vault and "${" in vault:
94
+ try:
95
+ from core.runtime.path_resolver import resolve
96
+
97
+ vault = resolve(vault)
98
+ except Exception:
99
+ vault = ""
100
+ if vault and Path(vault).exists():
101
+ directory = vault
102
+ if not args.json_output:
103
+ print(f"Vault from config: {directory}", file=sys.stderr)
104
+
105
+ if not directory:
106
+ # profile.json vaultPath — set by `npx arkaos install` and the
107
+ # authoritative answer to "where is the user's vault".
108
+ profile_path = Path.home() / ".arkaos" / "profile.json"
109
+ if profile_path.exists():
110
+ try:
111
+ vault = json.loads(
112
+ profile_path.read_text(encoding="utf-8")
113
+ ).get("vaultPath", "")
114
+ except (json.JSONDecodeError, OSError):
115
+ vault = ""
85
116
  if vault and Path(vault).exists():
86
117
  directory = vault
118
+ if not args.json_output:
119
+ print(f"Vault from profile: {directory}", file=sys.stderr)
87
120
 
88
121
  if not directory:
89
122
  # Try common vault locations