clementine-agent 1.18.60 → 1.18.63

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 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
@@ -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);
@@ -5690,6 +5709,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5690
5709
  res.status(500).json({ error: String(err) });
5691
5710
  }
5692
5711
  });
5712
+ // ── Projects ─────────────────────────────────────────────────
5713
+ // Returns the project list from $CLEMENTINE_HOME/projects.json.
5714
+ // Used by the trick builder's Quick Add Step picker so users can
5715
+ // scaffold a step that runs in a specific project directory.
5716
+ app.get('/api/projects', (_req, res) => {
5717
+ try {
5718
+ const projects = loadProjectsMeta();
5719
+ res.json({ projects });
5720
+ }
5721
+ catch (err) {
5722
+ res.status(500).json({ error: String(err) });
5723
+ }
5724
+ });
5693
5725
  // ── Available Tools ──────────────────────────────────────────
5694
5726
  app.get('/api/available-tools', (_req, res) => {
5695
5727
  try {
@@ -19538,6 +19570,51 @@ let currentPage = 'home';
19538
19570
  var currentAgentSlug = null;
19539
19571
  var prevAgentSlugs = null;
19540
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
+
19541
19618
  // ── Routing ────────────────────────────────────────────────────
19542
19619
  //
19543
19620
  // Five top-level destinations: home, build, team, brain, settings.
@@ -24346,18 +24423,23 @@ async function sendChat() {
24346
24423
  sendBtn.textContent = 'Thinking...';
24347
24424
 
24348
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();
24349
24430
  var asstRow = document.createElement('div');
24350
24431
  asstRow.className = 'chat-assistant-row';
24351
24432
  var chatAv = document.createElement('div');
24352
24433
  chatAv.className = 'chat-avatar-sm';
24353
- chatAv.innerHTML = (lastStatusData.name || 'C').charAt(0).toUpperCase();
24434
+ chatAv.title = asstIdentity.name;
24435
+ chatAv.innerHTML = asstIdentity.initial;
24354
24436
  asstRow.appendChild(chatAv);
24355
24437
  var asstBubble = document.createElement('div');
24356
24438
  asstBubble.className = 'chat-bubble assistant';
24357
24439
  asstBubble.innerHTML = '<span style="color:var(--text-muted);font-style:italic">connecting...</span>';
24358
24440
  var asstMeta = document.createElement('div');
24359
24441
  asstMeta.className = 'chat-meta';
24360
- asstMeta.textContent = new Date().toLocaleTimeString();
24442
+ asstMeta.textContent = new Date().toLocaleTimeString() + (asstIdentity.slug !== 'clementine' ? ' · ' + asstIdentity.name : '');
24361
24443
  asstRow.appendChild(asstBubble);
24362
24444
  container.appendChild(asstRow);
24363
24445
  typing.remove();
@@ -25984,6 +26066,15 @@ function renderBuilderPreview(artifact, type) {
25984
26066
  + '<div id="builder-tools-panel" style="max-height:180px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:6px 8px;background:var(--bg-primary);margin-bottom:4px"></div>'
25985
26067
  + '<div style="font-size:10px;color:var(--text-muted)">Tools the trick will use. The chat sees these as a hint and weaves them into the steps.</div>'
25986
26068
  + '</div>'
26069
+ + '<div class="preview-field"><label>Quick add a step</label>'
26070
+ + '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:4px">'
26071
+ + '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27mcp\\x27)">+ MCP step</button>'
26072
+ + '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27cli\\x27)">+ CLI step</button>'
26073
+ + '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27project\\x27)">+ Project step</button>'
26074
+ + '</div>'
26075
+ + '<div id="quick-add-picker" style="display:none;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);max-height:240px;overflow-y:auto;padding:6px 8px;margin-bottom:4px"></div>'
26076
+ + '<div style="font-size:10px;color:var(--text-muted)">Pick a tool/CLI/project to seed a step. Clementine will follow up in chat to write the prompt.</div>'
26077
+ + '</div>'
25987
26078
  + '<div class="preview-field"><label>Steps (YAML/Markdown)</label><textarea rows="14" onchange="builderArtifact.steps=this.value">' + esc(artifact.steps || '') + '</textarea></div>';
25988
26079
  setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
25989
26080
  }
@@ -26150,6 +26241,214 @@ function syncBuilderLinkedTools() {
26150
26241
  }
26151
26242
  }
26152
26243
 
26244
+ // ── Trick builder: Quick Add Step ─────────
26245
+ // Three buttons in the workflow preview pane (+ MCP / + CLI / + Project)
26246
+ // scaffold a step block into builderArtifact.steps and auto-fire a chat
26247
+ // turn so Clementine writes the prompt for that step. The steps field
26248
+ // stays as a freeform YAML textarea — we just append, don't parse-edit.
26249
+
26250
+ function _nextStepId() {
26251
+ var current = (builderArtifact && builderArtifact.steps) || '';
26252
+ var ids = current.match(/step-(\\d+)/g) || [];
26253
+ var max = 0;
26254
+ for (var i = 0; i < ids.length; i++) {
26255
+ var n = parseInt(ids[i].replace('step-', ''), 10);
26256
+ if (!isNaN(n) && n > max) max = n;
26257
+ }
26258
+ return 'step-' + (max + 1);
26259
+ }
26260
+
26261
+ function _previousStepId() {
26262
+ var current = (builderArtifact && builderArtifact.steps) || '';
26263
+ var matches = current.match(/step-(\\d+)/g) || [];
26264
+ if (matches.length === 0) return null;
26265
+ // Highest numbered id
26266
+ var max = 0;
26267
+ for (var i = 0; i < matches.length; i++) {
26268
+ var n = parseInt(matches[i].replace('step-', ''), 10);
26269
+ if (!isNaN(n) && n > max) max = n;
26270
+ }
26271
+ return 'step-' + max;
26272
+ }
26273
+
26274
+ function _yamlEscape(s) {
26275
+ if (s == null) return '';
26276
+ return String(s).replace(/"/g, '\\\\"');
26277
+ }
26278
+
26279
+ function _appendStepYaml(stepId, dependsOn, body) {
26280
+ var current = (builderArtifact && builderArtifact.steps) || '';
26281
+ var dep = dependsOn ? '\\n dependsOn: [' + dependsOn + ']' : '';
26282
+ var block = stepId + ':\\n prompt: ""' + dep + '\\n' + body;
26283
+ builderArtifact.steps = current.trim() ? (current.trim() + '\\n\\n' + block) : block;
26284
+ }
26285
+
26286
+ async function openQuickAddPicker(kind) {
26287
+ var panel = document.getElementById('quick-add-picker');
26288
+ if (!panel) return;
26289
+ // Toggle close if already open for the same kind.
26290
+ if (panel.dataset.kind === kind && panel.style.display !== 'none') {
26291
+ panel.style.display = 'none';
26292
+ panel.dataset.kind = '';
26293
+ panel.innerHTML = '';
26294
+ return;
26295
+ }
26296
+ panel.dataset.kind = kind;
26297
+ panel.style.display = 'block';
26298
+ panel.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0">Loading…</div>';
26299
+
26300
+ try {
26301
+ if (kind === 'mcp') {
26302
+ var r = await apiFetch('/api/available-tools');
26303
+ var d = await r.json();
26304
+ var rows = [];
26305
+ for (var cat in d.categories) {
26306
+ var entries = d.categories[cat];
26307
+ for (var i = 0; i < entries.length; i++) {
26308
+ var t = entries[i];
26309
+ var name = typeof t === 'string' ? t : t.name;
26310
+ if (typeof name === 'string' && name.indexOf('mcp__') === 0) {
26311
+ // mcp__<server>__<tool>
26312
+ var parts = name.replace(/^mcp__/, '').split('__');
26313
+ if (parts.length < 2) continue;
26314
+ var server = parts[0];
26315
+ var tool = parts.slice(1).join('__');
26316
+ rows.push({
26317
+ label: server + ' / ' + tool,
26318
+ description: typeof t === 'object' && t.description ? t.description : '',
26319
+ resource: { server: server, tool: tool, description: typeof t === 'object' && t.description ? t.description : '' },
26320
+ });
26321
+ }
26322
+ }
26323
+ }
26324
+ _renderQuickAddPicker(panel, kind, rows);
26325
+ } else if (kind === 'cli') {
26326
+ var r2 = await apiFetch('/api/available-tools');
26327
+ var d2 = await r2.json();
26328
+ var entries2 = (d2.categories && d2.categories['CLI Tools']) || [];
26329
+ var rows2 = entries2.map(function(t) {
26330
+ return {
26331
+ label: t.name + (t.installed === false ? ' (not installed)' : ''),
26332
+ description: t.description || '',
26333
+ resource: { name: t.name, description: t.description || '' },
26334
+ };
26335
+ });
26336
+ _renderQuickAddPicker(panel, kind, rows2);
26337
+ } else if (kind === 'project') {
26338
+ var r3 = await apiFetch('/api/projects');
26339
+ var d3 = await r3.json();
26340
+ var projects = (d3 && d3.projects) || [];
26341
+ if (projects.length === 0) {
26342
+ panel.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0">No projects defined yet. Add one from the Projects page first.</div>';
26343
+ return;
26344
+ }
26345
+ var rows3 = projects.map(function(p) {
26346
+ return {
26347
+ label: p.name || p.path,
26348
+ description: p.path || '',
26349
+ resource: { name: p.name || p.path, path: p.path },
26350
+ };
26351
+ });
26352
+ _renderQuickAddPicker(panel, kind, rows3);
26353
+ }
26354
+ } catch (e) {
26355
+ panel.innerHTML = '<div style="font-size:11px;color:var(--red);padding:4px 0">Failed to load: ' + esc(String(e)) + '</div>';
26356
+ }
26357
+ }
26358
+
26359
+ function _renderQuickAddPicker(panel, kind, rows) {
26360
+ if (!rows || rows.length === 0) {
26361
+ panel.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0">Nothing to pick.</div>';
26362
+ return;
26363
+ }
26364
+ // Build the picker. Each row is a button that calls addQuickStep
26365
+ // with the resource and then closes the picker.
26366
+ var parts = [];
26367
+ for (var i = 0; i < rows.length; i++) {
26368
+ var r = rows[i];
26369
+ var rid = '_qap_' + kind + '_' + i;
26370
+ parts.push(
26371
+ '<button type="button" id="' + rid + '" data-idx="' + i + '" ' +
26372
+ 'style="display:block;width:100%;text-align:left;background:transparent;border:0;padding:4px 6px;' +
26373
+ 'border-radius:4px;cursor:pointer;font-size:11px;color:var(--text-primary)" ' +
26374
+ 'onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" ' +
26375
+ 'onmouseout="this.style.background=\\x27transparent\\x27">' +
26376
+ '<div style="font-weight:500">' + esc(r.label) + '</div>' +
26377
+ (r.description ? '<div style="font-size:10px;color:var(--text-muted)">' + esc(r.description) + '</div>' : '') +
26378
+ '</button>'
26379
+ );
26380
+ }
26381
+ panel.innerHTML = parts.join('');
26382
+ // Wire click handlers — each button picks its row by index.
26383
+ for (var j = 0; j < rows.length; j++) {
26384
+ (function(idx) {
26385
+ var btn = document.getElementById('_qap_' + kind + '_' + idx);
26386
+ if (btn) btn.onclick = function() {
26387
+ addQuickStep(kind, rows[idx].resource);
26388
+ panel.style.display = 'none';
26389
+ panel.dataset.kind = '';
26390
+ panel.innerHTML = '';
26391
+ };
26392
+ })(j);
26393
+ }
26394
+ }
26395
+
26396
+ function addQuickStep(kind, resource) {
26397
+ if (!builderArtifact) return;
26398
+ var stepId = _nextStepId();
26399
+ var prevId = _previousStepId();
26400
+ var body = '';
26401
+ if (kind === 'mcp') {
26402
+ body = ' kind: mcp\\n' +
26403
+ ' mcp:\\n' +
26404
+ ' server: "' + _yamlEscape(resource.server) + '"\\n' +
26405
+ ' tool: "' + _yamlEscape(resource.tool) + '"\\n' +
26406
+ ' inputs: {}\\n';
26407
+ } else if (kind === 'cli') {
26408
+ body = ' kind: cli\\n' +
26409
+ ' cli:\\n' +
26410
+ ' cmd: "' + _yamlEscape(resource.name) + '"\\n' +
26411
+ ' args: []\\n' +
26412
+ ' timeoutMs: 30000\\n';
26413
+ } else if (kind === 'project') {
26414
+ body = ' kind: prompt\\n' +
26415
+ ' workDir: "' + _yamlEscape(resource.path) + '"\\n';
26416
+ } else {
26417
+ return;
26418
+ }
26419
+ _appendStepYaml(stepId, prevId, body);
26420
+ // Re-render so the textarea picks up the new content.
26421
+ if (typeof renderBuilderPreview === 'function') {
26422
+ renderBuilderPreview(builderArtifact, 'workflow');
26423
+ }
26424
+ // Auto-fire chat so Clementine asks the right question.
26425
+ triggerStepChatPrompt(kind, stepId, resource);
26426
+ }
26427
+
26428
+ function triggerStepChatPrompt(kind, stepId, resource) {
26429
+ var msg = '';
26430
+ if (kind === 'mcp') {
26431
+ msg = '[STEP ADDED] ' + stepId + ' is an MCP call to ' +
26432
+ (resource.server || '') + '/' + (resource.tool || '') +
26433
+ (resource.description ? ' (' + resource.description + ')' : '') +
26434
+ '. Help me write the prompt for this step — what should it do, what inputs, what is the goal?';
26435
+ } else if (kind === 'cli') {
26436
+ msg = '[STEP ADDED] ' + stepId + ' runs the ' + (resource.name || '') + ' CLI' +
26437
+ (resource.description ? ' (' + resource.description + ')' : '') +
26438
+ '. What command/args should it run, and what should the prompt around it say?';
26439
+ } else if (kind === 'project') {
26440
+ msg = '[STEP ADDED] ' + stepId + ' runs in project ' + (resource.name || '') +
26441
+ ' (' + (resource.path || '') + '). What should it do in that project?';
26442
+ }
26443
+ if (!msg) return;
26444
+ // Reuse the same send path the chat input uses (mirrors builderQuick).
26445
+ var input = document.getElementById('builder-input');
26446
+ if (input) {
26447
+ input.value = msg;
26448
+ if (typeof sendBuilderChat === 'function') sendBuilderChat();
26449
+ }
26450
+ }
26451
+
26153
26452
  // ── Builder File Attachments ──────────────
26154
26453
  var _builderAttachments = [];
26155
26454
 
@@ -32206,6 +32505,21 @@ try {
32206
32505
  try {
32207
32506
  var container = document.getElementById('chat-messages');
32208
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);
32209
32523
  if (container && text) {
32210
32524
  var emptyState = container.querySelector('.empty-state');
32211
32525
  if (emptyState) emptyState.remove();
@@ -32213,20 +32527,24 @@ try {
32213
32527
  row.className = 'chat-assistant-row';
32214
32528
  var av = document.createElement('div');
32215
32529
  av.className = 'chat-avatar-sm';
32216
- av.innerHTML = (lastStatusData && lastStatusData.name ? lastStatusData.name : 'C').charAt(0).toUpperCase();
32530
+ av.title = deepIdentity.name;
32531
+ av.innerHTML = deepIdentity.initial;
32217
32532
  row.appendChild(av);
32218
32533
  var bubble = document.createElement('div');
32219
32534
  bubble.className = 'chat-bubble assistant';
32220
32535
  bubble.innerHTML = renderMd(text);
32221
32536
  var meta = document.createElement('div');
32222
32537
  meta.className = 'chat-meta';
32223
- 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';
32224
32541
  bubble.appendChild(meta);
32225
32542
  row.appendChild(bubble);
32226
32543
  container.appendChild(row);
32227
32544
  container.scrollTop = container.scrollHeight;
32228
32545
  } else {
32229
- 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');
32230
32548
  }
32231
32549
  } catch(e) { /* non-fatal */ }
32232
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) ───────────────────────────────────────
@@ -89,6 +89,7 @@ function buildSystemPrefix(type, agentSlug) {
89
89
  ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
90
90
  ` 4. Which model — ${MODELS.opus} (most capable), ${MODELS.sonnet} (balanced), or ${MODELS.haiku} (fastest). Leave model empty if the user doesn't care.\n` +
91
91
  `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
92
+ `If a user message starts with "[STEP ADDED]", they just clicked a Quick Add button to seed a step structure. Focus your reply on writing the prompt field for THAT step — ask one specific clarifying question, then update the artifact. Do NOT restructure the workflow or re-ask about goal/schedule/model.\n` +
92
93
  `When the user says "save" or approves, output the final artifact block — don't try to save it yourself, the dashboard handles persistence.]\n\n`;
93
94
  }
94
95
  return `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
@@ -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