clementine-agent 1.0.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/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/agent/agent-manager.d.ts +69 -0
- package/dist/agent/agent-manager.js +441 -0
- package/dist/agent/assistant.d.ts +225 -0
- package/dist/agent/assistant.js +3888 -0
- package/dist/agent/auto-update.d.ts +32 -0
- package/dist/agent/auto-update.js +186 -0
- package/dist/agent/daily-planner.d.ts +24 -0
- package/dist/agent/daily-planner.js +379 -0
- package/dist/agent/execution-advisor.d.ts +10 -0
- package/dist/agent/execution-advisor.js +272 -0
- package/dist/agent/hooks.d.ts +45 -0
- package/dist/agent/hooks.js +564 -0
- package/dist/agent/insight-engine.d.ts +66 -0
- package/dist/agent/insight-engine.js +225 -0
- package/dist/agent/intent-classifier.d.ts +48 -0
- package/dist/agent/intent-classifier.js +214 -0
- package/dist/agent/link-extractor.d.ts +19 -0
- package/dist/agent/link-extractor.js +90 -0
- package/dist/agent/mcp-bridge.d.ts +62 -0
- package/dist/agent/mcp-bridge.js +435 -0
- package/dist/agent/metacognition.d.ts +66 -0
- package/dist/agent/metacognition.js +221 -0
- package/dist/agent/orchestrator.d.ts +81 -0
- package/dist/agent/orchestrator.js +790 -0
- package/dist/agent/profiles.d.ts +22 -0
- package/dist/agent/profiles.js +91 -0
- package/dist/agent/prompt-cache.d.ts +24 -0
- package/dist/agent/prompt-cache.js +68 -0
- package/dist/agent/prompt-evolver.d.ts +28 -0
- package/dist/agent/prompt-evolver.js +279 -0
- package/dist/agent/role-scaffolds.d.ts +28 -0
- package/dist/agent/role-scaffolds.js +433 -0
- package/dist/agent/safe-restart.d.ts +41 -0
- package/dist/agent/safe-restart.js +150 -0
- package/dist/agent/self-improve.d.ts +66 -0
- package/dist/agent/self-improve.js +1706 -0
- package/dist/agent/session-event-log.d.ts +114 -0
- package/dist/agent/session-event-log.js +233 -0
- package/dist/agent/skill-extractor.d.ts +72 -0
- package/dist/agent/skill-extractor.js +435 -0
- package/dist/agent/source-mods.d.ts +61 -0
- package/dist/agent/source-mods.js +230 -0
- package/dist/agent/source-preflight.d.ts +25 -0
- package/dist/agent/source-preflight.js +100 -0
- package/dist/agent/stall-guard.d.ts +62 -0
- package/dist/agent/stall-guard.js +109 -0
- package/dist/agent/strategic-planner.d.ts +60 -0
- package/dist/agent/strategic-planner.js +352 -0
- package/dist/agent/team-bus.d.ts +89 -0
- package/dist/agent/team-bus.js +556 -0
- package/dist/agent/team-router.d.ts +26 -0
- package/dist/agent/team-router.js +37 -0
- package/dist/agent/tool-loop-detector.d.ts +59 -0
- package/dist/agent/tool-loop-detector.js +242 -0
- package/dist/agent/workflow-runner.d.ts +36 -0
- package/dist/agent/workflow-runner.js +317 -0
- package/dist/agent/workflow-variables.d.ts +16 -0
- package/dist/agent/workflow-variables.js +62 -0
- package/dist/channels/discord-agent-bot.d.ts +101 -0
- package/dist/channels/discord-agent-bot.js +881 -0
- package/dist/channels/discord-bot-manager.d.ts +80 -0
- package/dist/channels/discord-bot-manager.js +262 -0
- package/dist/channels/discord-utils.d.ts +51 -0
- package/dist/channels/discord-utils.js +293 -0
- package/dist/channels/discord.d.ts +12 -0
- package/dist/channels/discord.js +1832 -0
- package/dist/channels/slack-agent-bot.d.ts +73 -0
- package/dist/channels/slack-agent-bot.js +320 -0
- package/dist/channels/slack-bot-manager.d.ts +66 -0
- package/dist/channels/slack-bot-manager.js +236 -0
- package/dist/channels/slack-utils.d.ts +39 -0
- package/dist/channels/slack-utils.js +189 -0
- package/dist/channels/slack.d.ts +11 -0
- package/dist/channels/slack.js +196 -0
- package/dist/channels/telegram.d.ts +10 -0
- package/dist/channels/telegram.js +235 -0
- package/dist/channels/webhook.d.ts +9 -0
- package/dist/channels/webhook.js +78 -0
- package/dist/channels/whatsapp.d.ts +11 -0
- package/dist/channels/whatsapp.js +181 -0
- package/dist/cli/chat.d.ts +14 -0
- package/dist/cli/chat.js +220 -0
- package/dist/cli/cron.d.ts +17 -0
- package/dist/cli/cron.js +552 -0
- package/dist/cli/dashboard.d.ts +15 -0
- package/dist/cli/dashboard.js +17677 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2474 -0
- package/dist/cli/routes/delegations.d.ts +19 -0
- package/dist/cli/routes/delegations.js +154 -0
- package/dist/cli/routes/digest.d.ts +17 -0
- package/dist/cli/routes/digest.js +375 -0
- package/dist/cli/routes/goals.d.ts +14 -0
- package/dist/cli/routes/goals.js +258 -0
- package/dist/cli/routes/workflows.d.ts +18 -0
- package/dist/cli/routes/workflows.js +97 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +619 -0
- package/dist/cli/tunnel.d.ts +35 -0
- package/dist/cli/tunnel.js +141 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +278 -0
- package/dist/events/bus.d.ts +43 -0
- package/dist/events/bus.js +136 -0
- package/dist/gateway/cron-scheduler.d.ts +166 -0
- package/dist/gateway/cron-scheduler.js +1767 -0
- package/dist/gateway/delivery-queue.d.ts +30 -0
- package/dist/gateway/delivery-queue.js +110 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
- package/dist/gateway/heartbeat-scheduler.js +1298 -0
- package/dist/gateway/heartbeat.d.ts +3 -0
- package/dist/gateway/heartbeat.js +3 -0
- package/dist/gateway/lanes.d.ts +24 -0
- package/dist/gateway/lanes.js +76 -0
- package/dist/gateway/notifications.d.ts +29 -0
- package/dist/gateway/notifications.js +75 -0
- package/dist/gateway/router.d.ts +210 -0
- package/dist/gateway/router.js +1330 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1015 -0
- package/dist/memory/chunker.d.ts +28 -0
- package/dist/memory/chunker.js +226 -0
- package/dist/memory/consolidation.d.ts +44 -0
- package/dist/memory/consolidation.js +171 -0
- package/dist/memory/context-assembler.d.ts +50 -0
- package/dist/memory/context-assembler.js +149 -0
- package/dist/memory/embeddings.d.ts +38 -0
- package/dist/memory/embeddings.js +180 -0
- package/dist/memory/graph-store.d.ts +66 -0
- package/dist/memory/graph-store.js +613 -0
- package/dist/memory/mmr.d.ts +21 -0
- package/dist/memory/mmr.js +75 -0
- package/dist/memory/search.d.ts +26 -0
- package/dist/memory/search.js +67 -0
- package/dist/memory/store.d.ts +530 -0
- package/dist/memory/store.js +2022 -0
- package/dist/security/integrity.d.ts +24 -0
- package/dist/security/integrity.js +58 -0
- package/dist/security/patterns.d.ts +34 -0
- package/dist/security/patterns.js +110 -0
- package/dist/security/scanner.d.ts +32 -0
- package/dist/security/scanner.js +263 -0
- package/dist/tools/admin-tools.d.ts +12 -0
- package/dist/tools/admin-tools.js +1278 -0
- package/dist/tools/external-tools.d.ts +11 -0
- package/dist/tools/external-tools.js +1327 -0
- package/dist/tools/goal-tools.d.ts +9 -0
- package/dist/tools/goal-tools.js +159 -0
- package/dist/tools/mcp-server.d.ts +13 -0
- package/dist/tools/mcp-server.js +141 -0
- package/dist/tools/memory-tools.d.ts +10 -0
- package/dist/tools/memory-tools.js +568 -0
- package/dist/tools/session-tools.d.ts +6 -0
- package/dist/tools/session-tools.js +146 -0
- package/dist/tools/shared.d.ts +216 -0
- package/dist/tools/shared.js +340 -0
- package/dist/tools/team-tools.d.ts +6 -0
- package/dist/tools/team-tools.js +447 -0
- package/dist/tools/tool-meta.d.ts +34 -0
- package/dist/tools/tool-meta.js +133 -0
- package/dist/tools/vault-tools.d.ts +8 -0
- package/dist/tools/vault-tools.js +457 -0
- package/dist/types.d.ts +716 -0
- package/dist/types.js +16 -0
- package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
- package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
- package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
- package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
- package/dist/vault-migrations/helpers.d.ts +14 -0
- package/dist/vault-migrations/helpers.js +44 -0
- package/dist/vault-migrations/runner.d.ts +14 -0
- package/dist/vault-migrations/runner.js +139 -0
- package/dist/vault-migrations/types.d.ts +42 -0
- package/dist/vault-migrations/types.js +9 -0
- package/install.sh +320 -0
- package/package.json +84 -0
- package/scripts/postinstall.js +125 -0
- package/vault/00-System/AGENTS.md +66 -0
- package/vault/00-System/CRON.md +71 -0
- package/vault/00-System/HEARTBEAT.md +58 -0
- package/vault/00-System/MEMORY.md +16 -0
- package/vault/00-System/SOUL.md +96 -0
- package/vault/05-Tasks/TASKS.md +19 -0
- package/vault/06-Templates/_Daily-Template.md +28 -0
- package/vault/06-Templates/_People-Template.md +22 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation API routes — extracted from dashboard.ts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import type { DelegatedTask } from '../../types.js';
|
|
6
|
+
import type { Gateway } from '../../gateway/router.js';
|
|
7
|
+
export interface DelegationsRouterDeps {
|
|
8
|
+
agentsBase: string;
|
|
9
|
+
getGateway: () => Promise<Gateway>;
|
|
10
|
+
broadcastEvent: (event: {
|
|
11
|
+
type: string;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
}) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function readAllDelegations(agentsBase: string): (DelegatedTask & {
|
|
16
|
+
_agentDir: string;
|
|
17
|
+
})[];
|
|
18
|
+
export declare function delegationsRouter(deps: DelegationsRouterDeps): Router;
|
|
19
|
+
//# sourceMappingURL=delegations.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation API routes — extracted from dashboard.ts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, unlinkSync, statSync, } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
export function readAllDelegations(agentsBase) {
|
|
9
|
+
const all = [];
|
|
10
|
+
if (!existsSync(agentsBase))
|
|
11
|
+
return all;
|
|
12
|
+
for (const slug of readdirSync(agentsBase).filter(d => !d.startsWith('_') && statSync(path.join(agentsBase, d)).isDirectory())) {
|
|
13
|
+
const delDir = path.join(agentsBase, slug, 'delegations');
|
|
14
|
+
if (!existsSync(delDir))
|
|
15
|
+
continue;
|
|
16
|
+
for (const f of readdirSync(delDir).filter(f => f.endsWith('.json'))) {
|
|
17
|
+
try {
|
|
18
|
+
const task = JSON.parse(readFileSync(path.join(delDir, f), 'utf-8'));
|
|
19
|
+
all.push({ ...task, _agentDir: slug });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return all;
|
|
27
|
+
}
|
|
28
|
+
export function delegationsRouter(deps) {
|
|
29
|
+
const router = Router();
|
|
30
|
+
const { agentsBase, getGateway, broadcastEvent } = deps;
|
|
31
|
+
router.get('/', (_req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
let delegations = readAllDelegations(agentsBase);
|
|
34
|
+
const agent = _req.query.agent;
|
|
35
|
+
const status = _req.query.status;
|
|
36
|
+
const goalId = _req.query.goalId;
|
|
37
|
+
if (agent)
|
|
38
|
+
delegations = delegations.filter(d => d.toAgent === agent || d.fromAgent === agent);
|
|
39
|
+
if (status)
|
|
40
|
+
delegations = delegations.filter(d => d.status === status);
|
|
41
|
+
if (goalId)
|
|
42
|
+
delegations = delegations.filter(d => d.goalId === goalId);
|
|
43
|
+
res.json({ ok: true, delegations });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
router.post('/', express.json(), (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { fromAgent, toAgent, task, expectedOutput, goalId } = req.body;
|
|
52
|
+
if (!toAgent || !task) {
|
|
53
|
+
res.status(400).json({ ok: false, error: 'toAgent and task are required' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const id = Math.random().toString(16).slice(2, 10);
|
|
57
|
+
const delDir = path.join(agentsBase, toAgent, 'delegations');
|
|
58
|
+
if (!existsSync(delDir))
|
|
59
|
+
mkdirSync(delDir, { recursive: true });
|
|
60
|
+
const delegation = {
|
|
61
|
+
id, fromAgent: fromAgent || 'clementine', toAgent, task,
|
|
62
|
+
expectedOutput: expectedOutput || '', status: 'pending',
|
|
63
|
+
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
64
|
+
goalId: goalId || undefined,
|
|
65
|
+
};
|
|
66
|
+
writeFileSync(path.join(delDir, `${id}.json`), JSON.stringify(delegation, null, 2));
|
|
67
|
+
res.json({ ok: true, delegation });
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
router.put('/:id', express.json(), (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
const all = readAllDelegations(agentsBase);
|
|
76
|
+
const found = all.find(d => d.id === req.params.id);
|
|
77
|
+
if (!found) {
|
|
78
|
+
res.status(404).json({ ok: false, error: 'Delegation not found' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const filePath = path.join(agentsBase, found._agentDir, 'delegations', `${found.id}.json`);
|
|
82
|
+
const { status, result, task, expectedOutput } = req.body;
|
|
83
|
+
if (status !== undefined)
|
|
84
|
+
found.status = status;
|
|
85
|
+
if (result !== undefined)
|
|
86
|
+
found.result = result;
|
|
87
|
+
if (task !== undefined)
|
|
88
|
+
found.task = task;
|
|
89
|
+
if (expectedOutput !== undefined)
|
|
90
|
+
found.expectedOutput = expectedOutput;
|
|
91
|
+
found.updatedAt = new Date().toISOString();
|
|
92
|
+
const { _agentDir, ...clean } = found;
|
|
93
|
+
writeFileSync(filePath, JSON.stringify(clean, null, 2));
|
|
94
|
+
res.json({ ok: true, delegation: clean });
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
router.delete('/:id', (_req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const all = readAllDelegations(agentsBase);
|
|
103
|
+
const found = all.find(d => d.id === _req.params.id);
|
|
104
|
+
if (!found) {
|
|
105
|
+
res.status(404).json({ ok: false, error: 'Delegation not found' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
unlinkSync(path.join(agentsBase, found._agentDir, 'delegations', `${found.id}.json`));
|
|
109
|
+
res.json({ ok: true });
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
router.post('/:id/execute', async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const all = readAllDelegations(agentsBase);
|
|
118
|
+
const found = all.find(d => d.id === req.params.id);
|
|
119
|
+
if (!found) {
|
|
120
|
+
res.status(404).json({ ok: false, error: 'Delegation not found' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (found.status === 'in_progress') {
|
|
124
|
+
res.json({ ok: false, error: 'Already executing' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const filePath = path.join(agentsBase, found._agentDir, 'delegations', `${found.id}.json`);
|
|
128
|
+
const { _agentDir, ...clean } = found;
|
|
129
|
+
clean.status = 'in_progress';
|
|
130
|
+
clean.updatedAt = new Date().toISOString();
|
|
131
|
+
writeFileSync(filePath, JSON.stringify(clean, null, 2));
|
|
132
|
+
res.json({ ok: true, message: 'Delegation execution started' });
|
|
133
|
+
broadcastEvent({ type: 'delegation_started', data: { id: found.id, toAgent: found.toAgent } });
|
|
134
|
+
const prompt = `You have been delegated the following task:\n\n## Task\n${found.task}\n\n## Expected Output\n${found.expectedOutput || 'Complete the task to the best of your ability.'}\n\nComplete this task now.`;
|
|
135
|
+
getGateway().then(gw => gw.handleCronJob(`delegation:${found.id}`, prompt, 2, 15)).then(result => {
|
|
136
|
+
clean.status = 'completed';
|
|
137
|
+
clean.result = result;
|
|
138
|
+
clean.updatedAt = new Date().toISOString();
|
|
139
|
+
writeFileSync(filePath, JSON.stringify(clean, null, 2));
|
|
140
|
+
broadcastEvent({ type: 'delegation_complete', data: { id: found.id, toAgent: found.toAgent, status: 'completed' } });
|
|
141
|
+
}).catch(err => {
|
|
142
|
+
clean.status = 'pending';
|
|
143
|
+
clean.updatedAt = new Date().toISOString();
|
|
144
|
+
writeFileSync(filePath, JSON.stringify(clean, null, 2));
|
|
145
|
+
broadcastEvent({ type: 'delegation_complete', data: { id: found.id, toAgent: found.toAgent, status: 'error', error: String(err) } });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return router;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=delegations.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Digest + Voice API routes — extracted from dashboard.ts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import type { Gateway } from '../../gateway/router.js';
|
|
6
|
+
export interface DigestRouterDeps {
|
|
7
|
+
baseDir: string;
|
|
8
|
+
vaultDir: string;
|
|
9
|
+
goalsDir: string;
|
|
10
|
+
memoryDbPath: string;
|
|
11
|
+
parseEnvFile: () => Record<string, string>;
|
|
12
|
+
getGateway: () => Promise<Gateway>;
|
|
13
|
+
cached: <T>(key: string, ttlMs: number, compute: () => T) => T;
|
|
14
|
+
}
|
|
15
|
+
export declare function getDigestPrefs(prefsFile: string): Record<string, unknown>;
|
|
16
|
+
export declare function digestRouter(deps: DigestRouterDeps): Router;
|
|
17
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Digest + Voice API routes — extracted from dashboard.ts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, } from 'node:fs';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
export function getDigestPrefs(prefsFile) {
|
|
11
|
+
const defaults = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
schedule: '0 8 * * 1-5',
|
|
14
|
+
channels: { email: true, discord: true, slack: false, voice: false },
|
|
15
|
+
emailRecipient: '',
|
|
16
|
+
sections: { summary: true, goals: true, crons: true, activity: true, metrics: true, approvals: true },
|
|
17
|
+
quietHours: { start: 22, end: 8 },
|
|
18
|
+
};
|
|
19
|
+
if (!existsSync(prefsFile))
|
|
20
|
+
return defaults;
|
|
21
|
+
try {
|
|
22
|
+
return { ...defaults, ...JSON.parse(readFileSync(prefsFile, 'utf-8')) };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return defaults;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function digestRouter(deps) {
|
|
29
|
+
const router = Router();
|
|
30
|
+
const { baseDir, vaultDir, goalsDir, memoryDbPath, parseEnvFile, getGateway, cached } = deps;
|
|
31
|
+
const prefsFile = path.join(baseDir, 'digest-preferences.json');
|
|
32
|
+
const voiceCacheDir = path.join(baseDir, 'cache', 'voice');
|
|
33
|
+
// Graph API helper for email
|
|
34
|
+
let _graphTokenCache = null;
|
|
35
|
+
async function getGraphToken() {
|
|
36
|
+
if (_graphTokenCache && Date.now() < _graphTokenCache.expiresAt - 300_000)
|
|
37
|
+
return _graphTokenCache.accessToken;
|
|
38
|
+
const env = parseEnvFile();
|
|
39
|
+
const tenantId = env['MS_TENANT_ID'] || '';
|
|
40
|
+
const clientId = env['MS_CLIENT_ID'] || '';
|
|
41
|
+
const clientSecret = env['MS_CLIENT_SECRET'] || '';
|
|
42
|
+
if (!tenantId || !clientId || !clientSecret)
|
|
43
|
+
throw new Error('Outlook not configured');
|
|
44
|
+
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
45
|
+
const body = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, scope: 'https://graph.microsoft.com/.default', grant_type: 'client_credentials' });
|
|
46
|
+
const res = await fetch(tokenUrl, { method: 'POST', body });
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
throw new Error(`Graph token failed: ${res.status}`);
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
_graphTokenCache = { accessToken: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
|
|
51
|
+
return data.access_token;
|
|
52
|
+
}
|
|
53
|
+
async function sendDigestEmail(to, subject, htmlBody) {
|
|
54
|
+
const env = parseEnvFile();
|
|
55
|
+
const userEmail = env['MS_USER_EMAIL'] || '';
|
|
56
|
+
if (!userEmail)
|
|
57
|
+
throw new Error('MS_USER_EMAIL not configured');
|
|
58
|
+
const token = await getGraphToken();
|
|
59
|
+
const message = {
|
|
60
|
+
subject,
|
|
61
|
+
body: { contentType: 'HTML', content: htmlBody },
|
|
62
|
+
toRecipients: [{ emailAddress: { address: to } }],
|
|
63
|
+
};
|
|
64
|
+
const res = await fetch(`https://graph.microsoft.com/v1.0/users/${userEmail}/sendMail`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ message, saveToSentItems: true }),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
throw new Error(`Graph sendMail ${res.status}: ${text}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function composeDigest() {
|
|
75
|
+
const prefs = getDigestPrefs(prefsFile);
|
|
76
|
+
const secs = (prefs.sections || {});
|
|
77
|
+
const now = new Date();
|
|
78
|
+
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
79
|
+
const sections = {};
|
|
80
|
+
let officeSummary = '';
|
|
81
|
+
try {
|
|
82
|
+
const officeData = cached('digest-office', 30_000, () => {
|
|
83
|
+
const agentsDir = path.join(vaultDir, '00-System', 'agents');
|
|
84
|
+
const agents = [];
|
|
85
|
+
if (existsSync(agentsDir)) {
|
|
86
|
+
for (const slug of readdirSync(agentsDir).filter(d => !d.startsWith('_'))) {
|
|
87
|
+
const agentFile = path.join(agentsDir, slug, 'agent.md');
|
|
88
|
+
if (!existsSync(agentFile))
|
|
89
|
+
continue;
|
|
90
|
+
const { data } = matter(readFileSync(agentFile, 'utf-8'));
|
|
91
|
+
agents.push({ slug, name: data.name || slug, status: data.status || 'active' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { agents };
|
|
95
|
+
});
|
|
96
|
+
const activeCount = officeData.agents.filter((a) => a.status === 'active').length;
|
|
97
|
+
officeSummary = `${officeData.agents.length} agent(s), ${activeCount} active`;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
officeSummary = 'Could not load';
|
|
101
|
+
}
|
|
102
|
+
if (secs.goals !== false) {
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(goalsDir)) {
|
|
105
|
+
const files = readdirSync(goalsDir).filter(f => f.endsWith('.json'));
|
|
106
|
+
const goals = files.map(f => { try {
|
|
107
|
+
return JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
} }).filter(Boolean);
|
|
112
|
+
const active = goals.filter((g) => g.status === 'active');
|
|
113
|
+
const blocked = goals.filter((g) => g.status === 'blocked');
|
|
114
|
+
let goalText = `${active.length} active, ${blocked.length} blocked\n`;
|
|
115
|
+
active.slice(0, 5).forEach((g) => {
|
|
116
|
+
goalText += ` - ${g.title} [${g.priority}]`;
|
|
117
|
+
const na = g.nextActions;
|
|
118
|
+
if (na && na.length > 0)
|
|
119
|
+
goalText += ` → ${na[0]}`;
|
|
120
|
+
goalText += '\n';
|
|
121
|
+
});
|
|
122
|
+
sections.goals = goalText;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch { /* skip */ }
|
|
126
|
+
}
|
|
127
|
+
if (secs.crons !== false) {
|
|
128
|
+
try {
|
|
129
|
+
const runsDir = path.join(baseDir, 'cron', 'runs');
|
|
130
|
+
if (existsSync(runsDir)) {
|
|
131
|
+
let totalOk = 0, totalErr = 0, jobCount = 0;
|
|
132
|
+
for (const f of readdirSync(runsDir).filter(f => f.endsWith('.jsonl'))) {
|
|
133
|
+
const lines = readFileSync(path.join(runsDir, f), 'utf-8').trim().split('\n').filter(Boolean);
|
|
134
|
+
const today = now.toISOString().slice(0, 10);
|
|
135
|
+
const todayRuns = lines.filter(l => l.includes(today));
|
|
136
|
+
if (todayRuns.length > 0)
|
|
137
|
+
jobCount++;
|
|
138
|
+
for (const l of todayRuns) {
|
|
139
|
+
try {
|
|
140
|
+
const e = JSON.parse(l);
|
|
141
|
+
if (e.status === 'ok')
|
|
142
|
+
totalOk++;
|
|
143
|
+
else
|
|
144
|
+
totalErr++;
|
|
145
|
+
}
|
|
146
|
+
catch { /* skip */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
sections.crons = `${jobCount} job(s) ran today: ${totalOk} succeeded, ${totalErr} failed`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { /* skip */ }
|
|
153
|
+
}
|
|
154
|
+
if (secs.approvals !== false) {
|
|
155
|
+
try {
|
|
156
|
+
if (existsSync(memoryDbPath)) {
|
|
157
|
+
const Database = (await import('better-sqlite3')).default;
|
|
158
|
+
const db = new Database(memoryDbPath, { readonly: true });
|
|
159
|
+
try {
|
|
160
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM approval_queue WHERE status = 'pending'").get();
|
|
161
|
+
if (row && row.cnt > 0)
|
|
162
|
+
sections.approvals = `${row.cnt} pending approval(s)`;
|
|
163
|
+
}
|
|
164
|
+
catch { /* table may not exist */ }
|
|
165
|
+
db.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* skip */ }
|
|
169
|
+
}
|
|
170
|
+
let text = `Daily Digest — ${dateStr}\n${'='.repeat(40)}\n\nTeam: ${officeSummary}\n`;
|
|
171
|
+
if (sections.goals)
|
|
172
|
+
text += `\nGoals:\n${sections.goals}`;
|
|
173
|
+
if (sections.crons)
|
|
174
|
+
text += `\nCron Jobs: ${sections.crons}\n`;
|
|
175
|
+
if (sections.approvals)
|
|
176
|
+
text += `\nApprovals: ${sections.approvals}\n`;
|
|
177
|
+
const html = `<!DOCTYPE html><html><body style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:600px;margin:0 auto;color:#1a1a2e;padding:20px">
|
|
178
|
+
<div style="border-bottom:3px solid #ff8c21;padding-bottom:12px;margin-bottom:20px">
|
|
179
|
+
<h1 style="margin:0;font-size:22px;color:#1a1a2e">Daily Digest</h1>
|
|
180
|
+
<div style="color:#8a92a0;font-size:13px;margin-top:4px">${dateStr} · ${officeSummary}</div>
|
|
181
|
+
</div>
|
|
182
|
+
${sections.goals ? `<div style="margin-bottom:20px"><h3 style="margin:0 0 8px;font-size:15px;color:#ff8c21">Goals</h3><pre style="margin:0;font-size:13px;color:#5a6070;white-space:pre-wrap">${sections.goals.replace(/</g, '<')}</pre></div>` : ''}
|
|
183
|
+
${sections.crons ? `<div style="margin-bottom:20px"><h3 style="margin:0 0 8px;font-size:15px;color:#ff8c21">Cron Jobs</h3><p style="margin:0;font-size:13px;color:#5a6070">${sections.crons.replace(/</g, '<')}</p></div>` : ''}
|
|
184
|
+
${sections.approvals ? `<div style="margin-bottom:20px"><h3 style="margin:0 0 8px;font-size:15px;color:#ff8c21">Approvals</h3><p style="margin:0;font-size:13px;color:#e5534b;font-weight:600">${sections.approvals}</p></div>` : ''}
|
|
185
|
+
<div style="margin-top:24px;padding-top:12px;border-top:1px solid #d8dde5;font-size:11px;color:#8a92a0">Sent by Clementine Command Center</div>
|
|
186
|
+
</body></html>`;
|
|
187
|
+
return { subject: `Clementine Digest — ${dateStr}`, html, text, sections };
|
|
188
|
+
}
|
|
189
|
+
// Preferences
|
|
190
|
+
router.get('/preferences', (_req, res) => {
|
|
191
|
+
res.json({ ok: true, preferences: getDigestPrefs(prefsFile) });
|
|
192
|
+
});
|
|
193
|
+
router.put('/preferences', express.json(), (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const current = getDigestPrefs(prefsFile);
|
|
196
|
+
const updated = { ...current, ...req.body };
|
|
197
|
+
writeFileSync(prefsFile, JSON.stringify(updated, null, 2));
|
|
198
|
+
res.json({ ok: true, preferences: updated });
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// Preview
|
|
205
|
+
router.get('/preview', async (_req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const digest = await composeDigest();
|
|
208
|
+
res.json({ ok: true, ...digest });
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// Send
|
|
215
|
+
router.post('/send', async (_req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
const prefs = getDigestPrefs(prefsFile);
|
|
218
|
+
const channels = (prefs.channels || {});
|
|
219
|
+
const digest = await composeDigest();
|
|
220
|
+
const results = {};
|
|
221
|
+
if (channels.email) {
|
|
222
|
+
const recipient = prefs.emailRecipient || parseEnvFile()['MS_USER_EMAIL'] || '';
|
|
223
|
+
if (recipient) {
|
|
224
|
+
try {
|
|
225
|
+
await sendDigestEmail(recipient, digest.subject, digest.html);
|
|
226
|
+
results.email = 'sent';
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
results.email = 'error: ' + String(e);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
results.email = 'skipped: no recipient';
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (channels.discord || channels.slack) {
|
|
237
|
+
try {
|
|
238
|
+
const gw = await getGateway();
|
|
239
|
+
const dispatcher = gw.dispatcher || gw.notificationDispatcher;
|
|
240
|
+
if (dispatcher && typeof dispatcher.send === 'function') {
|
|
241
|
+
await dispatcher.send(`**${digest.subject}**\n\n${digest.text}`);
|
|
242
|
+
results.channels = 'sent';
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
results.channels = 'skipped: no dispatcher';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
results.channels = 'error: ' + String(e);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (channels.voice) {
|
|
253
|
+
try {
|
|
254
|
+
const env = parseEnvFile();
|
|
255
|
+
const apiKey = env['ELEVENLABS_API_KEY'] || '';
|
|
256
|
+
const voiceId = env['ELEVENLABS_VOICE_ID'] || '';
|
|
257
|
+
if (apiKey && voiceId) {
|
|
258
|
+
const hash = randomBytes(8).toString('hex');
|
|
259
|
+
if (!existsSync(voiceCacheDir))
|
|
260
|
+
mkdirSync(voiceCacheDir, { recursive: true });
|
|
261
|
+
const audioPath = path.join(voiceCacheDir, `${hash}.mp3`);
|
|
262
|
+
const ttsRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'xi-api-key': apiKey, 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ text: digest.text.slice(0, 4000), model_id: 'eleven_multilingual_v2' }),
|
|
266
|
+
});
|
|
267
|
+
if (ttsRes.ok) {
|
|
268
|
+
const buffer = Buffer.from(await ttsRes.arrayBuffer());
|
|
269
|
+
writeFileSync(audioPath, buffer);
|
|
270
|
+
results.voice = hash;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
results.voice = 'error: ElevenLabs ' + ttsRes.status;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
results.voice = 'skipped: ElevenLabs not configured';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
results.voice = 'error: ' + String(e);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
res.json({ ok: true, results });
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// Test
|
|
291
|
+
router.post('/test', async (_req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const digest = await composeDigest();
|
|
294
|
+
const prefs = getDigestPrefs(prefsFile);
|
|
295
|
+
const channels = (prefs.channels || {});
|
|
296
|
+
const results = {};
|
|
297
|
+
const recipient = prefs.emailRecipient || parseEnvFile()['MS_USER_EMAIL'] || '';
|
|
298
|
+
if (recipient && channels.email) {
|
|
299
|
+
try {
|
|
300
|
+
await sendDigestEmail(recipient, '[TEST] ' + digest.subject, digest.html);
|
|
301
|
+
results.email = 'sent';
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
results.email = 'error: ' + String(e);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (channels.discord || channels.slack) {
|
|
308
|
+
try {
|
|
309
|
+
const gw = await getGateway();
|
|
310
|
+
const dispatcher = gw.dispatcher || gw.notificationDispatcher;
|
|
311
|
+
if (dispatcher && typeof dispatcher.send === 'function') {
|
|
312
|
+
await dispatcher.send(`**[TEST] ${digest.subject}**\n\n${digest.text}`);
|
|
313
|
+
results.channels = 'sent';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
results.channels = 'error: ' + String(e);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
res.json({ ok: true, results, preview: { subject: digest.subject, text: digest.text } });
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
// Voice synthesis
|
|
327
|
+
router.post('/voice/synthesize', express.json(), async (req, res) => {
|
|
328
|
+
try {
|
|
329
|
+
const text = (req.body.text || '').slice(0, 5000);
|
|
330
|
+
if (!text) {
|
|
331
|
+
res.status(400).json({ ok: false, error: 'text is required' });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const env = parseEnvFile();
|
|
335
|
+
const apiKey = env['ELEVENLABS_API_KEY'] || '';
|
|
336
|
+
const voiceId = env['ELEVENLABS_VOICE_ID'] || '';
|
|
337
|
+
if (!apiKey || !voiceId) {
|
|
338
|
+
res.status(400).json({ ok: false, error: 'ElevenLabs not configured' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const hash = randomBytes(8).toString('hex');
|
|
342
|
+
if (!existsSync(voiceCacheDir))
|
|
343
|
+
mkdirSync(voiceCacheDir, { recursive: true });
|
|
344
|
+
const audioPath = path.join(voiceCacheDir, `${hash}.mp3`);
|
|
345
|
+
const ttsRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'xi-api-key': apiKey, 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify({ text, model_id: 'eleven_multilingual_v2' }),
|
|
349
|
+
});
|
|
350
|
+
if (!ttsRes.ok) {
|
|
351
|
+
res.status(502).json({ ok: false, error: 'ElevenLabs error: ' + ttsRes.status });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const buffer = Buffer.from(await ttsRes.arrayBuffer());
|
|
355
|
+
writeFileSync(audioPath, buffer);
|
|
356
|
+
res.json({ ok: true, url: `/api/voice/audio/${hash}`, hash });
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
router.get('/voice/audio/:hash', (req, res) => {
|
|
363
|
+
const hash = req.params.hash.replace(/[^a-f0-9]/g, '');
|
|
364
|
+
const audioPath = path.join(voiceCacheDir, `${hash}.mp3`);
|
|
365
|
+
if (!existsSync(audioPath)) {
|
|
366
|
+
res.status(404).json({ error: 'Audio not found' });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
res.setHeader('Content-Type', 'audio/mpeg');
|
|
370
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
371
|
+
res.send(readFileSync(audioPath));
|
|
372
|
+
});
|
|
373
|
+
return router;
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goals API routes — extracted from dashboard.ts
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import type { Gateway } from '../../gateway/router.js';
|
|
6
|
+
export interface GoalsRouterDeps {
|
|
7
|
+
goalsDir: string;
|
|
8
|
+
cronRunsDir: string;
|
|
9
|
+
vaultDir: string;
|
|
10
|
+
cronFile: string;
|
|
11
|
+
getGateway: () => Promise<Gateway>;
|
|
12
|
+
}
|
|
13
|
+
export declare function goalsRouter(deps: GoalsRouterDeps): Router;
|
|
14
|
+
//# sourceMappingURL=goals.d.ts.map
|