clementine-agent 1.18.91 → 1.18.92

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.
@@ -489,6 +489,34 @@ export async function runAgent(prompt, opts) {
489
489
  durationMs: Date.now() - startedAt,
490
490
  finalTextChars: finalText.length,
491
491
  }, 'runAgent: query complete');
492
+ // PRD §6 Phase 4e: subagent transcript backfill (Path C). The SDK persists
493
+ // every subagent's full message stream to ~/.claude/projects/<encoded-cwd>/
494
+ // <sessionId>/subagents/agent-*.jsonl. Path A only sees the parent's Task
495
+ // tool_use, so subagent-internal LLM/tool calls are invisible without this.
496
+ // Best-effort — telemetry must never block the run from returning.
497
+ try {
498
+ const { backfillSubagentEvents } = await import('./subagent-backfill.js');
499
+ const projectCwd = sdkOptionsRaw.cwd || BASE_DIR;
500
+ const backfillResult = await backfillSubagentEvents({
501
+ runId,
502
+ sessionId,
503
+ cwd: projectCwd,
504
+ eventLog,
505
+ startSeq: eventSeq,
506
+ });
507
+ eventSeq += backfillResult.backfilled;
508
+ if (backfillResult.backfilled > 0) {
509
+ logger.info({
510
+ runId,
511
+ sessionId,
512
+ backfilled: backfillResult.backfilled,
513
+ agents: backfillResult.agents,
514
+ }, 'runAgent: subagent backfill (Path C) complete');
515
+ }
516
+ }
517
+ catch (err) {
518
+ logger.debug({ err }, 'runAgent: subagent backfill failed (non-fatal)');
519
+ }
492
520
  return {
493
521
  text: finalText,
494
522
  totalCostUsd,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * PRD §6 / Phase 4e — Path C: subagent transcript backfill.
3
+ *
4
+ * The Claude Agent SDK persists every subagent's full message stream to
5
+ * ~/.claude/projects/<encoded-cwd>/<sessionId>/subagents/agent-*.jsonl
6
+ *
7
+ * The parent run's in-process tap (path A in `run-agent.ts`) only sees the
8
+ * top-level Task tool_use + tool_result, so subagent-internal LLM/tool calls
9
+ * are invisible in the Run detail waterfall. After the parent run ends, this
10
+ * module reads any matching agent-*.jsonl files for the run's sessionId and
11
+ * appends synthesized Event rows so the waterfall can render nested subagent
12
+ * activity.
13
+ *
14
+ * Best-effort by design — never throws back to the caller. Missing dir / parse
15
+ * errors / timing skew are all acceptable; the worst case is the parent run
16
+ * looks the same as before the backfill.
17
+ */
18
+ import type { EventLog } from '../gateway/event-log.js';
19
+ /**
20
+ * Encode a cwd path the way the SDK does for `~/.claude/projects/<encoded>`:
21
+ * every slash, backslash, and whitespace character becomes `-`. Confirmed
22
+ * against existing on-disk dirs (e.g. paths containing spaces and `..`).
23
+ */
24
+ export declare function encodeProjectCwd(cwd: string): string;
25
+ interface BackfillResult {
26
+ /** Number of synthesized RunEvent rows appended to the run's event log. */
27
+ backfilled: number;
28
+ /** Number of agent-*.jsonl files parsed. */
29
+ agents: number;
30
+ /** Resolved subagents directory (helpful in audit logs when nothing matches). */
31
+ scannedDir: string | null;
32
+ }
33
+ interface BackfillOpts {
34
+ runId: string;
35
+ sessionId: string;
36
+ cwd: string;
37
+ /** Pass the parent run's EventLog so we can append in-place. */
38
+ eventLog: EventLog;
39
+ /** Sequence number to start at. The caller already wrote N events; we
40
+ * continue from there to keep the file ordered. */
41
+ startSeq: number;
42
+ }
43
+ /**
44
+ * Scan ~/.claude/projects/<encoded(cwd)>/<sessionId>/subagents/agent-*.jsonl
45
+ * and append synthesized RunEvent rows to the parent run's event log.
46
+ *
47
+ * The function is fire-and-forget from runAgent's POV — it never rejects.
48
+ */
49
+ export declare function backfillSubagentEvents(opts: BackfillOpts): Promise<BackfillResult>;
50
+ export {};
51
+ //# sourceMappingURL=subagent-backfill.d.ts.map
@@ -0,0 +1,177 @@
1
+ /**
2
+ * PRD §6 / Phase 4e — Path C: subagent transcript backfill.
3
+ *
4
+ * The Claude Agent SDK persists every subagent's full message stream to
5
+ * ~/.claude/projects/<encoded-cwd>/<sessionId>/subagents/agent-*.jsonl
6
+ *
7
+ * The parent run's in-process tap (path A in `run-agent.ts`) only sees the
8
+ * top-level Task tool_use + tool_result, so subagent-internal LLM/tool calls
9
+ * are invisible in the Run detail waterfall. After the parent run ends, this
10
+ * module reads any matching agent-*.jsonl files for the run's sessionId and
11
+ * appends synthesized Event rows so the waterfall can render nested subagent
12
+ * activity.
13
+ *
14
+ * Best-effort by design — never throws back to the caller. Missing dir / parse
15
+ * errors / timing skew are all acceptable; the worst case is the parent run
16
+ * looks the same as before the backfill.
17
+ */
18
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import pino from 'pino';
22
+ const logger = pino({ name: 'clementine.subagent-backfill' });
23
+ /**
24
+ * Encode a cwd path the way the SDK does for `~/.claude/projects/<encoded>`:
25
+ * every slash, backslash, and whitespace character becomes `-`. Confirmed
26
+ * against existing on-disk dirs (e.g. paths containing spaces and `..`).
27
+ */
28
+ export function encodeProjectCwd(cwd) {
29
+ return cwd.replace(/[/\\\s.]/g, '-');
30
+ }
31
+ /**
32
+ * Walk a single agent-*.jsonl file and synthesize RunEvent rows.
33
+ * Returns the events as a flat array; the caller appends them to the
34
+ * shared event log so we can offset their `seq` correctly.
35
+ */
36
+ function synthesizeFromFile(filePath, runId) {
37
+ const out = [];
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(filePath, 'utf-8');
41
+ }
42
+ catch (err) {
43
+ logger.debug({ err, filePath }, 'subagent-backfill: read failed');
44
+ return out;
45
+ }
46
+ const lines = raw.split('\n');
47
+ // The agentId in the filename (agent-<id>.jsonl) is the stable handle for
48
+ // this subagent; we tag every synthesized event with it so the waterfall
49
+ // can group them under one swimlane.
50
+ const baseName = path.basename(filePath, '.jsonl'); // "agent-a333f70"
51
+ const agentId = baseName.replace(/^agent-/, '');
52
+ for (const line of lines) {
53
+ if (!line.trim())
54
+ continue;
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(line);
58
+ }
59
+ catch {
60
+ // Tolerate the rare half-flushed line at EOF.
61
+ continue;
62
+ }
63
+ const ts = parsed.timestamp || new Date().toISOString();
64
+ const slug = parsed.slug;
65
+ const subagentId = parsed.agentId || agentId;
66
+ const blocks = parsed.message?.content;
67
+ if (!Array.isArray(blocks))
68
+ continue;
69
+ for (const block of blocks) {
70
+ // Build the same RunEvent kinds the parent run uses, but tagged with
71
+ // agentId/subagentSlug + source='backfill' so the Run detail viewer
72
+ // can render them in a nested swimlane. seq is filled in by the
73
+ // caller; common fields go through to match path A's shape.
74
+ const common = {
75
+ runId,
76
+ ts,
77
+ agentId: subagentId,
78
+ subagentSlug: slug,
79
+ source: 'backfill',
80
+ };
81
+ if (block.type === 'text' && block.text) {
82
+ out.push({ ...common, kind: 'llm_text', text: block.text, seq: -1 });
83
+ }
84
+ else if (block.type === 'thinking' && block.thinking) {
85
+ out.push({ ...common, kind: 'thinking', thinking: block.thinking, seq: -1 });
86
+ }
87
+ else if (block.type === 'tool_use') {
88
+ out.push({
89
+ ...common,
90
+ kind: 'tool_call',
91
+ toolName: block.name || 'unknown',
92
+ toolUseId: block.id,
93
+ toolInput: block.input,
94
+ seq: -1,
95
+ });
96
+ }
97
+ else if (block.type === 'tool_result') {
98
+ const previewSrc = typeof block.content === 'string'
99
+ ? block.content
100
+ : JSON.stringify(block.content ?? '');
101
+ out.push({
102
+ ...common,
103
+ kind: 'tool_result',
104
+ toolUseId: block.tool_use_id,
105
+ // truncate huge tool_results to keep event-log size sane
106
+ toolResult: previewSrc.slice(0, 4000),
107
+ ...(block.is_error ? { toolError: previewSrc.slice(0, 1000) } : {}),
108
+ seq: -1,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * Scan ~/.claude/projects/<encoded(cwd)>/<sessionId>/subagents/agent-*.jsonl
117
+ * and append synthesized RunEvent rows to the parent run's event log.
118
+ *
119
+ * The function is fire-and-forget from runAgent's POV — it never rejects.
120
+ */
121
+ export async function backfillSubagentEvents(opts) {
122
+ const { runId, sessionId, cwd, eventLog, startSeq } = opts;
123
+ const result = { backfilled: 0, agents: 0, scannedDir: null };
124
+ if (!sessionId || !cwd)
125
+ return result;
126
+ let projectsRoot;
127
+ try {
128
+ projectsRoot = path.join(os.homedir(), '.claude', 'projects');
129
+ if (!existsSync(projectsRoot))
130
+ return result;
131
+ }
132
+ catch {
133
+ return result;
134
+ }
135
+ const encoded = encodeProjectCwd(cwd);
136
+ const subDir = path.join(projectsRoot, encoded, sessionId, 'subagents');
137
+ result.scannedDir = subDir;
138
+ if (!existsSync(subDir))
139
+ return result;
140
+ let files;
141
+ try {
142
+ files = readdirSync(subDir).filter((f) => f.startsWith('agent-') && f.endsWith('.jsonl'));
143
+ }
144
+ catch (err) {
145
+ logger.debug({ err, subDir }, 'subagent-backfill: readdir failed');
146
+ return result;
147
+ }
148
+ if (files.length === 0)
149
+ return result;
150
+ // Aggregate across all subagent files, then sort by ts so the waterfall
151
+ // renders in chronological order regardless of which agent file we read first.
152
+ const all = [];
153
+ for (const f of files) {
154
+ const fp = path.join(subDir, f);
155
+ const synthesized = synthesizeFromFile(fp, runId);
156
+ if (synthesized.length > 0) {
157
+ all.push(...synthesized);
158
+ result.agents += 1;
159
+ }
160
+ }
161
+ all.sort((a, b) => {
162
+ const ta = a.ts ?? '';
163
+ const tb = b.ts ?? '';
164
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
165
+ });
166
+ // Stamp seq + append. EventLog.append swallows its own errors so we just
167
+ // call it in a loop. The starting seq comes from the caller (the parent
168
+ // run's last writeEvent counter) so backfill rows sort after live rows.
169
+ let seq = startSeq;
170
+ for (const ev of all) {
171
+ ev.seq = seq++;
172
+ eventLog.append(ev);
173
+ result.backfilled += 1;
174
+ }
175
+ return result;
176
+ }
177
+ //# sourceMappingURL=subagent-backfill.js.map
@@ -5925,10 +5925,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5925
5925
  }
5926
5926
  });
5927
5927
  // ── Available Tools ──────────────────────────────────────────
5928
- app.get('/api/available-tools', (_req, res) => {
5928
+ app.get('/api/available-tools', async (_req, res) => {
5929
5929
  try {
5930
- const data = cached('available-tools', 30_000, () => {
5930
+ const data = await cachedAsync('available-tools', 60_000, async () => {
5931
5931
  const apiStatus = getApiConnectionStatus();
5932
+ let composioError = null;
5932
5933
  const categories = {
5933
5934
  'CLI Tools': discoverCliTools(),
5934
5935
  'Core SDK': [
@@ -6016,19 +6017,92 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
6016
6017
  if (globalMcp.length > 0) {
6017
6018
  categories['Global MCP Servers'] = globalMcp;
6018
6019
  }
6019
- // Discover project-scoped MCP servers
6020
- const projectMcp = {};
6020
+ // Local Projects: surface every linked project as a single togglable
6021
+ // entry so an agent can be granted access to a whole project (and the
6022
+ // dashboard can render the project's MCP servers as nested children).
6023
+ // Source of truth is `cachedProjects`, populated by the background
6024
+ // workspace scanner.
6021
6025
  const projects = cachedProjects ?? [];
6026
+ if (projects.length > 0) {
6027
+ categories['Local Projects'] = projects.map((p) => {
6028
+ const exists = (() => {
6029
+ try {
6030
+ return existsSync(p.path);
6031
+ }
6032
+ catch {
6033
+ return false;
6034
+ }
6035
+ })();
6036
+ return {
6037
+ name: `project:${p.path}`,
6038
+ description: p.description || p.path,
6039
+ type: 'project',
6040
+ path: p.path,
6041
+ projectName: p.name,
6042
+ connected: exists,
6043
+ mcpServers: Array.isArray(p.mcpServers) ? p.mcpServers : [],
6044
+ hasClaude: !!p.hasClaude,
6045
+ };
6046
+ });
6047
+ }
6048
+ // Project-scoped MCP servers — flatten what was previously a sibling
6049
+ // `projectMcp` map (which the renderer ignored) into one category per
6050
+ // server, so they show up alongside everything else in the picker.
6051
+ const projectMcpByServer = {};
6022
6052
  for (const p of projects) {
6023
6053
  if (p.mcpServers.length) {
6024
6054
  for (const server of p.mcpServers) {
6025
- if (!projectMcp[server])
6026
- projectMcp[server] = [];
6027
- projectMcp[server].push({ name: `mcp__${server} (all tools)`, description: `Project MCP: ${p.name}`, type: 'project-mcp' });
6055
+ if (!projectMcpByServer[server])
6056
+ projectMcpByServer[server] = [];
6057
+ projectMcpByServer[server].push({
6058
+ name: `mcp__${server}`,
6059
+ description: `Project MCP from ${p.name}`,
6060
+ type: 'project-mcp',
6061
+ connected: true,
6062
+ });
6028
6063
  }
6029
6064
  }
6030
6065
  }
6031
- return { categories, projectMcp };
6066
+ for (const [server, entries] of Object.entries(projectMcpByServer)) {
6067
+ // De-dup if the same server appears in multiple projects.
6068
+ const seen = new Set();
6069
+ const deduped = entries.filter((e) => {
6070
+ if (seen.has(e.name))
6071
+ return false;
6072
+ seen.add(e.name);
6073
+ return true;
6074
+ });
6075
+ categories[`Project MCP — ${server}`] = deduped;
6076
+ }
6077
+ // Composio Toolkits: 1000+ third-party services. We surface every
6078
+ // toolkit so the agent owner can opt in; the `connected` pill tells
6079
+ // them whether OAuth is wired up. Failures are non-fatal — we just
6080
+ // omit the category and stash the error for the client.
6081
+ try {
6082
+ const composio = await import('../integrations/composio/client.js');
6083
+ if (composio.isComposioEnabled()) {
6084
+ const [catalog, connected] = await Promise.all([
6085
+ composio.listAllToolkits(),
6086
+ composio.listConnectedToolkits(),
6087
+ ]);
6088
+ const connectedSet = new Set(connected.map((c) => c.slug));
6089
+ const toolkits = catalog.map((tk) => ({
6090
+ name: `composio:${tk.slug}`,
6091
+ description: tk.description || tk.name || tk.slug,
6092
+ type: 'composio',
6093
+ connected: connectedSet.has(tk.slug),
6094
+ displayName: tk.name || tk.slug,
6095
+ }));
6096
+ if (toolkits.length > 0) {
6097
+ categories['Composio Toolkits'] = toolkits;
6098
+ }
6099
+ }
6100
+ }
6101
+ catch (err) {
6102
+ composioError = err?.message ?? String(err);
6103
+ console.error('[available-tools] composio fetch failed:', composioError);
6104
+ }
6105
+ return { categories, composioError };
6032
6106
  });
6033
6107
  res.json(data);
6034
6108
  }
@@ -24816,11 +24890,21 @@ function renderRunDetailWaterfall(events, runId, jobName) {
24816
24890
 
24817
24891
  var rowId = 'run-evt-' + j;
24818
24892
  var canExpand = !!fullContent && fullContent.length > preview.length;
24819
- html += '<div style="display:grid;grid-template-columns:90px 110px 1fr;gap:14px;padding:10px 20px;border-bottom:1px solid var(--border);align-items:start">';
24893
+ // PRD §6 Phase 4e: backfilled subagent events get an indented row + a
24894
+ // pill showing the subagent slug, so the waterfall makes it obvious
24895
+ // which spans came from a delegated agent vs the parent task.
24896
+ var isSubagent = ev.source === 'backfill' || !!ev.subagentSlug;
24897
+ var rowBg = isSubagent ? 'background:var(--bg-secondary);' : '';
24898
+ var rowPad = isSubagent ? 'padding:10px 20px 10px 48px;' : 'padding:10px 20px;';
24899
+ var subagentBadge = isSubagent
24900
+ ? '<span title="Backfilled from subagent transcript" style="display:inline-block;background:var(--purple,#8b5cf6)20;color:var(--purple,#8b5cf6);padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500;margin-right:6px">↳ ' + esc(ev.subagentSlug || ev.agentId || 'subagent') + '</span>'
24901
+ : '';
24902
+ html += '<div style="display:grid;grid-template-columns:90px 110px 1fr;gap:14px;' + rowPad + 'border-bottom:1px solid var(--border);align-items:start;' + rowBg + '">';
24820
24903
  html += '<div style="font-size:10px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;line-height:18px">' + esc(offsetLabel) + '</div>';
24821
24904
  html += '<div><span style="display:inline-block;background:' + color + '20;color:' + color + ';padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:0.04em">' + esc(label) + '</span></div>';
24822
24905
  html += '<div style="min-width:0">';
24823
24906
  html += '<div style="font-size:12px;color:var(--text-primary);line-height:1.45;word-break:break-word">'
24907
+ + subagentBadge
24824
24908
  + esc(preview)
24825
24909
  + (pairedDuration ? '<span style="color:var(--text-muted);font-size:11px"> ' + esc(pairedDuration) + '</span>' : '')
24826
24910
  + '</div>';
@@ -44,6 +44,25 @@ export declare const clementineJsonSchema: z.ZodObject<{
44
44
  ask_first: "ask_first";
45
45
  act_when_safe: "act_when_safe";
46
46
  }>>;
47
+ profile: z.ZodOptional<z.ZodObject<{
48
+ systemPrompt: z.ZodOptional<z.ZodString>;
49
+ model: z.ZodOptional<z.ZodString>;
50
+ allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
51
+ allowedProjects: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
+ allowedUsers: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
+ channels: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ budgetMonthlyCents: z.ZodOptional<z.ZodNumber>;
55
+ goalSlugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
+ sendPolicy: z.ZodOptional<z.ZodObject<{
57
+ maxDailyEmails: z.ZodOptional<z.ZodNumber>;
58
+ requiresApproval: z.ZodOptional<z.ZodEnum<{
59
+ none: "none";
60
+ "first-in-sequence": "first-in-sequence";
61
+ all: "all";
62
+ }>>;
63
+ businessHoursOnly: z.ZodOptional<z.ZodBoolean>;
64
+ }, z.core.$strip>>;
65
+ }, z.core.$strip>>;
47
66
  }, z.core.$strip>>;
48
67
  models: z.ZodOptional<z.ZodObject<{
49
68
  default: z.ZodOptional<z.ZodString>;
@@ -32,6 +32,28 @@ export const clementineJsonSchema = z.object({
32
32
  responseStyle: z.enum(['concise', 'balanced', 'detailed']).optional(),
33
33
  progressVisibility: z.enum(['quiet', 'normal', 'detailed']).optional(),
34
34
  autonomy: z.enum(['ask_first', 'balanced', 'act_when_safe']).optional(),
35
+ /**
36
+ * Dashboard-managed profile for the main agent (Clementine herself).
37
+ * Mirrors the per-agent edit surface so Tasks → Team can edit the
38
+ * primary persona without forcing users into Settings or env files.
39
+ * Every field is optional; absent fields fall through to the existing
40
+ * env / compiled defaults via computeEffectiveConfig.
41
+ */
42
+ profile: z.object({
43
+ systemPrompt: z.string().optional(),
44
+ model: z.string().optional(),
45
+ allowedTools: z.array(z.string()).optional(),
46
+ allowedProjects: z.array(z.string()).optional(),
47
+ allowedUsers: z.array(z.string()).optional(),
48
+ channels: z.array(z.string()).optional(),
49
+ budgetMonthlyCents: z.number().nonnegative().optional(),
50
+ goalSlugs: z.array(z.string()).optional(),
51
+ sendPolicy: z.object({
52
+ maxDailyEmails: z.number().nonnegative().optional(),
53
+ requiresApproval: z.enum(['none', 'first-in-sequence', 'all']).optional(),
54
+ businessHoursOnly: z.boolean().optional(),
55
+ }).optional(),
56
+ }).optional(),
35
57
  }).optional(),
36
58
  models: z.object({
37
59
  default: z.string().optional(),
package/dist/types.d.ts CHANGED
@@ -445,8 +445,18 @@ export interface RunEvent {
445
445
  costUsd?: number;
446
446
  /** Stop reason from ResultMessage when kind='session_end'. */
447
447
  stopReason?: string;
448
- /** Subagent id when kind='subagent_*'. */
448
+ /** Subagent id when kind='subagent_*' OR when an event was synthesized from
449
+ * a subagent transcript via Path C backfill (subagent-backfill.ts). */
449
450
  agentId?: string;
451
+ /** PRD §6 Phase 4e: subagent slug ("bright-petting-kahn") for friendly
452
+ * display in the Run detail waterfall. Only populated for events
453
+ * synthesized from subagent transcripts. */
454
+ subagentSlug?: string;
455
+ /** PRD §6 Phase 4e: marks events as backfilled from a subagent transcript
456
+ * rather than captured live by the in-process tap. The Run detail viewer
457
+ * renders these in a nested swimlane and labels the source so users know
458
+ * the data came from disk after the run. */
459
+ source?: 'live' | 'backfill';
450
460
  }
451
461
  /**
452
462
  * PRD §9 / 1.18.87: 11-category failure taxonomy. Replaces the existing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.91",
3
+ "version": "1.18.92",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",