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.
Files changed (36) hide show
  1. package/.env.example +5 -0
  2. package/build/icons/Clementine.iconset/icon_128x128.png +0 -0
  3. package/build/icons/Clementine.iconset/icon_128x128@2x.png +0 -0
  4. package/build/icons/Clementine.iconset/icon_16x16.png +0 -0
  5. package/build/icons/Clementine.iconset/icon_16x16@2x.png +0 -0
  6. package/build/icons/Clementine.iconset/icon_256x256.png +0 -0
  7. package/build/icons/Clementine.iconset/icon_256x256@2x.png +0 -0
  8. package/build/icons/Clementine.iconset/icon_32x32.png +0 -0
  9. package/build/icons/Clementine.iconset/icon_32x32@2x.png +0 -0
  10. package/build/icons/Clementine.iconset/icon_512x512.png +0 -0
  11. package/build/icons/Clementine.iconset/icon_512x512@2x.png +0 -0
  12. package/build/icons/clementine.png +0 -0
  13. package/build/icons/icon.icns +0 -0
  14. package/build/icons/tray.png +0 -0
  15. package/build/icons/tray@2x.png +0 -0
  16. package/dist/agent/agent-definitions.js +3 -2
  17. package/dist/cli/dashboard.js +94 -6
  18. package/dist/config.d.ts +5 -0
  19. package/dist/config.js +5 -0
  20. package/dist/desktop/main.d.ts +8 -0
  21. package/dist/desktop/main.js +330 -0
  22. package/dist/gateway/cron-diagnostic-turn.d.ts +0 -3
  23. package/dist/gateway/cron-diagnostic-turn.js +0 -6
  24. package/dist/gateway/cron-scheduler.d.ts +21 -0
  25. package/dist/gateway/cron-scheduler.js +53 -12
  26. package/dist/gateway/notifications.d.ts +24 -0
  27. package/dist/gateway/notifications.js +80 -2
  28. package/dist/gateway/recent-context.js +4 -57
  29. package/dist/gateway/router.js +0 -29
  30. package/dist/memory/store.js +21 -1
  31. package/dist/tools/admin-tools.js +11 -0
  32. package/dist/voice/gemini-live.d.ts +34 -0
  33. package/dist/voice/gemini-live.js +263 -0
  34. package/electron-builder.yml +36 -0
  35. package/package.json +13 -1
  36. 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
@@ -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. Call `list_broken_jobs` to see what is currently broken with their cached diagnoses.',
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',
@@ -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: { sessionKey: context?.sessionKey ?? null, text },
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.innerHTML = (lastStatusData.name || 'C').charAt(0).toUpperCase();
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.innerHTML = (lastStatusData && lastStatusData.name ? lastStatusData.name : 'C').charAt(0).toUpperCase();
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() + ' \u00b7 deep task';
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
- toast('Deep task result ready \u2014 open chat to view.', 'info');
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,8 @@
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
+ export {};
8
+ //# sourceMappingURL=main.d.ts.map
@@ -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.