arkaos 2.3.5 → 2.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 CHANGED
@@ -1 +1 @@
1
- 2.3.5
1
+ 2.4.0
@@ -178,25 +178,45 @@ onUnmounted(() => {
178
178
  })
179
179
 
180
180
  async function handleIngest() {
181
- if (!detectedType.value) return
181
+ if (!detectedType.value && activeInputMode.value !== 'text') return
182
182
 
183
183
  ingestError.value = null
184
- const source = ingestUrl.value.trim() || ingestFile.value?.name || ''
185
- const type = detectedType.value
186
-
187
- // Clear form immediately so user can submit more
188
- ingestUrl.value = ''
189
- clearFile()
190
- pasteText.value = ''
191
- pasteTitle.value = ''
192
184
 
193
185
  try {
194
- await $fetch<IngestResponse>(`${apiBase}/api/knowledge/ingest`, {
195
- method: 'POST',
196
- body: { source, type } satisfies IngestRequest,
197
- })
186
+ // File upload — use multipart form
187
+ if (activeInputMode.value === 'file' && ingestFile.value) {
188
+ const formData = new FormData()
189
+ formData.append('file', ingestFile.value)
190
+ await $fetch(`${apiBase}/api/knowledge/upload-file`, {
191
+ method: 'POST',
192
+ body: formData,
193
+ })
194
+ }
195
+ // Text paste — save to temp file via API
196
+ else if (activeInputMode.value === 'text' && pasteText.value.length > 10) {
197
+ await $fetch(`${apiBase}/api/knowledge/ingest`, {
198
+ method: 'POST',
199
+ body: { source: pasteText.value.slice(0, 100), type: 'markdown', text: pasteText.value, title: pasteTitle.value },
200
+ })
201
+ }
202
+ // URL or Research — standard ingest
203
+ else {
204
+ const source = ingestUrl.value.trim()
205
+ const type = detectedType.value
206
+ if (!source || !type) return
207
+ await $fetch(`${apiBase}/api/knowledge/ingest`, {
208
+ method: 'POST',
209
+ body: { source, type },
210
+ })
211
+ }
212
+
213
+ // Clear form immediately
214
+ ingestUrl.value = ''
215
+ clearFile()
216
+ pasteText.value = ''
217
+ pasteTitle.value = ''
198
218
 
199
- // Refresh jobs table + connect WebSocket for live updates
219
+ // Refresh jobs table + connect WebSocket
200
220
  fetchJobs()
201
221
  connectWebSocket()
202
222
  } catch (err) {
@@ -13,19 +13,22 @@ const VERSION = JSON.parse(readFileSync(join(ARKAOS_ROOT, "package.json"), "utf-
13
13
  export async function install({ runtime, path, force }) {
14
14
  const startTime = Date.now();
15
15
  const config = getRuntimeConfig(runtime);
16
- const installDir = path || join(homedir(), ".arkaos");
17
- const isUpgrade = existsSync(join(installDir, "install-manifest.json"));
16
+ const isUpgrade = existsSync(join(path || join(homedir(), ".arkaos"), "install-manifest.json"));
18
17
 
19
18
  console.log(`
20
19
  ╔══════════════════════════════════════════════════════════╗
21
20
  ║ ArkaOS v${VERSION} — The Operating System for AI Agent Teams ║
22
21
  ╚══════════════════════════════════════════════════════════╝
23
22
 
24
- Runtime: ${config.name}
25
- Install dir: ${installDir}
26
- Mode: ${isUpgrade ? "Upgrade" : "Fresh install"}
23
+ Runtime: ${config.name}
24
+ Mode: ${isUpgrade ? "Upgrade" : "Fresh install"}
27
25
  `);
28
26
 
27
+ // ═══ Interactive Setup ═══
28
+ const { runSetupPrompts } = await import("./prompts.js");
29
+ const userConfig = await runSetupPrompts(isUpgrade);
30
+ const installDir = userConfig.installDir;
31
+
29
32
  // ═══ Step 1: Create directories ═══
30
33
  step(1, 12, "Creating directories...");
31
34
  ensureDir(installDir);
@@ -52,9 +55,9 @@ export async function install({ runtime, path, force }) {
52
55
  const pythonCmd = checkPython();
53
56
  ok(`Found: ${pythonCmd}`);
54
57
 
55
- // ═══ Step 4: Install Python core + ALL dependencies ═══
58
+ // ═══ Step 4: Install Python core + dependencies based on user choices ═══
56
59
  step(4, 12, "Installing Python dependencies (this may take a minute)...");
57
- installAllPythonDeps(pythonCmd);
60
+ installAllPythonDeps(pythonCmd, userConfig);
58
61
 
59
62
  // ═══ Step 5: Copy configuration files ═══
60
63
  step(5, 12, "Copying configuration files...");
@@ -83,32 +86,60 @@ export async function install({ runtime, path, force }) {
83
86
  writeFileSync(join(skillsDir, ".arkaos-root"), ARKAOS_ROOT);
84
87
 
85
88
  const profilePath = join(installDir, "profile.json");
86
- if (!existsSync(profilePath)) {
87
- writeFileSync(profilePath, JSON.stringify({
88
- version: "2",
89
- created: new Date().toISOString(),
90
- }, null, 2));
91
- ok("New profile created");
92
- } else {
93
- ok("Existing profile preserved");
89
+ const profile = {
90
+ version: "2",
91
+ language: userConfig.language,
92
+ market: userConfig.market,
93
+ role: userConfig.role,
94
+ company: userConfig.company,
95
+ projectsDir: userConfig.projectsDir,
96
+ vaultPath: userConfig.vaultPath,
97
+ created: existsSync(profilePath)
98
+ ? JSON.parse(readFileSync(profilePath, "utf-8")).created
99
+ : new Date().toISOString(),
100
+ updated: new Date().toISOString(),
101
+ };
102
+ writeFileSync(profilePath, JSON.stringify(profile, null, 2));
103
+ ok("Profile saved");
104
+
105
+ // Save API keys if provided
106
+ if (userConfig.openaiKey || userConfig.googleKey || userConfig.falKey) {
107
+ const keysPath = join(installDir, "keys.json");
108
+ const keys = existsSync(keysPath) ? JSON.parse(readFileSync(keysPath, "utf-8")) : {};
109
+ if (userConfig.openaiKey) keys.OPENAI_API_KEY = userConfig.openaiKey;
110
+ if (userConfig.googleKey) keys.GOOGLE_API_KEY = userConfig.googleKey;
111
+ if (userConfig.falKey) keys.FAL_API_KEY = userConfig.falKey;
112
+ writeFileSync(keysPath, JSON.stringify(keys, null, 2));
113
+ try { chmodSync(keysPath, 0o600); } catch {}
114
+ ok("API keys saved");
94
115
  }
95
116
 
96
- // ═══ Step 10: Index knowledge base (if vault configured) ═══
97
- step(10, 12, "Checking knowledge base...");
98
- const kbDb = join(installDir, "knowledge.db");
99
- if (!existsSync(kbDb)) {
117
+ // ═══ Step 10: Index knowledge base ═══
118
+ step(10, 12, "Setting up knowledge base...");
119
+ if (userConfig.installKnowledge) {
120
+ const kbDb = join(installDir, "knowledge.db");
121
+ // Index ArkaOS skills first
100
122
  try {
101
123
  execSync(`${pythonCmd} "${join(ARKAOS_ROOT, "scripts", "knowledge-index.py")}" --dir "${join(ARKAOS_ROOT, "departments")}" --db "${kbDb}"`, {
102
- stdio: "pipe",
103
- timeout: 60000,
104
- env: { ...process.env, ARKAOS_ROOT },
124
+ stdio: "pipe", timeout: 60000, env: { ...process.env, ARKAOS_ROOT },
105
125
  });
106
- ok("ArkaOS skills indexed into knowledge base");
126
+ ok("ArkaOS skills indexed");
107
127
  } catch {
108
- warn("Knowledge indexing skipped (run 'npx arkaos index' later)");
128
+ warn("Skill indexing skipped");
129
+ }
130
+ // Index user's Obsidian vault if provided
131
+ if (userConfig.vaultPath && existsSync(userConfig.vaultPath)) {
132
+ try {
133
+ execSync(`${pythonCmd} "${join(ARKAOS_ROOT, "scripts", "knowledge-index.py")}" --vault "${userConfig.vaultPath}" --db "${kbDb}"`, {
134
+ stdio: "pipe", timeout: 120000, env: { ...process.env, ARKAOS_ROOT },
135
+ });
136
+ ok(`Obsidian vault indexed: ${userConfig.vaultPath}`);
137
+ } catch {
138
+ warn("Vault indexing skipped (run 'npx arkaos index' later)");
139
+ }
109
140
  }
110
141
  } else {
111
- ok("Knowledge base already exists");
142
+ ok("Knowledge base skipped (install later with 'npx arkaos index')");
112
143
  }
113
144
 
114
145
  // ═══ Step 11: Verify installation ═══
@@ -213,7 +244,7 @@ function checkPython() {
213
244
  process.exit(1);
214
245
  }
215
246
 
216
- function installAllPythonDeps(pythonCmd) {
247
+ function installAllPythonDeps(pythonCmd, userConfig = {}) {
217
248
  // Core dependencies
218
249
  const coreDeps = "pyyaml pydantic rich click jinja2";
219
250
  // Knowledge + Vector DB
@@ -225,7 +256,11 @@ function installAllPythonDeps(pythonCmd) {
225
256
  // Transcription
226
257
  const transcriptionDeps = "faster-whisper";
227
258
 
228
- const allDeps = `${coreDeps} ${knowledgeDeps} ${ingestDeps} ${dashboardDeps}`;
259
+ // Build deps list based on user choices
260
+ let allDeps = coreDeps;
261
+ if (userConfig.installKnowledge !== false) allDeps += ` ${knowledgeDeps}`;
262
+ allDeps += ` ${ingestDeps}`;
263
+ if (userConfig.installDashboard !== false) allDeps += ` ${dashboardDeps}`;
229
264
 
230
265
  try {
231
266
  // Try uv first (faster)
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Interactive prompts for ArkaOS installer.
3
+ * Asks user for directories, language, market, preferences.
4
+ * Nothing is hardcoded — everything comes from the user.
5
+ */
6
+
7
+ import { createInterface } from "node:readline";
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+
12
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
13
+
14
+ function ask(question, defaultValue = "") {
15
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
16
+ return new Promise((resolve) => {
17
+ rl.question(` ${question}${suffix}: `, (answer) => {
18
+ resolve(answer.trim() || defaultValue);
19
+ });
20
+ });
21
+ }
22
+
23
+ function askYN(question, defaultYes = true) {
24
+ const suffix = defaultYes ? " [Y/n]" : " [y/N]";
25
+ return new Promise((resolve) => {
26
+ rl.question(` ${question}${suffix}: `, (answer) => {
27
+ const a = answer.trim().toLowerCase();
28
+ if (!a) resolve(defaultYes);
29
+ else resolve(a === "y" || a === "yes");
30
+ });
31
+ });
32
+ }
33
+
34
+ function askChoice(question, options) {
35
+ return new Promise((resolve) => {
36
+ console.log(` ${question}`);
37
+ options.forEach((opt, i) => {
38
+ console.log(` ${i + 1}) ${opt.label}`);
39
+ });
40
+ rl.question(` Choose [1-${options.length}]: `, (answer) => {
41
+ const idx = parseInt(answer.trim()) - 1;
42
+ if (idx >= 0 && idx < options.length) {
43
+ resolve(options[idx].value);
44
+ } else {
45
+ resolve(options[0].value); // Default to first
46
+ }
47
+ });
48
+ });
49
+ }
50
+
51
+ export async function runSetupPrompts(isUpgrade = false) {
52
+ console.log(`
53
+ ╔══════════════════════════════════════════════════════╗
54
+ ║ ArkaOS Setup — Let's configure your environment ║
55
+ ╚══════════════════════════════════════════════════════╝
56
+ `);
57
+
58
+ const config = {};
59
+
60
+ // ── Language ──
61
+ config.language = await askChoice("What is your primary language?", [
62
+ { label: "English", value: "en" },
63
+ { label: "Português", value: "pt" },
64
+ { label: "Español", value: "es" },
65
+ { label: "Français", value: "fr" },
66
+ { label: "Deutsch", value: "de" },
67
+ { label: "Italiano", value: "it" },
68
+ { label: "中文 (Chinese)", value: "zh" },
69
+ { label: "日本語 (Japanese)", value: "ja" },
70
+ { label: "한국어 (Korean)", value: "ko" },
71
+ { label: "Other", value: "other" },
72
+ ]);
73
+
74
+ if (config.language === "other") {
75
+ config.language = await ask("Enter language code (e.g., nl, pl, ru)");
76
+ }
77
+
78
+ // ── Market/Country ──
79
+ config.market = await ask("What is your primary market/country?", "");
80
+ console.log(" (e.g., United States, Portugal, Brazil, Germany, Global)");
81
+ if (!config.market) {
82
+ config.market = await ask("Market/Country");
83
+ }
84
+
85
+ // ── Role ──
86
+ config.role = await askChoice("What best describes your role?", [
87
+ { label: "Developer / Engineer", value: "developer" },
88
+ { label: "Founder / CEO", value: "founder" },
89
+ { label: "Marketing / Growth", value: "marketing" },
90
+ { label: "Product Manager", value: "product" },
91
+ { label: "Designer", value: "designer" },
92
+ { label: "Consultant / Agency", value: "consultant" },
93
+ { label: "Other", value: "other" },
94
+ ]);
95
+
96
+ // ── Company ──
97
+ config.company = await ask("Company or organization name (optional)", "");
98
+
99
+ // ── Directories ──
100
+ console.log("\n ── Directories ──\n");
101
+
102
+ config.projectsDir = await ask(
103
+ "Where are your projects?",
104
+ join(homedir(), "Projects")
105
+ );
106
+
107
+ config.vaultPath = await ask(
108
+ "Where is your Obsidian vault? (leave empty if none)",
109
+ ""
110
+ );
111
+ if (config.vaultPath && !existsSync(config.vaultPath)) {
112
+ console.log(` ⚠ Directory not found: ${config.vaultPath}`);
113
+ const create = await askYN("Create it?", false);
114
+ if (!create) config.vaultPath = "";
115
+ }
116
+
117
+ config.installDir = await ask(
118
+ "ArkaOS data directory",
119
+ join(homedir(), ".arkaos")
120
+ );
121
+
122
+ // ── Features ──
123
+ console.log("\n ── Optional Features ──\n");
124
+
125
+ config.installDashboard = await askYN("Install monitoring dashboard?", true);
126
+ config.installKnowledge = await askYN("Install knowledge base (vector DB)?", true);
127
+ config.installTranscription = await askYN("Install audio transcription (Whisper)?", false);
128
+
129
+ // ── API Keys (optional) ──
130
+ console.log("\n ── API Keys (optional, can be configured later) ──\n");
131
+
132
+ config.openaiKey = await ask("OpenAI API key (for Whisper, embeddings — leave empty to skip)", "");
133
+ config.googleKey = await ask("Google API key (Gemini, Nano Banana — leave empty to skip)", "");
134
+ config.falKey = await ask("fal.ai API key (image/video generation — leave empty to skip)", "");
135
+
136
+ // ── Summary ──
137
+ console.log(`
138
+ ── Configuration Summary ──
139
+
140
+ Language: ${config.language}
141
+ Market: ${config.market || "(not set)"}
142
+ Role: ${config.role}
143
+ Company: ${config.company || "(not set)"}
144
+ Projects dir: ${config.projectsDir}
145
+ Obsidian vault: ${config.vaultPath || "(none)"}
146
+ Install dir: ${config.installDir}
147
+ Dashboard: ${config.installDashboard ? "Yes" : "No"}
148
+ Knowledge DB: ${config.installKnowledge ? "Yes" : "No"}
149
+ Transcription: ${config.installTranscription ? "Yes" : "No"}
150
+ OpenAI key: ${config.openaiKey ? "configured" : "not set"}
151
+ Google key: ${config.googleKey ? "configured" : "not set"}
152
+ fal.ai key: ${config.falKey ? "configured" : "not set"}
153
+ `);
154
+
155
+ const confirmed = await askYN("Proceed with this configuration?", true);
156
+ if (!confirmed) {
157
+ console.log("\n Installation cancelled.\n");
158
+ rl.close();
159
+ process.exit(0);
160
+ }
161
+
162
+ rl.close();
163
+ return config;
164
+ }
165
+
166
+ export function closePrompts() {
167
+ try { rl.close(); } catch {}
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.3.5"
3
+ version = "2.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"}
@@ -345,6 +345,71 @@ def job_cancel(job_id: str):
345
345
  return {"error": "Can only cancel queued jobs"}
346
346
 
347
347
 
348
+ @app.post("/api/knowledge/upload")
349
+ async def knowledge_upload(file: Any = None):
350
+ """Upload a file for ingestion."""
351
+ from fastapi import UploadFile, File as FastAPIFile
352
+ # Re-import with proper type
353
+ pass
354
+
355
+
356
+ # Actual upload endpoint with proper signature
357
+ from fastapi import UploadFile, File as FastAPIFile
358
+
359
+ @app.post("/api/knowledge/upload-file")
360
+ async def knowledge_upload_file(file: UploadFile):
361
+ """Upload and ingest a file (PDF, audio, markdown)."""
362
+ import threading
363
+
364
+ media_dir = Path.home() / ".arkaos" / "media"
365
+ media_dir.mkdir(parents=True, exist_ok=True)
366
+
367
+ # Save uploaded file
368
+ file_path = media_dir / file.filename
369
+ content = await file.read()
370
+ file_path.write_bytes(content)
371
+
372
+ source = str(file_path)
373
+ from core.knowledge.ingest import detect_source_type
374
+ source_type = detect_source_type(source)
375
+
376
+ store = _get_vector_store()
377
+ if not store:
378
+ from core.knowledge.vector_store import VectorStore
379
+ kb_db = Path.home() / ".arkaos" / "knowledge.db"
380
+ kb_db.parent.mkdir(parents=True, exist_ok=True)
381
+ store = VectorStore(kb_db)
382
+
383
+ job_mgr = _get_job_manager()
384
+ job = job_mgr.create(source=source, source_type=source_type, title=file.filename)
385
+ job_id = job.id
386
+
387
+ def run_ingest():
388
+ from core.jobs.manager import JobManager as _JM
389
+ from core.knowledge.ingest import IngestEngine
390
+ local_mgr = _JM()
391
+ engine = IngestEngine(store)
392
+ def on_progress(pct, msg):
393
+ status = "embedding" if "embed" in msg.lower() or "index" in msg.lower() else "processing"
394
+ local_mgr.update_progress(job_id, pct, msg, status)
395
+ broadcast_from_thread({"type": "job_progress", "job_id": job_id, "progress": pct, "message": msg, "status": status})
396
+ try:
397
+ local_mgr.start(job_id)
398
+ result = engine.ingest(source, source_type, on_progress=on_progress)
399
+ if result.success:
400
+ local_mgr.complete(job_id, chunks_created=result.chunks_created)
401
+ broadcast_from_thread({"type": "job_complete", "job_id": job_id, "chunks_created": result.chunks_created})
402
+ else:
403
+ local_mgr.fail(job_id, result.error)
404
+ broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": result.error})
405
+ except Exception as e:
406
+ local_mgr.fail(job_id, str(e))
407
+ broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": str(e)})
408
+
409
+ threading.Thread(target=run_ingest, daemon=True).start()
410
+ return {"job_id": job_id, "source_type": source_type, "filename": file.filename, "status": "queued"}
411
+
412
+
348
413
  @app.post("/api/knowledge/ingest")
349
414
  def knowledge_ingest(body: dict):
350
415
  """Ingest content into the knowledge base. Runs in background with SQLite job tracking."""
@@ -352,6 +417,21 @@ def knowledge_ingest(body: dict):
352
417
 
353
418
  source = body.get("source", "")
354
419
  source_type = body.get("type", "")
420
+ text_content = body.get("text", "")
421
+ text_title = body.get("title", "")
422
+
423
+ # Handle direct text paste — save to temp markdown file
424
+ if text_content and len(text_content) > 10:
425
+ media_dir = Path.home() / ".arkaos" / "media"
426
+ media_dir.mkdir(parents=True, exist_ok=True)
427
+ safe_name = "".join(c if c.isalnum() or c in " -_" else "" for c in (text_title or source)[:40]).strip() or "pasted-text"
428
+ text_path = media_dir / f"{safe_name}.md"
429
+ # Add title as heading
430
+ md_content = f"# {text_title}\n\n{text_content}" if text_title else text_content
431
+ text_path.write_text(md_content, encoding="utf-8")
432
+ source = str(text_path)
433
+ source_type = "markdown"
434
+
355
435
  if not source:
356
436
  return {"error": "source is required"}
357
437