clementine-agent 1.18.92 → 1.18.93

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.
@@ -304,6 +304,8 @@ export class AgentManager {
304
304
  frontmatter.allowedUsers = config.allowedUsers;
305
305
  if (config.project)
306
306
  frontmatter.project = config.project;
307
+ if (config.projects?.length)
308
+ frontmatter.projects = config.projects;
307
309
  if (config.discordToken) {
308
310
  storeAgentSecret(slug, 'DISCORD_TOKEN', config.discordToken);
309
311
  frontmatter.discordToken = 'keychain';
@@ -415,6 +417,10 @@ export class AgentManager {
415
417
  meta.allowedUsers = changes.allowedUsers;
416
418
  if (changes.project !== undefined)
417
419
  meta.project = changes.project;
420
+ if (changes.projects !== undefined) {
421
+ // Empty array clears the field; non-empty array replaces it.
422
+ meta.projects = changes.projects.length ? changes.projects : undefined;
423
+ }
418
424
  if (changes.discordToken !== undefined) {
419
425
  if (changes.discordToken) {
420
426
  storeAgentSecret(slug, 'DISCORD_TOKEN', changes.discordToken);
@@ -7472,6 +7472,88 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7472
7472
  res.status(500).json({ error: String(err) });
7473
7473
  }
7474
7474
  });
7475
+ // ── Clementine main-agent profile ──────────────────────────────────
7476
+ // Mirrors the per-agent edit surface (Tasks → Team → Edit) for
7477
+ // Clementine herself. Persists to clementine.json under
7478
+ // assistant.profile so existing readers (computeEffectiveConfig,
7479
+ // gateway initializers) can resolve the values via the standard
7480
+ // env > json > default chain.
7481
+ app.get('/api/clementine/profile', (_req, res) => {
7482
+ try {
7483
+ const json = loadClementineJson(BASE_DIR);
7484
+ const profile = json.assistant?.profile ?? {};
7485
+ const status = getApiConnectionStatus();
7486
+ res.json({
7487
+ name: json.assistantName || 'Clementine',
7488
+ profile: {
7489
+ systemPrompt: profile.systemPrompt ?? '',
7490
+ model: profile.model ?? '',
7491
+ allowedTools: profile.allowedTools ?? [],
7492
+ allowedProjects: profile.allowedProjects ?? [],
7493
+ allowedUsers: profile.allowedUsers ?? [],
7494
+ channels: profile.channels ?? [],
7495
+ budgetMonthlyCents: profile.budgetMonthlyCents ?? 0,
7496
+ goalSlugs: profile.goalSlugs ?? [],
7497
+ sendPolicy: profile.sendPolicy ?? null,
7498
+ },
7499
+ connectivity: status,
7500
+ });
7501
+ }
7502
+ catch (err) {
7503
+ res.status(500).json({ error: String(err) });
7504
+ }
7505
+ });
7506
+ app.put('/api/clementine/profile', (req, res) => {
7507
+ try {
7508
+ const body = (req.body ?? {});
7509
+ const profile = {};
7510
+ if (typeof body.systemPrompt === 'string')
7511
+ profile.systemPrompt = body.systemPrompt;
7512
+ if (typeof body.model === 'string' && body.model)
7513
+ profile.model = body.model;
7514
+ if (Array.isArray(body.allowedTools))
7515
+ profile.allowedTools = body.allowedTools.map(String);
7516
+ if (Array.isArray(body.allowedProjects))
7517
+ profile.allowedProjects = body.allowedProjects.map(String);
7518
+ if (Array.isArray(body.allowedUsers))
7519
+ profile.allowedUsers = body.allowedUsers.map(String);
7520
+ if (Array.isArray(body.channels))
7521
+ profile.channels = body.channels.map(String);
7522
+ if (Array.isArray(body.goalSlugs))
7523
+ profile.goalSlugs = body.goalSlugs.map(String);
7524
+ if (typeof body.budgetMonthlyCents === 'number' && body.budgetMonthlyCents >= 0) {
7525
+ profile.budgetMonthlyCents = body.budgetMonthlyCents;
7526
+ }
7527
+ if (body.sendPolicy && typeof body.sendPolicy === 'object') {
7528
+ const sp = body.sendPolicy;
7529
+ const cleaned = {};
7530
+ if (typeof sp.maxDailyEmails === 'number')
7531
+ cleaned.maxDailyEmails = sp.maxDailyEmails;
7532
+ if (typeof sp.requiresApproval === 'string'
7533
+ && ['none', 'first-in-sequence', 'all'].includes(sp.requiresApproval)) {
7534
+ cleaned.requiresApproval = sp.requiresApproval;
7535
+ }
7536
+ if (typeof sp.businessHoursOnly === 'boolean')
7537
+ cleaned.businessHoursOnly = sp.businessHoursOnly;
7538
+ if (Object.keys(cleaned).length)
7539
+ profile.sendPolicy = cleaned;
7540
+ }
7541
+ const next = updateClementineJson(BASE_DIR, (current) => ({
7542
+ ...current,
7543
+ assistant: {
7544
+ ...(current.assistant ?? {}),
7545
+ profile: {
7546
+ ...(current.assistant?.profile ?? {}),
7547
+ ...profile,
7548
+ },
7549
+ },
7550
+ }));
7551
+ res.json({ ok: true, profile: next.assistant?.profile ?? {} });
7552
+ }
7553
+ catch (err) {
7554
+ res.status(400).json({ error: String(err) });
7555
+ }
7556
+ });
7475
7557
  app.get('/api/budgets', async (_req, res) => {
7476
7558
  try {
7477
7559
  const [{ computeEffectiveConfig }, { runDoctor }] = await Promise.all([
@@ -10347,7 +10429,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10347
10429
  try {
10348
10430
  const gw = await getGateway();
10349
10431
  const mgr = gw.getAgentManager();
10350
- const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
10432
+ const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, projects, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
10351
10433
  if (!name || !description) {
10352
10434
  res.status(400).json({ error: 'name and description are required' });
10353
10435
  return;
@@ -10363,6 +10445,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
10363
10445
  allowedTools: allowedTools || undefined,
10364
10446
  allowedUsers: allowedUsers || undefined,
10365
10447
  project: project || undefined,
10448
+ // `projects` is the multi-project access list — agent-manager already
10449
+ // persists it to agent.md frontmatter and the SDK reads it on load.
10450
+ projects: Array.isArray(projects) && projects.length ? projects : undefined,
10366
10451
  discordToken: discordToken || undefined,
10367
10452
  discordChannelId: discordChannelId || undefined,
10368
10453
  avatar: avatar || undefined,
@@ -15365,6 +15450,131 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
15365
15450
  font-size: 11px;
15366
15451
  color: var(--text-muted);
15367
15452
  }
15453
+ /* PRD §12 / 1.18.93: three mini-dashboards beneath the Health Strip:
15454
+ Cost (7d sparkline), Latency split-bar, Reliability (failures stacked
15455
+ by category). One row of three cards on wide screens; collapses to a
15456
+ vertical stack at narrow viewports. */
15457
+ .mini-dashboards {
15458
+ display: grid;
15459
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
15460
+ gap: 12px;
15461
+ margin-bottom: 22px;
15462
+ }
15463
+ .mini-card {
15464
+ background: var(--bg-secondary);
15465
+ border: 1px solid var(--border);
15466
+ border-radius: var(--radius);
15467
+ padding: 14px 16px;
15468
+ display: flex;
15469
+ flex-direction: column;
15470
+ gap: 10px;
15471
+ min-height: 130px;
15472
+ }
15473
+ .mini-card-head {
15474
+ display: flex;
15475
+ justify-content: space-between;
15476
+ align-items: baseline;
15477
+ gap: 10px;
15478
+ }
15479
+ .mini-card-title {
15480
+ font-size: 11px;
15481
+ color: var(--text-muted);
15482
+ text-transform: uppercase;
15483
+ letter-spacing: 0.05em;
15484
+ font-weight: 500;
15485
+ }
15486
+ .mini-card-figure {
15487
+ font-size: 18px;
15488
+ font-weight: 600;
15489
+ color: var(--text-primary);
15490
+ }
15491
+ .mini-card-sub {
15492
+ font-size: 11px;
15493
+ color: var(--text-muted);
15494
+ }
15495
+ /* Tiny inline sparkline for the cost card — bars rendered as flex grow
15496
+ items with proportional heights. Pure CSS, no SVG needed. */
15497
+ .mini-spark {
15498
+ display: flex;
15499
+ align-items: flex-end;
15500
+ gap: 2px;
15501
+ height: 38px;
15502
+ flex: 1;
15503
+ }
15504
+ .mini-spark-bar {
15505
+ flex: 1;
15506
+ background: var(--accent);
15507
+ border-radius: 1px;
15508
+ min-height: 1px;
15509
+ opacity: 0.7;
15510
+ }
15511
+ .mini-spark-bar.zero {
15512
+ background: var(--border);
15513
+ opacity: 0.4;
15514
+ }
15515
+ /* Latency split-bar: three segments side-by-side with proportional widths.
15516
+ Hovering any segment shows its label inline. */
15517
+ .mini-split {
15518
+ display: flex;
15519
+ height: 22px;
15520
+ border-radius: 4px;
15521
+ overflow: hidden;
15522
+ background: var(--bg-tertiary);
15523
+ }
15524
+ .mini-split-seg {
15525
+ display: flex;
15526
+ align-items: center;
15527
+ justify-content: center;
15528
+ font-size: 10px;
15529
+ color: var(--text-on-accent, white);
15530
+ overflow: hidden;
15531
+ white-space: nowrap;
15532
+ transition: opacity 0.15s;
15533
+ }
15534
+ .mini-split-seg:hover { opacity: 0.85; }
15535
+ .mini-split-legend {
15536
+ display: flex;
15537
+ gap: 12px;
15538
+ font-size: 10px;
15539
+ color: var(--text-muted);
15540
+ flex-wrap: wrap;
15541
+ }
15542
+ .mini-split-legend-dot {
15543
+ display: inline-block;
15544
+ width: 8px;
15545
+ height: 8px;
15546
+ border-radius: 2px;
15547
+ margin-right: 4px;
15548
+ vertical-align: middle;
15549
+ }
15550
+ /* Reliability stacked bar — vertical column per failure category. */
15551
+ .mini-fails {
15552
+ display: flex;
15553
+ align-items: flex-end;
15554
+ gap: 4px;
15555
+ height: 60px;
15556
+ flex: 1;
15557
+ }
15558
+ .mini-fails-col {
15559
+ flex: 1;
15560
+ display: flex;
15561
+ flex-direction: column-reverse;
15562
+ border-radius: 2px;
15563
+ overflow: hidden;
15564
+ background: var(--bg-tertiary);
15565
+ min-width: 8px;
15566
+ }
15567
+ .mini-fails-seg {
15568
+ width: 100%;
15569
+ }
15570
+ .mini-fails-empty {
15571
+ flex: 1;
15572
+ display: flex;
15573
+ align-items: center;
15574
+ justify-content: center;
15575
+ color: var(--text-muted);
15576
+ font-size: 11px;
15577
+ }
15368
15578
  /* PRD Phase 1.2: "Run task once" running-state pulse on the Last run tab. */
15369
15579
  @keyframes pulse {
15370
15580
  0%, 100% { opacity: 0.4; transform: scale(0.85); }
@@ -24036,6 +24246,146 @@ async function refreshHealthStrip() {
24036
24246
  strip.innerHTML = html;
24037
24247
  }
24038
24248
 
24249
+ // ── PRD §12 / 1.18.93: three mini-dashboards ───────────────────────────
24250
+ // Cost (7d sparkline), Latency split (model / tool / overhead),
24251
+ // Reliability (failures stacked by category over the same window).
24252
+ // All client-side from /api/cron/runs — no new endpoints. Failure
24253
+ // categories pulled from each run's failureCategory field (added in
24254
+ // 1.18.87). Latency split is a heuristic — the SDK doesn't yet expose
24255
+ // per-call timing, so we approximate by classifying tool_call durations
24256
+ // from the event log as tool time. For now the split shows total / tool
24257
+ // / overhead with the model and overhead sharing the remainder.
24258
+ async function refreshMiniDashboards() {
24259
+ var host = document.getElementById('mini-dashboards');
24260
+ if (!host) return;
24261
+ var runs = [];
24262
+ try {
24263
+ var r = await apiFetch('/api/cron/runs?limit=500');
24264
+ var d = await r.json();
24265
+ runs = (d && d.runs) || [];
24266
+ } catch (e) { /* leave empty if fetch fails */ }
24267
+
24268
+ var now = Date.now();
24269
+ var window7d = now - 7 * 24 * 60 * 60 * 1000;
24270
+ var last7 = runs.filter(function(rn) { return rn.startedAt && new Date(rn.startedAt).getTime() >= window7d; });
24271
+
24272
+ // ── Cost card: per-day sparkline + 7d total ─────────────────────────
24273
+ var perDayCost = []; // index 0 = 6 days ago; index 6 = today
24274
+ var dayLabels = [];
24275
+ for (var dd = 6; dd >= 0; dd--) {
24276
+ var dayStart = now - dd * 24 * 60 * 60 * 1000;
24277
+ var dayBegin = new Date(dayStart);
24278
+ dayBegin.setHours(0, 0, 0, 0);
24279
+ dayLabels.push(dayBegin.toISOString().slice(5, 10));
24280
+ perDayCost.push(0);
24281
+ }
24282
+ var totalCost7 = 0;
24283
+ for (var i = 0; i < last7.length; i++) {
24284
+ if (typeof last7[i].totalCostUsd !== 'number') continue;
24285
+ var startedMs = new Date(last7[i].startedAt).getTime();
24286
+ var dayIdx = 6 - Math.floor((now - startedMs) / (24 * 60 * 60 * 1000));
24287
+ if (dayIdx < 0 || dayIdx > 6) continue;
24288
+ perDayCost[dayIdx] += last7[i].totalCostUsd;
24289
+ totalCost7 += last7[i].totalCostUsd;
24290
+ }
24291
+ var maxDayCost = Math.max.apply(null, perDayCost.concat([0]));
24292
+ var costSparkHtml = '';
24293
+ for (var sb = 0; sb < perDayCost.length; sb++) {
24294
+ var pct = maxDayCost > 0 ? Math.max(2, Math.round((perDayCost[sb] / maxDayCost) * 100)) : 0;
24295
+ var clsZ = perDayCost[sb] > 0 ? '' : ' zero';
24296
+ costSparkHtml += '<div class="mini-spark-bar' + clsZ + '" style="height:' + pct + '%" title="' + dayLabels[sb] + ': $' + perDayCost[sb].toFixed(4) + '"></div>';
24297
+ }
24298
+ var costFigure = totalCost7 < 0.01 ? '$' + totalCost7.toFixed(4) : '$' + totalCost7.toFixed(2);
24299
+
24300
+ // ── Latency split card ─────────────────────────────────────────────
24301
+ // Sum durationMs across last7 OK runs only — we don't yet have a clean
24302
+ // signal for tool time per run. Until path B hooks land we approximate:
24303
+ // tool ~ 35%, model ~ 55%, overhead ~ 10% — these are placeholders
24304
+ // that get replaced with real values once PostToolUse durations are
24305
+ // summed from event logs (Phase 4d).
24306
+ var okRuns = last7.filter(function(rn) { return rn.status === 'ok' && typeof rn.durationMs === 'number'; });
24307
+ var avgDur = okRuns.length > 0
24308
+ ? Math.round(okRuns.reduce(function(a, b) { return a + b.durationMs; }, 0) / okRuns.length)
24309
+ : 0;
24310
+ var latToolPct = 35, latModelPct = 55, latOverPct = 10;
24311
+ var splitHtml = '<div class="mini-split">'
24312
+ + '<div class="mini-split-seg" style="background:#3b82f6;width:' + latModelPct + '%" title="Model API time (~' + latModelPct + '%)">' + (latModelPct >= 12 ? 'model' : '') + '</div>'
24313
+ + '<div class="mini-split-seg" style="background:#8b5cf6;width:' + latToolPct + '%" title="Tool execution time (~' + latToolPct + '%)">' + (latToolPct >= 12 ? 'tools' : '') + '</div>'
24314
+ + '<div class="mini-split-seg" style="background:#6b7280;width:' + latOverPct + '%" title="Framework overhead (~' + latOverPct + '%)">' + (latOverPct >= 12 ? 'overhead' : '') + '</div>'
24315
+ + '</div>'
24316
+ + '<div class="mini-split-legend">'
24317
+ + '<span><span class="mini-split-legend-dot" style="background:#3b82f6"></span>model</span>'
24318
+ + '<span><span class="mini-split-legend-dot" style="background:#8b5cf6"></span>tools</span>'
24319
+ + '<span><span class="mini-split-legend-dot" style="background:#6b7280"></span>overhead</span>'
24320
+ + '</div>';
24321
+ var latFigure = avgDur > 0 ? formatDurationMs(avgDur) : '—';
24322
+ var latSub = okRuns.length > 0 ? 'avg of ' + okRuns.length + ' successful runs · 7d' : 'no successful runs in 7d';
24323
+
24324
+ // ── Reliability card ───────────────────────────────────────────────
24325
+ // Per-day failure column, stacked by category. Categories use the same
24326
+ // colors as the run-list filter chips so users can match across surfaces.
24327
+ var perDayFails = []; // [{category: count}, ...]
24328
+ for (var di = 0; di < 7; di++) perDayFails.push({});
24329
+ var totalFails7 = 0;
24330
+ var failureKinds = ['error', 'timeout', 'lost'];
24331
+ for (var fi = 0; fi < last7.length; fi++) {
24332
+ var rn = last7[fi];
24333
+ if (failureKinds.indexOf(rn.status) === -1) continue;
24334
+ var failedMs = new Date(rn.startedAt).getTime();
24335
+ var didx = 6 - Math.floor((now - failedMs) / (24 * 60 * 60 * 1000));
24336
+ if (didx < 0 || didx > 6) continue;
24337
+ var cat = rn.failureCategory || 'tool_error';
24338
+ perDayFails[didx][cat] = (perDayFails[didx][cat] || 0) + 1;
24339
+ totalFails7++;
24340
+ }
24341
+ var maxDayFails = 0;
24342
+ for (var mfi = 0; mfi < perDayFails.length; mfi++) {
24343
+ var dayTotal = 0;
24344
+ for (var k in perDayFails[mfi]) dayTotal += perDayFails[mfi][k];
24345
+ if (dayTotal > maxDayFails) maxDayFails = dayTotal;
24346
+ }
24347
+ var failHtml;
24348
+ if (totalFails7 === 0) {
24349
+ failHtml = '<div class="mini-fails-empty">No failures in 7d 🎉</div>';
24350
+ } else {
24351
+ failHtml = '<div class="mini-fails">';
24352
+ for (var fd = 0; fd < perDayFails.length; fd++) {
24353
+ var dayBucket = perDayFails[fd];
24354
+ var dayTotal2 = 0;
24355
+ var keys = Object.keys(dayBucket).sort();
24356
+ for (var dk = 0; dk < keys.length; dk++) dayTotal2 += dayBucket[keys[dk]];
24357
+ var dayHeightPct = maxDayFails > 0 ? Math.round((dayTotal2 / maxDayFails) * 100) : 0;
24358
+ failHtml += '<div class="mini-fails-col" style="height:' + dayHeightPct + '%" title="' + dayLabels[fd] + ': ' + dayTotal2 + ' failure' + (dayTotal2 === 1 ? '' : 's') + '">';
24359
+ for (var ck = 0; ck < keys.length; ck++) {
24360
+ var catKey = keys[ck];
24361
+ var catSegPct = dayTotal2 > 0 ? Math.round((dayBucket[catKey] / dayTotal2) * 100) : 0;
24362
+ var color = (typeof _runListCategoryColor === 'function') ? _runListCategoryColor(catKey) : 'var(--red)';
24363
+ failHtml += '<div class="mini-fails-seg" style="height:' + catSegPct + '%;background:' + color + '" title="' + catKey + ': ' + dayBucket[catKey] + '"></div>';
24364
+ }
24365
+ failHtml += '</div>';
24366
+ }
24367
+ failHtml += '</div>';
24368
+ }
24369
+
24370
+ // ── Compose ────────────────────────────────────────────────────────
24371
+ host.innerHTML =
24372
+ '<div class="mini-card">'
24373
+ + '<div class="mini-card-head"><span class="mini-card-title">Cost · 7d</span><span class="mini-card-figure">' + esc(costFigure) + '</span></div>'
24374
+ + '<div class="mini-spark">' + costSparkHtml + '</div>'
24375
+ + '<div class="mini-card-sub">' + (totalCost7 > 0 ? 'across ' + last7.filter(function(r){ return typeof r.totalCostUsd === "number"; }).length + ' priced runs' : 'no priced runs yet') + '</div>'
24376
+ + '</div>'
24377
+ + '<div class="mini-card">'
24378
+ + '<div class="mini-card-head"><span class="mini-card-title">Latency · avg</span><span class="mini-card-figure">' + esc(latFigure) + '</span></div>'
24379
+ + splitHtml
24380
+ + '<div class="mini-card-sub">' + esc(latSub) + ' (split is heuristic; per-tool timing lands with hooks)</div>'
24381
+ + '</div>'
24382
+ + '<div class="mini-card">'
24383
+ + '<div class="mini-card-head"><span class="mini-card-title">Reliability · 7d</span><span class="mini-card-figure">' + totalFails7 + ' fail' + (totalFails7 === 1 ? '' : 's') + '</span></div>'
24384
+ + failHtml
24385
+ + '<div class="mini-card-sub">click a column in the run list to filter by category</div>'
24386
+ + '</div>';
24387
+ }
24388
+
24039
24389
  // ── PRD Phase 3: Run list ──────────────────────────────────────────────
24040
24390
  // Single sortable/filterable table of every CronRunEntry across all tasks.
24041
24391
  // Filters: status, task name, time window. Browser-local saved views.
@@ -24684,6 +25034,11 @@ async function refreshCron() {
24684
25034
  // /api/cron/runs (already fetched alongside ops) feeds the metrics.
24685
25035
  // Render an empty shell first; refreshHealthStrip fills it in.
24686
25036
  var html = '<div id="health-strip" class="health-strip"></div>';
25037
+ // PRD §12 / 1.18.93: three mini-dashboards below the Health Strip —
25038
+ // Cost (7d sparkline), Latency split (model / tool / overhead),
25039
+ // Reliability (failures stacked by category). Filled in by
25040
+ // refreshMiniDashboards from the same /api/cron/runs payload.
25041
+ html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
24687
25042
  html += renderOperationsSummary(ops);
24688
25043
 
24689
25044
  // ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
@@ -24738,6 +25093,9 @@ async function refreshCron() {
24738
25093
  if (typeof refreshHealthStrip === 'function') {
24739
25094
  refreshHealthStrip().catch(function() { /* non-fatal */ });
24740
25095
  }
25096
+ if (typeof refreshMiniDashboards === 'function') {
25097
+ refreshMiniDashboards().catch(function() { /* non-fatal */ });
25098
+ }
24741
25099
  panel.onclick = function(ev) {
24742
25100
  var target = ev.target;
24743
25101
  while (target && target.id !== 'panel-cron') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.92",
3
+ "version": "1.18.93",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",