daemora 1.0.4 → 1.0.6
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/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +29 -26
- package/config/mcp.json +126 -66
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +134 -16
- package/src/agents/systemPrompt.js +427 -0
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +281 -55
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +15 -1
- package/src/config/models.js +314 -78
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +111 -11
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +81 -6
- package/src/index.js +725 -59
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +74 -4
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +125 -75
- package/src/skills/SkillLoader.js +132 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +7 -1
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +236 -12
- package/src/webhooks/WebhookHandler.js +107 -0
- package/src/systemPrompt.js +0 -528
|
@@ -5,12 +5,15 @@ import taskQueue from "../core/TaskQueue.js";
|
|
|
5
5
|
import eventBus from "../core/EventBus.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Heartbeat - periodic proactive
|
|
8
|
+
* Heartbeat - periodic proactive agent turns.
|
|
9
9
|
*
|
|
10
|
-
* Reads HEARTBEAT.md for user-defined
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Reads HEARTBEAT.md for user-defined instructions. Every N minutes,
|
|
11
|
+
* enqueues a heartbeat task with the HEARTBEAT.md content as prompt.
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Active hours: skip runs outside configurable window (default 8-22)
|
|
15
|
+
* - Duplicate suppression: skip if HEARTBEAT.md unchanged within 24h
|
|
16
|
+
* - Configurable via env vars or config
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
class Heartbeat {
|
|
@@ -21,11 +24,14 @@ class Heartbeat {
|
|
|
21
24
|
this.intervalMinutes = config.heartbeatIntervalMinutes;
|
|
22
25
|
this.lastCheck = null;
|
|
23
26
|
this.checkCount = 0;
|
|
27
|
+
this._lastContentHash = null;
|
|
28
|
+
this._lastContentAt = 0;
|
|
29
|
+
|
|
30
|
+
// Active hours config (env override or defaults)
|
|
31
|
+
this.activeHourStart = parseInt(process.env.HEARTBEAT_ACTIVE_START || "8", 10);
|
|
32
|
+
this.activeHourEnd = parseInt(process.env.HEARTBEAT_ACTIVE_END || "22", 10);
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
/**
|
|
27
|
-
* Start the heartbeat.
|
|
28
|
-
*/
|
|
29
35
|
start() {
|
|
30
36
|
if (!config.daemonMode) {
|
|
31
37
|
console.log(`[Heartbeat] Skipped - daemon mode not enabled`);
|
|
@@ -44,16 +50,13 @@ class Heartbeat {
|
|
|
44
50
|
);
|
|
45
51
|
|
|
46
52
|
console.log(
|
|
47
|
-
`[Heartbeat] Started (every ${this.intervalMinutes}
|
|
53
|
+
`[Heartbeat] Started (every ${this.intervalMinutes}min, active ${this.activeHourStart}:00-${this.activeHourEnd}:00)`
|
|
48
54
|
);
|
|
49
55
|
|
|
50
56
|
// Run first check after 1 minute
|
|
51
57
|
setTimeout(() => this.check(), 60000);
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
/**
|
|
55
|
-
* Stop the heartbeat.
|
|
56
|
-
*/
|
|
57
60
|
stop() {
|
|
58
61
|
this.running = false;
|
|
59
62
|
if (this.timer) {
|
|
@@ -63,20 +66,53 @@ class Heartbeat {
|
|
|
63
66
|
console.log(`[Heartbeat] Stopped`);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
_isActiveHour() {
|
|
70
|
+
const hour = new Date().getHours();
|
|
71
|
+
return hour >= this.activeHourStart && hour < this.activeHourEnd;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_simpleHash(str) {
|
|
75
|
+
let hash = 0;
|
|
76
|
+
for (let i = 0; i < str.length; i++) {
|
|
77
|
+
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
|
|
78
|
+
}
|
|
79
|
+
return hash;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
async check() {
|
|
70
83
|
if (!this.running) return;
|
|
71
84
|
|
|
85
|
+
// Active hours check
|
|
86
|
+
if (!this._isActiveHour()) {
|
|
87
|
+
console.log(`[Heartbeat] Outside active hours (${this.activeHourStart}:00-${this.activeHourEnd}:00) — skipping`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!existsSync(this.heartbeatPath)) return;
|
|
92
|
+
|
|
72
93
|
try {
|
|
73
|
-
const instructions = readFileSync(this.heartbeatPath, "utf-8");
|
|
94
|
+
const instructions = readFileSync(this.heartbeatPath, "utf-8").trim();
|
|
95
|
+
if (!instructions) {
|
|
96
|
+
console.log(`[Heartbeat] HEARTBEAT.md is empty — skipping`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Duplicate suppression: skip if same content within 24h
|
|
101
|
+
const contentHash = this._simpleHash(instructions);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (contentHash === this._lastContentHash && (now - this._lastContentAt) < 24 * 60 * 60 * 1000) {
|
|
104
|
+
console.log(`[Heartbeat] HEARTBEAT.md unchanged within 24h — skipping`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this._lastContentHash = contentHash;
|
|
108
|
+
this._lastContentAt = now;
|
|
109
|
+
|
|
74
110
|
this.checkCount++;
|
|
75
111
|
this.lastCheck = new Date().toISOString();
|
|
76
112
|
|
|
77
113
|
console.log(`[Heartbeat] Check #${this.checkCount}...`);
|
|
78
114
|
|
|
79
|
-
const prompt = `
|
|
115
|
+
const prompt = `[Heartbeat check #${this.checkCount}] Follow the instructions in HEARTBEAT.md. If everything looks fine, respond "All clear." If something needs attention, describe what you found and take action.
|
|
80
116
|
|
|
81
117
|
Instructions from HEARTBEAT.md:
|
|
82
118
|
${instructions}
|
|
@@ -86,8 +122,9 @@ Current time: ${new Date().toISOString()}`;
|
|
|
86
122
|
taskQueue.enqueue({
|
|
87
123
|
input: prompt,
|
|
88
124
|
channel: "heartbeat",
|
|
89
|
-
sessionId:
|
|
125
|
+
sessionId: "heartbeat",
|
|
90
126
|
priority: 2,
|
|
127
|
+
type: "heartbeat",
|
|
91
128
|
});
|
|
92
129
|
|
|
93
130
|
eventBus.emitEvent("heartbeat:check", {
|
|
@@ -98,13 +135,11 @@ Current time: ${new Date().toISOString()}`;
|
|
|
98
135
|
}
|
|
99
136
|
}
|
|
100
137
|
|
|
101
|
-
/**
|
|
102
|
-
* Get stats.
|
|
103
|
-
*/
|
|
104
138
|
stats() {
|
|
105
139
|
return {
|
|
106
140
|
running: this.running,
|
|
107
141
|
intervalMinutes: this.intervalMinutes,
|
|
142
|
+
activeHours: `${this.activeHourStart}:00-${this.activeHourEnd}:00`,
|
|
108
143
|
lastCheck: this.lastCheck,
|
|
109
144
|
checkCount: this.checkCount,
|
|
110
145
|
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cleanup.js - Data retention management.
|
|
3
|
+
*
|
|
4
|
+
* Cleans up old task files, audit logs, cost logs, and stale sub-agent sessions.
|
|
5
|
+
* Configurable via CLEANUP_AFTER_DAYS env var (default: 30, 0 = never delete).
|
|
6
|
+
*/
|
|
7
|
+
import { readdirSync, statSync, unlinkSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { config } from "../config/default.js";
|
|
10
|
+
|
|
11
|
+
const CLEANUP_DAYS = parseInt(process.env.CLEANUP_AFTER_DAYS || "30", 10);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run cleanup across all data directories.
|
|
15
|
+
* @param {number} [days] - Override retention days (0 = skip)
|
|
16
|
+
* @returns {{ tasks: number, audit: number, costs: number, sessions: number, total: number }}
|
|
17
|
+
*/
|
|
18
|
+
export function runCleanup(days = CLEANUP_DAYS) {
|
|
19
|
+
if (days <= 0) return { tasks: 0, audit: 0, costs: 0, sessions: 0, total: 0 };
|
|
20
|
+
|
|
21
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
22
|
+
const results = {
|
|
23
|
+
tasks: cleanDir(config.tasksDir, cutoff, ".json"),
|
|
24
|
+
audit: cleanDir(config.auditDir, cutoff, ".jsonl"),
|
|
25
|
+
costs: cleanDir(config.costsDir, cutoff, ".jsonl"),
|
|
26
|
+
sessions: cleanStaleSessions(cutoff),
|
|
27
|
+
total: 0,
|
|
28
|
+
};
|
|
29
|
+
results.total = results.tasks + results.audit + results.costs + results.sessions;
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Delete files older than cutoff in a directory.
|
|
35
|
+
*/
|
|
36
|
+
function cleanDir(dirPath, cutoffMs, ext) {
|
|
37
|
+
if (!existsSync(dirPath)) return 0;
|
|
38
|
+
let deleted = 0;
|
|
39
|
+
try {
|
|
40
|
+
const files = readdirSync(dirPath).filter(f => f.endsWith(ext));
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const filePath = join(dirPath, file);
|
|
43
|
+
try {
|
|
44
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
45
|
+
if (mtime < cutoffMs) {
|
|
46
|
+
unlinkSync(filePath);
|
|
47
|
+
deleted++;
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
return deleted;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clean sub-agent sessions (telegram-123--coder.json) that are stale.
|
|
57
|
+
* Main user sessions (no "--") are kept regardless.
|
|
58
|
+
*/
|
|
59
|
+
function cleanStaleSessions(cutoffMs) {
|
|
60
|
+
if (!existsSync(config.sessionsDir)) return 0;
|
|
61
|
+
let deleted = 0;
|
|
62
|
+
try {
|
|
63
|
+
const files = readdirSync(config.sessionsDir).filter(f => f.endsWith(".json"));
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const name = file.slice(0, -5);
|
|
66
|
+
// Only clean sub-agent sessions (contain "--")
|
|
67
|
+
if (!name.includes("--")) continue;
|
|
68
|
+
const filePath = join(config.sessionsDir, file);
|
|
69
|
+
try {
|
|
70
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
71
|
+
if (mtime < cutoffMs) {
|
|
72
|
+
unlinkSync(filePath);
|
|
73
|
+
deleted++;
|
|
74
|
+
}
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
return deleted;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get storage stats without deleting anything.
|
|
83
|
+
*/
|
|
84
|
+
export function getStorageStats() {
|
|
85
|
+
return {
|
|
86
|
+
tasks: countDir(config.tasksDir, ".json"),
|
|
87
|
+
audit: countDir(config.auditDir, ".jsonl"),
|
|
88
|
+
costs: countDir(config.costsDir, ".jsonl"),
|
|
89
|
+
sessions: countDir(config.sessionsDir, ".json"),
|
|
90
|
+
retentionDays: CLEANUP_DAYS || "never",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function countDir(dirPath, ext) {
|
|
95
|
+
if (!existsSync(dirPath)) return { files: 0, sizeKB: 0 };
|
|
96
|
+
try {
|
|
97
|
+
const files = readdirSync(dirPath).filter(f => f.endsWith(ext));
|
|
98
|
+
let totalSize = 0;
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
try { totalSize += statSync(join(dirPath, file)).size; } catch {}
|
|
101
|
+
}
|
|
102
|
+
return { files: files.length, sizeKB: Math.round(totalSize / 1024) };
|
|
103
|
+
} catch {
|
|
104
|
+
return { files: 0, sizeKB: 0 };
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/services/sessions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
|
3
3
|
import { config } from "../config/default.js";
|
|
4
4
|
|
|
5
5
|
const SESSIONS_DIR = config.sessionsDir;
|
|
@@ -17,6 +17,7 @@ export function createSession(existingId = null) {
|
|
|
17
17
|
messages: [],
|
|
18
18
|
};
|
|
19
19
|
sessions.set(sessionId, session);
|
|
20
|
+
saveSession(session);
|
|
20
21
|
return session;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -59,6 +60,43 @@ export function setMessages(sessionId, messages) {
|
|
|
59
60
|
return session;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* List sub-agent session IDs. If prefix given, only returns sessions starting with `{prefix}--`.
|
|
65
|
+
*/
|
|
66
|
+
export function listSessions(prefix = null) {
|
|
67
|
+
try {
|
|
68
|
+
const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith(".json"));
|
|
69
|
+
let sessionIds = files.map(f => f.slice(0, -5));
|
|
70
|
+
if (prefix) {
|
|
71
|
+
// Return only sub-agent sessions for this parent
|
|
72
|
+
sessionIds = sessionIds.filter(id => id.startsWith(prefix + "--"));
|
|
73
|
+
} else {
|
|
74
|
+
// Exclude sub-agent sessions (contain "--") from top-level listing
|
|
75
|
+
sessionIds = sessionIds.filter(id => !id.includes("--"));
|
|
76
|
+
}
|
|
77
|
+
return sessionIds;
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear a session — removes messages from memory and deletes file from disk.
|
|
85
|
+
*/
|
|
86
|
+
export function clearSession(sessionId) {
|
|
87
|
+
let found = false;
|
|
88
|
+
if (sessions.has(sessionId)) {
|
|
89
|
+
sessions.delete(sessionId);
|
|
90
|
+
found = true;
|
|
91
|
+
}
|
|
92
|
+
const filePath = `${SESSIONS_DIR}/${sessionId}.json`;
|
|
93
|
+
if (existsSync(filePath)) {
|
|
94
|
+
unlinkSync(filePath);
|
|
95
|
+
found = true;
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
62
100
|
function saveSession(session) {
|
|
63
101
|
const filePath = `${SESSIONS_DIR}/${session.sessionId}.json`;
|
|
64
102
|
writeFileSync(filePath, JSON.stringify(session, null, 2));
|
package/src/setup/wizard.js
CHANGED
|
@@ -7,7 +7,53 @@ import { banner, stepHeader, kv, summaryTable, completeBanner, t, S } from "./th
|
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const ROOT_DIR = join(__dirname, "..", "..");
|
|
10
|
-
const TOTAL_STEPS =
|
|
10
|
+
const TOTAL_STEPS = 9;
|
|
11
|
+
const OLLAMA_EMBED_MODEL = "all-minilm";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pull all-minilm embedding model (if Ollama available) and pre-embed all skills.
|
|
15
|
+
* This runs during setup so the agent has instant skill matching from first task.
|
|
16
|
+
*/
|
|
17
|
+
async function setupSkillEmbeddings(provider, envConfig, spin) {
|
|
18
|
+
const { execSync } = await import("child_process");
|
|
19
|
+
const hasOllama = (() => {
|
|
20
|
+
try { execSync("ollama --version", { stdio: "ignore" }); return true; } catch { return false; }
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
// Pull local embedding model as primary (no API key) or fallback (has API key)
|
|
24
|
+
const hasApiEmbedding = envConfig.OPENAI_API_KEY || envConfig.GOOGLE_AI_API_KEY;
|
|
25
|
+
|
|
26
|
+
if (hasOllama) {
|
|
27
|
+
const purpose = hasApiEmbedding ? "offline fallback" : "skill matching";
|
|
28
|
+
spin.message(`Pulling ${OLLAMA_EMBED_MODEL} for ${purpose} (22M params, ~45MB)`);
|
|
29
|
+
try {
|
|
30
|
+
execSync(`ollama pull ${OLLAMA_EMBED_MODEL}`, { stdio: "ignore", timeout: 120_000 });
|
|
31
|
+
p.log.success(`${S.check} Embedding model ${t.bold(OLLAMA_EMBED_MODEL)} ready (${purpose})`);
|
|
32
|
+
} catch {
|
|
33
|
+
if (!hasApiEmbedding) {
|
|
34
|
+
p.log.warn(`Could not pull ${OLLAMA_EMBED_MODEL}. Skill matching will use built-in TF-IDF.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pre-embed all skills (works with any provider: API, Ollama, or TF-IDF)
|
|
40
|
+
spin.message("Pre-embedding skills for instant matching");
|
|
41
|
+
try {
|
|
42
|
+
// Temporarily set env vars so the embedding provider can detect them
|
|
43
|
+
if (envConfig.OPENAI_API_KEY) process.env.OPENAI_API_KEY = envConfig.OPENAI_API_KEY;
|
|
44
|
+
if (envConfig.GOOGLE_AI_API_KEY) process.env.GOOGLE_AI_API_KEY = envConfig.GOOGLE_AI_API_KEY;
|
|
45
|
+
if (provider === "ollama" || hasOllama) process.env.OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
46
|
+
|
|
47
|
+
const skillLoader = (await import("../skills/SkillLoader.js")).default;
|
|
48
|
+
skillLoader.load();
|
|
49
|
+
await skillLoader.embedSkills();
|
|
50
|
+
|
|
51
|
+
const count = skillLoader.list().length;
|
|
52
|
+
p.log.success(`${S.check} ${count} skills embedded for instant matching`);
|
|
53
|
+
} catch {
|
|
54
|
+
p.log.info("Skill embedding deferred to first startup.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
11
57
|
|
|
12
58
|
function cancelled() {
|
|
13
59
|
p.cancel("Setup cancelled.");
|
|
@@ -646,8 +692,25 @@ export async function runSetupWizard() {
|
|
|
646
692
|
|
|
647
693
|
p.log.success(`Daemon: ${t.bold(daemonMode ? "Enabled" : "Disabled")}`);
|
|
648
694
|
|
|
649
|
-
// ━━━ Step 7:
|
|
650
|
-
stepHeader(7, TOTAL_STEPS, "
|
|
695
|
+
// ━━━ Step 7: Data Cleanup ━━━
|
|
696
|
+
stepHeader(7, TOTAL_STEPS, "Data Cleanup");
|
|
697
|
+
|
|
698
|
+
const cleanupDays = guard(await p.select({
|
|
699
|
+
message: "Auto-delete old tasks, logs & sessions after how many days?",
|
|
700
|
+
options: [
|
|
701
|
+
{ value: "30", label: "30 days", hint: "recommended" },
|
|
702
|
+
{ value: "7", label: "7 days", hint: "aggressive — saves most space" },
|
|
703
|
+
{ value: "90", label: "90 days", hint: "keep 3 months of history" },
|
|
704
|
+
{ value: "365", label: "1 year", hint: "long-term retention" },
|
|
705
|
+
{ value: "0", label: "Never", hint: "keep everything forever" },
|
|
706
|
+
],
|
|
707
|
+
}));
|
|
708
|
+
envConfig.CLEANUP_AFTER_DAYS = cleanupDays;
|
|
709
|
+
|
|
710
|
+
p.log.success(`Cleanup: ${t.bold(cleanupDays === "0" ? "Never" : cleanupDays + " days")}`);
|
|
711
|
+
|
|
712
|
+
// ━━━ Step 8: MCP Servers ━━━
|
|
713
|
+
stepHeader(8, TOTAL_STEPS, "MCP Tool Servers");
|
|
651
714
|
|
|
652
715
|
p.note(
|
|
653
716
|
[
|
|
@@ -667,90 +730,69 @@ export async function runSetupWizard() {
|
|
|
667
730
|
mcpConfig = { mcpServers: {} };
|
|
668
731
|
}
|
|
669
732
|
|
|
670
|
-
// ── Preset servers
|
|
733
|
+
// ── Preset servers (dynamically built from config/mcp.json) ──────────────
|
|
671
734
|
p.log.info(`Press ${t.bold("space")} to select, ${t.bold("enter")} to confirm`);
|
|
672
735
|
|
|
736
|
+
const isPlaceholder = (v) => !v || v.startsWith("YOUR_") || v === "" || v.startsWith("${");
|
|
737
|
+
const allServers = Object.entries(mcpConfig.mcpServers || {})
|
|
738
|
+
.filter(([k]) => !k.startsWith("_comment"))
|
|
739
|
+
.map(([name, cfg]) => {
|
|
740
|
+
const envKeys = cfg.env ? Object.keys(cfg.env) : [];
|
|
741
|
+
const headerKeys = cfg.headers ? Object.keys(cfg.headers) : [];
|
|
742
|
+
const needsCreds = envKeys.some(k => isPlaceholder(cfg.env[k]))
|
|
743
|
+
|| headerKeys.some(k => isPlaceholder(cfg.headers[k]));
|
|
744
|
+
const comment = mcpConfig.mcpServers[`_comment_${name}`] || "";
|
|
745
|
+
const desc = comment.replace(/^[^-]*-\s*/, "").trim();
|
|
746
|
+
const hint = desc
|
|
747
|
+
? `${desc}${needsCreds ? " \u2014 needs credentials" : " \u2014 no key needed"}`
|
|
748
|
+
: needsCreds ? "needs credentials" : "no key needed";
|
|
749
|
+
return { value: name, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " "), hint, needsCreds, envKeys, headerKeys, cfg };
|
|
750
|
+
});
|
|
751
|
+
|
|
673
752
|
const mcpChoices = guard(await p.multiselect({
|
|
674
753
|
message: "Enable built-in MCP servers",
|
|
675
|
-
options:
|
|
676
|
-
{ value: "github", label: "GitHub", hint: "Repos, PRs, issues \u2014 needs token" },
|
|
677
|
-
{ value: "brave-search", label: "Brave Search", hint: "Web search \u2014 needs API key" },
|
|
678
|
-
{ value: "memory", label: "Memory", hint: "Knowledge graph \u2014 no key needed" },
|
|
679
|
-
{ value: "filesystem", label: "Filesystem", hint: "File access \u2014 no key needed" },
|
|
680
|
-
{ value: "fetch", label: "Web Fetch", hint: "Page to text \u2014 no key needed" },
|
|
681
|
-
{ value: "git", label: "Git", hint: "Repo operations \u2014 no key needed" },
|
|
682
|
-
{ value: "slack", label: "Slack", hint: "Workspace \u2014 needs bot token" },
|
|
683
|
-
{ value: "sentry", label: "Sentry", hint: "Error tracking \u2014 needs auth token" },
|
|
684
|
-
],
|
|
754
|
+
options: allServers.map(({ value, label, hint }) => ({ value, label, hint })),
|
|
685
755
|
required: false,
|
|
686
756
|
}));
|
|
687
757
|
|
|
688
|
-
for (const
|
|
689
|
-
|
|
758
|
+
for (const serverName of mcpChoices) {
|
|
759
|
+
const serverInfo = allServers.find(s => s.value === serverName);
|
|
760
|
+
if (!serverInfo || !mcpConfig.mcpServers[serverName]) continue;
|
|
690
761
|
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
].join("\n"),
|
|
699
|
-
"Get GitHub Token"
|
|
700
|
-
);
|
|
701
|
-
const ghToken = guard(await p.password({ message: "GitHub Personal Access Token" }));
|
|
702
|
-
if (ghToken) {
|
|
703
|
-
mcpConfig.mcpServers.github.enabled = true;
|
|
704
|
-
mcpConfig.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN = ghToken;
|
|
762
|
+
if (serverInfo.needsCreds) {
|
|
763
|
+
// Dynamically prompt for each env/header credential
|
|
764
|
+
const credKeys = serverInfo.envKeys.filter(k => isPlaceholder(serverInfo.cfg.env?.[k]));
|
|
765
|
+
const headerCredKeys = serverInfo.headerKeys.filter(k => isPlaceholder(serverInfo.cfg.headers?.[k]));
|
|
766
|
+
|
|
767
|
+
if (credKeys.length > 0 || headerCredKeys.length > 0) {
|
|
768
|
+
p.log.info(`${t.bold(serverInfo.label)} requires ${credKeys.length + headerCredKeys.length} credential(s)`);
|
|
705
769
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const braveKey = guard(await p.password({ message: "Brave Search API key" }));
|
|
716
|
-
if (braveKey) {
|
|
717
|
-
mcpConfig.mcpServers["brave-search"].enabled = true;
|
|
718
|
-
mcpConfig.mcpServers["brave-search"].env.BRAVE_API_KEY = braveKey;
|
|
770
|
+
|
|
771
|
+
let allFilled = true;
|
|
772
|
+
for (const key of credKeys) {
|
|
773
|
+
const val = guard(await p.password({ message: `${key} for ${serverInfo.label}` }));
|
|
774
|
+
if (val) {
|
|
775
|
+
mcpConfig.mcpServers[serverName].env[key] = val;
|
|
776
|
+
} else {
|
|
777
|
+
allFilled = false;
|
|
778
|
+
}
|
|
719
779
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
"5. Get Team ID from workspace URL or Slack settings",
|
|
728
|
-
].join("\n"),
|
|
729
|
-
"Get Slack Token"
|
|
730
|
-
);
|
|
731
|
-
const slackToken = guard(await p.password({ message: "Slack Bot Token (xoxb-...)" }));
|
|
732
|
-
const slackTeam = guard(await p.text({ message: "Slack Team ID" }));
|
|
733
|
-
if (slackToken && mcpConfig.mcpServers.slack) {
|
|
734
|
-
mcpConfig.mcpServers.slack.enabled = true;
|
|
735
|
-
mcpConfig.mcpServers.slack.env.SLACK_BOT_TOKEN = slackToken;
|
|
736
|
-
mcpConfig.mcpServers.slack.env.SLACK_TEAM_ID = slackTeam;
|
|
780
|
+
for (const key of headerCredKeys) {
|
|
781
|
+
const val = guard(await p.password({ message: `${key} for ${serverInfo.label}` }));
|
|
782
|
+
if (val) {
|
|
783
|
+
mcpConfig.mcpServers[serverName].headers[key] = val;
|
|
784
|
+
} else {
|
|
785
|
+
allFilled = false;
|
|
786
|
+
}
|
|
737
787
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
[
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
"3. Select scopes: project:read, event:read",
|
|
744
|
-
].join("\n"),
|
|
745
|
-
"Get Sentry Token"
|
|
746
|
-
);
|
|
747
|
-
const sentryToken = guard(await p.password({ message: "Sentry Auth Token" }));
|
|
748
|
-
if (sentryToken && mcpConfig.mcpServers.sentry) {
|
|
749
|
-
mcpConfig.mcpServers.sentry.enabled = true;
|
|
750
|
-
mcpConfig.mcpServers.sentry.env.SENTRY_AUTH_TOKEN = sentryToken;
|
|
788
|
+
|
|
789
|
+
if (allFilled) {
|
|
790
|
+
mcpConfig.mcpServers[serverName].enabled = true;
|
|
791
|
+
} else {
|
|
792
|
+
p.log.warn(`${serverInfo.label}: missing credentials — saved but not enabled`);
|
|
751
793
|
}
|
|
752
794
|
} else {
|
|
753
|
-
mcpConfig.mcpServers[
|
|
795
|
+
mcpConfig.mcpServers[serverName].enabled = true;
|
|
754
796
|
}
|
|
755
797
|
}
|
|
756
798
|
|
|
@@ -962,7 +1004,7 @@ export async function runSetupWizard() {
|
|
|
962
1004
|
}
|
|
963
1005
|
|
|
964
1006
|
// ━━━ Step 8: Secret Vault ━━━
|
|
965
|
-
stepHeader(
|
|
1007
|
+
stepHeader(9, TOTAL_STEPS, "Secret Vault");
|
|
966
1008
|
|
|
967
1009
|
p.note(
|
|
968
1010
|
[
|
|
@@ -1101,6 +1143,14 @@ export async function runSetupWizard() {
|
|
|
1101
1143
|
}
|
|
1102
1144
|
}
|
|
1103
1145
|
|
|
1146
|
+
// Pull embedding model & pre-embed skills
|
|
1147
|
+
spin.message("Setting up skill embeddings");
|
|
1148
|
+
try {
|
|
1149
|
+
await setupSkillEmbeddings(provider, envConfig, spin);
|
|
1150
|
+
} catch {
|
|
1151
|
+
// Non-fatal — TF-IDF fallback will handle it
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1104
1154
|
spin.stop(`${S.check} Configuration saved`);
|
|
1105
1155
|
|
|
1106
1156
|
// ━━━ Summary ━━━
|