arkaos 4.3.6 → 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/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_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/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
- 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 +2 -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/knowledge-index.py +35 -2
- package/scripts/start-dashboard.ps1 +34 -14
- package/scripts/synapse-bridge.py +3 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Windows PTY session backend (ConPTY via pywinpty).
|
|
2
|
+
|
|
3
|
+
The POSIX backend in ``session.py`` builds on ``pty.fork`` + fcntl /
|
|
4
|
+
termios, none of which exist on Windows. This module provides the same
|
|
5
|
+
``TerminalSession`` surface on top of pywinpty's ConPTY wrapper, so the
|
|
6
|
+
dashboard terminal works on Windows. ``TerminalSessionManager.create``
|
|
7
|
+
selects it when ``os.name == "nt"``.
|
|
8
|
+
|
|
9
|
+
IO model
|
|
10
|
+
--------
|
|
11
|
+
pywinpty's ``read`` is blocking and there is no pollable fd, so the POSIX
|
|
12
|
+
approach (``loop.add_reader(master_fd)``) does not apply. Instead each
|
|
13
|
+
session owns one daemon reader thread that drains the pty for the
|
|
14
|
+
session's whole lifetime: it always appends to the bounded scrollback
|
|
15
|
+
(so a client reconnecting after navigating away replays what it missed)
|
|
16
|
+
and, when a WebSocket is attached, forwards each chunk to that single
|
|
17
|
+
``listener``. Decoupling the reader from the connection avoids two
|
|
18
|
+
readers racing on the same pty across a reload/supersede.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Callable, Optional
|
|
26
|
+
|
|
27
|
+
from core.terminal import audit
|
|
28
|
+
|
|
29
|
+
DEFAULT_SCROLLBACK_BYTES = 512 * 1024
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WindowsTerminalSession:
|
|
33
|
+
"""A ConPTY-backed shell + the bookkeeping the manager relies on.
|
|
34
|
+
|
|
35
|
+
Mirrors the public surface of ``session.TerminalSession``:
|
|
36
|
+
``read``/``write``/``resize``/``scrollback``/``is_alive``/``kill``/
|
|
37
|
+
``to_dict`` plus the ``session_id``/``shell``/``cwd``/``exit_code``
|
|
38
|
+
attributes. ``master_fd`` is always ``-1`` — the dashboard WebSocket
|
|
39
|
+
handler uses that to pick the thread-listener pump instead of
|
|
40
|
+
``add_reader``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
session_id: str,
|
|
46
|
+
shell: str,
|
|
47
|
+
cwd: str,
|
|
48
|
+
cols: int = 120,
|
|
49
|
+
rows: int = 32,
|
|
50
|
+
scrollback_bytes: int = DEFAULT_SCROLLBACK_BYTES,
|
|
51
|
+
) -> None:
|
|
52
|
+
import winpty # lazy: the dependency is only needed on Windows
|
|
53
|
+
|
|
54
|
+
self.session_id = session_id
|
|
55
|
+
self.shell = shell
|
|
56
|
+
self.cwd = cwd
|
|
57
|
+
self.created_at = time.time()
|
|
58
|
+
self.last_activity = time.monotonic()
|
|
59
|
+
self.exit_code: Optional[int] = None
|
|
60
|
+
self.title = ""
|
|
61
|
+
self.scrollback_max = max(0, int(scrollback_bytes))
|
|
62
|
+
self._scrollback = bytearray()
|
|
63
|
+
self._closed = False
|
|
64
|
+
self.master_fd = -1 # no pollable fd on Windows
|
|
65
|
+
|
|
66
|
+
# ConPTY dimensions are (rows, cols).
|
|
67
|
+
self._proc = winpty.PtyProcess.spawn(
|
|
68
|
+
shell,
|
|
69
|
+
cwd=cwd,
|
|
70
|
+
dimensions=(max(1, int(rows)), max(1, int(cols))),
|
|
71
|
+
)
|
|
72
|
+
self.pid = self._proc.pid
|
|
73
|
+
|
|
74
|
+
self._listener: Optional[Callable[[bytes], None]] = None
|
|
75
|
+
self._lock = threading.Lock()
|
|
76
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True)
|
|
77
|
+
self._reader.start()
|
|
78
|
+
|
|
79
|
+
# -- reader thread ------------------------------------------------------
|
|
80
|
+
def _read_loop(self) -> None:
|
|
81
|
+
"""Drain the pty for the session lifetime; feed scrollback + listener."""
|
|
82
|
+
while not self._closed:
|
|
83
|
+
try:
|
|
84
|
+
chunk = self._proc.read(8192)
|
|
85
|
+
except EOFError:
|
|
86
|
+
break
|
|
87
|
+
except OSError:
|
|
88
|
+
break
|
|
89
|
+
if not chunk:
|
|
90
|
+
if not self._proc_alive():
|
|
91
|
+
break
|
|
92
|
+
continue
|
|
93
|
+
raw = chunk.encode("utf-8", "replace") if isinstance(chunk, str) else bytes(chunk)
|
|
94
|
+
self.last_activity = time.monotonic()
|
|
95
|
+
# Record + capture the listener under the lock so attach() can
|
|
96
|
+
# snapshot scrollback and start receiving with no gap and no dup.
|
|
97
|
+
with self._lock:
|
|
98
|
+
self._record(raw)
|
|
99
|
+
listener = self._listener
|
|
100
|
+
if listener is not None:
|
|
101
|
+
try:
|
|
102
|
+
listener(raw)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
self._closed = True
|
|
106
|
+
|
|
107
|
+
def attach(self, listener: Callable[[bytes], None]) -> bytes:
|
|
108
|
+
"""Atomically snapshot scrollback and register the live output sink.
|
|
109
|
+
|
|
110
|
+
Returns the scrollback to replay. Any chunk recorded after this
|
|
111
|
+
snapshot is delivered to ``listener`` (no gap, no duplicate).
|
|
112
|
+
"""
|
|
113
|
+
with self._lock:
|
|
114
|
+
snapshot = bytes(self._scrollback)
|
|
115
|
+
self._listener = listener
|
|
116
|
+
return snapshot
|
|
117
|
+
|
|
118
|
+
def set_listener(self, listener: Optional[Callable[[bytes], None]]) -> None:
|
|
119
|
+
"""Register (or clear with ``None``) the live output sink."""
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._listener = listener
|
|
122
|
+
|
|
123
|
+
def _proc_alive(self) -> bool:
|
|
124
|
+
try:
|
|
125
|
+
return self._proc.isalive()
|
|
126
|
+
except OSError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# -- output -------------------------------------------------------------
|
|
130
|
+
def read(self, max_bytes: int = 4096) -> bytes:
|
|
131
|
+
"""Compatibility shim: a session-owned thread does the real reading.
|
|
132
|
+
|
|
133
|
+
The POSIX backend is polled via ``read``; on Windows reading happens
|
|
134
|
+
in ``_read_loop``, so direct callers get an empty read.
|
|
135
|
+
"""
|
|
136
|
+
return b""
|
|
137
|
+
|
|
138
|
+
def _record(self, data: bytes) -> None:
|
|
139
|
+
if self.scrollback_max <= 0:
|
|
140
|
+
return
|
|
141
|
+
self._scrollback += data
|
|
142
|
+
if len(self._scrollback) > self.scrollback_max:
|
|
143
|
+
del self._scrollback[: -self.scrollback_max]
|
|
144
|
+
|
|
145
|
+
def scrollback(self) -> bytes:
|
|
146
|
+
return bytes(self._scrollback)
|
|
147
|
+
|
|
148
|
+
# -- input / control ----------------------------------------------------
|
|
149
|
+
def write(self, data: bytes) -> int:
|
|
150
|
+
if self._closed or not data:
|
|
151
|
+
return 0
|
|
152
|
+
text = (
|
|
153
|
+
data.decode("utf-8", "replace")
|
|
154
|
+
if isinstance(data, (bytes, bytearray))
|
|
155
|
+
else str(data)
|
|
156
|
+
)
|
|
157
|
+
try:
|
|
158
|
+
self._proc.write(text)
|
|
159
|
+
except (OSError, EOFError):
|
|
160
|
+
self._closed = True
|
|
161
|
+
return 0
|
|
162
|
+
self.last_activity = time.monotonic()
|
|
163
|
+
return len(data)
|
|
164
|
+
|
|
165
|
+
def resize(self, cols: int, rows: int) -> None:
|
|
166
|
+
if self._closed:
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
self._proc.setwinsize(max(1, int(rows)), max(1, int(cols)))
|
|
170
|
+
except OSError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def is_alive(self) -> bool:
|
|
174
|
+
if self._closed:
|
|
175
|
+
return False
|
|
176
|
+
if not self._proc_alive():
|
|
177
|
+
self.exit_code = getattr(self._proc, "exitstatus", None)
|
|
178
|
+
self._closed = True
|
|
179
|
+
return False
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def kill(self, sig: Optional[int] = None) -> None:
|
|
183
|
+
self._closed = True
|
|
184
|
+
self._listener = None
|
|
185
|
+
try:
|
|
186
|
+
self._proc.terminate(force=True)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def _close_fd(self) -> None:
|
|
191
|
+
self._listener = None
|
|
192
|
+
if not self._closed:
|
|
193
|
+
try:
|
|
194
|
+
self._proc.terminate(force=True)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
self._scrollback.clear()
|
|
198
|
+
self._closed = True
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict[str, Any]:
|
|
201
|
+
idle_s = max(0.0, time.monotonic() - self.last_activity)
|
|
202
|
+
return {
|
|
203
|
+
"session_id": self.session_id,
|
|
204
|
+
"shell": self.shell,
|
|
205
|
+
"cwd": self.cwd,
|
|
206
|
+
"title": self.title or self.session_id,
|
|
207
|
+
"created_at": self.created_at,
|
|
208
|
+
"idle_seconds": round(idle_s, 1),
|
|
209
|
+
"alive": self.is_alive(),
|
|
210
|
+
"exit_code": self.exit_code,
|
|
211
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -42,6 +42,7 @@ from dataclasses import dataclass
|
|
|
42
42
|
from pathlib import Path
|
|
43
43
|
|
|
44
44
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
45
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
45
46
|
|
|
46
47
|
_safe_session_id = _safe_session_id_module.safe_session_id
|
|
47
48
|
|
|
@@ -63,7 +64,7 @@ class GraceState:
|
|
|
63
64
|
|
|
64
65
|
def _base_dir() -> Path:
|
|
65
66
|
override = os.environ.get("ARKA_FLOW_AUTH_DIR", "").strip()
|
|
66
|
-
return Path(override) if override else
|
|
67
|
+
return Path(override) if override else arkaos_temp_dir("arkaos-flow-auth")
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def _auth_path(session_id: str) -> Path | None:
|
|
@@ -25,6 +25,7 @@ from datetime import datetime, timezone
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
27
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
28
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
28
29
|
from core.workflow import flow_authorization, marker_cache
|
|
29
30
|
|
|
30
31
|
try:
|
|
@@ -210,7 +211,7 @@ ASSISTANT_WINDOW = 20
|
|
|
210
211
|
CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
|
|
211
212
|
BYPASS_AUDIT_PATH = Path.home() / ".arkaos" / "audit" / "bypass.log"
|
|
212
213
|
TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "enforcement.jsonl"
|
|
213
|
-
FLOW_REQUIRED_DIR =
|
|
214
|
+
FLOW_REQUIRED_DIR = arkaos_temp_dir("arkaos-wf-required")
|
|
214
215
|
|
|
215
216
|
|
|
216
217
|
@dataclass
|
|
@@ -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",
|
|
Binary file
|
|
Binary file
|