clementine-agent 1.18.59 → 1.18.61
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.
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import pino from 'pino';
|
|
20
|
-
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, } from '../config.js';
|
|
20
|
+
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, BUDGET, } from '../config.js';
|
|
21
21
|
import { runAgent } from './run-agent.js';
|
|
22
22
|
import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
|
|
23
23
|
import { buildAutonomousMemoryContext } from './run-agent-context.js';
|
|
@@ -281,7 +281,12 @@ export async function runAgentCron(opts) {
|
|
|
281
281
|
profile: opts.profile,
|
|
282
282
|
});
|
|
283
283
|
// ── Run via canonical runAgent ────────────────────────────────────
|
|
284
|
-
|
|
284
|
+
// Per-tier cap from config (BUDGET.cronT1 / BUDGET.cronT2). Sourced
|
|
285
|
+
// from env / clementine.json / dashboard writes. 0 means uncapped —
|
|
286
|
+
// we pass undefined so runAgent omits the SDK option entirely.
|
|
287
|
+
// Caller can still override via opts.maxBudgetUsd.
|
|
288
|
+
const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
|
|
289
|
+
const maxBudget = opts.maxBudgetUsd ?? (configuredCap > 0 ? configuredCap : undefined);
|
|
285
290
|
const effort = tier >= 2 ? 'high' : 'medium';
|
|
286
291
|
logger.info({
|
|
287
292
|
job: opts.jobName,
|
|
@@ -302,7 +307,7 @@ export async function runAgentCron(opts) {
|
|
|
302
307
|
memoryStore: opts.memoryStore,
|
|
303
308
|
model: opts.model,
|
|
304
309
|
effort,
|
|
305
|
-
maxBudgetUsd: maxBudget,
|
|
310
|
+
...(maxBudget !== undefined ? { maxBudgetUsd: maxBudget } : {}),
|
|
306
311
|
maxTurns: opts.maxTurns,
|
|
307
312
|
abortSignal: opts.abortSignal,
|
|
308
313
|
extraMcpServers: mcp.servers,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* through the canonical runAgent() instead of buildOptions+query.
|
|
15
15
|
*/
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { OWNER_NAME, MODELS, } from '../config.js';
|
|
17
|
+
import { OWNER_NAME, MODELS, BUDGET, } from '../config.js';
|
|
18
18
|
const OWNER = OWNER_NAME || 'the user';
|
|
19
19
|
function formatDate(d) {
|
|
20
20
|
return d.toLocaleDateString('en-US', {
|
|
@@ -65,6 +65,10 @@ export async function runAgentHeartbeat(opts) {
|
|
|
65
65
|
profile: opts.profile?.slug,
|
|
66
66
|
promptChars: prompt.length,
|
|
67
67
|
}, 'runAgentHeartbeat: dispatching to runAgent (no tools)');
|
|
68
|
+
// Heartbeat cap from config (BUDGET.heartbeat). Sourced from env /
|
|
69
|
+
// clementine.json / dashboard writes. 0 = uncapped — runAgent
|
|
70
|
+
// omits the SDK option in that case.
|
|
71
|
+
const heartbeatBudget = opts.maxBudgetUsd ?? (BUDGET.heartbeat > 0 ? BUDGET.heartbeat : undefined);
|
|
68
72
|
const sessionKey = `heartbeat:${opts.profile?.slug ?? 'clementine'}`;
|
|
69
73
|
const result = await runAgent(prompt, {
|
|
70
74
|
sessionKey,
|
|
@@ -73,7 +77,7 @@ export async function runAgentHeartbeat(opts) {
|
|
|
73
77
|
memoryStore: opts.memoryStore,
|
|
74
78
|
model: opts.model ?? MODELS.haiku,
|
|
75
79
|
effort: 'low',
|
|
76
|
-
maxBudgetUsd:
|
|
80
|
+
...(heartbeatBudget !== undefined ? { maxBudgetUsd: heartbeatBudget } : {}),
|
|
77
81
|
maxTurns: 1,
|
|
78
82
|
// No tools — heartbeats are decision-only. Empty list bypasses the
|
|
79
83
|
// CORE_TOOLS_FOR_AGENT_PARENT default and stops the SDK from
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -62,8 +62,15 @@ function buildRunAgentEnv() {
|
|
|
62
62
|
return env;
|
|
63
63
|
}
|
|
64
64
|
const logger = pino({ name: 'clementine.run-agent' });
|
|
65
|
+
// Last-resort fallbacks for callers that pass NO maxBudgetUsd. The
|
|
66
|
+
// production callers (`runAgent` from gateway/router, runAgentCron,
|
|
67
|
+
// runAgentHeartbeat) read `BUDGET.*` from src/config.ts — which is
|
|
68
|
+
// itself sourced from env / clementine.json / dashboard writes — and
|
|
69
|
+
// pass it explicitly. Chat is intentionally omitted: the chat path
|
|
70
|
+
// must always go through `BUDGET.chat` (0 = uncapped), never a silent
|
|
71
|
+
// hardcoded floor. If `source: 'chat'` ever lands here without an
|
|
72
|
+
// explicit budget, we treat it as uncapped.
|
|
65
73
|
const DEFAULT_BUDGETS = {
|
|
66
|
-
chat: 0.50,
|
|
67
74
|
cron: 1.00,
|
|
68
75
|
heartbeat: 0.25,
|
|
69
76
|
'team-task': 1.00,
|
|
@@ -97,7 +104,13 @@ const CORE_TOOLS_FOR_AGENT_PARENT = [
|
|
|
97
104
|
export async function runAgent(prompt, opts) {
|
|
98
105
|
const source = opts.source ?? 'chat';
|
|
99
106
|
const effort = opts.effort ?? DEFAULT_EFFORTS[source] ?? 'medium';
|
|
100
|
-
|
|
107
|
+
// 0 (or undefined) means "no cap" — matches the dashboard's
|
|
108
|
+
// "Remove spend caps" preset contract. We omit `maxBudgetUsd` from
|
|
109
|
+
// sdkOptions entirely in that case so the SDK runs uncapped.
|
|
110
|
+
const requestedBudget = opts.maxBudgetUsd ?? DEFAULT_BUDGETS[source];
|
|
111
|
+
const maxBudgetUsd = typeof requestedBudget === 'number' && requestedBudget > 0
|
|
112
|
+
? requestedBudget
|
|
113
|
+
: undefined;
|
|
101
114
|
const startedAt = Date.now();
|
|
102
115
|
// Build the AgentDefinition map. Caller can override; otherwise we
|
|
103
116
|
// use the standard system subagents + hired-agent profiles.
|
|
@@ -187,8 +200,8 @@ export async function runAgent(prompt, opts) {
|
|
|
187
200
|
allowDangerouslySkipPermissions: true,
|
|
188
201
|
cwd: BASE_DIR,
|
|
189
202
|
env: subprocessEnv,
|
|
190
|
-
maxBudgetUsd,
|
|
191
203
|
effort,
|
|
204
|
+
...(maxBudgetUsd !== undefined ? { maxBudgetUsd } : {}),
|
|
192
205
|
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
|
193
206
|
...(opts.model ? { model: opts.model } : {}),
|
|
194
207
|
...(opts.resumeSessionId ? { resume: opts.resumeSessionId } : {}),
|
|
@@ -201,7 +214,7 @@ export async function runAgent(prompt, opts) {
|
|
|
201
214
|
profile: opts.profile?.slug,
|
|
202
215
|
forceSubagent: opts.forceSubagent,
|
|
203
216
|
effort,
|
|
204
|
-
maxBudgetUsd,
|
|
217
|
+
maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
|
|
205
218
|
agentCount: Object.keys(agents).length,
|
|
206
219
|
allowedToolCount: allowedTools.length,
|
|
207
220
|
}, 'runAgent: starting query');
|
|
@@ -212,76 +225,93 @@ export async function runAgent(prompt, opts) {
|
|
|
212
225
|
let subtype = 'unknown';
|
|
213
226
|
let usage;
|
|
214
227
|
const stream = query({ prompt: effectivePrompt, options: sdkOptions });
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
try {
|
|
229
|
+
for await (const message of stream) {
|
|
230
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
231
|
+
sessionId = message.session_id ?? '';
|
|
232
|
+
logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (message.type === 'assistant') {
|
|
236
|
+
const am = message;
|
|
237
|
+
const blocks = (am.message?.content ?? []);
|
|
238
|
+
for (const block of blocks) {
|
|
239
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
240
|
+
finalText += block.text;
|
|
241
|
+
if (opts.onText) {
|
|
242
|
+
try {
|
|
243
|
+
await opts.onText(block.text);
|
|
244
|
+
}
|
|
245
|
+
catch { /* streaming is best-effort */ }
|
|
230
246
|
}
|
|
231
|
-
catch { /* streaming is best-effort */ }
|
|
232
247
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
249
|
+
if (opts.onToolActivity) {
|
|
250
|
+
try {
|
|
251
|
+
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
252
|
+
}
|
|
253
|
+
catch { /* best-effort */ }
|
|
238
254
|
}
|
|
239
|
-
catch { /* best-effort */ }
|
|
240
255
|
}
|
|
241
256
|
}
|
|
257
|
+
continue;
|
|
242
258
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (r)
|
|
258
|
-
finalText = r;
|
|
259
|
-
}
|
|
260
|
-
// Mirror cost to usage_log. Same shape as the existing
|
|
261
|
-
// logQueryResult, but standalone so we don't depend on
|
|
262
|
-
// PersonalAssistant's instance state.
|
|
263
|
-
const modelUsage = result.modelUsage;
|
|
264
|
-
if (opts.memoryStore && modelUsage) {
|
|
265
|
-
try {
|
|
266
|
-
opts.memoryStore.logUsage({
|
|
267
|
-
sessionKey: `${source}:${opts.sessionKey}`,
|
|
268
|
-
source: `runagent.${source}`,
|
|
269
|
-
modelUsage,
|
|
270
|
-
numTurns,
|
|
271
|
-
durationMs: Date.now() - startedAt,
|
|
272
|
-
agentSlug: opts.profile?.slug,
|
|
273
|
-
totalCostUsd: totalCostUsd,
|
|
274
|
-
});
|
|
259
|
+
if (message.type === 'result') {
|
|
260
|
+
const result = message;
|
|
261
|
+
sessionId = sessionId || (result.session_id ?? '');
|
|
262
|
+
subtype = result.subtype ?? 'unknown';
|
|
263
|
+
numTurns = result.num_turns ?? numTurns;
|
|
264
|
+
totalCostUsd = result.total_cost_usd ?? 0;
|
|
265
|
+
const u = result.usage;
|
|
266
|
+
if (u)
|
|
267
|
+
usage = u;
|
|
268
|
+
if (subtype === 'success') {
|
|
269
|
+
// success carries `result` field with the final text.
|
|
270
|
+
const r = result.result;
|
|
271
|
+
if (r)
|
|
272
|
+
finalText = r;
|
|
275
273
|
}
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Mirror cost to usage_log. Same shape as the existing
|
|
275
|
+
// logQueryResult, but standalone so we don't depend on
|
|
276
|
+
// PersonalAssistant's instance state.
|
|
277
|
+
const modelUsage = result.modelUsage;
|
|
278
|
+
if (opts.memoryStore && modelUsage) {
|
|
279
|
+
try {
|
|
280
|
+
opts.memoryStore.logUsage({
|
|
281
|
+
sessionKey: `${source}:${opts.sessionKey}`,
|
|
282
|
+
source: `runagent.${source}`,
|
|
283
|
+
modelUsage,
|
|
284
|
+
numTurns,
|
|
285
|
+
durationMs: Date.now() - startedAt,
|
|
286
|
+
agentSlug: opts.profile?.slug,
|
|
287
|
+
totalCostUsd: totalCostUsd,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
logger.debug({ err }, 'runAgent: usage logging failed (non-fatal)');
|
|
292
|
+
}
|
|
278
293
|
}
|
|
294
|
+
continue;
|
|
279
295
|
}
|
|
280
|
-
|
|
296
|
+
// Other message types (UserMessage with tool_result, StreamEvent,
|
|
297
|
+
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
298
|
+
// handles compaction internally; we just let it run.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
// Translate the SDK's budget-exhaustion throw into a message that
|
|
303
|
+
// tells the user (a) what cap tripped and (b) how to raise it.
|
|
304
|
+
// The raw SDK string ("Claude Code returned an error result:
|
|
305
|
+
// Reached maximum budget ($0.5)") leaks through the channel layer
|
|
306
|
+
// as a generic "Something went wrong:" with no actionable hint.
|
|
307
|
+
const msg = String(err?.message ?? err);
|
|
308
|
+
if (/Reached maximum budget|error_max_budget_usd/i.test(msg)) {
|
|
309
|
+
const cap = maxBudgetUsd?.toFixed(2) ?? '?';
|
|
310
|
+
const envKey = `BUDGET_${source.toUpperCase().replace(/-/g, '_')}_USD`;
|
|
311
|
+
throw new Error(`Hit the $${cap} ${source} budget cap before finishing. ` +
|
|
312
|
+
`Raise it in the dashboard (Budgets & Costs) or set ${envKey}=0 to remove caps.`);
|
|
281
313
|
}
|
|
282
|
-
|
|
283
|
-
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
284
|
-
// handles compaction internally; we just let it run.
|
|
314
|
+
throw err;
|
|
285
315
|
}
|
|
286
316
|
logger.info({
|
|
287
317
|
sessionKey: opts.sessionKey,
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5690,6 +5690,19 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5690
5690
|
res.status(500).json({ error: String(err) });
|
|
5691
5691
|
}
|
|
5692
5692
|
});
|
|
5693
|
+
// ── Projects ─────────────────────────────────────────────────
|
|
5694
|
+
// Returns the project list from $CLEMENTINE_HOME/projects.json.
|
|
5695
|
+
// Used by the trick builder's Quick Add Step picker so users can
|
|
5696
|
+
// scaffold a step that runs in a specific project directory.
|
|
5697
|
+
app.get('/api/projects', (_req, res) => {
|
|
5698
|
+
try {
|
|
5699
|
+
const projects = loadProjectsMeta();
|
|
5700
|
+
res.json({ projects });
|
|
5701
|
+
}
|
|
5702
|
+
catch (err) {
|
|
5703
|
+
res.status(500).json({ error: String(err) });
|
|
5704
|
+
}
|
|
5705
|
+
});
|
|
5693
5706
|
// ── Available Tools ──────────────────────────────────────────
|
|
5694
5707
|
app.get('/api/available-tools', (_req, res) => {
|
|
5695
5708
|
try {
|
|
@@ -6949,7 +6962,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6949
6962
|
}
|
|
6950
6963
|
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
6951
6964
|
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
6952
|
-
message = 'Removed spend caps by setting all budget values to 0.
|
|
6965
|
+
message = 'Removed spend caps by setting all budget values to 0. Restart Clementine for the change to take effect on running workers. (1M context mode is separate — use Force 200K or Safe Recovery for 1M errors.)';
|
|
6953
6966
|
}
|
|
6954
6967
|
else {
|
|
6955
6968
|
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
|
@@ -25984,6 +25997,15 @@ function renderBuilderPreview(artifact, type) {
|
|
|
25984
25997
|
+ '<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
25998
|
+ '<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
25999
|
+ '</div>'
|
|
26000
|
+
+ '<div class="preview-field"><label>Quick add a step</label>'
|
|
26001
|
+
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:4px">'
|
|
26002
|
+
+ '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27mcp\\x27)">+ MCP step</button>'
|
|
26003
|
+
+ '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27cli\\x27)">+ CLI step</button>'
|
|
26004
|
+
+ '<button type="button" class="btn btn-sm" onclick="openQuickAddPicker(\\x27project\\x27)">+ Project step</button>'
|
|
26005
|
+
+ '</div>'
|
|
26006
|
+
+ '<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>'
|
|
26007
|
+
+ '<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>'
|
|
26008
|
+
+ '</div>'
|
|
25987
26009
|
+ '<div class="preview-field"><label>Steps (YAML/Markdown)</label><textarea rows="14" onchange="builderArtifact.steps=this.value">' + esc(artifact.steps || '') + '</textarea></div>';
|
|
25988
26010
|
setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
|
|
25989
26011
|
}
|
|
@@ -26150,6 +26172,214 @@ function syncBuilderLinkedTools() {
|
|
|
26150
26172
|
}
|
|
26151
26173
|
}
|
|
26152
26174
|
|
|
26175
|
+
// ── Trick builder: Quick Add Step ─────────
|
|
26176
|
+
// Three buttons in the workflow preview pane (+ MCP / + CLI / + Project)
|
|
26177
|
+
// scaffold a step block into builderArtifact.steps and auto-fire a chat
|
|
26178
|
+
// turn so Clementine writes the prompt for that step. The steps field
|
|
26179
|
+
// stays as a freeform YAML textarea — we just append, don't parse-edit.
|
|
26180
|
+
|
|
26181
|
+
function _nextStepId() {
|
|
26182
|
+
var current = (builderArtifact && builderArtifact.steps) || '';
|
|
26183
|
+
var ids = current.match(/step-(\\d+)/g) || [];
|
|
26184
|
+
var max = 0;
|
|
26185
|
+
for (var i = 0; i < ids.length; i++) {
|
|
26186
|
+
var n = parseInt(ids[i].replace('step-', ''), 10);
|
|
26187
|
+
if (!isNaN(n) && n > max) max = n;
|
|
26188
|
+
}
|
|
26189
|
+
return 'step-' + (max + 1);
|
|
26190
|
+
}
|
|
26191
|
+
|
|
26192
|
+
function _previousStepId() {
|
|
26193
|
+
var current = (builderArtifact && builderArtifact.steps) || '';
|
|
26194
|
+
var matches = current.match(/step-(\\d+)/g) || [];
|
|
26195
|
+
if (matches.length === 0) return null;
|
|
26196
|
+
// Highest numbered id
|
|
26197
|
+
var max = 0;
|
|
26198
|
+
for (var i = 0; i < matches.length; i++) {
|
|
26199
|
+
var n = parseInt(matches[i].replace('step-', ''), 10);
|
|
26200
|
+
if (!isNaN(n) && n > max) max = n;
|
|
26201
|
+
}
|
|
26202
|
+
return 'step-' + max;
|
|
26203
|
+
}
|
|
26204
|
+
|
|
26205
|
+
function _yamlEscape(s) {
|
|
26206
|
+
if (s == null) return '';
|
|
26207
|
+
return String(s).replace(/"/g, '\\\\"');
|
|
26208
|
+
}
|
|
26209
|
+
|
|
26210
|
+
function _appendStepYaml(stepId, dependsOn, body) {
|
|
26211
|
+
var current = (builderArtifact && builderArtifact.steps) || '';
|
|
26212
|
+
var dep = dependsOn ? '\\n dependsOn: [' + dependsOn + ']' : '';
|
|
26213
|
+
var block = stepId + ':\\n prompt: ""' + dep + '\\n' + body;
|
|
26214
|
+
builderArtifact.steps = current.trim() ? (current.trim() + '\\n\\n' + block) : block;
|
|
26215
|
+
}
|
|
26216
|
+
|
|
26217
|
+
async function openQuickAddPicker(kind) {
|
|
26218
|
+
var panel = document.getElementById('quick-add-picker');
|
|
26219
|
+
if (!panel) return;
|
|
26220
|
+
// Toggle close if already open for the same kind.
|
|
26221
|
+
if (panel.dataset.kind === kind && panel.style.display !== 'none') {
|
|
26222
|
+
panel.style.display = 'none';
|
|
26223
|
+
panel.dataset.kind = '';
|
|
26224
|
+
panel.innerHTML = '';
|
|
26225
|
+
return;
|
|
26226
|
+
}
|
|
26227
|
+
panel.dataset.kind = kind;
|
|
26228
|
+
panel.style.display = 'block';
|
|
26229
|
+
panel.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0">Loading…</div>';
|
|
26230
|
+
|
|
26231
|
+
try {
|
|
26232
|
+
if (kind === 'mcp') {
|
|
26233
|
+
var r = await apiFetch('/api/available-tools');
|
|
26234
|
+
var d = await r.json();
|
|
26235
|
+
var rows = [];
|
|
26236
|
+
for (var cat in d.categories) {
|
|
26237
|
+
var entries = d.categories[cat];
|
|
26238
|
+
for (var i = 0; i < entries.length; i++) {
|
|
26239
|
+
var t = entries[i];
|
|
26240
|
+
var name = typeof t === 'string' ? t : t.name;
|
|
26241
|
+
if (typeof name === 'string' && name.indexOf('mcp__') === 0) {
|
|
26242
|
+
// mcp__<server>__<tool>
|
|
26243
|
+
var parts = name.replace(/^mcp__/, '').split('__');
|
|
26244
|
+
if (parts.length < 2) continue;
|
|
26245
|
+
var server = parts[0];
|
|
26246
|
+
var tool = parts.slice(1).join('__');
|
|
26247
|
+
rows.push({
|
|
26248
|
+
label: server + ' / ' + tool,
|
|
26249
|
+
description: typeof t === 'object' && t.description ? t.description : '',
|
|
26250
|
+
resource: { server: server, tool: tool, description: typeof t === 'object' && t.description ? t.description : '' },
|
|
26251
|
+
});
|
|
26252
|
+
}
|
|
26253
|
+
}
|
|
26254
|
+
}
|
|
26255
|
+
_renderQuickAddPicker(panel, kind, rows);
|
|
26256
|
+
} else if (kind === 'cli') {
|
|
26257
|
+
var r2 = await apiFetch('/api/available-tools');
|
|
26258
|
+
var d2 = await r2.json();
|
|
26259
|
+
var entries2 = (d2.categories && d2.categories['CLI Tools']) || [];
|
|
26260
|
+
var rows2 = entries2.map(function(t) {
|
|
26261
|
+
return {
|
|
26262
|
+
label: t.name + (t.installed === false ? ' (not installed)' : ''),
|
|
26263
|
+
description: t.description || '',
|
|
26264
|
+
resource: { name: t.name, description: t.description || '' },
|
|
26265
|
+
};
|
|
26266
|
+
});
|
|
26267
|
+
_renderQuickAddPicker(panel, kind, rows2);
|
|
26268
|
+
} else if (kind === 'project') {
|
|
26269
|
+
var r3 = await apiFetch('/api/projects');
|
|
26270
|
+
var d3 = await r3.json();
|
|
26271
|
+
var projects = (d3 && d3.projects) || [];
|
|
26272
|
+
if (projects.length === 0) {
|
|
26273
|
+
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>';
|
|
26274
|
+
return;
|
|
26275
|
+
}
|
|
26276
|
+
var rows3 = projects.map(function(p) {
|
|
26277
|
+
return {
|
|
26278
|
+
label: p.name || p.path,
|
|
26279
|
+
description: p.path || '',
|
|
26280
|
+
resource: { name: p.name || p.path, path: p.path },
|
|
26281
|
+
};
|
|
26282
|
+
});
|
|
26283
|
+
_renderQuickAddPicker(panel, kind, rows3);
|
|
26284
|
+
}
|
|
26285
|
+
} catch (e) {
|
|
26286
|
+
panel.innerHTML = '<div style="font-size:11px;color:var(--red);padding:4px 0">Failed to load: ' + esc(String(e)) + '</div>';
|
|
26287
|
+
}
|
|
26288
|
+
}
|
|
26289
|
+
|
|
26290
|
+
function _renderQuickAddPicker(panel, kind, rows) {
|
|
26291
|
+
if (!rows || rows.length === 0) {
|
|
26292
|
+
panel.innerHTML = '<div style="font-size:11px;color:var(--text-muted);padding:4px 0">Nothing to pick.</div>';
|
|
26293
|
+
return;
|
|
26294
|
+
}
|
|
26295
|
+
// Build the picker. Each row is a button that calls addQuickStep
|
|
26296
|
+
// with the resource and then closes the picker.
|
|
26297
|
+
var parts = [];
|
|
26298
|
+
for (var i = 0; i < rows.length; i++) {
|
|
26299
|
+
var r = rows[i];
|
|
26300
|
+
var rid = '_qap_' + kind + '_' + i;
|
|
26301
|
+
parts.push(
|
|
26302
|
+
'<button type="button" id="' + rid + '" data-idx="' + i + '" ' +
|
|
26303
|
+
'style="display:block;width:100%;text-align:left;background:transparent;border:0;padding:4px 6px;' +
|
|
26304
|
+
'border-radius:4px;cursor:pointer;font-size:11px;color:var(--text-primary)" ' +
|
|
26305
|
+
'onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" ' +
|
|
26306
|
+
'onmouseout="this.style.background=\\x27transparent\\x27">' +
|
|
26307
|
+
'<div style="font-weight:500">' + esc(r.label) + '</div>' +
|
|
26308
|
+
(r.description ? '<div style="font-size:10px;color:var(--text-muted)">' + esc(r.description) + '</div>' : '') +
|
|
26309
|
+
'</button>'
|
|
26310
|
+
);
|
|
26311
|
+
}
|
|
26312
|
+
panel.innerHTML = parts.join('');
|
|
26313
|
+
// Wire click handlers — each button picks its row by index.
|
|
26314
|
+
for (var j = 0; j < rows.length; j++) {
|
|
26315
|
+
(function(idx) {
|
|
26316
|
+
var btn = document.getElementById('_qap_' + kind + '_' + idx);
|
|
26317
|
+
if (btn) btn.onclick = function() {
|
|
26318
|
+
addQuickStep(kind, rows[idx].resource);
|
|
26319
|
+
panel.style.display = 'none';
|
|
26320
|
+
panel.dataset.kind = '';
|
|
26321
|
+
panel.innerHTML = '';
|
|
26322
|
+
};
|
|
26323
|
+
})(j);
|
|
26324
|
+
}
|
|
26325
|
+
}
|
|
26326
|
+
|
|
26327
|
+
function addQuickStep(kind, resource) {
|
|
26328
|
+
if (!builderArtifact) return;
|
|
26329
|
+
var stepId = _nextStepId();
|
|
26330
|
+
var prevId = _previousStepId();
|
|
26331
|
+
var body = '';
|
|
26332
|
+
if (kind === 'mcp') {
|
|
26333
|
+
body = ' kind: mcp\\n' +
|
|
26334
|
+
' mcp:\\n' +
|
|
26335
|
+
' server: "' + _yamlEscape(resource.server) + '"\\n' +
|
|
26336
|
+
' tool: "' + _yamlEscape(resource.tool) + '"\\n' +
|
|
26337
|
+
' inputs: {}\\n';
|
|
26338
|
+
} else if (kind === 'cli') {
|
|
26339
|
+
body = ' kind: cli\\n' +
|
|
26340
|
+
' cli:\\n' +
|
|
26341
|
+
' cmd: "' + _yamlEscape(resource.name) + '"\\n' +
|
|
26342
|
+
' args: []\\n' +
|
|
26343
|
+
' timeoutMs: 30000\\n';
|
|
26344
|
+
} else if (kind === 'project') {
|
|
26345
|
+
body = ' kind: prompt\\n' +
|
|
26346
|
+
' workDir: "' + _yamlEscape(resource.path) + '"\\n';
|
|
26347
|
+
} else {
|
|
26348
|
+
return;
|
|
26349
|
+
}
|
|
26350
|
+
_appendStepYaml(stepId, prevId, body);
|
|
26351
|
+
// Re-render so the textarea picks up the new content.
|
|
26352
|
+
if (typeof renderBuilderPreview === 'function') {
|
|
26353
|
+
renderBuilderPreview(builderArtifact, 'workflow');
|
|
26354
|
+
}
|
|
26355
|
+
// Auto-fire chat so Clementine asks the right question.
|
|
26356
|
+
triggerStepChatPrompt(kind, stepId, resource);
|
|
26357
|
+
}
|
|
26358
|
+
|
|
26359
|
+
function triggerStepChatPrompt(kind, stepId, resource) {
|
|
26360
|
+
var msg = '';
|
|
26361
|
+
if (kind === 'mcp') {
|
|
26362
|
+
msg = '[STEP ADDED] ' + stepId + ' is an MCP call to ' +
|
|
26363
|
+
(resource.server || '') + '/' + (resource.tool || '') +
|
|
26364
|
+
(resource.description ? ' (' + resource.description + ')' : '') +
|
|
26365
|
+
'. Help me write the prompt for this step — what should it do, what inputs, what is the goal?';
|
|
26366
|
+
} else if (kind === 'cli') {
|
|
26367
|
+
msg = '[STEP ADDED] ' + stepId + ' runs the ' + (resource.name || '') + ' CLI' +
|
|
26368
|
+
(resource.description ? ' (' + resource.description + ')' : '') +
|
|
26369
|
+
'. What command/args should it run, and what should the prompt around it say?';
|
|
26370
|
+
} else if (kind === 'project') {
|
|
26371
|
+
msg = '[STEP ADDED] ' + stepId + ' runs in project ' + (resource.name || '') +
|
|
26372
|
+
' (' + (resource.path || '') + '). What should it do in that project?';
|
|
26373
|
+
}
|
|
26374
|
+
if (!msg) return;
|
|
26375
|
+
// Reuse the same send path the chat input uses (mirrors builderQuick).
|
|
26376
|
+
var input = document.getElementById('builder-input');
|
|
26377
|
+
if (input) {
|
|
26378
|
+
input.value = msg;
|
|
26379
|
+
if (typeof sendBuilderChat === 'function') sendBuilderChat();
|
|
26380
|
+
}
|
|
26381
|
+
}
|
|
26382
|
+
|
|
26153
26383
|
// ── Builder File Attachments ──────────────
|
|
26154
26384
|
var _builderAttachments = [];
|
|
26155
26385
|
|
|
@@ -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`;
|
package/dist/gateway/router.js
CHANGED
|
@@ -10,7 +10,7 @@ import pino from 'pino';
|
|
|
10
10
|
import { oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
|
|
11
11
|
import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
|
|
12
12
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
13
|
-
import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
13
|
+
import { MODELS, BUDGET, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
14
14
|
import { scanner } from '../security/scanner.js';
|
|
15
15
|
import { lanes } from './lanes.js';
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
@@ -1820,7 +1820,13 @@ export class Gateway {
|
|
|
1820
1820
|
// Builder cost knobs: Haiku is plenty for JSON drafting,
|
|
1821
1821
|
// tight budget, no tools surfaced in the system prompt.
|
|
1822
1822
|
const builderModel = isBuilderSession ? MODELS.haiku : effectiveModel;
|
|
1823
|
-
|
|
1823
|
+
// Builder stays tight ($0.10 — Haiku JSON drafting only).
|
|
1824
|
+
// Regular chat reads BUDGET.chat from config (env / clementine.json /
|
|
1825
|
+
// dashboard writes). 0 = uncapped — the runAgent layer omits the
|
|
1826
|
+
// SDK option entirely in that case.
|
|
1827
|
+
const chatBudget = isBuilderSession
|
|
1828
|
+
? 0.10
|
|
1829
|
+
: (BUDGET.chat > 0 ? BUDGET.chat : undefined);
|
|
1824
1830
|
const builderAllowedTools = isBuilderSession ? [] : undefined;
|
|
1825
1831
|
logger.info({
|
|
1826
1832
|
sessionKey: effectiveSessionKey,
|
|
@@ -1841,7 +1847,7 @@ export class Gateway {
|
|
|
1841
1847
|
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
1842
1848
|
...(builderModel ? { model: builderModel } : {}),
|
|
1843
1849
|
...(maxTurns ? { maxTurns } : {}),
|
|
1844
|
-
...(
|
|
1850
|
+
...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
|
|
1845
1851
|
...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
|
|
1846
1852
|
...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
|
|
1847
1853
|
...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
|