clementine-agent 1.18.55 → 1.18.57

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.
@@ -18,6 +18,7 @@ import cron from 'node-cron';
18
18
  import { TunnelManager } from './tunnel.js';
19
19
  import { AgentManager } from '../agent/agent-manager.js';
20
20
  import { discoverMcpServers, getClaudeIntegrations } from '../agent/mcp-bridge.js';
21
+ import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
21
22
  import { AGENTS_DIR, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
22
23
  import { parseTasks } from '../tools/shared.js';
23
24
  import { todayISO } from '../gateway/cron-scheduler.js';
@@ -8019,100 +8020,29 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8019
8020
  }
8020
8021
  });
8021
8022
  // ── Builder chat endpoint ──────────────────────────────────────────
8022
- // Track which builder sessions have received the full system prefix
8023
- const builderSessionInited = new Set();
8024
8023
  app.post('/api/builder/chat', async (req, res) => {
8025
8024
  const { message, artifactType, agentSlug, currentArtifact, attachments, linkedTools } = req.body;
8026
8025
  if (!message || typeof message !== 'string') {
8027
8026
  res.status(400).json({ error: 'message is required' });
8028
8027
  return;
8029
8028
  }
8030
- const type = artifactType || 'skill';
8031
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8032
- const isFirstMessage = !builderSessionInited.has(sessionKey);
8033
- // ── Artifact state (compact JSON — no pretty-print to save tokens) ──
8034
- const artifactContext = currentArtifact
8035
- ? `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`
8036
- : '';
8037
- // ── File attachments — decode base64 text files and inject contents ──
8038
- let fileContext = '';
8039
- if (Array.isArray(attachments) && attachments.length > 0) {
8040
- const fileParts = [];
8041
- for (const att of attachments) {
8042
- if (att.filename && att.content) {
8043
- try {
8044
- const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
8045
- // Cap each file at 4K chars to keep context reasonable
8046
- const trimmed = decoded.length > 4000 ? decoded.slice(0, 4000) + '\n... (truncated)' : decoded;
8047
- fileParts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
8048
- }
8049
- catch { /* skip binary files */ }
8050
- }
8051
- }
8052
- if (fileParts.length > 0) {
8053
- fileContext = `\n[REFERENCE FILES — the user attached these for context]\n${fileParts.join('\n\n')}\n`;
8054
- }
8055
- }
8056
- // ── Linked tools context ──
8057
- let toolContext = '';
8058
- if (Array.isArray(linkedTools) && linkedTools.length > 0) {
8059
- toolContext = `\n[LINKED TOOLS — this skill should use these tools: ${linkedTools.join(', ')}]\n`;
8060
- }
8061
- // ── Build the enriched message ──
8062
- let enrichedMessage;
8063
- if (isFirstMessage) {
8064
- // Full system prefix on first message only
8065
- const agentContext = agentSlug ? `You are building this for the agent "${agentSlug}". The skill/cron will be scoped to this agent specifically.\n` : '';
8066
- const builderPrefix = type === 'skill'
8067
- ? `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
8068
- '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
8069
- `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
8070
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8071
- : type === 'cron'
8072
- ? `[BUILDER MODE: You are helping build a scheduled cron job. As you develop the job, output the current state as a JSON block:\n` +
8073
- '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","mode":"standard","enabled":true}\n```\n' +
8074
- `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, what tier (1=read-only, 2=read-write), and whether it should run in unleashed mode.\n` +
8075
- `IMPORTANT: Cron jobs automatically pull in matching skills (learned procedures) at runtime. If the user describes a workflow that should be reusable, suggest creating it as a skill first, then building the cron job that references those trigger keywords. This way the cron gets smarter over time as skills improve.\n` +
8076
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8077
- : type === 'agent'
8078
- ? `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
8079
- '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
8080
- `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use (haiku/sonnet/opus), its personality/system prompt, which channel it should operate in, and its security tier.\n` +
8081
- `Help the user think about what makes a good agent: clear role, specific tools, focused personality. Keep it conversational — one question at a time.\n` +
8082
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8083
- : type === 'workflow'
8084
- ? `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
8085
- `\n` +
8086
- `Hard rules:\n` +
8087
- ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
8088
- ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
8089
- ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
8090
- `\n` +
8091
- `As you develop the trick, output the current state as a JSON block:\n` +
8092
- '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
8093
- `Ask about (in roughly this order, one at a time):\n` +
8094
- ` 1. The goal (one sentence is fine — confirm it back).\n` +
8095
- ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
8096
- ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
8097
- ` 4. Which model — claude-opus-4-7 (most capable), claude-sonnet-4-6 (balanced), or claude-haiku-4-5-20251001 (fastest). Leave model empty if the user doesn't care.\n` +
8098
- `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
8099
- `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`
8100
- : `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
8101
- enrichedMessage = builderPrefix + fileContext + toolContext + artifactContext + message;
8102
- builderSessionInited.add(sessionKey);
8103
- }
8104
- else {
8105
- // Subsequent messages: just artifact state + files + tools + user message (no repeated prefix)
8106
- enrichedMessage = fileContext + toolContext + artifactContext + message;
8107
- }
8029
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8108
8030
  try {
8109
8031
  const gateway = await getGateway();
8110
- // Builder generates JSON artifacts no tool calls. Pin the session
8111
- // toolset to 'none' so buildOptions strips all MCP servers and tool
8112
- // schemas from the system prompt. Without this, every tiny builder
8113
- // turn writes 60–280 KB of cache_creation for tool schemas the
8114
- // model never uses.
8115
- gateway.setSessionToolset(sessionKey, 'none');
8032
+ // First-message detection uses the SDK session ID set after the
8033
+ // first runAgent turn returns, persisted across daemon restarts.
8034
+ // Anchoring on this means we send the system prefix exactly once
8035
+ // per genuine new conversation, not once per process lifetime.
8036
+ const isFirstMessage = !gateway.assistant.getSdkSessionId(sessionKey);
8037
+ const enrichedMessage = buildBuilderEnrichedMessage({
8038
+ message,
8039
+ artifactType,
8040
+ agentSlug,
8041
+ currentArtifact,
8042
+ attachments,
8043
+ linkedTools,
8044
+ isFirstMessage,
8045
+ });
8116
8046
  const response = await gateway.handleMessage(sessionKey, enrichedMessage);
8117
8047
  // Parse any json-artifact blocks from the response
8118
8048
  let artifact = null;
@@ -8165,84 +8095,20 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8165
8095
  // before the gateway warms up (otherwise some HTTP intermediaries hold
8166
8096
  // the response until first body byte).
8167
8097
  writeEvent('progress', { status: 'connecting…' });
8168
- // ── Same enrichment as /api/builder/chat (system prefix on first turn,
8169
- // artifact + files + tools on every turn). Inlined to keep the diff
8170
- // contained; refactor into a helper if a third endpoint shows up.
8171
- const type = artifactType || 'skill';
8172
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8173
- const isFirstMessage = !builderSessionInited.has(sessionKey);
8174
- const artifactContext = currentArtifact
8175
- ? `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`
8176
- : '';
8177
- let fileContext = '';
8178
- if (Array.isArray(attachments) && attachments.length > 0) {
8179
- const fileParts = [];
8180
- for (const att of attachments) {
8181
- if (att.filename && att.content) {
8182
- try {
8183
- const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
8184
- const trimmed = decoded.length > 4000 ? decoded.slice(0, 4000) + '\n... (truncated)' : decoded;
8185
- fileParts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
8186
- }
8187
- catch { /* skip binary files */ }
8188
- }
8189
- }
8190
- if (fileParts.length > 0) {
8191
- fileContext = `\n[REFERENCE FILES — the user attached these for context]\n${fileParts.join('\n\n')}\n`;
8192
- }
8193
- }
8194
- let toolContext = '';
8195
- if (Array.isArray(linkedTools) && linkedTools.length > 0) {
8196
- toolContext = `\n[LINKED TOOLS — this skill should use these tools: ${linkedTools.join(', ')}]\n`;
8197
- }
8198
- let enrichedMessage;
8199
- if (isFirstMessage) {
8200
- const agentContext = agentSlug ? `You are building this for the agent "${agentSlug}". The skill/cron will be scoped to this agent specifically.\n` : '';
8201
- const builderPrefix = type === 'skill'
8202
- ? `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
8203
- '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
8204
- `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
8205
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8206
- : type === 'cron'
8207
- ? `[BUILDER MODE: You are helping build a scheduled cron job. As you develop the job, output the current state as a JSON block:\n` +
8208
- '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","mode":"standard","enabled":true}\n```\n' +
8209
- `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, what tier (1=read-only, 2=read-write), and whether it should run in unleashed mode.\n` +
8210
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8211
- : type === 'agent'
8212
- ? `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
8213
- '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
8214
- `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use, its personality/system prompt, which channel it should operate in, and its security tier.\n` +
8215
- `Keep it conversational — one question at a time. When the user says "save" or approves, output the final artifact block.]\n\n`
8216
- : type === 'workflow'
8217
- ? `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
8218
- `\n` +
8219
- `Hard rules:\n` +
8220
- ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
8221
- ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
8222
- ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
8223
- `\n` +
8224
- `As you develop the trick, output the current state as a JSON block:\n` +
8225
- '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
8226
- `Ask about (in roughly this order, one at a time):\n` +
8227
- ` 1. The goal (one sentence is fine — confirm it back).\n` +
8228
- ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
8229
- ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
8230
- ` 4. Which model — claude-opus-4-7 (most capable), claude-sonnet-4-6 (balanced), or claude-haiku-4-5-20251001 (fastest). Leave model empty if the user doesn't care.\n` +
8231
- `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
8232
- `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`
8233
- : `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
8234
- enrichedMessage = builderPrefix + fileContext + toolContext + artifactContext + message;
8235
- builderSessionInited.add(sessionKey);
8236
- }
8237
- else {
8238
- enrichedMessage = fileContext + toolContext + artifactContext + message;
8239
- }
8098
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8240
8099
  try {
8241
8100
  writeEvent('progress', { status: 'thinking…' });
8242
8101
  const gateway = await getGateway();
8243
- // Builder generates JSON artifacts — no tool calls. Pin to 'none'
8244
- // toolset so the SDK system prompt drops the tool inventory.
8245
- gateway.setSessionToolset(sessionKey, 'none');
8102
+ const isFirstMessage = !gateway.assistant.getSdkSessionId(sessionKey);
8103
+ const enrichedMessage = buildBuilderEnrichedMessage({
8104
+ message,
8105
+ artifactType,
8106
+ agentSlug,
8107
+ currentArtifact,
8108
+ attachments,
8109
+ linkedTools,
8110
+ isFirstMessage,
8111
+ });
8246
8112
  let lastText = '';
8247
8113
  const response = await gateway.handleMessage(sessionKey, enrichedMessage, async (text) => {
8248
8114
  lastText = text ?? '';
@@ -8277,11 +8143,14 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8277
8143
  }
8278
8144
  });
8279
8145
  // Reset builder session when user clicks "New"
8280
- app.post('/api/builder/reset', (_req, res) => {
8146
+ app.post('/api/builder/reset', async (_req, res) => {
8281
8147
  const { artifactType, agentSlug } = _req.body;
8282
- const type = artifactType || 'skill';
8283
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8284
- builderSessionInited.delete(sessionKey);
8148
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8149
+ // Clearing the SDK session ID is what makes the next turn detect
8150
+ // as "first" and re-emit the system prefix. The per-process Set
8151
+ // has been replaced by the persisted SDK session map.
8152
+ const gateway = await getGateway();
8153
+ gateway.assistant.clearSession(sessionKey);
8285
8154
  res.json({ ok: true });
8286
8155
  });
8287
8156
  // Test a skill by sending a trigger message through the gateway with skill context
@@ -26037,10 +25906,10 @@ function renderBuilderPreview(artifact, type) {
26037
25906
  + '<option value="2"' + (artifact.tier === 2 ? ' selected' : '') + '>2 — Read+Write</option>'
26038
25907
  + '<option value="3"' + (artifact.tier === 3 ? ' selected' : '') + '>3 — Full</option>'
26039
25908
  + '</select></div>'
26040
- + '<div class="preview-field"><label>Mode</label><select onchange="builderArtifact.mode=this.value">'
26041
- + '<option value="standard"' + (artifact.mode !== 'unleashed' ? ' selected' : '') + '>Standard</option>'
26042
- + '<option value="unleashed"' + (artifact.mode === 'unleashed' ? ' selected' : '') + '>Unleashed</option>'
26043
- + '</select></div>'
25909
+ + '<div class="preview-field"><label>Linked Tools</label>'
25910
+ + '<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>'
25911
+ + '<div style="font-size:10px;color:var(--text-muted)">Pick tools the cron should use. The chat sees these as a hint, and they steer how the prompt is written.</div>'
25912
+ + '</div>'
26044
25913
  + '<div class="preview-field"><label>Prompt</label><textarea rows="12" onchange="builderArtifact.prompt=this.value">' + esc(artifact.prompt || '') + '</textarea></div>'
26045
25914
  + '<div class="preview-field"><label>Reference Files</label>'
26046
25915
  + '<div id="builder-attachments-list"></div>'
@@ -26050,6 +25919,7 @@ function renderBuilderPreview(artifact, type) {
26050
25919
  + '</label>'
26051
25920
  + '<div style="font-size:10px;color:var(--text-muted);margin-top:4px">Files injected into the agent prompt at runtime</div>'
26052
25921
  + '</div>';
25922
+ setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
26053
25923
  } else if (type === 'agent') {
26054
25924
  html = '<div class="preview-field"><label>Name</label><input type="text" value="' + esc(artifact.name || '') + '" onchange="builderArtifact.name=this.value"></div>'
26055
25925
  + '<div class="preview-field"><label>Description / Role</label><input type="text" value="' + esc(artifact.description || '') + '" onchange="builderArtifact.description=this.value"></div>'
@@ -26060,13 +25930,22 @@ function renderBuilderPreview(artifact, type) {
26060
25930
  + '<option value="opus"' + (artifact.model === 'opus' ? ' selected' : '') + '>Opus</option>'
26061
25931
  + '</select></div>'
26062
25932
  + '<div class="preview-field"><label>Personality / System Prompt</label><textarea rows="8" onchange="builderArtifact.personality=this.value">' + esc(artifact.personality || '') + '</textarea></div>'
26063
- + '<div class="preview-field"><label>Tools (comma-separated)</label><input type="text" value="' + esc((artifact.tools || []).join(', ')) + '" onchange="builderArtifact.tools=this.value.split(\\x27,\\x27).map(function(t){return t.trim()}).filter(Boolean)"></div>'
25933
+ + '<div class="preview-field"><label>Allowed Tools</label>'
25934
+ + '<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>'
25935
+ + '<div style="font-size:10px;color:var(--text-muted)">These become the agent\\x27s allowedTools — they\\x27re honored at the main-agent level when this agent runs as a profile.</div>'
25936
+ + '</div>'
26064
25937
  + '<div class="preview-field"><label>Channel</label><input type="text" value="' + esc(artifact.channel || '') + '" onchange="builderArtifact.channel=this.value" placeholder="e.g. discord, slack"></div>';
25938
+ setTimeout(function() { loadBuilderToolOptions(artifact.tools || _builderLinkedTools); }, 50);
26065
25939
  } else if (type === 'workflow') {
26066
25940
  html = '<div class="preview-field"><label>Workflow Name</label><input type="text" value="' + esc(artifact.name || '') + '" onchange="builderArtifact.name=this.value"></div>'
26067
25941
  + '<div class="preview-field"><label>Description</label><input type="text" value="' + esc(artifact.description || '') + '" onchange="builderArtifact.description=this.value"></div>'
26068
25942
  + '<div class="preview-field"><label>Trigger Schedule (cron, optional)</label><input type="text" value="' + esc(artifact.schedule || '') + '" onchange="builderArtifact.schedule=this.value" placeholder="e.g. 0 9 * * 1 (Mondays at 9am)"></div>'
25943
+ + '<div class="preview-field"><label>Linked Tools</label>'
25944
+ + '<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>'
25945
+ + '<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>'
25946
+ + '</div>'
26069
25947
  + '<div class="preview-field"><label>Steps (YAML/Markdown)</label><textarea rows="14" onchange="builderArtifact.steps=this.value">' + esc(artifact.steps || '') + '</textarea></div>';
25948
+ setTimeout(function() { loadBuilderToolOptions(artifact.toolsUsed || _builderLinkedTools); }, 50);
26070
25949
  }
26071
25950
  preview.innerHTML = html;
26072
25951
  // Load existing attachments if editing
@@ -26124,8 +26003,13 @@ async function saveBuilderArtifact() {
26124
26003
  var type = document.getElementById('builder-type').value;
26125
26004
  try {
26126
26005
  var agentSlug = (document.getElementById('builder-agent') || {}).value || '';
26127
- // Sync linked tools into artifact before saving
26128
- if (type === 'skill') builderArtifact.toolsUsed = _builderLinkedTools;
26006
+ // Sync linked tools into artifact before saving — picker is shared
26007
+ // across types but the persisted field name differs.
26008
+ if (type === 'agent') {
26009
+ builderArtifact.tools = _builderLinkedTools;
26010
+ } else {
26011
+ builderArtifact.toolsUsed = _builderLinkedTools;
26012
+ }
26129
26013
  var r = await apiJson('POST', '/api/builder/save', {
26130
26014
  artifactType: type,
26131
26015
  artifact: builderArtifact,
@@ -26212,8 +26096,18 @@ async function loadBuilderToolOptions(selectedTools) {
26212
26096
 
26213
26097
  function syncBuilderLinkedTools() {
26214
26098
  _builderLinkedTools = Array.from(document.querySelectorAll('.builder-tool-cb:checked')).map(function(cb) { return cb.value; });
26215
- // Also sync into the artifact so it saves with toolsUsed
26216
- if (builderArtifact) builderArtifact.toolsUsed = _builderLinkedTools;
26099
+ if (!builderArtifact) return;
26100
+ // Field name differs per artifact type: agent stores into "tools"
26101
+ // (which becomes profile.team.allowedTools on save). Skill, cron and
26102
+ // workflow store into "toolsUsed" — carried for reference, since the
26103
+ // runtime LINKED TOOLS context block is rebuilt from picker state
26104
+ // each turn.
26105
+ var type = (document.getElementById('builder-type') || {}).value;
26106
+ if (type === 'agent') {
26107
+ builderArtifact.tools = _builderLinkedTools;
26108
+ } else {
26109
+ builderArtifact.toolsUsed = _builderLinkedTools;
26110
+ }
26217
26111
  }
26218
26112
 
26219
26113
  // ── Builder File Attachments ──────────────
@@ -0,0 +1,36 @@
1
+ export type BuilderType = 'skill' | 'cron' | 'agent' | 'workflow';
2
+ export interface BuildBuilderMessageOptions {
3
+ /** What the user typed in the chat box. */
4
+ message: string;
5
+ /** Type of artifact being drafted. Defaults to 'skill'. */
6
+ artifactType?: string;
7
+ /** When set, the artifact is scoped to this hired agent. */
8
+ agentSlug?: string;
9
+ /** Current artifact JSON the dashboard is holding. Re-sent each
10
+ * turn so the agent has the live state in front of it. */
11
+ currentArtifact?: unknown;
12
+ /** Files the user dragged in for context. Each entry has a
13
+ * filename + base64-encoded content. Decoded + capped per file. */
14
+ attachments?: Array<{
15
+ filename?: string;
16
+ content?: string;
17
+ }>;
18
+ /** Pre-selected MCP / local tool names the artifact should use. */
19
+ linkedTools?: string[];
20
+ /** Whether this is the first turn of a new conversation (no prior
21
+ * SDK session). When true, the system prefix is included. */
22
+ isFirstMessage: boolean;
23
+ }
24
+ /**
25
+ * Compose the message that gets sent to gateway.handleMessage. The
26
+ * shape is: optional system prefix (first turn only), then file
27
+ * context, then linked-tools context, then current artifact state,
28
+ * then the user's literal message.
29
+ */
30
+ export declare function buildBuilderEnrichedMessage(opts: BuildBuilderMessageOptions): string;
31
+ /**
32
+ * Stable session-key for a builder conversation. Same key across
33
+ * /api/builder/chat and /api/builder/chat/stream so they share state.
34
+ */
35
+ export declare function builderSessionKey(artifactType: string | undefined, agentSlug: string | undefined): string;
36
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Build the enriched user message the trick builder sends to the
3
+ * canonical chat path. Single source of truth shared by both
4
+ * /api/builder/chat and /api/builder/chat/stream.
5
+ *
6
+ * Two non-obvious things:
7
+ *
8
+ * 1. "First message" detection uses the SDK session ID, not an
9
+ * in-memory Set. Daemon restart wipes in-memory state, so the
10
+ * prior implementation re-sent the system prefix on the very next
11
+ * turn even though SDK session resume kept the agent's memory of
12
+ * it. Anchoring detection to the persisted SDK session means
13
+ * the prefix is sent exactly once per genuine new conversation.
14
+ *
15
+ * 2. Model names referenced in the workflow prefix come from the
16
+ * MODELS config so they don't rot when models advance.
17
+ */
18
+ import { MODELS } from '../../config.js';
19
+ const FILE_MAX_CHARS = 4000;
20
+ function buildArtifactContext(currentArtifact) {
21
+ if (!currentArtifact)
22
+ return '';
23
+ return `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`;
24
+ }
25
+ function buildFileContext(attachments) {
26
+ if (!Array.isArray(attachments) || attachments.length === 0)
27
+ return '';
28
+ const parts = [];
29
+ for (const att of attachments) {
30
+ if (!att.filename || !att.content)
31
+ continue;
32
+ try {
33
+ const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
34
+ const trimmed = decoded.length > FILE_MAX_CHARS
35
+ ? decoded.slice(0, FILE_MAX_CHARS) + '\n... (truncated)'
36
+ : decoded;
37
+ parts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
38
+ }
39
+ catch {
40
+ // Binary file; skip.
41
+ }
42
+ }
43
+ if (parts.length === 0)
44
+ return '';
45
+ return `\n[REFERENCE FILES — the user attached these for context]\n${parts.join('\n\n')}\n`;
46
+ }
47
+ function buildToolContext(linkedTools) {
48
+ if (!Array.isArray(linkedTools) || linkedTools.length === 0)
49
+ return '';
50
+ return `\n[LINKED TOOLS — this artifact should use these tools: ${linkedTools.join(', ')}]\n`;
51
+ }
52
+ function buildSystemPrefix(type, agentSlug) {
53
+ const agentContext = agentSlug
54
+ ? `You are building this for the agent "${agentSlug}". The artifact will be scoped to this agent specifically.\n`
55
+ : '';
56
+ if (type === 'skill') {
57
+ return `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
58
+ '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
59
+ `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
60
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
61
+ }
62
+ if (type === 'cron') {
63
+ return `[BUILDER MODE: You are helping build a scheduled cron job. ${agentContext}As you develop the job, output the current state as a JSON block:\n` +
64
+ '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","enabled":true}\n```\n' +
65
+ `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, and what tier (1=read-only, 2=read-write). ` +
66
+ `Cron jobs automatically pull in matching skills (learned procedures) at runtime. If the user describes a workflow that should be reusable, suggest creating it as a skill first, then building the cron job that references those trigger keywords.\n` +
67
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
68
+ }
69
+ if (type === 'agent') {
70
+ return `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
71
+ '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
72
+ `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use (haiku/sonnet/opus), its personality/system prompt, which channel it should operate in, and its security tier.\n` +
73
+ `Help the user think about what makes a good agent: clear role, specific tools, focused personality. Keep it conversational — one question at a time.\n` +
74
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
75
+ }
76
+ if (type === 'workflow') {
77
+ return `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
78
+ `\n` +
79
+ `Hard rules:\n` +
80
+ ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
81
+ ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
82
+ ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
83
+ `\n` +
84
+ `As you develop the trick, output the current state as a JSON block:\n` +
85
+ '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
86
+ `Ask about (in roughly this order, one at a time):\n` +
87
+ ` 1. The goal (one sentence is fine — confirm it back).\n` +
88
+ ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
89
+ ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
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
+ `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
92
+ `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
+ return `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
95
+ }
96
+ /**
97
+ * Compose the message that gets sent to gateway.handleMessage. The
98
+ * shape is: optional system prefix (first turn only), then file
99
+ * context, then linked-tools context, then current artifact state,
100
+ * then the user's literal message.
101
+ */
102
+ export function buildBuilderEnrichedMessage(opts) {
103
+ const type = opts.artifactType || 'skill';
104
+ const fileContext = buildFileContext(opts.attachments);
105
+ const toolContext = buildToolContext(opts.linkedTools);
106
+ const artifactContext = buildArtifactContext(opts.currentArtifact);
107
+ const prefix = opts.isFirstMessage ? buildSystemPrefix(type, opts.agentSlug) : '';
108
+ return prefix + fileContext + toolContext + artifactContext + opts.message;
109
+ }
110
+ /**
111
+ * Stable session-key for a builder conversation. Same key across
112
+ * /api/builder/chat and /api/builder/chat/stream so they share state.
113
+ */
114
+ export function builderSessionKey(artifactType, agentSlug) {
115
+ const type = artifactType || 'skill';
116
+ return `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
117
+ }
118
+ //# sourceMappingURL=prompt.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.55",
3
+ "version": "1.18.57",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",