arkaos 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -107
- package/VERSION +1 -1
- package/config/hooks/cwd-changed.ps1 +144 -0
- package/config/hooks/post-tool-use.ps1 +347 -0
- package/config/hooks/post-tool-use.sh +6 -6
- package/config/hooks/pre-compact.ps1 +238 -0
- package/config/hooks/pre-compact.sh +10 -6
- package/config/hooks/session-start.ps1 +109 -0
- package/config/hooks/session-start.sh +1 -1
- package/config/hooks/user-prompt-submit.ps1 +287 -0
- package/config/hooks/user-prompt-submit.sh +5 -2
- package/config/statusline.ps1 +160 -0
- package/core/cognition/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/capture/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/capture/__pycache__/collector.cpython-313.pyc +0 -0
- package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
- package/core/cognition/insights/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/insights/__pycache__/store.cpython-313.pyc +0 -0
- package/core/cognition/memory/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/memory/__pycache__/obsidian.cpython-313.pyc +0 -0
- package/core/cognition/memory/__pycache__/schemas.cpython-313.pyc +0 -0
- package/core/cognition/memory/__pycache__/vector.cpython-313.pyc +0 -0
- package/core/cognition/memory/__pycache__/writer.cpython-313.pyc +0 -0
- package/core/cognition/research/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/research/__pycache__/profiler.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/cli.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/__pycache__/platform.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +77 -21
- package/core/cognition/scheduler/platform.py +43 -12
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/vector_store.py +50 -25
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/layers.py +2 -2
- package/installer/adapters/claude-code.js +72 -45
- package/installer/cli.js +19 -6
- package/installer/doctor.js +130 -18
- package/installer/index.js +592 -149
- package/installer/platform.js +20 -0
- package/installer/prompts.js +109 -5
- package/installer/python-resolver.js +251 -0
- package/installer/update.js +497 -62
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/scripts/start-dashboard.ps1 +271 -0
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
|
+
import time as time_mod
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
13
|
from datetime import datetime, time
|
|
13
14
|
from pathlib import Path
|
|
@@ -113,9 +114,34 @@ class ArkaScheduler:
|
|
|
113
114
|
and current_time.minute == schedule.run_time.minute
|
|
114
115
|
)
|
|
115
116
|
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _resolve_claude_binary() -> str:
|
|
119
|
+
"""Resolve the Claude CLI binary by checking known install locations.
|
|
120
|
+
|
|
121
|
+
In daemon context (launchd/systemd/schtasks), PATH is minimal and shell
|
|
122
|
+
aliases don't exist, so we check absolute paths first.
|
|
123
|
+
"""
|
|
124
|
+
home = Path.home()
|
|
125
|
+
candidates = [
|
|
126
|
+
home / ".local" / "bin" / "claude",
|
|
127
|
+
home / ".arkaos" / "bin" / "arka-claude",
|
|
128
|
+
]
|
|
129
|
+
for candidate in candidates:
|
|
130
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
131
|
+
return str(candidate)
|
|
132
|
+
# Fallback to PATH lookup (works in interactive shells)
|
|
133
|
+
found = shutil.which("claude") or shutil.which("arka-claude")
|
|
134
|
+
if found:
|
|
135
|
+
return found
|
|
136
|
+
raise FileNotFoundError(
|
|
137
|
+
"Claude CLI not found. Checked: "
|
|
138
|
+
+ ", ".join(str(c) for c in candidates)
|
|
139
|
+
+ " and PATH lookup."
|
|
140
|
+
)
|
|
141
|
+
|
|
116
142
|
def _build_command(self, schedule: ScheduleConfig) -> list[str]:
|
|
117
143
|
"""Build the Claude CLI invocation for a schedule."""
|
|
118
|
-
claude_bin =
|
|
144
|
+
claude_bin = self._resolve_claude_binary()
|
|
119
145
|
prompt_path = os.path.expanduser(schedule.prompt_file)
|
|
120
146
|
prompt_content = Path(prompt_path).read_text(encoding="utf-8")
|
|
121
147
|
return [claude_bin, "-p", prompt_content, "--dangerously-skip-permissions"]
|
|
@@ -131,33 +157,63 @@ class ArkaScheduler:
|
|
|
131
157
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
132
158
|
return log_dir / f"{today}.log"
|
|
133
159
|
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _daemon_env() -> dict[str, str]:
|
|
162
|
+
"""Build an environment with PATH that includes known Claude locations.
|
|
163
|
+
|
|
164
|
+
Daemons (launchd/systemd) inherit a minimal PATH. We extend it so that
|
|
165
|
+
any child processes spawned by Claude can also find common tools.
|
|
166
|
+
"""
|
|
167
|
+
home = str(Path.home())
|
|
168
|
+
extra_paths = [
|
|
169
|
+
os.path.join(home, ".local", "bin"),
|
|
170
|
+
os.path.join(home, ".arkaos", "bin"),
|
|
171
|
+
"/usr/local/bin",
|
|
172
|
+
]
|
|
173
|
+
env = os.environ.copy()
|
|
174
|
+
existing = env.get("PATH", "/usr/bin:/bin")
|
|
175
|
+
env["PATH"] = ":".join(extra_paths) + ":" + existing
|
|
176
|
+
return env
|
|
177
|
+
|
|
178
|
+
def _run_attempt(
|
|
179
|
+
self, cmd: list[str], log_file: Path, attempt: int, timeout: int,
|
|
180
|
+
) -> bool:
|
|
181
|
+
"""Run a single attempt of a scheduled command. Returns True on success."""
|
|
182
|
+
env = self._daemon_env()
|
|
183
|
+
with open(log_file, "a", encoding="utf-8") as lf:
|
|
184
|
+
lf.write(f"\n--- attempt {attempt} at {datetime.now().isoformat()} ---\n")
|
|
185
|
+
lf.write(f"cmd: {cmd[0]}\n")
|
|
186
|
+
try:
|
|
187
|
+
result = subprocess.run(
|
|
188
|
+
cmd, stdout=lf, stderr=lf, timeout=timeout, env=env,
|
|
189
|
+
)
|
|
190
|
+
if result.returncode == 0:
|
|
191
|
+
return True
|
|
192
|
+
lf.write(f"exit code: {result.returncode}\n")
|
|
193
|
+
except subprocess.TimeoutExpired:
|
|
194
|
+
lf.write("TIMEOUT\n")
|
|
195
|
+
except Exception as exc: # noqa: BLE001
|
|
196
|
+
lf.write(f"ERROR: {exc}\n")
|
|
197
|
+
return False
|
|
198
|
+
|
|
134
199
|
def execute(self, schedule: ScheduleConfig) -> bool:
|
|
135
|
-
"""Run the scheduled command
|
|
200
|
+
"""Run the scheduled command with retries and backoff."""
|
|
136
201
|
log_file = self._log_path(schedule.command)
|
|
137
|
-
|
|
138
|
-
attempts = 0
|
|
202
|
+
timeout = schedule.timeout_minutes * 60
|
|
139
203
|
max_attempts = schedule.max_retries + 1 if schedule.retry_on_fail else 1
|
|
140
204
|
|
|
141
|
-
|
|
142
|
-
attempts += 1
|
|
205
|
+
try:
|
|
143
206
|
cmd = self._build_command(schedule)
|
|
207
|
+
except FileNotFoundError as exc:
|
|
144
208
|
with open(log_file, "a", encoding="utf-8") as lf:
|
|
145
|
-
lf.write(f"\n---
|
|
146
|
-
|
|
147
|
-
result = subprocess.run(
|
|
148
|
-
cmd,
|
|
149
|
-
stdout=lf,
|
|
150
|
-
stderr=lf,
|
|
151
|
-
timeout=timeout_seconds,
|
|
152
|
-
)
|
|
153
|
-
if result.returncode == 0:
|
|
154
|
-
return True
|
|
155
|
-
lf.write(f"exit code: {result.returncode}\n")
|
|
156
|
-
except subprocess.TimeoutExpired:
|
|
157
|
-
lf.write("TIMEOUT\n")
|
|
158
|
-
except Exception as exc: # noqa: BLE001
|
|
159
|
-
lf.write(f"ERROR: {exc}\n")
|
|
209
|
+
lf.write(f"\n--- at {datetime.now().isoformat()} ---\nFATAL: {exc}\n")
|
|
210
|
+
return False
|
|
160
211
|
|
|
212
|
+
for attempt in range(1, max_attempts + 1):
|
|
213
|
+
if self._run_attempt(cmd, log_file, attempt, timeout):
|
|
214
|
+
return True
|
|
215
|
+
if attempt < max_attempts:
|
|
216
|
+
time_mod.sleep(30 * attempt)
|
|
161
217
|
return False
|
|
162
218
|
|
|
163
219
|
# ------------------------------------------------------------------
|
|
@@ -15,9 +15,30 @@ def _default_daemon_script() -> str:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _python_executable() -> str:
|
|
18
|
+
"""Resolve Python, preferring the ArkaOS venv over random system pythons.
|
|
19
|
+
|
|
20
|
+
shutil.which("python3") in a daemon context may pick up a venv from an
|
|
21
|
+
unrelated project (e.g., ai-jarvis). We check the ArkaOS venv first.
|
|
22
|
+
"""
|
|
23
|
+
home = Path.home()
|
|
24
|
+
# Check both common venv directory names
|
|
25
|
+
for venv_dir in ("venv", ".venv"):
|
|
26
|
+
arkaos_venv = home / ".arkaos" / venv_dir / "bin" / "python3"
|
|
27
|
+
if arkaos_venv.is_file():
|
|
28
|
+
return str(arkaos_venv)
|
|
29
|
+
# Windows venv layout
|
|
30
|
+
arkaos_venv_win = home / ".arkaos" / venv_dir / "Scripts" / "python.exe"
|
|
31
|
+
if arkaos_venv_win.is_file():
|
|
32
|
+
return str(arkaos_venv_win)
|
|
18
33
|
return shutil.which("python3") or sys.executable
|
|
19
34
|
|
|
20
35
|
|
|
36
|
+
def _daemon_path_value() -> str:
|
|
37
|
+
"""Return a PATH string with known Claude CLI locations for daemon contexts."""
|
|
38
|
+
home = str(Path.home())
|
|
39
|
+
return f"{home}/.local/bin:{home}/.arkaos/bin:/usr/local/bin:/usr/bin:/bin"
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
class PlatformAdapter(ABC):
|
|
22
43
|
"""Abstract base for OS-level service management."""
|
|
23
44
|
|
|
@@ -55,27 +76,35 @@ class MacOSAdapter(PlatformAdapter):
|
|
|
55
76
|
def _plist_path(self) -> str:
|
|
56
77
|
return str(Path(self._plist_dir) / f"{self._LABEL}.plist")
|
|
57
78
|
|
|
79
|
+
def _plist_env_block(self) -> str:
|
|
80
|
+
"""Generate the EnvironmentVariables section of the plist."""
|
|
81
|
+
home = str(Path.home())
|
|
82
|
+
return (
|
|
83
|
+
"\t<key>EnvironmentVariables</key>\n\t<dict>\n"
|
|
84
|
+
f"\t\t<key>PATH</key>\n\t\t<string>{_daemon_path_value()}</string>\n"
|
|
85
|
+
f"\t\t<key>HOME</key>\n\t\t<string>{home}</string>\n"
|
|
86
|
+
"\t</dict>\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
58
89
|
def _generate_plist(self) -> str:
|
|
90
|
+
"""Generate the launchd plist XML for the scheduler daemon."""
|
|
59
91
|
python = _python_executable()
|
|
60
92
|
log_dir = Path.home() / ".arkaos" / "logs"
|
|
61
|
-
stdout = str(log_dir / "scheduler-stdout.log")
|
|
62
|
-
stderr = str(log_dir / "scheduler-stderr.log")
|
|
63
93
|
return (
|
|
64
94
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
65
95
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"'
|
|
66
96
|
' "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
|
67
|
-
'<plist version="1.0">\n'
|
|
68
|
-
"<dict>\n"
|
|
97
|
+
'<plist version="1.0">\n<dict>\n'
|
|
69
98
|
f"\t<key>Label</key>\n\t<string>{self._LABEL}</string>\n"
|
|
70
|
-
f"\t<key>ProgramArguments</key>\n"
|
|
71
|
-
f"\t
|
|
72
|
-
f"\
|
|
73
|
-
|
|
99
|
+
f"\t<key>ProgramArguments</key>\n\t<array>\n"
|
|
100
|
+
f"\t\t<string>{python}</string>\n"
|
|
101
|
+
f"\t\t<string>{self._daemon_script}</string>\n\t</array>\n"
|
|
102
|
+
+ self._plist_env_block()
|
|
103
|
+
+ "\t<key>RunAtLoad</key>\n\t<true/>\n"
|
|
74
104
|
"\t<key>KeepAlive</key>\n\t<true/>\n"
|
|
75
|
-
f"\t<key>StandardOutPath</key>\n\t<string>{stdout}</string>\n"
|
|
76
|
-
f"\t<key>StandardErrorPath</key>\n\t<string>{stderr}</string>\n"
|
|
77
|
-
"</dict>\n"
|
|
78
|
-
"</plist>\n"
|
|
105
|
+
f"\t<key>StandardOutPath</key>\n\t<string>{log_dir / 'scheduler-stdout.log'}</string>\n"
|
|
106
|
+
f"\t<key>StandardErrorPath</key>\n\t<string>{log_dir / 'scheduler-stderr.log'}</string>\n"
|
|
107
|
+
"</dict>\n</plist>\n"
|
|
79
108
|
)
|
|
80
109
|
|
|
81
110
|
def install_service(self) -> bool:
|
|
@@ -143,6 +172,7 @@ class LinuxAdapter(PlatformAdapter):
|
|
|
143
172
|
return str(Path(self._service_dir) / self._SERVICE_NAME)
|
|
144
173
|
|
|
145
174
|
def _generate_unit(self) -> str:
|
|
175
|
+
"""Generate the systemd unit file for the scheduler daemon."""
|
|
146
176
|
python = _python_executable()
|
|
147
177
|
return (
|
|
148
178
|
"[Unit]\n"
|
|
@@ -150,6 +180,7 @@ class LinuxAdapter(PlatformAdapter):
|
|
|
150
180
|
"After=network.target\n\n"
|
|
151
181
|
"[Service]\n"
|
|
152
182
|
"Type=simple\n"
|
|
183
|
+
f"Environment=PATH={_daemon_path_value()}\n"
|
|
153
184
|
f"ExecStart={python} {self._daemon_script}\n"
|
|
154
185
|
"Restart=on-failure\n"
|
|
155
186
|
"RestartSec=60\n\n"
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Vector store — SQLite-
|
|
1
|
+
"""Vector store — SQLite-vec backed semantic search.
|
|
2
2
|
|
|
3
3
|
Stores document chunks with embeddings for fast similarity search.
|
|
4
|
-
Graceful degradation: works without sqlite-
|
|
4
|
+
Graceful degradation: works without sqlite-vec (brute-force fallback).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
@@ -13,25 +13,25 @@ from typing import Any, Optional
|
|
|
13
13
|
from core.knowledge.embedder import embed, embed_batch, EMBEDDING_DIMS
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
"""Try to load sqlite-
|
|
16
|
+
def _load_vec(db: sqlite3.Connection) -> bool:
|
|
17
|
+
"""Try to load sqlite-vec extension."""
|
|
18
18
|
try:
|
|
19
19
|
db.enable_load_extension(True)
|
|
20
|
-
import
|
|
21
|
-
|
|
20
|
+
import sqlite_vec
|
|
21
|
+
sqlite_vec.load(db)
|
|
22
22
|
return True
|
|
23
23
|
except (ImportError, Exception):
|
|
24
24
|
return False
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class VectorStore:
|
|
28
|
-
"""SQLite-
|
|
28
|
+
"""SQLite-vec backed vector store for knowledge retrieval."""
|
|
29
29
|
|
|
30
30
|
def __init__(self, db_path: str | Path = ":memory:") -> None:
|
|
31
31
|
self._db_path = str(db_path)
|
|
32
32
|
self._db = sqlite3.connect(self._db_path)
|
|
33
33
|
self._db.row_factory = sqlite3.Row
|
|
34
|
-
self.
|
|
34
|
+
self._vec_available = _load_vec(self._db)
|
|
35
35
|
self._init_schema()
|
|
36
36
|
|
|
37
37
|
def _init_schema(self) -> None:
|
|
@@ -50,15 +50,39 @@ class VectorStore:
|
|
|
50
50
|
CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source);
|
|
51
51
|
CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(file_hash);
|
|
52
52
|
""")
|
|
53
|
-
|
|
53
|
+
self._migrate_vss_to_vec()
|
|
54
|
+
if self._vec_available:
|
|
54
55
|
try:
|
|
55
56
|
self._db.execute(
|
|
56
|
-
f"CREATE VIRTUAL TABLE IF NOT EXISTS
|
|
57
|
+
f"CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(embedding float[{EMBEDDING_DIMS}])"
|
|
57
58
|
)
|
|
58
59
|
except Exception:
|
|
59
|
-
self.
|
|
60
|
+
self._vec_available = False
|
|
60
61
|
self._db.commit()
|
|
61
62
|
|
|
63
|
+
def _migrate_vss_to_vec(self) -> None:
|
|
64
|
+
"""Drop legacy sqlite-vss tables if present.
|
|
65
|
+
|
|
66
|
+
The vss_chunks virtual table used a different schema and query
|
|
67
|
+
syntax that is incompatible with sqlite-vec. Dropping it forces a
|
|
68
|
+
clean re-index on the next /arka index invocation. The chunks
|
|
69
|
+
table (with raw embeddings) is preserved — only the virtual table
|
|
70
|
+
index is removed.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
tables = [
|
|
74
|
+
r[0]
|
|
75
|
+
for r in self._db.execute(
|
|
76
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'vss_%'"
|
|
77
|
+
).fetchall()
|
|
78
|
+
]
|
|
79
|
+
if tables:
|
|
80
|
+
for t in tables:
|
|
81
|
+
self._db.execute(f"DROP TABLE IF EXISTS {t}")
|
|
82
|
+
self._db.commit()
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
62
86
|
def index_chunks(
|
|
63
87
|
self,
|
|
64
88
|
texts: list[str],
|
|
@@ -90,9 +114,9 @@ class VectorStore:
|
|
|
90
114
|
(text, heading, source, file_hash, meta_json, emb_blob),
|
|
91
115
|
)
|
|
92
116
|
|
|
93
|
-
if self.
|
|
117
|
+
if self._vec_available and emb_blob:
|
|
94
118
|
self._db.execute(
|
|
95
|
-
"INSERT INTO
|
|
119
|
+
"INSERT INTO vec_chunks (rowid, embedding) VALUES (?, ?)",
|
|
96
120
|
(cursor.lastrowid, emb_blob),
|
|
97
121
|
)
|
|
98
122
|
count += 1
|
|
@@ -112,23 +136,24 @@ class VectorStore:
|
|
|
112
136
|
|
|
113
137
|
query_emb = embed(query)
|
|
114
138
|
|
|
115
|
-
if query_emb and self.
|
|
139
|
+
if query_emb and self._vec_available:
|
|
116
140
|
try:
|
|
117
|
-
return self.
|
|
141
|
+
return self._vec_search(query_emb, top_k)
|
|
118
142
|
except Exception:
|
|
119
143
|
return self._keyword_search(query, top_k)
|
|
120
144
|
|
|
121
145
|
# Fallback: keyword search
|
|
122
146
|
return self._keyword_search(query, top_k)
|
|
123
147
|
|
|
124
|
-
def
|
|
125
|
-
"""Vector similarity search via sqlite-
|
|
148
|
+
def _vec_search(self, query_emb: list[float], top_k: int) -> list[dict]:
|
|
149
|
+
"""Vector similarity search via sqlite-vec."""
|
|
126
150
|
query_blob = _vec_to_blob(query_emb)
|
|
127
151
|
rows = self._db.execute("""
|
|
128
152
|
SELECT c.text, c.heading, c.source, c.metadata, v.distance
|
|
129
|
-
FROM
|
|
153
|
+
FROM vec_chunks v
|
|
130
154
|
JOIN chunks c ON c.id = v.rowid
|
|
131
|
-
WHERE
|
|
155
|
+
WHERE v.embedding MATCH ? AND k = ?
|
|
156
|
+
ORDER BY v.distance
|
|
132
157
|
""", (query_blob, top_k)).fetchall()
|
|
133
158
|
|
|
134
159
|
return [
|
|
@@ -136,7 +161,7 @@ class VectorStore:
|
|
|
136
161
|
"text": r["text"],
|
|
137
162
|
"heading": r["heading"],
|
|
138
163
|
"source": r["source"],
|
|
139
|
-
"score": 1.0
|
|
164
|
+
"score": 1.0 / (1.0 + r["distance"]), # Convert distance to similarity
|
|
140
165
|
"metadata": json.loads(r["metadata"]),
|
|
141
166
|
}
|
|
142
167
|
for r in rows
|
|
@@ -176,10 +201,10 @@ class VectorStore:
|
|
|
176
201
|
|
|
177
202
|
def remove_file(self, source: str) -> int:
|
|
178
203
|
"""Remove all chunks from a source file."""
|
|
179
|
-
if self.
|
|
204
|
+
if self._vec_available:
|
|
180
205
|
rows = self._db.execute("SELECT id FROM chunks WHERE source = ?", (source,)).fetchall()
|
|
181
206
|
for r in rows:
|
|
182
|
-
self._db.execute("DELETE FROM
|
|
207
|
+
self._db.execute("DELETE FROM vec_chunks WHERE rowid = ?", (r["id"],))
|
|
183
208
|
deleted = self._db.execute("DELETE FROM chunks WHERE source = ?", (source,)).rowcount
|
|
184
209
|
self._db.commit()
|
|
185
210
|
return deleted
|
|
@@ -191,14 +216,14 @@ class VectorStore:
|
|
|
191
216
|
return {
|
|
192
217
|
"total_chunks": total,
|
|
193
218
|
"total_files": sources,
|
|
194
|
-
"
|
|
219
|
+
"vec_available": self._vec_available,
|
|
195
220
|
"db_path": self._db_path,
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
def clear(self) -> None:
|
|
199
224
|
"""Remove all data."""
|
|
200
|
-
if self.
|
|
201
|
-
self._db.execute("DELETE FROM
|
|
225
|
+
if self._vec_available:
|
|
226
|
+
self._db.execute("DELETE FROM vec_chunks")
|
|
202
227
|
self._db.execute("DELETE FROM chunks")
|
|
203
228
|
self._db.commit()
|
|
204
229
|
|
|
Binary file
|
package/core/synapse/layers.py
CHANGED
|
@@ -133,7 +133,7 @@ DEPARTMENT_PATTERNS: dict[str, str] = {
|
|
|
133
133
|
"marketing": r"\b(social|content|campaign|post|instagram|linkedin|twitter|tiktok|seo|marketing|ads|email.?campaign|growth|viral)\b",
|
|
134
134
|
"finance": r"\b(budget|invoice|revenue|forecast|profit|loss|roi|margin|cash.?flow|financial|invest|valuation|pricing)\b",
|
|
135
135
|
"ecom": r"\b(store|product|shop|shopify|ecommerce|catalog|inventory|cart|checkout|pricing|marketplace)\b",
|
|
136
|
-
"strategy": r"\b(strategy|brainstorm|market|swot|
|
|
136
|
+
"strategy": r"\b(strategy|brainstorm|market|swot|competitors?|roadmap|pivot|growth|porter|blue.?ocean|positioning)\b",
|
|
137
137
|
"ops": r"\b(task|automate|meeting|workflow|process|schedule|sop|integration|zapier|n8n)\b",
|
|
138
138
|
"kb": r"\b(learn|persona|knowledge|youtube|transcribe|article|research|zettelkasten|note)\b",
|
|
139
139
|
"brand": r"\b(brand|logo|colors|palette|mockup|photoshoot|brand.?identity|brand.?guide|mood.?board|naming|visual.?design|motion|ux|ui|wireframe)\b",
|
|
@@ -143,7 +143,7 @@ DEPARTMENT_PATTERNS: dict[str, str] = {
|
|
|
143
143
|
"content": r"\b(viral|hook|script|repurpose|youtube|tiktok|reels|shorts|newsletter|creator)\b",
|
|
144
144
|
"pm": r"\b(sprint|backlog|standup|retro|scrum|kanban|story|estimate|roadmap|agile)\b",
|
|
145
145
|
"lead": r"\b(leadership|delegation|1on1|feedback|culture|hiring|performance.?review|team.?build)\b",
|
|
146
|
-
"sales": r"\b(pipeline|proposal|discovery.?call|objection|negotiate|deal|close|prospect|spin|challenger)\b",
|
|
146
|
+
"sales": r"\b(pipeline|proposal|discovery.?call|objection|negotiate|deal|close|prospect|spin|challenger|cold.?email|outreach)\b",
|
|
147
147
|
"org": r"\b(org.?design|hiring.?plan|onboarding|remote|meeting.?optimize|compensation|decision.?framework)\b",
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
|
|
5
|
+
const IS_WINDOWS = platform() === "win32";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a complete inner hook-entry object for Claude Code's settings.json.
|
|
9
|
+
*
|
|
10
|
+
* On Unix we point `command` directly at the `.sh` script and rely on the
|
|
11
|
+
* shebang for interpreter selection; no `shell` field is needed because
|
|
12
|
+
* Claude Code's default is `bash`.
|
|
13
|
+
*
|
|
14
|
+
* On Windows we point `command` at the `.ps1` script and set the
|
|
15
|
+
* documented `shell: "powershell"` hook field so Claude Code spawns
|
|
16
|
+
* PowerShell directly and pipes the hook payload to the script's stdin
|
|
17
|
+
* natively. This is the pattern prescribed by the upstream hooks
|
|
18
|
+
* reference at https://code.claude.com/docs/en/hooks :
|
|
19
|
+
*
|
|
20
|
+
* shell (no, optional): Shell to use for this hook. Accepts
|
|
21
|
+
* "bash" (default) or "powershell". Setting "powershell" runs the
|
|
22
|
+
* command via PowerShell on Windows. Does not require
|
|
23
|
+
* CLAUDE_CODE_USE_POWERSHELL_TOOL since hooks spawn PowerShell
|
|
24
|
+
* directly.
|
|
25
|
+
*
|
|
26
|
+
* Earlier versions of this adapter embedded the full
|
|
27
|
+
* `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`
|
|
28
|
+
* command line in `command` without setting `shell`. Claude Code then
|
|
29
|
+
* defaulted to `shell: "bash"`, which on Windows routes through a
|
|
30
|
+
* compatibility layer that drops the stdin pipe before the PowerShell
|
|
31
|
+
* script reads it. `SessionStart` still appeared to work (it only
|
|
32
|
+
* writes stdout), but every other hook received a 0-byte stdin, hit
|
|
33
|
+
* the `IsNullOrWhiteSpace` guard at the top of each `.ps1`, and
|
|
34
|
+
* silently exited. This commit fixes that by emitting the canonical
|
|
35
|
+
* `shell: "powershell"` field and letting Claude Code handle the
|
|
36
|
+
* PowerShell invocation itself.
|
|
37
|
+
*/
|
|
38
|
+
function hookEntry(hooksDir, name, timeout) {
|
|
39
|
+
if (IS_WINDOWS) {
|
|
40
|
+
return {
|
|
41
|
+
type: "command",
|
|
42
|
+
command: join(hooksDir, `${name}.ps1`),
|
|
43
|
+
shell: "powershell",
|
|
44
|
+
timeout,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
type: "command",
|
|
49
|
+
command: join(hooksDir, `${name}.sh`),
|
|
50
|
+
timeout,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
3
53
|
|
|
4
54
|
export default {
|
|
5
55
|
configureHooks(config, installDir) {
|
|
@@ -29,70 +79,47 @@ export default {
|
|
|
29
79
|
}
|
|
30
80
|
|
|
31
81
|
// SessionStart — Branded greeting + version drift detection
|
|
82
|
+
// Timeout 15s: PowerShell cold-start on Windows VMs can exceed 5s.
|
|
32
83
|
settings.hooks.SessionStart = [
|
|
33
|
-
{
|
|
34
|
-
hooks: [
|
|
35
|
-
{
|
|
36
|
-
type: "command",
|
|
37
|
-
command: join(hooksDir, "session-start.sh"),
|
|
38
|
-
timeout: 5,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
},
|
|
84
|
+
{ hooks: [hookEntry(hooksDir, "session-start", 15)] },
|
|
42
85
|
];
|
|
43
86
|
|
|
44
87
|
// UserPromptSubmit — Synapse v2 context injection
|
|
45
88
|
settings.hooks.UserPromptSubmit = [
|
|
46
|
-
{
|
|
47
|
-
hooks: [
|
|
48
|
-
{
|
|
49
|
-
type: "command",
|
|
50
|
-
command: join(hooksDir, "user-prompt-submit.sh"),
|
|
51
|
-
timeout: 10,
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
},
|
|
89
|
+
{ hooks: [hookEntry(hooksDir, "user-prompt-submit", 10)] },
|
|
55
90
|
];
|
|
56
91
|
|
|
57
92
|
// PostToolUse — Error tracking
|
|
58
93
|
settings.hooks.PostToolUse = [
|
|
59
|
-
{
|
|
60
|
-
hooks: [
|
|
61
|
-
{
|
|
62
|
-
type: "command",
|
|
63
|
-
command: join(hooksDir, "post-tool-use.sh"),
|
|
64
|
-
timeout: 5,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
94
|
+
{ hooks: [hookEntry(hooksDir, "post-tool-use", 5)] },
|
|
68
95
|
];
|
|
69
96
|
|
|
70
97
|
// PreCompact — Session digest
|
|
71
98
|
settings.hooks.PreCompact = [
|
|
72
|
-
{
|
|
73
|
-
hooks: [
|
|
74
|
-
{
|
|
75
|
-
type: "command",
|
|
76
|
-
command: join(hooksDir, "pre-compact.sh"),
|
|
77
|
-
timeout: 30,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
},
|
|
99
|
+
{ hooks: [hookEntry(hooksDir, "pre-compact", 30)] },
|
|
81
100
|
];
|
|
82
101
|
|
|
83
102
|
// CwdChanged — Project/ecosystem auto-detection
|
|
84
103
|
settings.hooks.CwdChanged = [
|
|
85
|
-
{
|
|
86
|
-
hooks: [
|
|
87
|
-
{
|
|
88
|
-
type: "command",
|
|
89
|
-
command: join(hooksDir, "cwd-changed.sh"),
|
|
90
|
-
timeout: 5,
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
},
|
|
104
|
+
{ hooks: [hookEntry(hooksDir, "cwd-changed", 5)] },
|
|
94
105
|
];
|
|
95
106
|
|
|
107
|
+
// Statusline — ArkaOS branded status bar
|
|
108
|
+
const configDir = join(installDir, "config");
|
|
109
|
+
const statuslineFile = IS_WINDOWS ? "statusline.ps1" : "statusline.sh";
|
|
110
|
+
const statuslinePath = join(configDir, statuslineFile);
|
|
111
|
+
if (existsSync(statuslinePath)) {
|
|
112
|
+
if (IS_WINDOWS) {
|
|
113
|
+
settings.statusline = {
|
|
114
|
+
command: `powershell -NoProfile -ExecutionPolicy Bypass -File "${statuslinePath}"`,
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
settings.statusline = {
|
|
118
|
+
command: statuslinePath,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
97
124
|
console.log(" Claude Code hooks configured.");
|
|
98
125
|
},
|
package/installer/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { install } from "./index.js";
|
|
8
8
|
import { detectRuntime } from "./detect-runtime.js";
|
|
9
|
+
import { IS_WINDOWS } from "./platform.js";
|
|
10
|
+
import { getArkaosPython } from "./python-resolver.js";
|
|
9
11
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
@@ -108,9 +110,16 @@ async function main() {
|
|
|
108
110
|
|
|
109
111
|
case "dashboard": {
|
|
110
112
|
const { execSync: execDash } = await import("node:child_process");
|
|
111
|
-
|
|
113
|
+
// join(__dirname, "..") is cross-platform; the previous regex
|
|
114
|
+
// `/\/installer$/` used forward slashes and did not match the
|
|
115
|
+
// Windows backslash-separated path, leaving repoRootDash pointing
|
|
116
|
+
// at the installer directory instead of the repo root.
|
|
117
|
+
const repoRootDash = join(__dirname, "..");
|
|
118
|
+
const dashCmd = IS_WINDOWS
|
|
119
|
+
? `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${join(repoRootDash, "scripts", "start-dashboard.ps1")}"`
|
|
120
|
+
: `bash "${repoRootDash}/scripts/start-dashboard.sh"`;
|
|
112
121
|
try {
|
|
113
|
-
execDash(
|
|
122
|
+
execDash(dashCmd, {
|
|
114
123
|
stdio: "inherit",
|
|
115
124
|
env: { ...process.env, ARKAOS_ROOT: repoRootDash },
|
|
116
125
|
});
|
|
@@ -121,9 +130,11 @@ async function main() {
|
|
|
121
130
|
case "index": {
|
|
122
131
|
const { execSync } = await import("node:child_process");
|
|
123
132
|
const indexArgs = positionals.slice(1).join(" ");
|
|
124
|
-
const repoRoot =
|
|
133
|
+
const repoRoot = join(__dirname, "..");
|
|
134
|
+
const pyIndex = getArkaosPython();
|
|
135
|
+
if (!pyIndex) { console.error("No Python found. Run: npx arkaos install"); process.exit(1); }
|
|
125
136
|
try {
|
|
126
|
-
execSync(`
|
|
137
|
+
execSync(`"${pyIndex}" "${join(repoRoot, "scripts", "knowledge-index.py")}" ${indexArgs || ""}`, {
|
|
127
138
|
stdio: "inherit",
|
|
128
139
|
env: { ...process.env, ARKAOS_ROOT: repoRoot },
|
|
129
140
|
});
|
|
@@ -135,9 +146,11 @@ async function main() {
|
|
|
135
146
|
const { execSync } = await import("node:child_process");
|
|
136
147
|
const query = positionals.slice(1).join(" ");
|
|
137
148
|
if (!query) { console.error("Usage: npx arkaos search \"your query\""); process.exit(1); }
|
|
138
|
-
const repoRoot2 =
|
|
149
|
+
const repoRoot2 = join(__dirname, "..");
|
|
150
|
+
const pySearch = getArkaosPython();
|
|
151
|
+
if (!pySearch) { console.error("No Python found. Run: npx arkaos install"); process.exit(1); }
|
|
139
152
|
try {
|
|
140
|
-
execSync(`
|
|
153
|
+
execSync(`"${pySearch}" "${join(repoRoot2, "scripts", "knowledge-index.py")}" --search "${query}"`, {
|
|
141
154
|
stdio: "inherit",
|
|
142
155
|
env: { ...process.env, ARKAOS_ROOT: repoRoot2 },
|
|
143
156
|
});
|