clementine-agent 1.18.61 → 1.18.64
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 +5 -0
- package/build/icons/Clementine.iconset/icon_128x128.png +0 -0
- package/build/icons/Clementine.iconset/icon_128x128@2x.png +0 -0
- package/build/icons/Clementine.iconset/icon_16x16.png +0 -0
- package/build/icons/Clementine.iconset/icon_16x16@2x.png +0 -0
- package/build/icons/Clementine.iconset/icon_256x256.png +0 -0
- package/build/icons/Clementine.iconset/icon_256x256@2x.png +0 -0
- package/build/icons/Clementine.iconset/icon_32x32.png +0 -0
- package/build/icons/Clementine.iconset/icon_32x32@2x.png +0 -0
- package/build/icons/Clementine.iconset/icon_512x512.png +0 -0
- package/build/icons/Clementine.iconset/icon_512x512@2x.png +0 -0
- package/build/icons/clementine.png +0 -0
- package/build/icons/icon.icns +0 -0
- package/build/icons/tray.png +0 -0
- package/build/icons/tray@2x.png +0 -0
- package/dist/agent/agent-definitions.js +3 -2
- package/dist/cli/dashboard.js +94 -6
- package/dist/config.d.ts +5 -0
- package/dist/config.js +5 -0
- package/dist/desktop/main.d.ts +8 -0
- package/dist/desktop/main.js +330 -0
- package/dist/gateway/cron-diagnostic-turn.d.ts +0 -3
- package/dist/gateway/cron-diagnostic-turn.js +0 -6
- package/dist/gateway/cron-scheduler.d.ts +21 -0
- package/dist/gateway/cron-scheduler.js +53 -12
- package/dist/gateway/notifications.d.ts +24 -0
- package/dist/gateway/notifications.js +80 -2
- package/dist/gateway/recent-context.js +4 -57
- package/dist/gateway/router.js +0 -29
- package/dist/memory/store.js +21 -1
- package/dist/tools/admin-tools.js +11 -0
- package/dist/voice/gemini-live.d.ts +34 -0
- package/dist/voice/gemini-live.js +263 -0
- package/electron-builder.yml +36 -0
- package/package.json +13 -1
- package/scripts/build-desktop-mac.sh +132 -0
package/.env.example
CHANGED
|
@@ -36,6 +36,11 @@ WEBHOOK_SECRET=
|
|
|
36
36
|
GROQ_API_KEY=
|
|
37
37
|
ELEVENLABS_API_KEY=
|
|
38
38
|
ELEVENLABS_VOICE_ID=
|
|
39
|
+
VOICE_ENABLED=false
|
|
40
|
+
VOICE_PROVIDER=gemini-live
|
|
41
|
+
GEMINI_API_KEY=
|
|
42
|
+
GEMINI_LIVE_MODEL=gemini-3.1-flash-live-preview
|
|
43
|
+
GEMINI_LIVE_VOICE_NAME=Kore
|
|
39
44
|
|
|
40
45
|
# ── Video (optional) ─────────────────────────────────────────────────
|
|
41
46
|
GOOGLE_API_KEY=
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -54,10 +54,10 @@ const CRON_FIXER_PROMPT = [
|
|
|
54
54
|
'You are the cron-fix specialist. You diagnose and apply fixes to broken cron jobs.',
|
|
55
55
|
'',
|
|
56
56
|
'Workflow:',
|
|
57
|
-
'1.
|
|
57
|
+
'1. If you already know the job name (parent named it, or notification context names it), call `cron_diagnose` first — it returns the bounded recent-run summary, phase status, and inferred root cause in one shot. If you need a list of currently failing jobs, call `list_broken_jobs` instead.',
|
|
58
58
|
'2. For each job the user/parent asked about, check the proposed fix:',
|
|
59
59
|
' - confidence=high + risk=low + autoApply=true → call `apply_broken_job_fix`.',
|
|
60
|
-
' - Otherwise → describe the diagnosis and ask the parent for explicit approval.',
|
|
60
|
+
' - Otherwise → describe the diagnosis and ask the parent for explicit approval before any manual repair.',
|
|
61
61
|
'3. After applying a fix, the verification system auto-rolls-back if the next 3 runs do not improve. You do NOT need to monitor manually.',
|
|
62
62
|
'',
|
|
63
63
|
'Return: a one-paragraph summary of what you applied (or what is blocking apply), per job.',
|
|
@@ -154,6 +154,7 @@ export function buildAgentMap(opts = {}) {
|
|
|
154
154
|
prompt: CRON_FIXER_PROMPT,
|
|
155
155
|
model: 'sonnet',
|
|
156
156
|
tools: [
|
|
157
|
+
'mcp__clementine-tools__cron_diagnose',
|
|
157
158
|
'mcp__clementine-tools__list_broken_jobs',
|
|
158
159
|
'mcp__clementine-tools__apply_broken_job_fix',
|
|
159
160
|
'mcp__clementine-tools__cron_list',
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -181,9 +181,28 @@ async function getGateway() {
|
|
|
181
181
|
dispatcher.register('dashboard', async (text, context) => {
|
|
182
182
|
if (!dashboardSseBroadcast)
|
|
183
183
|
return;
|
|
184
|
+
// Forward agent identity so the chat panel can render the
|
|
185
|
+
// originating hired agent's name + initial — same UX guarantee
|
|
186
|
+
// Discord/Slack already give via per-agent bot identities.
|
|
187
|
+
// agentSlug arrives populated by either the explicit caller
|
|
188
|
+
// (cron-scheduler.dispatchContextForJob etc.) or by the
|
|
189
|
+
// dispatcher's session-key inference safety net.
|
|
190
|
+
let agentName = null;
|
|
191
|
+
if (context?.agentSlug && context.agentSlug !== 'clementine') {
|
|
192
|
+
try {
|
|
193
|
+
const profile = gatewayInstance?.getAgentManager().get(context.agentSlug);
|
|
194
|
+
agentName = profile?.name ?? null;
|
|
195
|
+
}
|
|
196
|
+
catch { /* best-effort — fall back to slug */ }
|
|
197
|
+
}
|
|
184
198
|
dashboardSseBroadcast({
|
|
185
199
|
type: 'deep_result',
|
|
186
|
-
data: {
|
|
200
|
+
data: {
|
|
201
|
+
sessionKey: context?.sessionKey ?? null,
|
|
202
|
+
agentSlug: context?.agentSlug ?? null,
|
|
203
|
+
agentName,
|
|
204
|
+
text,
|
|
205
|
+
},
|
|
187
206
|
});
|
|
188
207
|
});
|
|
189
208
|
gatewayInstance.setDispatcher(dispatcher);
|
|
@@ -19551,6 +19570,51 @@ let currentPage = 'home';
|
|
|
19551
19570
|
var currentAgentSlug = null;
|
|
19552
19571
|
var prevAgentSlugs = null;
|
|
19553
19572
|
|
|
19573
|
+
// Browser-side cache of /api/agents — feeds chat-bubble identity rendering
|
|
19574
|
+
// so a hired agent's response shows their name/initial instead of
|
|
19575
|
+
// Clementine's. Refreshed on profile switch + on deep_result events
|
|
19576
|
+
// where the cache is missing the slug. Same UX guarantee Discord/Slack
|
|
19577
|
+
// give via per-agent bot identities.
|
|
19578
|
+
var __teamAgentsByslug = {};
|
|
19579
|
+
var __teamAgentsLoadedAt = 0;
|
|
19580
|
+
async function ensureTeamAgentCache(force) {
|
|
19581
|
+
if (!force && __teamAgentsLoadedAt && (Date.now() - __teamAgentsLoadedAt < 60_000)) return;
|
|
19582
|
+
try {
|
|
19583
|
+
var r = await apiFetch('/api/agents');
|
|
19584
|
+
if (!r.ok) return;
|
|
19585
|
+
var agents = await r.json();
|
|
19586
|
+
var next = {};
|
|
19587
|
+
if (Array.isArray(agents)) {
|
|
19588
|
+
for (var i = 0; i < agents.length; i++) {
|
|
19589
|
+
var a = agents[i];
|
|
19590
|
+
if (a && a.slug) next[a.slug] = { slug: a.slug, name: a.name || a.slug, avatar: a.avatar || null };
|
|
19591
|
+
}
|
|
19592
|
+
}
|
|
19593
|
+
__teamAgentsByslug = next;
|
|
19594
|
+
__teamAgentsLoadedAt = Date.now();
|
|
19595
|
+
} catch (e) { /* non-fatal — fall back to Clementine */ }
|
|
19596
|
+
}
|
|
19597
|
+
|
|
19598
|
+
/**
|
|
19599
|
+
* Resolve the display identity for an assistant chat bubble.
|
|
19600
|
+
*
|
|
19601
|
+
* getAssistantIdentity() → active chat profile (or Clementine)
|
|
19602
|
+
* getAssistantIdentity(slugFromSseEvent) → that specific agent
|
|
19603
|
+
*
|
|
19604
|
+
* Returns { slug, name, initial }. Always safe — falls back to
|
|
19605
|
+
* Clementine when nothing matches. Initial is uppercase first char,
|
|
19606
|
+
* suitable for a .chat-avatar-sm bubble.
|
|
19607
|
+
*/
|
|
19608
|
+
function getAssistantIdentity(explicitSlug) {
|
|
19609
|
+
var slug = explicitSlug || currentAgentSlug || '';
|
|
19610
|
+
if (slug && slug !== 'clementine' && __teamAgentsByslug[slug]) {
|
|
19611
|
+
var a = __teamAgentsByslug[slug];
|
|
19612
|
+
return { slug: a.slug, name: a.name, initial: (a.name || 'A').charAt(0).toUpperCase() };
|
|
19613
|
+
}
|
|
19614
|
+
var clemName = (lastStatusData && lastStatusData.name) ? lastStatusData.name : 'Clementine';
|
|
19615
|
+
return { slug: 'clementine', name: clemName, initial: clemName.charAt(0).toUpperCase() };
|
|
19616
|
+
}
|
|
19617
|
+
|
|
19554
19618
|
// ── Routing ────────────────────────────────────────────────────
|
|
19555
19619
|
//
|
|
19556
19620
|
// Five top-level destinations: home, build, team, brain, settings.
|
|
@@ -24359,18 +24423,23 @@ async function sendChat() {
|
|
|
24359
24423
|
sendBtn.textContent = 'Thinking...';
|
|
24360
24424
|
|
|
24361
24425
|
try {
|
|
24426
|
+
// Identity follows the active chat profile so a hired agent's
|
|
24427
|
+
// reply renders with their initial — same as Discord/Slack.
|
|
24428
|
+
await ensureTeamAgentCache();
|
|
24429
|
+
var asstIdentity = getAssistantIdentity();
|
|
24362
24430
|
var asstRow = document.createElement('div');
|
|
24363
24431
|
asstRow.className = 'chat-assistant-row';
|
|
24364
24432
|
var chatAv = document.createElement('div');
|
|
24365
24433
|
chatAv.className = 'chat-avatar-sm';
|
|
24366
|
-
chatAv.
|
|
24434
|
+
chatAv.title = asstIdentity.name;
|
|
24435
|
+
chatAv.innerHTML = asstIdentity.initial;
|
|
24367
24436
|
asstRow.appendChild(chatAv);
|
|
24368
24437
|
var asstBubble = document.createElement('div');
|
|
24369
24438
|
asstBubble.className = 'chat-bubble assistant';
|
|
24370
24439
|
asstBubble.innerHTML = '<span style="color:var(--text-muted);font-style:italic">connecting...</span>';
|
|
24371
24440
|
var asstMeta = document.createElement('div');
|
|
24372
24441
|
asstMeta.className = 'chat-meta';
|
|
24373
|
-
asstMeta.textContent = new Date().toLocaleTimeString();
|
|
24442
|
+
asstMeta.textContent = new Date().toLocaleTimeString() + (asstIdentity.slug !== 'clementine' ? ' · ' + asstIdentity.name : '');
|
|
24374
24443
|
asstRow.appendChild(asstBubble);
|
|
24375
24444
|
container.appendChild(asstRow);
|
|
24376
24445
|
typing.remove();
|
|
@@ -32436,6 +32505,21 @@ try {
|
|
|
32436
32505
|
try {
|
|
32437
32506
|
var container = document.getElementById('chat-messages');
|
|
32438
32507
|
var text = (evt.data && evt.data.text) ? evt.data.text : '';
|
|
32508
|
+
// Identity from the SSE payload \u2014 server-side
|
|
32509
|
+
// dashboard sender resolves agentSlug + agentName from
|
|
32510
|
+
// the dispatcher context (cron jobs, hired-agent
|
|
32511
|
+
// workflows, heartbeats), so a Sasha-cron completion
|
|
32512
|
+
// renders with Sasha's initial + name in the meta line
|
|
32513
|
+
// instead of looking like Clementine sent it. Falls
|
|
32514
|
+
// back to Clementine when the event is unowned.
|
|
32515
|
+
var deepSlug = (evt.data && evt.data.agentSlug) ? evt.data.agentSlug : null;
|
|
32516
|
+
var deepName = (evt.data && evt.data.agentName) ? evt.data.agentName : null;
|
|
32517
|
+
// Lazy-refresh the cache if the SSE event names a slug we
|
|
32518
|
+
// haven't seen yet (e.g. an agent hired since page load).
|
|
32519
|
+
if (deepSlug && !__teamAgentsByslug[deepSlug]) { ensureTeamAgentCache(true); }
|
|
32520
|
+
var deepIdentity = deepName
|
|
32521
|
+
? { slug: deepSlug, name: deepName, initial: deepName.charAt(0).toUpperCase() }
|
|
32522
|
+
: getAssistantIdentity(deepSlug);
|
|
32439
32523
|
if (container && text) {
|
|
32440
32524
|
var emptyState = container.querySelector('.empty-state');
|
|
32441
32525
|
if (emptyState) emptyState.remove();
|
|
@@ -32443,20 +32527,24 @@ try {
|
|
|
32443
32527
|
row.className = 'chat-assistant-row';
|
|
32444
32528
|
var av = document.createElement('div');
|
|
32445
32529
|
av.className = 'chat-avatar-sm';
|
|
32446
|
-
av.
|
|
32530
|
+
av.title = deepIdentity.name;
|
|
32531
|
+
av.innerHTML = deepIdentity.initial;
|
|
32447
32532
|
row.appendChild(av);
|
|
32448
32533
|
var bubble = document.createElement('div');
|
|
32449
32534
|
bubble.className = 'chat-bubble assistant';
|
|
32450
32535
|
bubble.innerHTML = renderMd(text);
|
|
32451
32536
|
var meta = document.createElement('div');
|
|
32452
32537
|
meta.className = 'chat-meta';
|
|
32453
|
-
meta.textContent = new Date().toLocaleTimeString()
|
|
32538
|
+
meta.textContent = new Date().toLocaleTimeString()
|
|
32539
|
+
+ (deepIdentity.slug && deepIdentity.slug !== 'clementine' ? ' \u00b7 ' + deepIdentity.name : '')
|
|
32540
|
+
+ ' \u00b7 deep task';
|
|
32454
32541
|
bubble.appendChild(meta);
|
|
32455
32542
|
row.appendChild(bubble);
|
|
32456
32543
|
container.appendChild(row);
|
|
32457
32544
|
container.scrollTop = container.scrollHeight;
|
|
32458
32545
|
} else {
|
|
32459
|
-
|
|
32546
|
+
var who = (deepIdentity.slug && deepIdentity.slug !== 'clementine') ? deepIdentity.name : 'Deep task';
|
|
32547
|
+
toast(who + ' result ready \u2014 open chat to view.', 'info');
|
|
32460
32548
|
}
|
|
32461
32549
|
} catch(e) { /* non-fatal */ }
|
|
32462
32550
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -132,6 +132,11 @@ export declare const WEBHOOK_BIND: string;
|
|
|
132
132
|
export declare const GROQ_API_KEY: string;
|
|
133
133
|
export declare const ELEVENLABS_API_KEY: string;
|
|
134
134
|
export declare const ELEVENLABS_VOICE_ID: string;
|
|
135
|
+
export declare const VOICE_ENABLED: boolean;
|
|
136
|
+
export declare const VOICE_PROVIDER: string;
|
|
137
|
+
export declare const GEMINI_API_KEY: string;
|
|
138
|
+
export declare const GEMINI_LIVE_MODEL: string;
|
|
139
|
+
export declare const GEMINI_LIVE_VOICE_NAME: string;
|
|
135
140
|
export declare const GOOGLE_API_KEY: string;
|
|
136
141
|
export declare const MS_TENANT_ID: string;
|
|
137
142
|
export declare const MS_CLIENT_ID: string;
|
package/dist/config.js
CHANGED
|
@@ -467,6 +467,11 @@ export const WEBHOOK_BIND = getEnv('WEBHOOK_BIND', '127.0.0.1');
|
|
|
467
467
|
export const GROQ_API_KEY = getSecret('GROQ_API_KEY');
|
|
468
468
|
export const ELEVENLABS_API_KEY = getSecret('ELEVENLABS_API_KEY');
|
|
469
469
|
export const ELEVENLABS_VOICE_ID = getEnv('ELEVENLABS_VOICE_ID');
|
|
470
|
+
export const VOICE_ENABLED = getEnv('VOICE_ENABLED', 'false').toLowerCase() === 'true';
|
|
471
|
+
export const VOICE_PROVIDER = getEnv('VOICE_PROVIDER', 'gemini-live');
|
|
472
|
+
export const GEMINI_API_KEY = getSecret('GEMINI_API_KEY') || getSecret('GOOGLE_API_KEY');
|
|
473
|
+
export const GEMINI_LIVE_MODEL = getEnv('GEMINI_LIVE_MODEL', 'gemini-3.1-flash-live-preview');
|
|
474
|
+
export const GEMINI_LIVE_VOICE_NAME = getEnv('GEMINI_LIVE_VOICE_NAME', 'Kore');
|
|
470
475
|
// ── Video ────────────────────────────────────────────────────────────
|
|
471
476
|
export const GOOGLE_API_KEY = getSecret('GOOGLE_API_KEY');
|
|
472
477
|
// ── Outlook (Microsoft Graph) ───────────────────────────────────────
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine desktop shell.
|
|
3
|
+
*
|
|
4
|
+
* This process owns a native Electron window and supervises the existing
|
|
5
|
+
* dashboard server. The dashboard remains the single UI source of truth.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
function loadElectron() {
|
|
16
|
+
try {
|
|
17
|
+
return require('electron');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
console.error('Electron is not installed. Run `npm install`, then `npm run desktop`.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const electron = loadElectron();
|
|
25
|
+
const { app, BrowserWindow, Menu, Tray, nativeImage, shell, dialog, session } = electron;
|
|
26
|
+
app.setName('Clementine');
|
|
27
|
+
if (app.setAppUserModelId)
|
|
28
|
+
app.setAppUserModelId('com.clementine.assistant');
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
32
|
+
const CLI_ENTRY = path.join(PACKAGE_ROOT, 'dist', 'cli', 'index.js');
|
|
33
|
+
const ICONS_DIR = path.join(PACKAGE_ROOT, 'build', 'icons');
|
|
34
|
+
const BASE_DIR = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
35
|
+
const DEFAULT_PORT = Number(process.env.CLEMENTINE_DESKTOP_PORT || process.env.DASHBOARD_PORT || 3030);
|
|
36
|
+
const NODE_BINARY = process.env.CLEMENTINE_NODE_PATH || 'node';
|
|
37
|
+
let mainWindow = null;
|
|
38
|
+
let tray = null;
|
|
39
|
+
let dashboardProcess = null;
|
|
40
|
+
let dashboardUrl = `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
41
|
+
let observedPort = null;
|
|
42
|
+
let isQuitting = false;
|
|
43
|
+
let restartTimer = null;
|
|
44
|
+
let suppressNextDashboardExitRestart = false;
|
|
45
|
+
function sleep(ms) {
|
|
46
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
function statusHtml(title, detail) {
|
|
49
|
+
const safeTitle = title.replace(/[<&>]/g, '');
|
|
50
|
+
const safeDetail = detail.replace(/[<&>]/g, '');
|
|
51
|
+
return 'data:text/html;charset=utf-8,' + encodeURIComponent('<!doctype html><html><head><meta charset="utf-8">' +
|
|
52
|
+
'<style>body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;' +
|
|
53
|
+
'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#111827;color:#f9fafb}' +
|
|
54
|
+
'.box{max-width:520px;padding:28px;text-align:center}.title{font-size:22px;font-weight:700;margin-bottom:10px}' +
|
|
55
|
+
'.detail{font-size:13px;line-height:1.5;color:#cbd5e1}</style></head><body><div class="box">' +
|
|
56
|
+
'<div class="title">' + safeTitle + '</div><div class="detail">' + safeDetail + '</div>' +
|
|
57
|
+
'</div></body></html>');
|
|
58
|
+
}
|
|
59
|
+
function loadStatus(title, detail) {
|
|
60
|
+
if (!mainWindow)
|
|
61
|
+
return;
|
|
62
|
+
mainWindow.loadURL(statusHtml(title, detail)).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
function resolveIconPath(name) {
|
|
65
|
+
const electronProcess = process;
|
|
66
|
+
const packagedRoot = typeof electronProcess.resourcesPath === 'string'
|
|
67
|
+
? path.join(electronProcess.resourcesPath, 'build', 'icons')
|
|
68
|
+
: ICONS_DIR;
|
|
69
|
+
const candidates = [
|
|
70
|
+
path.join(ICONS_DIR, name),
|
|
71
|
+
path.join(packagedRoot, name),
|
|
72
|
+
];
|
|
73
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
74
|
+
}
|
|
75
|
+
function loadIcon(name) {
|
|
76
|
+
const icon = nativeImage.createFromPath(resolveIconPath(name));
|
|
77
|
+
return icon.isEmpty() ? nativeImage.createEmpty() : icon;
|
|
78
|
+
}
|
|
79
|
+
function probeDashboard(port) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const req = http.get({ hostname: '127.0.0.1', port, path: '/', timeout: 600 }, (res) => {
|
|
82
|
+
res.resume();
|
|
83
|
+
resolve(Boolean(res.statusCode && res.statusCode >= 200 && res.statusCode < 500));
|
|
84
|
+
});
|
|
85
|
+
req.on('timeout', () => {
|
|
86
|
+
req.destroy();
|
|
87
|
+
resolve(false);
|
|
88
|
+
});
|
|
89
|
+
req.on('error', () => resolve(false));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function waitForDashboard(startPort, timeoutMs = 25_000) {
|
|
93
|
+
const deadline = Date.now() + timeoutMs;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
if (observedPort && await probeDashboard(observedPort))
|
|
96
|
+
return observedPort;
|
|
97
|
+
for (let port = startPort; port < startPort + 10; port++) {
|
|
98
|
+
if (await probeDashboard(port))
|
|
99
|
+
return port;
|
|
100
|
+
}
|
|
101
|
+
await sleep(400);
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Dashboard did not become ready on ports ${startPort}-${startPort + 9}`);
|
|
104
|
+
}
|
|
105
|
+
async function findRunningDashboard(startPort) {
|
|
106
|
+
if (observedPort && await probeDashboard(observedPort))
|
|
107
|
+
return observedPort;
|
|
108
|
+
for (let port = startPort; port < startPort + 10; port++) {
|
|
109
|
+
if (await probeDashboard(port))
|
|
110
|
+
return port;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function attachDashboardLogs(child) {
|
|
115
|
+
const onChunk = (chunk) => {
|
|
116
|
+
const text = chunk.toString();
|
|
117
|
+
const match = text.match(/http:\/\/localhost:(\d+)/);
|
|
118
|
+
if (match)
|
|
119
|
+
observedPort = Number(match[1]);
|
|
120
|
+
if (process.env.CLEMENTINE_DESKTOP_DEBUG === '1')
|
|
121
|
+
process.stdout.write(text);
|
|
122
|
+
};
|
|
123
|
+
child.stdout?.on('data', onChunk);
|
|
124
|
+
child.stderr?.on('data', (chunk) => {
|
|
125
|
+
if (process.env.CLEMENTINE_DESKTOP_DEBUG === '1')
|
|
126
|
+
process.stderr.write(chunk);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function startDashboardProcess(port) {
|
|
130
|
+
if (!existsSync(CLI_ENTRY)) {
|
|
131
|
+
throw new Error(`Missing ${CLI_ENTRY}. Run npm run build before starting the desktop app.`);
|
|
132
|
+
}
|
|
133
|
+
observedPort = null;
|
|
134
|
+
const child = spawn(NODE_BINARY, [CLI_ENTRY, 'dashboard', '-p', String(port)], {
|
|
135
|
+
cwd: PACKAGE_ROOT,
|
|
136
|
+
env: {
|
|
137
|
+
...process.env,
|
|
138
|
+
CLEMENTINE_HOME: BASE_DIR,
|
|
139
|
+
CLEMENTINE_NO_OPEN: '1',
|
|
140
|
+
__CLEM_DASHBOARD_CHILD: '1',
|
|
141
|
+
},
|
|
142
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
143
|
+
});
|
|
144
|
+
attachDashboardLogs(child);
|
|
145
|
+
child.on('exit', () => {
|
|
146
|
+
if (dashboardProcess === child)
|
|
147
|
+
dashboardProcess = null;
|
|
148
|
+
if (suppressNextDashboardExitRestart) {
|
|
149
|
+
suppressNextDashboardExitRestart = false;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!isQuitting)
|
|
153
|
+
scheduleDashboardRestart();
|
|
154
|
+
});
|
|
155
|
+
return child;
|
|
156
|
+
}
|
|
157
|
+
async function startDashboardAndLoad() {
|
|
158
|
+
if (restartTimer) {
|
|
159
|
+
clearTimeout(restartTimer);
|
|
160
|
+
restartTimer = null;
|
|
161
|
+
}
|
|
162
|
+
loadStatus('Opening Clementine', 'Looking for the local dashboard...');
|
|
163
|
+
try {
|
|
164
|
+
const existingPort = await findRunningDashboard(DEFAULT_PORT);
|
|
165
|
+
if (existingPort) {
|
|
166
|
+
dashboardUrl = `http://127.0.0.1:${existingPort}`;
|
|
167
|
+
await mainWindow.loadURL(dashboardUrl);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
loadStatus('Starting Clementine', 'Opening the local dashboard server...');
|
|
171
|
+
if (!dashboardProcess)
|
|
172
|
+
dashboardProcess = startDashboardProcess(DEFAULT_PORT);
|
|
173
|
+
const actualPort = await waitForDashboard(DEFAULT_PORT);
|
|
174
|
+
dashboardUrl = `http://127.0.0.1:${actualPort}`;
|
|
175
|
+
await mainWindow.loadURL(dashboardUrl);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
loadStatus('Clementine is offline', message);
|
|
180
|
+
dialog.showErrorBox('Clementine desktop failed to start', message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function scheduleDashboardRestart() {
|
|
184
|
+
loadStatus('Reconnecting', 'The dashboard process stopped. Restarting it now...');
|
|
185
|
+
if (restartTimer)
|
|
186
|
+
clearTimeout(restartTimer);
|
|
187
|
+
restartTimer = setTimeout(() => {
|
|
188
|
+
restartTimer = null;
|
|
189
|
+
startDashboardAndLoad().catch(() => undefined);
|
|
190
|
+
}, 1200);
|
|
191
|
+
}
|
|
192
|
+
function stopDashboard(suppressRestart = true) {
|
|
193
|
+
if (!dashboardProcess)
|
|
194
|
+
return;
|
|
195
|
+
const child = dashboardProcess;
|
|
196
|
+
dashboardProcess = null;
|
|
197
|
+
suppressNextDashboardExitRestart = suppressRestart;
|
|
198
|
+
try {
|
|
199
|
+
child.kill('SIGTERM');
|
|
200
|
+
}
|
|
201
|
+
catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
function restartDashboard() {
|
|
204
|
+
stopDashboard(true);
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
startDashboardAndLoad().catch(() => undefined);
|
|
207
|
+
}, 700);
|
|
208
|
+
}
|
|
209
|
+
function createWindow() {
|
|
210
|
+
mainWindow = new BrowserWindow({
|
|
211
|
+
width: 1280,
|
|
212
|
+
height: 860,
|
|
213
|
+
minWidth: 980,
|
|
214
|
+
minHeight: 680,
|
|
215
|
+
title: 'Clementine',
|
|
216
|
+
backgroundColor: '#111827',
|
|
217
|
+
icon: resolveIconPath('clementine.png'),
|
|
218
|
+
webPreferences: {
|
|
219
|
+
contextIsolation: true,
|
|
220
|
+
nodeIntegration: false,
|
|
221
|
+
sandbox: true,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
mainWindow.on('close', (event) => {
|
|
225
|
+
if (isQuitting)
|
|
226
|
+
return;
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
mainWindow.hide();
|
|
229
|
+
});
|
|
230
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
231
|
+
shell.openExternal(url).catch(() => undefined);
|
|
232
|
+
return { action: 'deny' };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function showWindow() {
|
|
236
|
+
if (!mainWindow)
|
|
237
|
+
createWindow();
|
|
238
|
+
mainWindow.show();
|
|
239
|
+
mainWindow.focus();
|
|
240
|
+
}
|
|
241
|
+
function installMenu() {
|
|
242
|
+
const template = [
|
|
243
|
+
{
|
|
244
|
+
label: 'Clementine',
|
|
245
|
+
submenu: [
|
|
246
|
+
{ label: 'Show Clementine', click: showWindow },
|
|
247
|
+
{ label: 'Open in Browser', click: () => shell.openExternal(dashboardUrl).catch(() => undefined) },
|
|
248
|
+
{
|
|
249
|
+
label: 'Restart Dashboard',
|
|
250
|
+
click: restartDashboard,
|
|
251
|
+
},
|
|
252
|
+
{ type: 'separator' },
|
|
253
|
+
{
|
|
254
|
+
label: 'Quit Clementine',
|
|
255
|
+
accelerator: 'CmdOrCtrl+Q',
|
|
256
|
+
click: () => {
|
|
257
|
+
isQuitting = true;
|
|
258
|
+
app.quit();
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
label: 'View',
|
|
265
|
+
submenu: [
|
|
266
|
+
{ role: 'reload' },
|
|
267
|
+
{ role: 'toggleDevTools' },
|
|
268
|
+
{ type: 'separator' },
|
|
269
|
+
{ role: 'resetZoom' },
|
|
270
|
+
{ role: 'zoomIn' },
|
|
271
|
+
{ role: 'zoomOut' },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
276
|
+
}
|
|
277
|
+
function installTray() {
|
|
278
|
+
const trayIcon = loadIcon(process.platform === 'darwin' ? 'trayTemplate.png' : 'tray.png');
|
|
279
|
+
tray = new Tray(trayIcon.isEmpty() ? loadIcon('tray.png') : trayIcon);
|
|
280
|
+
tray.setToolTip('Clementine');
|
|
281
|
+
tray.setContextMenu(Menu.buildFromTemplate([
|
|
282
|
+
{ label: 'Show Clementine', click: showWindow },
|
|
283
|
+
{ label: 'Restart Dashboard', click: restartDashboard },
|
|
284
|
+
{ type: 'separator' },
|
|
285
|
+
{ label: 'Quit', click: () => { isQuitting = true; app.quit(); } },
|
|
286
|
+
]));
|
|
287
|
+
tray.on('click', showWindow);
|
|
288
|
+
}
|
|
289
|
+
function installPermissions() {
|
|
290
|
+
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
|
291
|
+
const url = webContents.getURL?.() || '';
|
|
292
|
+
const local = url.startsWith('http://127.0.0.1:') || url.startsWith('http://localhost:');
|
|
293
|
+
callback(local && (permission === 'media' || permission === 'microphone'));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (!app.requestSingleInstanceLock()) {
|
|
297
|
+
app.quit();
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
app.on('second-instance', () => showWindow());
|
|
301
|
+
app.whenReady().then(() => {
|
|
302
|
+
const appIcon = loadIcon('clementine.png');
|
|
303
|
+
if (!appIcon.isEmpty() && app.dock)
|
|
304
|
+
app.dock.setIcon(appIcon);
|
|
305
|
+
if (app.setAboutPanelOptions) {
|
|
306
|
+
app.setAboutPanelOptions({
|
|
307
|
+
applicationName: 'Clementine',
|
|
308
|
+
applicationVersion: app.getVersion(),
|
|
309
|
+
iconPath: resolveIconPath('clementine.png'),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
installPermissions();
|
|
313
|
+
installMenu();
|
|
314
|
+
installTray();
|
|
315
|
+
createWindow();
|
|
316
|
+
startDashboardAndLoad().catch(() => undefined);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
app.on('activate', () => showWindow());
|
|
320
|
+
app.on('before-quit', () => {
|
|
321
|
+
isQuitting = true;
|
|
322
|
+
if (tray) {
|
|
323
|
+
try {
|
|
324
|
+
tray.destroy();
|
|
325
|
+
}
|
|
326
|
+
catch { /* ignore */ }
|
|
327
|
+
}
|
|
328
|
+
stopDashboard();
|
|
329
|
+
});
|
|
330
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -20,7 +20,4 @@ export declare function detectCronDiagnosticRequest(text: string, opts?: {
|
|
|
20
20
|
export declare function buildCronDiagnosticResponseForRequest(request: CronDiagnosticRequest, opts?: {
|
|
21
21
|
baseDir: string;
|
|
22
22
|
}): string | null;
|
|
23
|
-
export declare function buildCronDiagnosticResponse(text: string, opts?: {
|
|
24
|
-
baseDir: string;
|
|
25
|
-
}): string | null;
|
|
26
23
|
//# sourceMappingURL=cron-diagnostic-turn.d.ts.map
|
|
@@ -274,10 +274,4 @@ export function buildCronDiagnosticResponseForRequest(request, opts = { baseDir:
|
|
|
274
274
|
}
|
|
275
275
|
return lines.join('\n');
|
|
276
276
|
}
|
|
277
|
-
export function buildCronDiagnosticResponse(text, opts = { baseDir: process.env.CLEMENTINE_HOME || '' }) {
|
|
278
|
-
const request = detectCronDiagnosticRequest(text, { baseDir: opts.baseDir });
|
|
279
|
-
if (!request || !opts.baseDir)
|
|
280
|
-
return null;
|
|
281
|
-
return buildCronDiagnosticResponseForRequest(request, opts);
|
|
282
|
-
}
|
|
283
277
|
//# sourceMappingURL=cron-diagnostic-turn.js.map
|
|
@@ -128,6 +128,27 @@ export declare class CronScheduler {
|
|
|
128
128
|
* verification is pending and DMs the verdict if so.
|
|
129
129
|
*/
|
|
130
130
|
private _logRun;
|
|
131
|
+
/**
|
|
132
|
+
* Single source of truth for the NotificationContext attached to any
|
|
133
|
+
* dispatch that originated from a cron job. Threads `agentSlug` (so
|
|
134
|
+
* the channel layer routes via the owning hired agent's bot when one
|
|
135
|
+
* exists) and a deterministic `sessionKey` (so the inference safety
|
|
136
|
+
* net at the dispatcher can recover routing if a future caller drops
|
|
137
|
+
* `agentSlug`). Use this — never construct contexts inline — so new
|
|
138
|
+
* dispatch sites in this file can't silently leak hired-agent results
|
|
139
|
+
* into Clementine's main DM.
|
|
140
|
+
*/
|
|
141
|
+
private dispatchContextForJob;
|
|
142
|
+
/**
|
|
143
|
+
* Variant for dispatch sites that only have the job name in scope
|
|
144
|
+
* (MCP triggers, fix-verification, anywhere downstream of the run
|
|
145
|
+
* loop). Looks up the owning agentSlug from the live job table —
|
|
146
|
+
* falling back to a name-only context when the job is unknown
|
|
147
|
+
* (e.g. one-shot triggers without a CRON.md entry).
|
|
148
|
+
*/
|
|
149
|
+
private dispatchContextForJobName;
|
|
150
|
+
/** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
|
|
151
|
+
private dispatchContextForWorkflow;
|
|
131
152
|
private runJob;
|
|
132
153
|
/**
|
|
133
154
|
* Log an advisor event to the events JSONL file for dashboard surfacing.
|