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.
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/bin/arka-claude +35 -4
- package/bin/arka-py +22 -7
- package/config/hooks/_lib/arka_python.ps1 +31 -0
- package/config/hooks/_lib/arka_python.sh +38 -0
- package/config/hooks/post-tool-use.ps1 +8 -0
- package/config/hooks/post-tool-use.sh +11 -5
- package/config/hooks/pre-tool-use.ps1 +11 -6
- package/config/hooks/pre-tool-use.sh +11 -6
- package/config/hooks/session-start.ps1 +10 -0
- package/config/hooks/session-start.sh +5 -0
- package/config/hooks/stop.ps1 +9 -1
- package/config/hooks/stop.sh +11 -5
- package/config/hooks/user-prompt-submit.ps1 +18 -0
- package/config/hooks/user-prompt-submit.sh +11 -5
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +7 -3
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +2 -1
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +29 -9
- package/core/hooks/stop.py +4 -3
- package/core/hooks/user_prompt_submit.py +5 -4
- package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/indexer.py +1 -1
- package/core/knowledge/vector_store.py +10 -3
- package/core/runtime/__pycache__/model_router.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/model_router.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/model_routing_check.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
- package/core/runtime/gateway/__init__.py +28 -0
- package/core/runtime/gateway/__main__.py +30 -0
- package/core/runtime/gateway/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/litellm_config.cpython-312.pyc +0 -0
- package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/litellm_config.cpython-314.pyc +0 -0
- package/core/runtime/gateway/litellm_config.py +205 -0
- package/core/runtime/model_routing_check.py +71 -0
- package/core/runtime/model_routing_context.py +22 -1
- package/core/runtime/native_usage.py +2 -1
- package/core/shared/__pycache__/temp_paths.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/temp_paths.cpython-314.pyc +0 -0
- package/core/shared/temp_paths.py +40 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
- package/core/synapse/kb_cache.py +3 -2
- package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/ai_mcp_decider.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/content_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/policy_loader.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
- package/core/sync/agent_provisioner.py +5 -5
- package/core/sync/ai_mcp_decider.py +2 -2
- package/core/sync/content_syncer.py +14 -14
- package/core/sync/descriptor_syncer.py +2 -2
- package/core/sync/discovery.py +4 -4
- package/core/sync/engine.py +4 -4
- package/core/sync/manifest.py +1 -1
- package/core/sync/mcp_optimizer.py +5 -5
- package/core/sync/mcp_syncer.py +3 -3
- package/core/sync/policy_loader.py +1 -1
- package/core/sync/reporter.py +1 -1
- package/core/sync/settings_syncer.py +2 -2
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session_windows.cpython-313.pyc +0 -0
- package/core/terminal/session.py +66 -11
- package/core/terminal/session_windows.py +211 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
- package/core/workflow/flow_authorization.py +2 -1
- package/core/workflow/flow_enforcer.py +2 -1
- package/core/workflow/marker_cache.py +2 -1
- package/core/workflow/research_gate.py +2 -1
- package/dashboard/app/pages/index.vue +6 -1
- package/installer/cli.js +5 -4
- package/installer/core-snapshot.js +53 -0
- package/installer/doctor.js +8 -3
- package/installer/index.js +29 -6
- package/installer/migrate.js +7 -4
- package/installer/update.js +13 -0
- package/package.json +1 -1
- package/pyproject.toml +8 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +69 -33
- package/scripts/gateway-ensure.sh +93 -0
- package/scripts/knowledge-index.py +35 -2
- package/scripts/start-dashboard.ps1 +34 -14
- 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
|
|
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 =
|
|
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)
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/installer/doctor.js
CHANGED
|
@@ -99,15 +99,20 @@ const checks = [
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: "repo-path",
|
|
102
|
-
description: "
|
|
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
|
|
115
|
+
fix: () => "Run: npx arkaos@latest update (recreates the ~/.arkaos/lib core snapshot)",
|
|
111
116
|
},
|
|
112
117
|
{
|
|
113
118
|
name: "profile",
|
package/installer/index.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
package/installer/migrate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/installer/update.js
CHANGED
|
@@ -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
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "arkaos-core"
|
|
3
|
-
version = "4.
|
|
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",
|
|
Binary file
|
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -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 =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
3011
|
-
except OSError:
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
|
3064
|
-
# connection tearing down must not
|
|
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
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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 =
|
|
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
|