clementine-agent 1.18.187 → 1.18.189

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.
@@ -90,6 +90,14 @@ export interface DiscoveryCandidate {
90
90
  * Search the filesystem for folders that could plausibly be the
91
91
  * project the user is mentioning. Used when the registry has no match.
92
92
  *
93
+ * 1.18.189 — search order:
94
+ * 1. Spotlight (`mdfind`) on macOS — instant, system-indexed,
95
+ * finds folders ANYWHERE on disk by name. Critical when the
96
+ * project is at depth 2+ ("~/Documents/Work/team-coaches")
97
+ * or the owner only knows part of the name.
98
+ * 2. Direct walk of DEFAULT_DISCOVERY_ROOTS (depth 1) — fallback
99
+ * for non-macOS and edge cases where Spotlight is disabled.
100
+ *
93
101
  * Returns candidates sorted by composite score (best first). The
94
102
  * caller — typically the chat agent via the `project_discover` tool —
95
103
  * inspects the list and decides whether to ask the owner for
@@ -102,6 +110,7 @@ export declare function discoverProjectCandidates(searchTerm: string, opts?: {
102
110
  searchRoots?: string[];
103
111
  maxResults?: number;
104
112
  nowMs?: number;
113
+ disableSpotlight?: boolean;
105
114
  }): DiscoveryCandidate[];
106
115
  /**
107
116
  * Heuristic: does the user message indicate they're disputing prior
@@ -29,6 +29,7 @@
29
29
  * Pure functions where possible; filesystem discovery is the only
30
30
  * I/O. Safe to call from any layer that has a sessionKey.
31
31
  */
32
+ import { execSync } from 'node:child_process';
32
33
  import fs from 'node:fs';
33
34
  import os from 'node:os';
34
35
  import path from 'node:path';
@@ -150,6 +151,14 @@ export function resolveProjectFromMessage(message, opts = {}) {
150
151
  * Search the filesystem for folders that could plausibly be the
151
152
  * project the user is mentioning. Used when the registry has no match.
152
153
  *
154
+ * 1.18.189 — search order:
155
+ * 1. Spotlight (`mdfind`) on macOS — instant, system-indexed,
156
+ * finds folders ANYWHERE on disk by name. Critical when the
157
+ * project is at depth 2+ ("~/Documents/Work/team-coaches")
158
+ * or the owner only knows part of the name.
159
+ * 2. Direct walk of DEFAULT_DISCOVERY_ROOTS (depth 1) — fallback
160
+ * for non-macOS and edge cases where Spotlight is disabled.
161
+ *
153
162
  * Returns candidates sorted by composite score (best first). The
154
163
  * caller — typically the chat agent via the `project_discover` tool —
155
164
  * inspects the list and decides whether to ask the owner for
@@ -167,6 +176,55 @@ export function discoverProjectCandidates(searchTerm, opts = {}) {
167
176
  const nowMs = opts.nowMs ?? Date.now();
168
177
  const candidates = [];
169
178
  const seen = new Set();
179
+ const consider = (full) => {
180
+ if (seen.has(full))
181
+ return;
182
+ seen.add(full);
183
+ let stat;
184
+ try {
185
+ stat = fs.statSync(full);
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ if (!stat.isDirectory())
191
+ return;
192
+ const basename = path.basename(full).toLowerCase();
193
+ if (DISCOVERY_IGNORE.has(path.basename(full)))
194
+ return;
195
+ if (basename.startsWith('.'))
196
+ return;
197
+ // Skip anything under our own ignore roots even if Spotlight indexed them.
198
+ if (/\/(node_modules|\.git|Library|\.cache|\.Trash)\//.test(full))
199
+ return;
200
+ const nameScore = computeNameScore(basename, term);
201
+ if (nameScore === 0)
202
+ return;
203
+ const recencyScore = computeRecencyScore(stat.mtimeMs, nowMs);
204
+ const { score: contentScore, summary: contentSummary } = computeContentScore(full);
205
+ const totalScore = 0.6 * nameScore + 0.25 * contentScore + 0.15 * recencyScore;
206
+ candidates.push({
207
+ path: full,
208
+ basename: path.basename(full),
209
+ nameScore,
210
+ recencyScore,
211
+ contentScore,
212
+ totalScore,
213
+ contentSummary,
214
+ });
215
+ };
216
+ // ── 1. Spotlight (macOS) ─────────────────────────────────────────
217
+ if (!opts.disableSpotlight && process.platform === 'darwin') {
218
+ try {
219
+ const spotlightHits = mdfindFolders(term);
220
+ for (const full of spotlightHits)
221
+ consider(full);
222
+ }
223
+ catch {
224
+ // Spotlight unavailable / disabled — fall through to walk.
225
+ }
226
+ }
227
+ // ── 2. Direct walk of standard roots (depth 1) ───────────────────
170
228
  for (const root of roots) {
171
229
  if (!root || !fs.existsSync(root))
172
230
  continue;
@@ -182,42 +240,38 @@ export function discoverProjectCandidates(searchTerm, opts = {}) {
182
240
  continue;
183
241
  if (DISCOVERY_IGNORE.has(entry))
184
242
  continue;
185
- const full = path.join(root, entry);
186
- if (seen.has(full))
187
- continue;
188
- seen.add(full);
189
- let stat;
190
- try {
191
- stat = fs.statSync(full);
192
- }
193
- catch {
194
- continue;
195
- }
196
- if (!stat.isDirectory())
197
- continue;
198
- const basename = entry.toLowerCase();
199
- const nameScore = computeNameScore(basename, term);
200
- // Cheap pre-filter: skip if name has zero overlap with the term.
201
- if (nameScore === 0)
202
- continue;
203
- const recencyScore = computeRecencyScore(stat.mtimeMs, nowMs);
204
- const { score: contentScore, summary: contentSummary } = computeContentScore(full);
205
- // Weighted composite. Name dominates; content + recency are tiebreakers.
206
- const totalScore = 0.6 * nameScore + 0.25 * contentScore + 0.15 * recencyScore;
207
- candidates.push({
208
- path: full,
209
- basename: entry,
210
- nameScore,
211
- recencyScore,
212
- contentScore,
213
- totalScore,
214
- contentSummary,
215
- });
243
+ consider(path.join(root, entry));
216
244
  }
217
245
  }
218
246
  candidates.sort((a, b) => b.totalScore - a.totalScore);
219
247
  return candidates.slice(0, maxResults);
220
248
  }
249
+ /**
250
+ * Run `mdfind` to find folders whose name matches the search term.
251
+ * macOS only. Returns absolute paths. Limits result count + total
252
+ * runtime so a vague query can't hang the agent.
253
+ */
254
+ function mdfindFolders(term) {
255
+ // kMDItemDisplayName — folder name as Finder shows it
256
+ // kMDItemContentTypeTree includes "public.folder" — restricts to folders
257
+ // The query: name contains term (case-insensitive) AND is a folder.
258
+ // Escape double quotes in the term defensively.
259
+ const safe = term.replace(/["\\]/g, '');
260
+ if (!safe)
261
+ return [];
262
+ const query = `kMDItemDisplayName == "*${safe}*"cd && kMDItemContentTypeTree == "public.folder"`;
263
+ try {
264
+ const out = execSync(`mdfind '${query}' 2>/dev/null | head -40`, {
265
+ timeout: 4_000,
266
+ maxBuffer: 256 * 1024,
267
+ encoding: 'utf-8',
268
+ });
269
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
270
+ }
271
+ catch {
272
+ return [];
273
+ }
274
+ }
221
275
  // ── Scoring helpers ──────────────────────────────────────────────────
222
276
  function computeNameScore(basename, term) {
223
277
  // Exact match: 1.0
@@ -47,6 +47,19 @@ const BACKGROUND_TASK_WORKER_PROMPT = [
47
47
  '',
48
48
  'Use TodoWrite for multi-step state. Process batch work in bounded chunks, checkpoint meaningful progress in durable artifacts when useful, and avoid repeating the same expensive read or tool call.',
49
49
  '',
50
+ '## Context budget — survive long jobs without thrashing (1.18.189)',
51
+ '',
52
+ 'Your context window is finite. Two failure patterns kill long jobs:',
53
+ '1. **Reading too much, too soon.** Don\'t `Read` a 10MB CSV to find one column — use `Bash head -100`, `awk -F, \'{print $3}\'`, or `Grep` first to confirm shape, then narrow.',
54
+ '2. **Searching by scanning.** Don\'t `Glob`/`Read` through `~/Documents` looking for a project — call `project_discover` with the project name; it uses Spotlight on macOS and returns ranked candidates instantly.',
55
+ '',
56
+ 'Tool-by-tool guidance:',
57
+ '- **Finding a project**: call `mcp__clementine-tools__project_discover` with the search term. ONE call. Returns candidates.',
58
+ '- **Linking a project**: after the owner confirms which candidate is right, call `mcp__clementine-tools__project_link`. Don\'t skip this — without linking, future turns can\'t resolve the project.',
59
+ '- **Deploying**: call `mcp__clementine-tools__project_deploy` — it runs the command AND curls the verify URL. Don\'t invent deploy commands by hand.',
60
+ '- **Reading large data files**: prefer `Bash head/wc/awk/jq` over `Read` for any file >500 lines.',
61
+ '- **Listing many files**: `Bash ls`/`find` with `head -N` is cheaper than `Glob` for big trees.',
62
+ '',
50
63
  'If credentials, missing scope, human approval, or an irreversible action blocks completion, stop with one concise blocker/question and the exact next action needed. Do not keep retrying blindly.',
51
64
  '',
52
65
  'Return only the final user-facing result: links or changed locations, counts, skipped/error records, and the next recommended action.',
@@ -217,7 +217,21 @@ export async function runAgent(prompt, opts) {
217
217
  // 0 (or undefined) means "no cap" — matches the dashboard's
218
218
  // "Remove spend caps" preset contract. We omit `maxBudgetUsd` from
219
219
  // sdkOptions entirely in that case so the SDK runs uncapped.
220
- const requestedBudget = opts.maxBudgetUsd ?? DEFAULT_BUDGETS[source];
220
+ //
221
+ // 1.18.188 — dashboard-is-boss extends to DEFAULT_BUDGETS too. When
222
+ // ALL global BUDGET_*_USD are 0 ("no budget anywhere" mode), the
223
+ // hardcoded DEFAULT_BUDGETS fallback yields. Without this, a bg task
224
+ // spawned from chat overflow (source='cron') still hit the $1.00
225
+ // hardcoded cron default even with every dashboard cap set to 0 —
226
+ // exactly the failure Zach reported on 2026-05-12. Resolution order:
227
+ // 1. Caller's explicit options.maxBudgetUsd wins (explicit > implicit)
228
+ // 2. If global all-zero → undefined (no cap)
229
+ // 3. Otherwise DEFAULT_BUDGETS[source] fires as last resort
230
+ const { BUDGET } = await import('../config.js');
231
+ const globalCapsAllZero = BUDGET.heartbeat === 0 && BUDGET.cronT1 === 0 &&
232
+ BUDGET.cronT2 === 0 && BUDGET.chat === 0;
233
+ const requestedBudget = opts.maxBudgetUsd
234
+ ?? (globalCapsAllZero ? undefined : DEFAULT_BUDGETS[source]);
221
235
  const maxBudgetUsd = typeof requestedBudget === 'number' && requestedBudget > 0
222
236
  ? requestedBudget
223
237
  : undefined;
@@ -49,8 +49,15 @@ const CHAT_TIMEOUT_MS = 10 * 60 * 1000;
49
49
  * Safety net so no session runs forever, even if active.
50
50
  * Primary guardrail is cost budget (maxBudgetUsd), not this timer. */
51
51
  const CHAT_MAX_WALL_MS = 30 * 60 * 1000;
52
- const CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS = 6_000;
53
- const CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS = 16_000;
52
+ // 1.18.189 tightened from 6_000 / 16_000 because the recovery prompt
53
+ // was eating ~22KB of the bg-task worker's context window before any
54
+ // real work started. On 2026-05-12 the worker autocompact-thrashed while
55
+ // reading project files; the new tighter caps give it ~10KB more headroom
56
+ // to do actual tool calls. The dropped content (older memory recall,
57
+ // less-relevant bg-task headlines) is recoverable via memory_search if
58
+ // the model actually needs it.
59
+ const CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS = 3_000;
60
+ const CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS = 8_000;
54
61
  const BACKGROUND_TASK_ID_RE = /\bbg-[a-z0-9]+-[a-f0-9]{6}\b/i;
55
62
  function collectRunToolNames(runId) {
56
63
  if (!runId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.187",
3
+ "version": "1.18.189",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",