clementine-agent 1.18.91 → 1.18.93
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.
- package/dist/agent/agent-manager.js +6 -0
- package/dist/agent/run-agent.js +28 -0
- package/dist/agent/subagent-backfill.d.ts +51 -0
- package/dist/agent/subagent-backfill.js +177 -0
- package/dist/cli/dashboard.js +452 -10
- package/dist/config/clementine-json.d.ts +19 -0
- package/dist/config/clementine-json.js +22 -0
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
|
@@ -304,6 +304,8 @@ export class AgentManager {
|
|
|
304
304
|
frontmatter.allowedUsers = config.allowedUsers;
|
|
305
305
|
if (config.project)
|
|
306
306
|
frontmatter.project = config.project;
|
|
307
|
+
if (config.projects?.length)
|
|
308
|
+
frontmatter.projects = config.projects;
|
|
307
309
|
if (config.discordToken) {
|
|
308
310
|
storeAgentSecret(slug, 'DISCORD_TOKEN', config.discordToken);
|
|
309
311
|
frontmatter.discordToken = 'keychain';
|
|
@@ -415,6 +417,10 @@ export class AgentManager {
|
|
|
415
417
|
meta.allowedUsers = changes.allowedUsers;
|
|
416
418
|
if (changes.project !== undefined)
|
|
417
419
|
meta.project = changes.project;
|
|
420
|
+
if (changes.projects !== undefined) {
|
|
421
|
+
// Empty array clears the field; non-empty array replaces it.
|
|
422
|
+
meta.projects = changes.projects.length ? changes.projects : undefined;
|
|
423
|
+
}
|
|
418
424
|
if (changes.discordToken !== undefined) {
|
|
419
425
|
if (changes.discordToken) {
|
|
420
426
|
storeAgentSecret(slug, 'DISCORD_TOKEN', changes.discordToken);
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -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
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
6020
|
-
|
|
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 (!
|
|
6026
|
-
|
|
6027
|
-
|
|
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
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
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;
|
|
6028
6098
|
}
|
|
6029
6099
|
}
|
|
6030
6100
|
}
|
|
6031
|
-
|
|
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
|
}
|
|
@@ -7398,6 +7472,88 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7398
7472
|
res.status(500).json({ error: String(err) });
|
|
7399
7473
|
}
|
|
7400
7474
|
});
|
|
7475
|
+
// ── Clementine main-agent profile ──────────────────────────────────
|
|
7476
|
+
// Mirrors the per-agent edit surface (Tasks → Team → Edit) for
|
|
7477
|
+
// Clementine herself. Persists to clementine.json under
|
|
7478
|
+
// assistant.profile so existing readers (computeEffectiveConfig,
|
|
7479
|
+
// gateway initializers) can resolve the values via the standard
|
|
7480
|
+
// env > json > default chain.
|
|
7481
|
+
app.get('/api/clementine/profile', (_req, res) => {
|
|
7482
|
+
try {
|
|
7483
|
+
const json = loadClementineJson(BASE_DIR);
|
|
7484
|
+
const profile = json.assistant?.profile ?? {};
|
|
7485
|
+
const status = getApiConnectionStatus();
|
|
7486
|
+
res.json({
|
|
7487
|
+
name: json.assistantName || 'Clementine',
|
|
7488
|
+
profile: {
|
|
7489
|
+
systemPrompt: profile.systemPrompt ?? '',
|
|
7490
|
+
model: profile.model ?? '',
|
|
7491
|
+
allowedTools: profile.allowedTools ?? [],
|
|
7492
|
+
allowedProjects: profile.allowedProjects ?? [],
|
|
7493
|
+
allowedUsers: profile.allowedUsers ?? [],
|
|
7494
|
+
channels: profile.channels ?? [],
|
|
7495
|
+
budgetMonthlyCents: profile.budgetMonthlyCents ?? 0,
|
|
7496
|
+
goalSlugs: profile.goalSlugs ?? [],
|
|
7497
|
+
sendPolicy: profile.sendPolicy ?? null,
|
|
7498
|
+
},
|
|
7499
|
+
connectivity: status,
|
|
7500
|
+
});
|
|
7501
|
+
}
|
|
7502
|
+
catch (err) {
|
|
7503
|
+
res.status(500).json({ error: String(err) });
|
|
7504
|
+
}
|
|
7505
|
+
});
|
|
7506
|
+
app.put('/api/clementine/profile', (req, res) => {
|
|
7507
|
+
try {
|
|
7508
|
+
const body = (req.body ?? {});
|
|
7509
|
+
const profile = {};
|
|
7510
|
+
if (typeof body.systemPrompt === 'string')
|
|
7511
|
+
profile.systemPrompt = body.systemPrompt;
|
|
7512
|
+
if (typeof body.model === 'string' && body.model)
|
|
7513
|
+
profile.model = body.model;
|
|
7514
|
+
if (Array.isArray(body.allowedTools))
|
|
7515
|
+
profile.allowedTools = body.allowedTools.map(String);
|
|
7516
|
+
if (Array.isArray(body.allowedProjects))
|
|
7517
|
+
profile.allowedProjects = body.allowedProjects.map(String);
|
|
7518
|
+
if (Array.isArray(body.allowedUsers))
|
|
7519
|
+
profile.allowedUsers = body.allowedUsers.map(String);
|
|
7520
|
+
if (Array.isArray(body.channels))
|
|
7521
|
+
profile.channels = body.channels.map(String);
|
|
7522
|
+
if (Array.isArray(body.goalSlugs))
|
|
7523
|
+
profile.goalSlugs = body.goalSlugs.map(String);
|
|
7524
|
+
if (typeof body.budgetMonthlyCents === 'number' && body.budgetMonthlyCents >= 0) {
|
|
7525
|
+
profile.budgetMonthlyCents = body.budgetMonthlyCents;
|
|
7526
|
+
}
|
|
7527
|
+
if (body.sendPolicy && typeof body.sendPolicy === 'object') {
|
|
7528
|
+
const sp = body.sendPolicy;
|
|
7529
|
+
const cleaned = {};
|
|
7530
|
+
if (typeof sp.maxDailyEmails === 'number')
|
|
7531
|
+
cleaned.maxDailyEmails = sp.maxDailyEmails;
|
|
7532
|
+
if (typeof sp.requiresApproval === 'string'
|
|
7533
|
+
&& ['none', 'first-in-sequence', 'all'].includes(sp.requiresApproval)) {
|
|
7534
|
+
cleaned.requiresApproval = sp.requiresApproval;
|
|
7535
|
+
}
|
|
7536
|
+
if (typeof sp.businessHoursOnly === 'boolean')
|
|
7537
|
+
cleaned.businessHoursOnly = sp.businessHoursOnly;
|
|
7538
|
+
if (Object.keys(cleaned).length)
|
|
7539
|
+
profile.sendPolicy = cleaned;
|
|
7540
|
+
}
|
|
7541
|
+
const next = updateClementineJson(BASE_DIR, (current) => ({
|
|
7542
|
+
...current,
|
|
7543
|
+
assistant: {
|
|
7544
|
+
...(current.assistant ?? {}),
|
|
7545
|
+
profile: {
|
|
7546
|
+
...(current.assistant?.profile ?? {}),
|
|
7547
|
+
...profile,
|
|
7548
|
+
},
|
|
7549
|
+
},
|
|
7550
|
+
}));
|
|
7551
|
+
res.json({ ok: true, profile: next.assistant?.profile ?? {} });
|
|
7552
|
+
}
|
|
7553
|
+
catch (err) {
|
|
7554
|
+
res.status(400).json({ error: String(err) });
|
|
7555
|
+
}
|
|
7556
|
+
});
|
|
7401
7557
|
app.get('/api/budgets', async (_req, res) => {
|
|
7402
7558
|
try {
|
|
7403
7559
|
const [{ computeEffectiveConfig }, { runDoctor }] = await Promise.all([
|
|
@@ -10273,7 +10429,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10273
10429
|
try {
|
|
10274
10430
|
const gw = await getGateway();
|
|
10275
10431
|
const mgr = gw.getAgentManager();
|
|
10276
|
-
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10432
|
+
const { name, description, personality, tier, model, channelName, teamChat, respondToAll, canMessage, allowedTools, allowedUsers, project, projects, discordToken, discordChannelId, avatar, slackBotToken, slackAppToken, slackChannelId, sendPolicy, role } = req.body;
|
|
10277
10433
|
if (!name || !description) {
|
|
10278
10434
|
res.status(400).json({ error: 'name and description are required' });
|
|
10279
10435
|
return;
|
|
@@ -10289,6 +10445,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10289
10445
|
allowedTools: allowedTools || undefined,
|
|
10290
10446
|
allowedUsers: allowedUsers || undefined,
|
|
10291
10447
|
project: project || undefined,
|
|
10448
|
+
// `projects` is the multi-project access list — agent-manager already
|
|
10449
|
+
// persists it to agent.md frontmatter and the SDK reads it on load.
|
|
10450
|
+
projects: Array.isArray(projects) && projects.length ? projects : undefined,
|
|
10292
10451
|
discordToken: discordToken || undefined,
|
|
10293
10452
|
discordChannelId: discordChannelId || undefined,
|
|
10294
10453
|
avatar: avatar || undefined,
|
|
@@ -15291,6 +15450,131 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15291
15450
|
font-size: 11px;
|
|
15292
15451
|
color: var(--text-muted);
|
|
15293
15452
|
}
|
|
15453
|
+
/* PRD §12 / 1.18.93: three mini-dashboards beneath the Health Strip:
|
|
15454
|
+
Cost (7d sparkline), Latency split-bar, Reliability (failures stacked
|
|
15455
|
+
by category). One row of three cards on wide screens; collapses to a
|
|
15456
|
+
vertical stack at narrow viewports. */
|
|
15457
|
+
.mini-dashboards {
|
|
15458
|
+
display: grid;
|
|
15459
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
15460
|
+
gap: 12px;
|
|
15461
|
+
margin-bottom: 22px;
|
|
15462
|
+
}
|
|
15463
|
+
.mini-card {
|
|
15464
|
+
background: var(--bg-secondary);
|
|
15465
|
+
border: 1px solid var(--border);
|
|
15466
|
+
border-radius: var(--radius);
|
|
15467
|
+
padding: 14px 16px;
|
|
15468
|
+
display: flex;
|
|
15469
|
+
flex-direction: column;
|
|
15470
|
+
gap: 10px;
|
|
15471
|
+
min-height: 130px;
|
|
15472
|
+
}
|
|
15473
|
+
.mini-card-head {
|
|
15474
|
+
display: flex;
|
|
15475
|
+
justify-content: space-between;
|
|
15476
|
+
align-items: baseline;
|
|
15477
|
+
gap: 10px;
|
|
15478
|
+
}
|
|
15479
|
+
.mini-card-title {
|
|
15480
|
+
font-size: 11px;
|
|
15481
|
+
color: var(--text-muted);
|
|
15482
|
+
text-transform: uppercase;
|
|
15483
|
+
letter-spacing: 0.05em;
|
|
15484
|
+
font-weight: 500;
|
|
15485
|
+
}
|
|
15486
|
+
.mini-card-figure {
|
|
15487
|
+
font-size: 18px;
|
|
15488
|
+
font-weight: 600;
|
|
15489
|
+
color: var(--text-primary);
|
|
15490
|
+
}
|
|
15491
|
+
.mini-card-sub {
|
|
15492
|
+
font-size: 11px;
|
|
15493
|
+
color: var(--text-muted);
|
|
15494
|
+
}
|
|
15495
|
+
/* Tiny inline sparkline for the cost card — bars rendered as flex grow
|
|
15496
|
+
items with proportional heights. Pure CSS, no SVG needed. */
|
|
15497
|
+
.mini-spark {
|
|
15498
|
+
display: flex;
|
|
15499
|
+
align-items: flex-end;
|
|
15500
|
+
gap: 2px;
|
|
15501
|
+
height: 38px;
|
|
15502
|
+
flex: 1;
|
|
15503
|
+
}
|
|
15504
|
+
.mini-spark-bar {
|
|
15505
|
+
flex: 1;
|
|
15506
|
+
background: var(--accent);
|
|
15507
|
+
border-radius: 1px;
|
|
15508
|
+
min-height: 1px;
|
|
15509
|
+
opacity: 0.7;
|
|
15510
|
+
}
|
|
15511
|
+
.mini-spark-bar.zero {
|
|
15512
|
+
background: var(--border);
|
|
15513
|
+
opacity: 0.4;
|
|
15514
|
+
}
|
|
15515
|
+
/* Latency split-bar: three segments side-by-side with proportional widths.
|
|
15516
|
+
Hovering any segment shows its label inline. */
|
|
15517
|
+
.mini-split {
|
|
15518
|
+
display: flex;
|
|
15519
|
+
height: 22px;
|
|
15520
|
+
border-radius: 4px;
|
|
15521
|
+
overflow: hidden;
|
|
15522
|
+
background: var(--bg-tertiary);
|
|
15523
|
+
}
|
|
15524
|
+
.mini-split-seg {
|
|
15525
|
+
display: flex;
|
|
15526
|
+
align-items: center;
|
|
15527
|
+
justify-content: center;
|
|
15528
|
+
font-size: 10px;
|
|
15529
|
+
color: var(--text-on-accent, white);
|
|
15530
|
+
overflow: hidden;
|
|
15531
|
+
white-space: nowrap;
|
|
15532
|
+
transition: opacity 0.15s;
|
|
15533
|
+
}
|
|
15534
|
+
.mini-split-seg:hover { opacity: 0.85; }
|
|
15535
|
+
.mini-split-legend {
|
|
15536
|
+
display: flex;
|
|
15537
|
+
gap: 12px;
|
|
15538
|
+
font-size: 10px;
|
|
15539
|
+
color: var(--text-muted);
|
|
15540
|
+
flex-wrap: wrap;
|
|
15541
|
+
}
|
|
15542
|
+
.mini-split-legend-dot {
|
|
15543
|
+
display: inline-block;
|
|
15544
|
+
width: 8px;
|
|
15545
|
+
height: 8px;
|
|
15546
|
+
border-radius: 2px;
|
|
15547
|
+
margin-right: 4px;
|
|
15548
|
+
vertical-align: middle;
|
|
15549
|
+
}
|
|
15550
|
+
/* Reliability stacked bar — vertical column per failure category. */
|
|
15551
|
+
.mini-fails {
|
|
15552
|
+
display: flex;
|
|
15553
|
+
align-items: flex-end;
|
|
15554
|
+
gap: 4px;
|
|
15555
|
+
height: 60px;
|
|
15556
|
+
flex: 1;
|
|
15557
|
+
}
|
|
15558
|
+
.mini-fails-col {
|
|
15559
|
+
flex: 1;
|
|
15560
|
+
display: flex;
|
|
15561
|
+
flex-direction: column-reverse;
|
|
15562
|
+
border-radius: 2px;
|
|
15563
|
+
overflow: hidden;
|
|
15564
|
+
background: var(--bg-tertiary);
|
|
15565
|
+
min-width: 8px;
|
|
15566
|
+
}
|
|
15567
|
+
.mini-fails-seg {
|
|
15568
|
+
width: 100%;
|
|
15569
|
+
}
|
|
15570
|
+
.mini-fails-empty {
|
|
15571
|
+
flex: 1;
|
|
15572
|
+
display: flex;
|
|
15573
|
+
align-items: center;
|
|
15574
|
+
justify-content: center;
|
|
15575
|
+
color: var(--text-muted);
|
|
15576
|
+
font-size: 11px;
|
|
15577
|
+
}
|
|
15294
15578
|
/* PRD Phase 1.2: "Run task once" running-state pulse on the Last run tab. */
|
|
15295
15579
|
@keyframes pulse {
|
|
15296
15580
|
0%, 100% { opacity: 0.4; transform: scale(0.85); }
|
|
@@ -23962,6 +24246,146 @@ async function refreshHealthStrip() {
|
|
|
23962
24246
|
strip.innerHTML = html;
|
|
23963
24247
|
}
|
|
23964
24248
|
|
|
24249
|
+
// ── PRD §12 / 1.18.93: three mini-dashboards ───────────────────────────
|
|
24250
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
24251
|
+
// Reliability (failures stacked by category over the same window).
|
|
24252
|
+
// All client-side from /api/cron/runs — no new endpoints. Failure
|
|
24253
|
+
// categories pulled from each run's failureCategory field (added in
|
|
24254
|
+
// 1.18.87). Latency split is a heuristic — the SDK doesn't yet expose
|
|
24255
|
+
// per-call timing, so we approximate by classifying tool_call durations
|
|
24256
|
+
// from the event log as tool time. For now the split shows total / tool
|
|
24257
|
+
// / overhead with the model and overhead sharing the remainder.
|
|
24258
|
+
async function refreshMiniDashboards() {
|
|
24259
|
+
var host = document.getElementById('mini-dashboards');
|
|
24260
|
+
if (!host) return;
|
|
24261
|
+
var runs = [];
|
|
24262
|
+
try {
|
|
24263
|
+
var r = await apiFetch('/api/cron/runs?limit=500');
|
|
24264
|
+
var d = await r.json();
|
|
24265
|
+
runs = (d && d.runs) || [];
|
|
24266
|
+
} catch (e) { /* leave empty if fetch fails */ }
|
|
24267
|
+
|
|
24268
|
+
var now = Date.now();
|
|
24269
|
+
var window7d = now - 7 * 24 * 60 * 60 * 1000;
|
|
24270
|
+
var last7 = runs.filter(function(rn) { return rn.startedAt && new Date(rn.startedAt).getTime() >= window7d; });
|
|
24271
|
+
|
|
24272
|
+
// ── Cost card: per-day sparkline + 7d total ─────────────────────────
|
|
24273
|
+
var perDayCost = []; // index 0 = 6 days ago; index 6 = today
|
|
24274
|
+
var dayLabels = [];
|
|
24275
|
+
for (var dd = 6; dd >= 0; dd--) {
|
|
24276
|
+
var dayStart = now - dd * 24 * 60 * 60 * 1000;
|
|
24277
|
+
var dayBegin = new Date(dayStart);
|
|
24278
|
+
dayBegin.setHours(0, 0, 0, 0);
|
|
24279
|
+
dayLabels.push(dayBegin.toISOString().slice(5, 10));
|
|
24280
|
+
perDayCost.push(0);
|
|
24281
|
+
}
|
|
24282
|
+
var totalCost7 = 0;
|
|
24283
|
+
for (var i = 0; i < last7.length; i++) {
|
|
24284
|
+
if (typeof last7[i].totalCostUsd !== 'number') continue;
|
|
24285
|
+
var startedMs = new Date(last7[i].startedAt).getTime();
|
|
24286
|
+
var dayIdx = 6 - Math.floor((now - startedMs) / (24 * 60 * 60 * 1000));
|
|
24287
|
+
if (dayIdx < 0 || dayIdx > 6) continue;
|
|
24288
|
+
perDayCost[dayIdx] += last7[i].totalCostUsd;
|
|
24289
|
+
totalCost7 += last7[i].totalCostUsd;
|
|
24290
|
+
}
|
|
24291
|
+
var maxDayCost = Math.max.apply(null, perDayCost.concat([0]));
|
|
24292
|
+
var costSparkHtml = '';
|
|
24293
|
+
for (var sb = 0; sb < perDayCost.length; sb++) {
|
|
24294
|
+
var pct = maxDayCost > 0 ? Math.max(2, Math.round((perDayCost[sb] / maxDayCost) * 100)) : 0;
|
|
24295
|
+
var clsZ = perDayCost[sb] > 0 ? '' : ' zero';
|
|
24296
|
+
costSparkHtml += '<div class="mini-spark-bar' + clsZ + '" style="height:' + pct + '%" title="' + dayLabels[sb] + ': $' + perDayCost[sb].toFixed(4) + '"></div>';
|
|
24297
|
+
}
|
|
24298
|
+
var costFigure = totalCost7 < 0.01 ? '$' + totalCost7.toFixed(4) : '$' + totalCost7.toFixed(2);
|
|
24299
|
+
|
|
24300
|
+
// ── Latency split card ─────────────────────────────────────────────
|
|
24301
|
+
// Sum durationMs across last7 OK runs only — we don't yet have a clean
|
|
24302
|
+
// signal for tool time per run. Until path B hooks land we approximate:
|
|
24303
|
+
// tool ~ 35%, model ~ 55%, overhead ~ 10% — these are placeholders
|
|
24304
|
+
// that get replaced with real values once PostToolUse durations are
|
|
24305
|
+
// summed from event logs (Phase 4d).
|
|
24306
|
+
var okRuns = last7.filter(function(rn) { return rn.status === 'ok' && typeof rn.durationMs === 'number'; });
|
|
24307
|
+
var avgDur = okRuns.length > 0
|
|
24308
|
+
? Math.round(okRuns.reduce(function(a, b) { return a + b.durationMs; }, 0) / okRuns.length)
|
|
24309
|
+
: 0;
|
|
24310
|
+
var latToolPct = 35, latModelPct = 55, latOverPct = 10;
|
|
24311
|
+
var splitHtml = '<div class="mini-split">'
|
|
24312
|
+
+ '<div class="mini-split-seg" style="background:#3b82f6;width:' + latModelPct + '%" title="Model API time (~' + latModelPct + '%)">' + (latModelPct >= 12 ? 'model' : '') + '</div>'
|
|
24313
|
+
+ '<div class="mini-split-seg" style="background:#8b5cf6;width:' + latToolPct + '%" title="Tool execution time (~' + latToolPct + '%)">' + (latToolPct >= 12 ? 'tools' : '') + '</div>'
|
|
24314
|
+
+ '<div class="mini-split-seg" style="background:#6b7280;width:' + latOverPct + '%" title="Framework overhead (~' + latOverPct + '%)">' + (latOverPct >= 12 ? 'overhead' : '') + '</div>'
|
|
24315
|
+
+ '</div>'
|
|
24316
|
+
+ '<div class="mini-split-legend">'
|
|
24317
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#3b82f6"></span>model</span>'
|
|
24318
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#8b5cf6"></span>tools</span>'
|
|
24319
|
+
+ '<span><span class="mini-split-legend-dot" style="background:#6b7280"></span>overhead</span>'
|
|
24320
|
+
+ '</div>';
|
|
24321
|
+
var latFigure = avgDur > 0 ? formatDurationMs(avgDur) : '—';
|
|
24322
|
+
var latSub = okRuns.length > 0 ? 'avg of ' + okRuns.length + ' successful runs · 7d' : 'no successful runs in 7d';
|
|
24323
|
+
|
|
24324
|
+
// ── Reliability card ───────────────────────────────────────────────
|
|
24325
|
+
// Per-day failure column, stacked by category. Categories use the same
|
|
24326
|
+
// colors as the run-list filter chips so users can match across surfaces.
|
|
24327
|
+
var perDayFails = []; // [{category: count}, ...]
|
|
24328
|
+
for (var di = 0; di < 7; di++) perDayFails.push({});
|
|
24329
|
+
var totalFails7 = 0;
|
|
24330
|
+
var failureKinds = ['error', 'timeout', 'lost'];
|
|
24331
|
+
for (var fi = 0; fi < last7.length; fi++) {
|
|
24332
|
+
var rn = last7[fi];
|
|
24333
|
+
if (failureKinds.indexOf(rn.status) === -1) continue;
|
|
24334
|
+
var failedMs = new Date(rn.startedAt).getTime();
|
|
24335
|
+
var didx = 6 - Math.floor((now - failedMs) / (24 * 60 * 60 * 1000));
|
|
24336
|
+
if (didx < 0 || didx > 6) continue;
|
|
24337
|
+
var cat = rn.failureCategory || 'tool_error';
|
|
24338
|
+
perDayFails[didx][cat] = (perDayFails[didx][cat] || 0) + 1;
|
|
24339
|
+
totalFails7++;
|
|
24340
|
+
}
|
|
24341
|
+
var maxDayFails = 0;
|
|
24342
|
+
for (var mfi = 0; mfi < perDayFails.length; mfi++) {
|
|
24343
|
+
var dayTotal = 0;
|
|
24344
|
+
for (var k in perDayFails[mfi]) dayTotal += perDayFails[mfi][k];
|
|
24345
|
+
if (dayTotal > maxDayFails) maxDayFails = dayTotal;
|
|
24346
|
+
}
|
|
24347
|
+
var failHtml;
|
|
24348
|
+
if (totalFails7 === 0) {
|
|
24349
|
+
failHtml = '<div class="mini-fails-empty">No failures in 7d 🎉</div>';
|
|
24350
|
+
} else {
|
|
24351
|
+
failHtml = '<div class="mini-fails">';
|
|
24352
|
+
for (var fd = 0; fd < perDayFails.length; fd++) {
|
|
24353
|
+
var dayBucket = perDayFails[fd];
|
|
24354
|
+
var dayTotal2 = 0;
|
|
24355
|
+
var keys = Object.keys(dayBucket).sort();
|
|
24356
|
+
for (var dk = 0; dk < keys.length; dk++) dayTotal2 += dayBucket[keys[dk]];
|
|
24357
|
+
var dayHeightPct = maxDayFails > 0 ? Math.round((dayTotal2 / maxDayFails) * 100) : 0;
|
|
24358
|
+
failHtml += '<div class="mini-fails-col" style="height:' + dayHeightPct + '%" title="' + dayLabels[fd] + ': ' + dayTotal2 + ' failure' + (dayTotal2 === 1 ? '' : 's') + '">';
|
|
24359
|
+
for (var ck = 0; ck < keys.length; ck++) {
|
|
24360
|
+
var catKey = keys[ck];
|
|
24361
|
+
var catSegPct = dayTotal2 > 0 ? Math.round((dayBucket[catKey] / dayTotal2) * 100) : 0;
|
|
24362
|
+
var color = (typeof _runListCategoryColor === 'function') ? _runListCategoryColor(catKey) : 'var(--red)';
|
|
24363
|
+
failHtml += '<div class="mini-fails-seg" style="height:' + catSegPct + '%;background:' + color + '" title="' + catKey + ': ' + dayBucket[catKey] + '"></div>';
|
|
24364
|
+
}
|
|
24365
|
+
failHtml += '</div>';
|
|
24366
|
+
}
|
|
24367
|
+
failHtml += '</div>';
|
|
24368
|
+
}
|
|
24369
|
+
|
|
24370
|
+
// ── Compose ────────────────────────────────────────────────────────
|
|
24371
|
+
host.innerHTML =
|
|
24372
|
+
'<div class="mini-card">'
|
|
24373
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Cost · 7d</span><span class="mini-card-figure">' + esc(costFigure) + '</span></div>'
|
|
24374
|
+
+ '<div class="mini-spark">' + costSparkHtml + '</div>'
|
|
24375
|
+
+ '<div class="mini-card-sub">' + (totalCost7 > 0 ? 'across ' + last7.filter(function(r){ return typeof r.totalCostUsd === "number"; }).length + ' priced runs' : 'no priced runs yet') + '</div>'
|
|
24376
|
+
+ '</div>'
|
|
24377
|
+
+ '<div class="mini-card">'
|
|
24378
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Latency · avg</span><span class="mini-card-figure">' + esc(latFigure) + '</span></div>'
|
|
24379
|
+
+ splitHtml
|
|
24380
|
+
+ '<div class="mini-card-sub">' + esc(latSub) + ' (split is heuristic; per-tool timing lands with hooks)</div>'
|
|
24381
|
+
+ '</div>'
|
|
24382
|
+
+ '<div class="mini-card">'
|
|
24383
|
+
+ '<div class="mini-card-head"><span class="mini-card-title">Reliability · 7d</span><span class="mini-card-figure">' + totalFails7 + ' fail' + (totalFails7 === 1 ? '' : 's') + '</span></div>'
|
|
24384
|
+
+ failHtml
|
|
24385
|
+
+ '<div class="mini-card-sub">click a column in the run list to filter by category</div>'
|
|
24386
|
+
+ '</div>';
|
|
24387
|
+
}
|
|
24388
|
+
|
|
23965
24389
|
// ── PRD Phase 3: Run list ──────────────────────────────────────────────
|
|
23966
24390
|
// Single sortable/filterable table of every CronRunEntry across all tasks.
|
|
23967
24391
|
// Filters: status, task name, time window. Browser-local saved views.
|
|
@@ -24610,6 +25034,11 @@ async function refreshCron() {
|
|
|
24610
25034
|
// /api/cron/runs (already fetched alongside ops) feeds the metrics.
|
|
24611
25035
|
// Render an empty shell first; refreshHealthStrip fills it in.
|
|
24612
25036
|
var html = '<div id="health-strip" class="health-strip"></div>';
|
|
25037
|
+
// PRD §12 / 1.18.93: three mini-dashboards below the Health Strip —
|
|
25038
|
+
// Cost (7d sparkline), Latency split (model / tool / overhead),
|
|
25039
|
+
// Reliability (failures stacked by category). Filled in by
|
|
25040
|
+
// refreshMiniDashboards from the same /api/cron/runs payload.
|
|
25041
|
+
html += '<div id="mini-dashboards" class="mini-dashboards"></div>';
|
|
24613
25042
|
html += renderOperationsSummary(ops);
|
|
24614
25043
|
|
|
24615
25044
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
@@ -24664,6 +25093,9 @@ async function refreshCron() {
|
|
|
24664
25093
|
if (typeof refreshHealthStrip === 'function') {
|
|
24665
25094
|
refreshHealthStrip().catch(function() { /* non-fatal */ });
|
|
24666
25095
|
}
|
|
25096
|
+
if (typeof refreshMiniDashboards === 'function') {
|
|
25097
|
+
refreshMiniDashboards().catch(function() { /* non-fatal */ });
|
|
25098
|
+
}
|
|
24667
25099
|
panel.onclick = function(ev) {
|
|
24668
25100
|
var target = ev.target;
|
|
24669
25101
|
while (target && target.id !== 'panel-cron') {
|
|
@@ -24816,11 +25248,21 @@ function renderRunDetailWaterfall(events, runId, jobName) {
|
|
|
24816
25248
|
|
|
24817
25249
|
var rowId = 'run-evt-' + j;
|
|
24818
25250
|
var canExpand = !!fullContent && fullContent.length > preview.length;
|
|
24819
|
-
|
|
25251
|
+
// PRD §6 Phase 4e: backfilled subagent events get an indented row + a
|
|
25252
|
+
// pill showing the subagent slug, so the waterfall makes it obvious
|
|
25253
|
+
// which spans came from a delegated agent vs the parent task.
|
|
25254
|
+
var isSubagent = ev.source === 'backfill' || !!ev.subagentSlug;
|
|
25255
|
+
var rowBg = isSubagent ? 'background:var(--bg-secondary);' : '';
|
|
25256
|
+
var rowPad = isSubagent ? 'padding:10px 20px 10px 48px;' : 'padding:10px 20px;';
|
|
25257
|
+
var subagentBadge = isSubagent
|
|
25258
|
+
? '<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>'
|
|
25259
|
+
: '';
|
|
25260
|
+
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
25261
|
html += '<div style="font-size:10px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace;line-height:18px">' + esc(offsetLabel) + '</div>';
|
|
24821
25262
|
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
25263
|
html += '<div style="min-width:0">';
|
|
24823
25264
|
html += '<div style="font-size:12px;color:var(--text-primary);line-height:1.45;word-break:break-word">'
|
|
25265
|
+
+ subagentBadge
|
|
24824
25266
|
+ esc(preview)
|
|
24825
25267
|
+ (pairedDuration ? '<span style="color:var(--text-muted);font-size:11px"> ' + esc(pairedDuration) + '</span>' : '')
|
|
24826
25268
|
+ '</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
|