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