clementine-agent 1.18.146 → 1.18.148

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.
@@ -351,7 +351,15 @@ export async function classifyRoute(userMessage, agents, gateway) {
351
351
  undefined, // maxHours
352
352
  undefined, // timeoutMs
353
353
  undefined, // successCriteria
354
- undefined);
354
+ undefined, // agentSlug
355
+ // 1.18.148 — F1/F2 pattern: meta-jobs run predictable to keep the
356
+ // prompt under Claude's input limit. Without these the classifier
357
+ // inherited MEMORY.md + team comms + auto-matched skills, blew up
358
+ // 12+ times in 8 hours (silent fallback to default route).
359
+ undefined, // pinnedSkills
360
+ [], // allowedTools
361
+ [], // allowedMcpServers
362
+ true);
355
363
  }
356
364
  catch (err) {
357
365
  logger.warn({ err }, 'Route classifier call failed');
@@ -45,8 +45,17 @@ const MAX_INJECTED_SKILLS = 4;
45
45
  * delegation is the one thing every cron must always be able to do).
46
46
  */
47
47
  export function computeEffectiveAllowedTools(jobAllow, profileAllow) {
48
- if (!jobAllow?.length)
48
+ // 1.18.148 — distinguish "no allowlist" (undefined → unrestricted) from
49
+ // "explicitly empty allowlist" (`[]` → deny all). Before this, both
50
+ // collapsed to `undefined` because of the `?.length` check, which meant
51
+ // meta-jobs passing `allowedTools: []` actually got the FULL tool set
52
+ // injected (and blew past the prompt limit when Composio toolkits piled
53
+ // on tool schemas). The fix: an explicit `[]` returns `['Agent']` only
54
+ // (just the SDK's required spawn-subagent tool).
55
+ if (jobAllow === undefined)
49
56
  return undefined;
57
+ if (jobAllow.length === 0)
58
+ return ['Agent']; // explicitly empty → minimal
50
59
  let result;
51
60
  if (profileAllow?.length) {
52
61
  const jobSet = new Set(jobAllow);
@@ -67,8 +76,16 @@ export function computeEffectiveAllowedTools(jobAllow, profileAllow) {
67
76
  * MCP allowlist set.
68
77
  */
69
78
  export function applyMcpAllowlist(servers, jobAllowedMcpServers) {
70
- if (!jobAllowedMcpServers?.length)
79
+ // 1.18.148 — empty array means "deny all MCP servers", not "no
80
+ // restriction". Before this, passing `[]` collapsed to `?.length === 0`
81
+ // and returned the unfiltered server map — so meta-jobs (insight-check,
82
+ // grade:*, route-classify, diagnose:*) got every Composio toolkit's
83
+ // tool schemas wired into their prompt and blew past Claude's input
84
+ // limit. 110+ "Prompt is too long" errors per 8 hours.
85
+ if (jobAllowedMcpServers === undefined)
71
86
  return servers;
87
+ if (jobAllowedMcpServers.length === 0)
88
+ return {};
72
89
  const allow = new Set(jobAllowedMcpServers);
73
90
  return Object.fromEntries(Object.entries(servers).filter(([name]) => allow.has(name)));
74
91
  }
@@ -94,8 +111,13 @@ export function applyMcpAllowlist(servers, jobAllowedMcpServers) {
94
111
  * unchanged.
95
112
  */
96
113
  export function widenAllowlistWithSkillTools(jobAllow, pinnedSkillTools) {
97
- if (!jobAllow?.length)
114
+ // 1.18.148 — preserve "explicitly empty" semantics. An empty array is a
115
+ // contract: "I want no tools." Skill-pin widening doesn't apply when no
116
+ // skills are pinned (which is the case for meta-jobs).
117
+ if (jobAllow === undefined)
98
118
  return undefined;
119
+ if (jobAllow.length === 0 && !pinnedSkillTools?.length)
120
+ return [];
99
121
  if (!pinnedSkillTools?.length)
100
122
  return [...jobAllow];
101
123
  return [...new Set([...jobAllow, ...pinnedSkillTools])];
@@ -130,8 +152,12 @@ export function extractMcpServersFromSkillBodies(bodies) {
130
152
  * allowlist; doesn't synthesize one when the cron is unrestricted.
131
153
  */
132
154
  export function widenMcpAllowlistWithSkillRefs(jobMcpAllow, skillReferencedServers) {
133
- if (!jobMcpAllow?.length)
155
+ // 1.18.148 — preserve "explicitly empty" semantics. See note on
156
+ // applyMcpAllowlist + widenAllowlistWithSkillTools above.
157
+ if (jobMcpAllow === undefined)
134
158
  return undefined;
159
+ if (jobMcpAllow.length === 0 && !skillReferencedServers.length)
160
+ return [];
135
161
  if (!skillReferencedServers.length)
136
162
  return [...jobMcpAllow];
137
163
  return [...new Set([...jobMcpAllow, ...skillReferencedServers])];
@@ -572,8 +598,10 @@ export async function buildCronExecutionPlan(opts) {
572
598
  const widenedJobMcpAllowlist = widenMcpAllowlistWithSkillRefs(opts.allowedMcpServers, skillReferencedMcpServers);
573
599
  // Per-trick MCP allowlist: post-filter on the profile-narrowed map.
574
600
  // Effective set = profile ∩ trick (widened).
601
+ // 1.18.148 — empty array means "deny all" not "no restriction" (was a
602
+ // silent prompt-bloat bug — see applyMcpAllowlist note above).
575
603
  const mcpServerMap = applyMcpAllowlist(mcp.servers, widenedJobMcpAllowlist);
576
- const allowSet = widenedJobMcpAllowlist?.length ? new Set(widenedJobMcpAllowlist) : null;
604
+ const allowSet = widenedJobMcpAllowlist === undefined ? null : new Set(widenedJobMcpAllowlist);
577
605
  const composioConnected = allowSet ? mcp.composioConnected.filter(n => allowSet.has(n)) : mcp.composioConnected;
578
606
  const externalConnected = allowSet ? mcp.externalConnected.filter(n => allowSet.has(n)) : mcp.externalConnected;
579
607
  const mcpServersApplied = Object.keys(mcpServerMap);
@@ -1754,6 +1754,28 @@ export async function cmdDashboard(opts) {
1754
1754
  }
1755
1755
  catch { /* ignore */ }
1756
1756
  };
1757
+ // 1.18.147 — Auto-open the dashboard URL in the user's default
1758
+ // browser once the child has had a chance to bind + write the
1759
+ // token file. Direct `clementine dashboard` invocations now match
1760
+ // the restart/update flow so the user never has to copy-paste a
1761
+ // token by hand. Honors NO_BROWSER=1 for headless / CI runs.
1762
+ if (process.env.NO_BROWSER !== '1') {
1763
+ setTimeout(() => {
1764
+ try {
1765
+ const tokenPath = path.join(BASE_DIR, '.dashboard-token');
1766
+ const token = existsSync(tokenPath) ? readFileSync(tokenPath, 'utf-8').trim() : '';
1767
+ if (!token)
1768
+ return;
1769
+ const url = `http://localhost:${childPort}/?token=${token}`;
1770
+ const platform = process.platform;
1771
+ const cmd = platform === 'darwin' ? 'open'
1772
+ : platform === 'win32' ? 'start'
1773
+ : 'xdg-open';
1774
+ spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
1775
+ }
1776
+ catch { /* best effort */ }
1777
+ }, 1500);
1778
+ }
1757
1779
  // Forward signals to child
1758
1780
  process.on('SIGINT', () => { child.kill('SIGINT'); cleanup(); process.exit(0); });
1759
1781
  process.on('SIGTERM', () => { child.kill('SIGTERM'); cleanup(); process.exit(0); });
package/dist/cli/index.js CHANGED
@@ -472,7 +472,32 @@ function cmdStop() {
472
472
  * the user's old browser tab (which would silently 401 on the stale
473
473
  * token) doesn't waste their time.
474
474
  */
475
- async function relaunchDashboardDetached() {
475
+ /**
476
+ * 1.18.147 — Open the dashboard URL in the user's default browser.
477
+ *
478
+ * Invoked after the dashboard child binds. Best-effort cross-platform:
479
+ * macOS uses `open`, Windows uses `start`, Linux falls back to
480
+ * `xdg-open`. Failures are swallowed (no native browser, headless
481
+ * SSH session, etc.) — the printed URL is still the source of truth
482
+ * the user can copy by hand.
483
+ *
484
+ * Honors NO_BROWSER=1 env var so CI / scripted runs don't get a
485
+ * spurious browser tab.
486
+ */
487
+ function openInBrowser(url) {
488
+ if (process.env.NO_BROWSER === '1')
489
+ return;
490
+ const platform = process.platform;
491
+ const cmd = platform === 'darwin' ? 'open'
492
+ : platform === 'win32' ? 'start'
493
+ : 'xdg-open';
494
+ try {
495
+ const { spawn: spawnProc } = require('node:child_process');
496
+ spawnProc(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
497
+ }
498
+ catch { /* no browser available; URL was already printed */ }
499
+ }
500
+ async function relaunchDashboardDetached(opts = {}) {
476
501
  try {
477
502
  const { spawn: spawnProc } = await import('node:child_process');
478
503
  const child = spawnProc('node', [path.join(PACKAGE_ROOT, 'dist/cli/index.js'), 'dashboard'], { detached: true, stdio: 'ignore' });
@@ -489,7 +514,13 @@ async function relaunchDashboardDetached() {
489
514
  }
490
515
  catch { /* token may not be ready yet */ }
491
516
  if (token) {
492
- console.log(` Dashboard relaunched: http://localhost:3030/?token=${token}`);
517
+ const url = `http://localhost:3030/?token=${token}`;
518
+ console.log(` Dashboard relaunched: ${url}`);
519
+ // 1.18.147 — auto-open the browser by default. Restart/update
520
+ // already imply user wants the dashboard back; making them copy a
521
+ // URL was a UX papercut.
522
+ if (opts.open !== false)
523
+ openInBrowser(url);
493
524
  }
494
525
  else {
495
526
  console.log(' Dashboard relaunched (token not ready — check `clementine status`).');
@@ -546,7 +546,15 @@ export async function diagnoseBrokenJob(broken, gateway) {
546
546
  undefined, // maxHours
547
547
  undefined, // timeoutMs
548
548
  undefined, // successCriteria
549
- undefined);
549
+ undefined, // agentSlug
550
+ // 1.18.148 — F1/F2 pattern: diagnostics are pure analysis of an
551
+ // existing run, no need for MEMORY.md / team / auto-skills. Without
552
+ // these flags the diagnostic prompt blew past Claude's input limit
553
+ // and broken jobs went undiagnosed silently.
554
+ undefined, // pinnedSkills
555
+ [], // allowedTools
556
+ [], // allowedMcpServers
557
+ true);
550
558
  }
551
559
  catch (err) {
552
560
  logger.warn({ err, job: broken.jobName }, 'Diagnostic LLM call failed');
@@ -140,7 +140,16 @@ export async function gradeRun(entry, gateway, jobPrompt) {
140
140
  undefined, // maxHours
141
141
  undefined, // timeoutMs
142
142
  undefined, // successCriteria
143
- undefined);
143
+ undefined, // agentSlug
144
+ // 1.18.148 — F1/F2 pattern: meta-jobs don't get user MEMORY.md /
145
+ // team comms / auto-matched skills, otherwise the prompt blows
146
+ // past Claude's input limit (110+ "Prompt is too long" errors/8h
147
+ // before this fix). Same shape applied to insight-check (1.18.132)
148
+ // and route-classify / failure-diagnostics in this same ship.
149
+ undefined, // pinnedSkills
150
+ [], // allowedTools — empty = no MCP injection
151
+ [], // allowedMcpServers — empty = no MCP servers wired
152
+ true);
144
153
  }
145
154
  catch (err) {
146
155
  logger.warn({ err, jobName: entry.jobName }, 'Outcome grader LLM call failed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.146",
3
+ "version": "1.18.148",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",