clementine-agent 1.18.188 → 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
|
-
|
|
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.',
|
package/dist/gateway/router.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
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)
|