clementine-agent 1.18.126 → 1.18.128
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/assistant.js +16 -0
- package/dist/agent/memory-events.d.ts +49 -0
- package/dist/agent/memory-events.js +52 -0
- package/dist/agent/run-agent-cron.js +17 -1
- package/dist/agent/skill-suppressions.d.ts +47 -0
- package/dist/agent/skill-suppressions.js +98 -0
- package/dist/cli/dashboard.js +591 -29
- package/package.json +2 -2
package/dist/agent/assistant.js
CHANGED
|
@@ -2913,6 +2913,22 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2913
2913
|
catch {
|
|
2914
2914
|
// Non-fatal — extraction logging should never block memory writes
|
|
2915
2915
|
}
|
|
2916
|
+
// 1.18.127 — surface a "📝 Noted: <fact>" toast in the
|
|
2917
|
+
// dashboard chat. Fire-and-forget through the memory-events
|
|
2918
|
+
// bus; if no listener (no dashboard active), nothing happens.
|
|
2919
|
+
try {
|
|
2920
|
+
const { emitMemoryExtraction, summarizeExtractionInput } = await import('./memory-events.js');
|
|
2921
|
+
emitMemoryExtraction({
|
|
2922
|
+
sessionKey: sessionKey ?? 'unknown',
|
|
2923
|
+
toolName: toolBaseName,
|
|
2924
|
+
summary: summarizeExtractionInput((block.input ?? {})),
|
|
2925
|
+
agentSlug: profile?.slug ?? null,
|
|
2926
|
+
at: new Date().toISOString(),
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
catch {
|
|
2930
|
+
// Non-fatal — visibility never blocks the write
|
|
2931
|
+
}
|
|
2916
2932
|
}
|
|
2917
2933
|
}
|
|
2918
2934
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory extraction event bus — 1.18.127.
|
|
3
|
+
*
|
|
4
|
+
* Lets the dashboard surface "📝 Noted: <fact>" toasts when the
|
|
5
|
+
* background auto-extraction Haiku writes to MEMORY.md / user_model
|
|
6
|
+
* after a chat exchange. Today extraction is fully silent: the user
|
|
7
|
+
* sees nothing happen, even though the agent may have just learned
|
|
8
|
+
* something important.
|
|
9
|
+
*
|
|
10
|
+
* Pattern: a single module-level listener slot. Dashboard registers
|
|
11
|
+
* one callback at startup; assistant.ts emits when an extraction tool
|
|
12
|
+
* call lands. Zero ordering guarantees, zero retention — fire-and-
|
|
13
|
+
* forget, just like the extraction itself.
|
|
14
|
+
*
|
|
15
|
+
* Why not EventEmitter: a single listener is enough today and a class
|
|
16
|
+
* import would be dead weight. Trivially upgrade-able if we ever need
|
|
17
|
+
* a fan-out.
|
|
18
|
+
*/
|
|
19
|
+
export interface MemoryExtractionEvent {
|
|
20
|
+
/** Source session that produced the extraction (e.g. "discord:dm:owner",
|
|
21
|
+
* "dashboard:web", "cron:morning-briefing"). */
|
|
22
|
+
sessionKey: string;
|
|
23
|
+
/** The MCP tool the extractor called — memory_write, note_create,
|
|
24
|
+
* task_add, note_take, user_model. */
|
|
25
|
+
toolName: string;
|
|
26
|
+
/** A short human-readable summary of what was learned. Built from the
|
|
27
|
+
* tool input by the emitter — typically the `content` or `text` field
|
|
28
|
+
* truncated to ~120 chars. Empty string when the input shape is unknown. */
|
|
29
|
+
summary: string;
|
|
30
|
+
/** Active hired-agent slug, when applicable. null = Clementine herself. */
|
|
31
|
+
agentSlug: string | null;
|
|
32
|
+
/** ISO timestamp of when the extraction landed. */
|
|
33
|
+
at: string;
|
|
34
|
+
}
|
|
35
|
+
type Listener = (event: MemoryExtractionEvent) => void;
|
|
36
|
+
/** Register the dashboard's SSE broadcaster. Calling again replaces the
|
|
37
|
+
* previous listener (one-shot slot). Pass `null` to clear. */
|
|
38
|
+
export declare function setMemoryExtractionListener(fn: Listener | null): void;
|
|
39
|
+
/** Emit an extraction event. Errors thrown by the listener are
|
|
40
|
+
* swallowed — visibility must never block the actual write. */
|
|
41
|
+
export declare function emitMemoryExtraction(event: MemoryExtractionEvent): void;
|
|
42
|
+
/**
|
|
43
|
+
* Pull a short, user-facing summary out of an MCP tool input payload.
|
|
44
|
+
* Each tool stores its content in a different key, so we look at the
|
|
45
|
+
* usual suspects in priority order and truncate.
|
|
46
|
+
*/
|
|
47
|
+
export declare function summarizeExtractionInput(toolInput: Record<string, unknown>): string;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=memory-events.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory extraction event bus — 1.18.127.
|
|
3
|
+
*
|
|
4
|
+
* Lets the dashboard surface "📝 Noted: <fact>" toasts when the
|
|
5
|
+
* background auto-extraction Haiku writes to MEMORY.md / user_model
|
|
6
|
+
* after a chat exchange. Today extraction is fully silent: the user
|
|
7
|
+
* sees nothing happen, even though the agent may have just learned
|
|
8
|
+
* something important.
|
|
9
|
+
*
|
|
10
|
+
* Pattern: a single module-level listener slot. Dashboard registers
|
|
11
|
+
* one callback at startup; assistant.ts emits when an extraction tool
|
|
12
|
+
* call lands. Zero ordering guarantees, zero retention — fire-and-
|
|
13
|
+
* forget, just like the extraction itself.
|
|
14
|
+
*
|
|
15
|
+
* Why not EventEmitter: a single listener is enough today and a class
|
|
16
|
+
* import would be dead weight. Trivially upgrade-able if we ever need
|
|
17
|
+
* a fan-out.
|
|
18
|
+
*/
|
|
19
|
+
let listener = null;
|
|
20
|
+
/** Register the dashboard's SSE broadcaster. Calling again replaces the
|
|
21
|
+
* previous listener (one-shot slot). Pass `null` to clear. */
|
|
22
|
+
export function setMemoryExtractionListener(fn) {
|
|
23
|
+
listener = fn;
|
|
24
|
+
}
|
|
25
|
+
/** Emit an extraction event. Errors thrown by the listener are
|
|
26
|
+
* swallowed — visibility must never block the actual write. */
|
|
27
|
+
export function emitMemoryExtraction(event) {
|
|
28
|
+
if (!listener)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
listener(event);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* never throw out of the extraction path */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pull a short, user-facing summary out of an MCP tool input payload.
|
|
39
|
+
* Each tool stores its content in a different key, so we look at the
|
|
40
|
+
* usual suspects in priority order and truncate.
|
|
41
|
+
*/
|
|
42
|
+
export function summarizeExtractionInput(toolInput) {
|
|
43
|
+
const candidates = ['content', 'text', 'fact', 'value', 'note', 'message', 'task'];
|
|
44
|
+
for (const key of candidates) {
|
|
45
|
+
const v = toolInput[key];
|
|
46
|
+
if (typeof v === 'string' && v.trim()) {
|
|
47
|
+
return v.trim().length > 120 ? v.trim().slice(0, 120) + '…' : v.trim();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=memory-events.js.map
|
|
@@ -306,9 +306,25 @@ export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSki
|
|
|
306
306
|
const skillQuery = jobName + ' ' + jobPrompt.slice(0, 200);
|
|
307
307
|
const suppressedNamesRaw = memoryStore
|
|
308
308
|
?.getSkillsToSuppress?.(agentSlug);
|
|
309
|
-
const
|
|
309
|
+
const autoSuppressed = Array.isArray(suppressedNamesRaw)
|
|
310
310
|
? new Set(suppressedNamesRaw)
|
|
311
311
|
: (suppressedNamesRaw ?? undefined);
|
|
312
|
+
// 1.18.127 — merge automatic feedback-driven suppressions (from the
|
|
313
|
+
// memory store) with manual user toggles (from skill-suppressions.json).
|
|
314
|
+
// Both sets pass through to loadSkillByName + searchSkills under the
|
|
315
|
+
// same `suppressedNames` parameter — the runtime doesn't care which
|
|
316
|
+
// source flagged a skill.
|
|
317
|
+
let suppressedNames = autoSuppressed;
|
|
318
|
+
try {
|
|
319
|
+
const { getManualSuppressions } = await import('./skill-suppressions.js');
|
|
320
|
+
const manual = getManualSuppressions(agentSlug);
|
|
321
|
+
if (manual.size > 0) {
|
|
322
|
+
suppressedNames = new Set([...(autoSuppressed ?? []), ...manual]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
logger.debug({ err }, 'manual suppression read failed (non-fatal)');
|
|
327
|
+
}
|
|
312
328
|
const prepared = [];
|
|
313
329
|
const seen = new Set();
|
|
314
330
|
// 1. Load pinned skills first via exact slug lookup. When the cron has
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual skill suppression list — 1.18.127.
|
|
3
|
+
*
|
|
4
|
+
* Complements the automatic feedback-driven suppression in
|
|
5
|
+
* `MemoryStore.getSkillsToSuppress` (which suppresses skills that
|
|
6
|
+
* accumulate ≥3 negative ratings with >50% negative rate in the last 60
|
|
7
|
+
* days). This file owns the *manual* list — what the user explicitly
|
|
8
|
+
* toggles in the dashboard ("don't ever auto-match this skill again").
|
|
9
|
+
*
|
|
10
|
+
* Storage: a single JSON file at `~/.clementine/skill-suppressions.json`
|
|
11
|
+
* with the shape:
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "global": ["my-buggy-skill", "stale-procedure"],
|
|
15
|
+
* "ross-the-sdr": ["sasha-only-skill"]
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Merged with the auto-suppression set inside `buildSkillContext` so
|
|
19
|
+
* the runtime sees one combined Set<string>. No schema migration; missing
|
|
20
|
+
* file = empty list.
|
|
21
|
+
*/
|
|
22
|
+
export interface SuppressionFile {
|
|
23
|
+
/** Global suppressions apply to every agent (Clementine + every hired agent). */
|
|
24
|
+
global: string[];
|
|
25
|
+
/** Per-agent suppressions apply only when the named agent is running. */
|
|
26
|
+
[agentSlug: string]: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the merged set of manually suppressed skill names for a given
|
|
30
|
+
* agent context. Includes the global list always; adds the agent-specific
|
|
31
|
+
* list when `agentSlug` is provided.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getManualSuppressions(agentSlug?: string | null): Set<string>;
|
|
34
|
+
/** List the full suppression file as the dashboard sees it. */
|
|
35
|
+
export declare function listAllSuppressions(): SuppressionFile;
|
|
36
|
+
/**
|
|
37
|
+
* Toggle a skill's suppression state for a given scope. Returns the
|
|
38
|
+
* resulting per-scope list so the UI can re-render without a refetch.
|
|
39
|
+
*
|
|
40
|
+
* - `scope === 'global'` writes to `data.global`
|
|
41
|
+
* - any other scope value treats it as an agent slug
|
|
42
|
+
*/
|
|
43
|
+
export declare function setSuppression(skillName: string, scope: string, suppressed: boolean): {
|
|
44
|
+
scope: string;
|
|
45
|
+
list: string[];
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=skill-suppressions.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual skill suppression list — 1.18.127.
|
|
3
|
+
*
|
|
4
|
+
* Complements the automatic feedback-driven suppression in
|
|
5
|
+
* `MemoryStore.getSkillsToSuppress` (which suppresses skills that
|
|
6
|
+
* accumulate ≥3 negative ratings with >50% negative rate in the last 60
|
|
7
|
+
* days). This file owns the *manual* list — what the user explicitly
|
|
8
|
+
* toggles in the dashboard ("don't ever auto-match this skill again").
|
|
9
|
+
*
|
|
10
|
+
* Storage: a single JSON file at `~/.clementine/skill-suppressions.json`
|
|
11
|
+
* with the shape:
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "global": ["my-buggy-skill", "stale-procedure"],
|
|
15
|
+
* "ross-the-sdr": ["sasha-only-skill"]
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Merged with the auto-suppression set inside `buildSkillContext` so
|
|
19
|
+
* the runtime sees one combined Set<string>. No schema migration; missing
|
|
20
|
+
* file = empty list.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
// Resolve lazily on each call so test environments (which override
|
|
26
|
+
// CLEMENTINE_HOME inside beforeEach) see the fresh value rather than
|
|
27
|
+
// the value snapshot at module-load time.
|
|
28
|
+
function baseDir() {
|
|
29
|
+
return process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
30
|
+
}
|
|
31
|
+
function suppressionsPath() {
|
|
32
|
+
return path.join(baseDir(), 'skill-suppressions.json');
|
|
33
|
+
}
|
|
34
|
+
function readFile() {
|
|
35
|
+
const filePath = suppressionsPath();
|
|
36
|
+
if (!existsSync(filePath))
|
|
37
|
+
return { global: [] };
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
40
|
+
if (!raw || typeof raw !== 'object')
|
|
41
|
+
return { global: [] };
|
|
42
|
+
const out = { global: Array.isArray(raw.global) ? raw.global.map(String) : [] };
|
|
43
|
+
for (const key of Object.keys(raw)) {
|
|
44
|
+
if (key === 'global')
|
|
45
|
+
continue;
|
|
46
|
+
if (Array.isArray(raw[key]))
|
|
47
|
+
out[key] = raw[key].map(String);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { global: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeFile(data) {
|
|
56
|
+
const dir = baseDir();
|
|
57
|
+
if (!existsSync(dir))
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
writeFileSync(suppressionsPath(), JSON.stringify(data, null, 2));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the merged set of manually suppressed skill names for a given
|
|
63
|
+
* agent context. Includes the global list always; adds the agent-specific
|
|
64
|
+
* list when `agentSlug` is provided.
|
|
65
|
+
*/
|
|
66
|
+
export function getManualSuppressions(agentSlug) {
|
|
67
|
+
const data = readFile();
|
|
68
|
+
const merged = new Set(data.global ?? []);
|
|
69
|
+
if (agentSlug && Array.isArray(data[agentSlug])) {
|
|
70
|
+
for (const name of data[agentSlug])
|
|
71
|
+
merged.add(name);
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
/** List the full suppression file as the dashboard sees it. */
|
|
76
|
+
export function listAllSuppressions() {
|
|
77
|
+
return readFile();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Toggle a skill's suppression state for a given scope. Returns the
|
|
81
|
+
* resulting per-scope list so the UI can re-render without a refetch.
|
|
82
|
+
*
|
|
83
|
+
* - `scope === 'global'` writes to `data.global`
|
|
84
|
+
* - any other scope value treats it as an agent slug
|
|
85
|
+
*/
|
|
86
|
+
export function setSuppression(skillName, scope, suppressed) {
|
|
87
|
+
const data = readFile();
|
|
88
|
+
const key = scope === 'global' ? 'global' : scope;
|
|
89
|
+
const list = new Set(Array.isArray(data[key]) ? data[key] : []);
|
|
90
|
+
if (suppressed)
|
|
91
|
+
list.add(skillName);
|
|
92
|
+
else
|
|
93
|
+
list.delete(skillName);
|
|
94
|
+
data[key] = [...list].sort();
|
|
95
|
+
writeFile(data);
|
|
96
|
+
return { scope: key, list: data[key] };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=skill-suppressions.js.map
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -19,7 +19,7 @@ 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
21
|
import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
|
|
22
|
-
import { AGENTS_DIR, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
22
|
+
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
23
23
|
import { parseTasks } from '../tools/shared.js';
|
|
24
24
|
import { todayISO, CronRunLog } from '../gateway/cron-scheduler.js';
|
|
25
25
|
import { goalsRouter } from './routes/goals.js';
|
|
@@ -3529,6 +3529,70 @@ export async function cmdDashboard(opts) {
|
|
|
3529
3529
|
app.get('/api/memory', async (_req, res) => {
|
|
3530
3530
|
res.json(await getMemory());
|
|
3531
3531
|
});
|
|
3532
|
+
// ── MEMORY.md inline editor (1.18.127) ────────────────────────────
|
|
3533
|
+
// GET / PUT for the long-term memory file. Per-agent variant via
|
|
3534
|
+
// ?agent=<slug>. mtimeMs round-tripped so the dashboard can detect
|
|
3535
|
+
// an out-of-band edit (Obsidian, agent extraction) and refuse to
|
|
3536
|
+
// clobber it on save.
|
|
3537
|
+
function resolveMemoryPath(agentParam) {
|
|
3538
|
+
if (!agentParam || agentParam === 'global')
|
|
3539
|
+
return MEMORY_FILE;
|
|
3540
|
+
// Agent slug — must match an existing agent directory. Refuse traversal.
|
|
3541
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(agentParam)) {
|
|
3542
|
+
throw new Error('invalid agent slug');
|
|
3543
|
+
}
|
|
3544
|
+
return path.join(AGENTS_DIR, agentParam, 'MEMORY.md');
|
|
3545
|
+
}
|
|
3546
|
+
app.get('/api/memory/md', (req, res) => {
|
|
3547
|
+
try {
|
|
3548
|
+
const agentParam = typeof req.query.agent === 'string' ? req.query.agent : undefined;
|
|
3549
|
+
const filePath = resolveMemoryPath(agentParam);
|
|
3550
|
+
if (!existsSync(filePath)) {
|
|
3551
|
+
return res.json({ content: '', mtimeMs: 0, agentSlug: agentParam ?? 'global', exists: false, filePath });
|
|
3552
|
+
}
|
|
3553
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
3554
|
+
const mtimeMs = statSync(filePath).mtimeMs;
|
|
3555
|
+
res.json({ content, mtimeMs, agentSlug: agentParam ?? 'global', exists: true, filePath });
|
|
3556
|
+
}
|
|
3557
|
+
catch (err) {
|
|
3558
|
+
res.status(400).json({ error: String(err instanceof Error ? err.message : err) });
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
app.put('/api/memory/md', (req, res) => {
|
|
3562
|
+
try {
|
|
3563
|
+
const agentParam = typeof req.query.agent === 'string' ? req.query.agent : undefined;
|
|
3564
|
+
const filePath = resolveMemoryPath(agentParam);
|
|
3565
|
+
const body = (req.body ?? {});
|
|
3566
|
+
if (typeof body.content !== 'string') {
|
|
3567
|
+
return res.status(400).json({ error: 'content (string) required' });
|
|
3568
|
+
}
|
|
3569
|
+
// Conflict detection: if the file already exists and its mtime has
|
|
3570
|
+
// moved past what the client last saw, refuse the write so we don't
|
|
3571
|
+
// clobber an Obsidian save or an agent extraction. Client surfaces a
|
|
3572
|
+
// "reload?" toast on 409.
|
|
3573
|
+
if (existsSync(filePath) && typeof body.expectedMtimeMs === 'number' && body.expectedMtimeMs > 0) {
|
|
3574
|
+
const currentMtime = statSync(filePath).mtimeMs;
|
|
3575
|
+
// 2-second tolerance covers same-second saves under filesystem mtime resolution.
|
|
3576
|
+
if (currentMtime - body.expectedMtimeMs > 2000) {
|
|
3577
|
+
return res.status(409).json({
|
|
3578
|
+
error: 'File changed on disk since you loaded it. Reload to see the latest version.',
|
|
3579
|
+
currentMtimeMs: currentMtime,
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
// Ensure parent dir exists (per-agent MEMORY.md is created lazily).
|
|
3584
|
+
const parentDir = path.dirname(filePath);
|
|
3585
|
+
if (!existsSync(parentDir)) {
|
|
3586
|
+
mkdirSync(parentDir, { recursive: true });
|
|
3587
|
+
}
|
|
3588
|
+
writeFileSync(filePath, body.content);
|
|
3589
|
+
const mtimeMs = statSync(filePath).mtimeMs;
|
|
3590
|
+
res.json({ ok: true, mtimeMs, bytes: Buffer.byteLength(body.content, 'utf-8') });
|
|
3591
|
+
}
|
|
3592
|
+
catch (err) {
|
|
3593
|
+
res.status(500).json({ error: String(err instanceof Error ? err.message : err) });
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3532
3596
|
app.get('/api/logs', (req, res) => {
|
|
3533
3597
|
const lines = parseInt(String(req.query.lines ?? '200'), 10);
|
|
3534
3598
|
res.json({ content: getLogs(lines) });
|
|
@@ -3696,6 +3760,21 @@ export async function cmdDashboard(opts) {
|
|
|
3696
3760
|
}
|
|
3697
3761
|
// Let the lazy-gateway dispatcher publish deep_result events through SSE.
|
|
3698
3762
|
dashboardSseBroadcast = broadcastEvent;
|
|
3763
|
+
// 1.18.127 — bridge memory-extraction events from assistant.ts → SSE.
|
|
3764
|
+
// The dashboard chat panel listens on the same SSE stream and renders
|
|
3765
|
+
// a "📝 Noted: <fact>" toast whenever the background extractor writes
|
|
3766
|
+
// something. Silent until then — no traffic when no extraction fires.
|
|
3767
|
+
(async () => {
|
|
3768
|
+
try {
|
|
3769
|
+
const { setMemoryExtractionListener } = await import('../agent/memory-events.js');
|
|
3770
|
+
setMemoryExtractionListener((event) => {
|
|
3771
|
+
broadcastEvent({ type: 'memory_extracted', data: event });
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
catch (err) {
|
|
3775
|
+
console.warn('Failed to wire memory-extraction SSE bridge:', err);
|
|
3776
|
+
}
|
|
3777
|
+
})();
|
|
3699
3778
|
// ── Builder event bridge ──────────────────────────────────────
|
|
3700
3779
|
// Forward events from src/dashboard/builder/events.ts through SSE so the
|
|
3701
3780
|
// Builder page can update its canvas live as the agent edits via MCP tools.
|
|
@@ -4504,6 +4583,41 @@ export async function cmdDashboard(opts) {
|
|
|
4504
4583
|
res.status(500).json({ ok: false, error: String(err) });
|
|
4505
4584
|
}
|
|
4506
4585
|
});
|
|
4586
|
+
// ── Skill suppressions (1.18.127) ──────────────────────────────────
|
|
4587
|
+
// Lets the user manually suppress skills from auto-match retrieval —
|
|
4588
|
+
// a complement to the memory store's automatic feedback-driven
|
|
4589
|
+
// suppression. Storage is a single JSON file under ~/.clementine/.
|
|
4590
|
+
app.get('/api/skills/suppressions', async (_req, res) => {
|
|
4591
|
+
try {
|
|
4592
|
+
const { listAllSuppressions } = await import('../agent/skill-suppressions.js');
|
|
4593
|
+
res.json({ ok: true, suppressions: listAllSuppressions() });
|
|
4594
|
+
}
|
|
4595
|
+
catch (err) {
|
|
4596
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
4597
|
+
}
|
|
4598
|
+
});
|
|
4599
|
+
app.put('/api/skills/suppressions/:name', async (req, res) => {
|
|
4600
|
+
try {
|
|
4601
|
+
const name = req.params.name;
|
|
4602
|
+
if (!name) {
|
|
4603
|
+
res.status(400).json({ ok: false, error: 'name required' });
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
const body = (req.body ?? {});
|
|
4607
|
+
const suppressed = body.suppressed === true;
|
|
4608
|
+
const scope = typeof body.scope === 'string' && body.scope.length > 0 ? body.scope : 'global';
|
|
4609
|
+
// Slug-shape validation when scope is per-agent (anything other than "global").
|
|
4610
|
+
if (scope !== 'global' && !/^[a-z0-9][a-z0-9-]{0,63}$/.test(scope)) {
|
|
4611
|
+
return res.status(400).json({ ok: false, error: 'invalid scope (must be "global" or a valid agent slug)' });
|
|
4612
|
+
}
|
|
4613
|
+
const { setSuppression } = await import('../agent/skill-suppressions.js');
|
|
4614
|
+
const result = setSuppression(name, scope, suppressed);
|
|
4615
|
+
res.json({ ok: true, ...result });
|
|
4616
|
+
}
|
|
4617
|
+
catch (err) {
|
|
4618
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4507
4621
|
// ── Skill migration (legacy .md → folder/SKILL.md) ─────────────────
|
|
4508
4622
|
// Two endpoints: per-skill and bulk. Both wrap migrateLegacySkill /
|
|
4509
4623
|
// migrateAllLegacySkills from skill-store.ts. The original .md is
|
|
@@ -7603,6 +7717,13 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
7603
7717
|
mcpServers: mcpInfo,
|
|
7604
7718
|
composioConnected: plan.composioConnected,
|
|
7605
7719
|
externalConnected: plan.externalConnected,
|
|
7720
|
+
// 1.18.127 — surface the scope-widening from 1.18.125 so the UI can
|
|
7721
|
+
// show "this skill brought in `Bash` + `gmail`" with attribution.
|
|
7722
|
+
// `widenedFromSkills.tools` and `.mcpServers` only contain entries
|
|
7723
|
+
// that were ADDED on top of the cron's own allowlists. Empty arrays
|
|
7724
|
+
// when the cron is unrestricted (skill scope was implicitly allowed)
|
|
7725
|
+
// or when no pinned skill widened anything.
|
|
7726
|
+
widenedFromSkills: plan.widenedFromSkills,
|
|
7606
7727
|
tier: plan.tier,
|
|
7607
7728
|
effort: plan.effort,
|
|
7608
7729
|
maxBudgetUsd: plan.maxBudgetUsd ?? null,
|
|
@@ -18652,6 +18773,31 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
18652
18773
|
</div>
|
|
18653
18774
|
<div class="card-body" id="panel-memory"><div class="skel-block"><div class="skel-row med"></div><div class="skel-row"></div><div class="skel-row short"></div></div></div>
|
|
18654
18775
|
</div>
|
|
18776
|
+
<!-- 1.18.127 — MEMORY.md inline editor. Lets the user seed
|
|
18777
|
+
long-term memory directly without leaving the dashboard.
|
|
18778
|
+
Per-agent toggle (global / Sasha / Ross / …) reads + writes
|
|
18779
|
+
the right MEMORY.md file. mtime conflict-detection blocks
|
|
18780
|
+
clobbering an Obsidian save mid-edit. -->
|
|
18781
|
+
<div class="card" style="margin-bottom:14px">
|
|
18782
|
+
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
18783
|
+
<span>Edit MEMORY.md</span>
|
|
18784
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
18785
|
+
<select id="memory-md-scope" onchange="loadMemoryMdEditor()" style="font-size:12px;padding:4px 6px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)">
|
|
18786
|
+
<option value="global">Global (Clementine)</option>
|
|
18787
|
+
</select>
|
|
18788
|
+
<button class="btn-sm" onclick="loadMemoryMdEditor()" title="Reload from disk" style="font-size:11px;padding:4px 10px">Reload</button>
|
|
18789
|
+
<button id="memory-md-save-btn" class="btn-sm btn-primary" onclick="saveMemoryMd()" style="font-size:11px;padding:4px 10px" disabled>Save</button>
|
|
18790
|
+
</div>
|
|
18791
|
+
</div>
|
|
18792
|
+
<div class="card-body" style="padding:14px">
|
|
18793
|
+
<div id="memory-md-status" style="font-size:11px;color:var(--text-muted);margin-bottom:8px">Loading…</div>
|
|
18794
|
+
<textarea id="memory-md-editor" placeholder="Loading…" style="width:100%;min-height:280px;padding:10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);resize:vertical" oninput="onMemoryMdInput()"></textarea>
|
|
18795
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:8px;font-size:11px;color:var(--text-muted)">
|
|
18796
|
+
<span id="memory-md-counter">0 chars</span>
|
|
18797
|
+
<span id="memory-md-path" style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace"></span>
|
|
18798
|
+
</div>
|
|
18799
|
+
</div>
|
|
18800
|
+
</div>
|
|
18655
18801
|
<div class="card" style="margin-bottom:14px">
|
|
18656
18802
|
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
|
|
18657
18803
|
<span>Recent writes</span>
|
|
@@ -20863,6 +21009,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20863
21009
|
<button class="btn-sm btn-primary" style="white-space:nowrap;padding:6px 12px;border-radius:6px;cursor:pointer" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
|
|
20864
21010
|
</div>
|
|
20865
21011
|
<div id="budget-health-content" style="margin-bottom:16px"><div class="empty-state">Loading budget health...</div></div>
|
|
21012
|
+
<!-- 1.18.127 — Notification preferences. Single toggle for now;
|
|
21013
|
+
room to grow into a full notification settings card. -->
|
|
21014
|
+
<div class="card" style="margin-bottom:16px">
|
|
21015
|
+
<div class="card-header">Notifications</div>
|
|
21016
|
+
<div class="card-body" style="padding:14px 16px;display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap">
|
|
21017
|
+
<div>
|
|
21018
|
+
<div style="font-size:13px;color:var(--text-primary);font-weight:500">Silent learning mode</div>
|
|
21019
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">When off, I show a small toast every time I save a fact / note / task to memory after a chat.</div>
|
|
21020
|
+
</div>
|
|
21021
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
21022
|
+
<input type="checkbox" id="silent-learning-toggle" onchange="toggleSilentLearning(this.checked)">
|
|
21023
|
+
<span style="font-size:12px;color:var(--text-secondary)">Silence</span>
|
|
21024
|
+
</label>
|
|
21025
|
+
</div>
|
|
21026
|
+
</div>
|
|
20866
21027
|
<div id="settings-content"><div class="empty-state">Loading settings...</div></div>
|
|
20867
21028
|
</div>
|
|
20868
21029
|
<div class="tab-pane" id="tab-settings-remote">
|
|
@@ -21093,6 +21254,21 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
21093
21254
|
</div>
|
|
21094
21255
|
</div>
|
|
21095
21256
|
|
|
21257
|
+
<!-- 1.18.128 — Project Context promoted to Basics. Was buried in
|
|
21258
|
+
the Scope tab where users were missing it. Selecting a project
|
|
21259
|
+
gives the task that project's CLAUDE.md, MCP config, and cwd —
|
|
21260
|
+
usually the single most impactful field after Prompt. -->
|
|
21261
|
+
<div class="cron-section-card" data-config-tab="basics">
|
|
21262
|
+
<h4>Project Context <span style="color:var(--text-muted);font-weight:normal;font-size:13px">(optional)</span></h4>
|
|
21263
|
+
<p class="cron-section-desc">Run this task inside a project directory. The agent picks up that project's <code>CLAUDE.md</code>, MCP config, and any context files alongside the cwd.</p>
|
|
21264
|
+
<div class="form-group">
|
|
21265
|
+
<select id="cron-workdir">
|
|
21266
|
+
<option value="">None — runs in default context</option>
|
|
21267
|
+
</select>
|
|
21268
|
+
<div class="form-hint">No projects yet? <a href="#" onclick="navigateTo(\\x27settings\\x27, { tab: \\x27projects\\x27 }); closeCronModal(); return false" style="color:var(--accent)">Add one →</a></div>
|
|
21269
|
+
</div>
|
|
21270
|
+
</div>
|
|
21271
|
+
|
|
21096
21272
|
<!-- Schedule -->
|
|
21097
21273
|
<div class="cron-section-card" data-config-tab="basics">
|
|
21098
21274
|
<h4>Schedule</h4>
|
|
@@ -21288,25 +21464,18 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
21288
21464
|
</div>
|
|
21289
21465
|
</div>
|
|
21290
21466
|
|
|
21291
|
-
<!-- ── Scope:
|
|
21467
|
+
<!-- ── Scope: extra read directories beyond the project cwd ── -->
|
|
21468
|
+
<!-- 1.18.128 — Project Context picker moved up to Basics. This
|
|
21469
|
+
section now only owns Additional read directories, which is
|
|
21470
|
+
a power-user feature anyway. -->
|
|
21292
21471
|
<div class="cron-section-card" data-config-tab="scope">
|
|
21293
21472
|
<h4>Scope</h4>
|
|
21294
|
-
<p class="cron-section-desc">
|
|
21295
|
-
<div class="form-row">
|
|
21296
|
-
<div class="form-group">
|
|
21297
|
-
<label class="form-label">Project Context <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
21298
|
-
<select id="cron-workdir">
|
|
21299
|
-
<option value="">None — runs in default context</option>
|
|
21300
|
-
</select>
|
|
21301
|
-
<div class="form-hint">Run inside a project directory. Agent gets that project's CLAUDE.md.</div>
|
|
21302
|
-
</div>
|
|
21303
|
-
</div>
|
|
21304
|
-
<!-- PRD Phase 1: read scope beyond cwd. One absolute path per line. -->
|
|
21473
|
+
<p class="cron-section-desc">Extra directories the agent gets read access to beyond the project cwd. Most tasks won't need this.</p>
|
|
21305
21474
|
<div class="form-row">
|
|
21306
21475
|
<div class="form-group" style="flex:1">
|
|
21307
21476
|
<label class="form-label">Additional read directories <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
21308
21477
|
<textarea id="cron-add-dirs" rows="2" placeholder="/Users/me/notes /Users/me/clients/acme" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
21309
|
-
<div class="form-hint">One absolute path per line. The
|
|
21478
|
+
<div class="form-hint">One absolute path per line. The Project Context above already gives the agent its cwd; use this for extra read scope only.</div>
|
|
21310
21479
|
</div>
|
|
21311
21480
|
</div>
|
|
21312
21481
|
</div>
|
|
@@ -22820,6 +22989,7 @@ function switchTab(group, tab) {
|
|
|
22820
22989
|
if (tab === 'search') {
|
|
22821
22990
|
// Consolidated Memory tab: search results + stats + MEMORY.md + recent writes + supersedes + coverage strip.
|
|
22822
22991
|
refreshMemory();
|
|
22992
|
+
if (typeof loadMemoryMdEditor === 'function') loadMemoryMdEditor();
|
|
22823
22993
|
if (typeof refreshRecentWrites === 'function') refreshRecentWrites();
|
|
22824
22994
|
if (typeof refreshRecentEpisodes === 'function') refreshRecentEpisodes();
|
|
22825
22995
|
if (typeof refreshCommitments === 'function') refreshCommitments();
|
|
@@ -22848,6 +23018,7 @@ function switchTab(group, tab) {
|
|
|
22848
23018
|
}
|
|
22849
23019
|
if (group === 'settings') {
|
|
22850
23020
|
if (tab === 'general' && typeof refreshSettings === 'function') refreshSettings();
|
|
23021
|
+
if (tab === 'general' && typeof initSilentLearningToggle === 'function') initSilentLearningToggle();
|
|
22851
23022
|
if (tab === 'integrations') { refreshSalesforce(); refreshComposioConnections(); refreshToolPreferences(); refreshMcpServers(); refreshClaudeIntegrations(); }
|
|
22852
23023
|
if (tab === 'remote') refreshRemoteAccess();
|
|
22853
23024
|
if (tab === 'security') refreshAuthSessions();
|
|
@@ -23885,6 +24056,28 @@ function dismissRestartRequiredBanner() {
|
|
|
23885
24056
|
if (existing) existing.remove();
|
|
23886
24057
|
}
|
|
23887
24058
|
|
|
24059
|
+
// 1.18.127 — silent learning mode toggle. Persisted to localStorage so
|
|
24060
|
+
// the preference survives page reloads. The dashboard SSE handler at
|
|
24061
|
+
// line ~39660 reads this flag to decide whether to render extraction
|
|
24062
|
+
// toasts when the background memory extractor writes facts.
|
|
24063
|
+
function toggleSilentLearning(silent) {
|
|
24064
|
+
try {
|
|
24065
|
+
if (silent) localStorage.setItem('clem-silent-learning', '1');
|
|
24066
|
+
else localStorage.removeItem('clem-silent-learning');
|
|
24067
|
+
toast(silent ? 'Silent learning ON — extraction toasts hidden.' : 'Silent learning OFF — you\\'ll see a toast when I save facts.', 'info');
|
|
24068
|
+
} catch (_) { /* localStorage may be disabled */ }
|
|
24069
|
+
}
|
|
24070
|
+
|
|
24071
|
+
// Restore the toggle state on page load so the checkbox reflects the
|
|
24072
|
+
// user's last preference. Defaults to OFF (visible toasts) for new users.
|
|
24073
|
+
function initSilentLearningToggle() {
|
|
24074
|
+
try {
|
|
24075
|
+
var box = document.getElementById('silent-learning-toggle');
|
|
24076
|
+
if (!box) return;
|
|
24077
|
+
box.checked = localStorage.getItem('clem-silent-learning') === '1';
|
|
24078
|
+
} catch (_) { /* non-fatal */ }
|
|
24079
|
+
}
|
|
24080
|
+
|
|
23888
24081
|
async function restartDaemonFromDashboard(skipConfirm) {
|
|
23889
24082
|
if (!skipConfirm && !confirm('Restart Clementine now? Active work may pause briefly while the daemon reloads.')) return;
|
|
23890
24083
|
toast('Restarting Clementine...', 'info');
|
|
@@ -26911,9 +27104,14 @@ async function refreshProjects(preloaded) {
|
|
|
26911
27104
|
? '<div style="color:var(--accent);margin-bottom:4px;font-size:12px">' + esc(p.userDescription) + '</div>'
|
|
26912
27105
|
: '';
|
|
26913
27106
|
const idx = projectsData.indexOf(p);
|
|
27107
|
+
// 1.18.128 — "+ New task in this project" CTA: opens the cron creation
|
|
27108
|
+
// modal with the project pre-selected as Project Context. Closes the
|
|
27109
|
+
// mental gap between "I have a project with built-up context" and
|
|
27110
|
+
// "I need to schedule a task that uses it."
|
|
27111
|
+
const newTaskBtn = '<button class="btn btn-sm" style="font-size:11px" onclick="openCronModalForProject(' + idx + ')" title="Create a scheduled task that runs inside this project">+ New task</button>';
|
|
26914
27112
|
const linkBtn = p.linked
|
|
26915
|
-
? '<button class="btn btn-sm" style="font-size:11px" onclick="openProjectEditorByIdx(' + idx + ')">Edit</button> <button class="btn btn-sm btn-danger" style="font-size:11px" onclick="unlinkProjectByIdx(' + idx + ')">Unlink</button>'
|
|
26916
|
-
: '<button class="btn btn-sm btn-primary" style="font-size:11px" onclick="openProjectEditorByIdx(' + idx + ')">Link</button>';
|
|
27113
|
+
? newTaskBtn + ' <button class="btn btn-sm" style="font-size:11px" onclick="openProjectEditorByIdx(' + idx + ')">Edit</button> <button class="btn btn-sm btn-danger" style="font-size:11px" onclick="unlinkProjectByIdx(' + idx + ')">Unlink</button>'
|
|
27114
|
+
: newTaskBtn + ' <button class="btn btn-sm btn-primary" style="font-size:11px" onclick="openProjectEditorByIdx(' + idx + ')">Link</button>';
|
|
26917
27115
|
html += '<div class="card" style="cursor:default">'
|
|
26918
27116
|
+ '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between">'
|
|
26919
27117
|
+ '<strong>' + esc(p.name) + '</strong>'
|
|
@@ -27275,12 +27473,18 @@ async function _openSkillModal(opts) {
|
|
|
27275
27473
|
+ '<input id="skill-modal-name" type="text" placeholder="e.g. morning-briefing" style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px;font-family:\\x27JetBrains Mono\\x27,monospace">'
|
|
27276
27474
|
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Display title <span style="color:var(--text-muted)">(optional, friendlier name)</span></label>'
|
|
27277
27475
|
+ '<input id="skill-modal-title" type="text" placeholder="e.g. Morning Briefing" style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px">'
|
|
27278
|
-
+ '<
|
|
27279
|
-
+
|
|
27476
|
+
+ '<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:4px">'
|
|
27477
|
+
+ '<label style="font-size:12px;color:var(--text-secondary);font-weight:500">Description <span style="color:var(--text-muted)">(what this skill does — used by Claude to know when to apply it)</span></label>'
|
|
27478
|
+
+ '<span id="skill-modal-desc-counter" style="font-size:10px;color:var(--text-muted);font-variant-numeric:tabular-nums">0 / 1024 chars</span>'
|
|
27479
|
+
+ '</div>'
|
|
27480
|
+
+ '<textarea id="skill-modal-desc" rows="2" oninput="updateSkillModalCounters()" placeholder="One paragraph: what does this skill do, when should Claude run it?" style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px;font-family:inherit;resize:vertical"></textarea>'
|
|
27280
27481
|
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Allowed tools <span style="color:var(--text-muted)">(comma-separated, leave blank for default)</span></label>'
|
|
27281
27482
|
+ '<input id="skill-modal-tools" type="text" placeholder="e.g. Read, Bash, mcp__supabase__list_tables" style="width:100%;padding:8px 10px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);margin-bottom:12px">'
|
|
27282
|
-
+ '<
|
|
27283
|
-
+
|
|
27483
|
+
+ '<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:4px">'
|
|
27484
|
+
+ '<label style="font-size:12px;color:var(--text-secondary);font-weight:500">Procedure <span style="color:var(--text-muted)">(Markdown — the actual steps Claude follows)</span></label>'
|
|
27485
|
+
+ '<span id="skill-modal-body-counter" style="font-size:10px;color:var(--text-muted);font-variant-numeric:tabular-nums">0 lines</span>'
|
|
27486
|
+
+ '</div>'
|
|
27487
|
+
+ '<textarea id="skill-modal-body" rows="14" oninput="updateSkillModalCounters()" placeholder="# Morning Briefing\\n\\nSteps Claude follows when this skill is invoked.\\n\\n1. Check the inbox.\\n2. Summarize.\\n3. Send to Discord." style="width:100%;padding:10px 12px;font-size:12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-family:\\x27JetBrains Mono\\x27,monospace;line-height:1.55;resize:vertical"></textarea>'
|
|
27284
27488
|
+ '<div id="skill-modal-error" style="display:none;color:var(--red);font-size:12px;margin-top:10px;padding:8px 10px;background:rgba(239,68,68,0.08);border:1px solid var(--red);border-radius:6px"></div>'
|
|
27285
27489
|
+ '</div>'
|
|
27286
27490
|
+ '<div style="display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--border);background:var(--bg-secondary)">'
|
|
@@ -27303,6 +27507,31 @@ async function _openSkillModal(opts) {
|
|
|
27303
27507
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
27304
27508
|
modal.style.display = 'flex';
|
|
27305
27509
|
document.getElementById('skill-modal-name').focus();
|
|
27510
|
+
if (typeof updateSkillModalCounters === 'function') updateSkillModalCounters();
|
|
27511
|
+
}
|
|
27512
|
+
|
|
27513
|
+
// 1.18.127 — live char/line counters under description + body. Color
|
|
27514
|
+
// shifts at 80% (amber) and 100% (red) of the Anthropic-spec ceilings
|
|
27515
|
+
// so the user knows before submission rather than after.
|
|
27516
|
+
function updateSkillModalCounters() {
|
|
27517
|
+
var descEl = document.getElementById('skill-modal-desc');
|
|
27518
|
+
var descCounter = document.getElementById('skill-modal-desc-counter');
|
|
27519
|
+
var bodyEl = document.getElementById('skill-modal-body');
|
|
27520
|
+
var bodyCounter = document.getElementById('skill-modal-body-counter');
|
|
27521
|
+
if (descEl && descCounter) {
|
|
27522
|
+
var n = (descEl.value || '').length;
|
|
27523
|
+
var pct = n / 1024;
|
|
27524
|
+
var color = pct >= 1 ? 'var(--red)' : pct >= 0.8 ? 'var(--yellow)' : 'var(--text-muted)';
|
|
27525
|
+
descCounter.textContent = n + ' / 1024 chars';
|
|
27526
|
+
descCounter.style.color = color;
|
|
27527
|
+
}
|
|
27528
|
+
if (bodyEl && bodyCounter) {
|
|
27529
|
+
var lines = (bodyEl.value || '').split('\\n').length;
|
|
27530
|
+
var bpct = lines / 500;
|
|
27531
|
+
var bcolor = bpct >= 1 ? 'var(--red)' : bpct >= 0.8 ? 'var(--yellow)' : 'var(--text-muted)';
|
|
27532
|
+
bodyCounter.textContent = lines + ' / 500 lines (recommended)';
|
|
27533
|
+
bodyCounter.style.color = bcolor;
|
|
27534
|
+
}
|
|
27306
27535
|
}
|
|
27307
27536
|
|
|
27308
27537
|
function closeSkillModal() {
|
|
@@ -27622,11 +27851,54 @@ async function showSkillDetail(name) {
|
|
|
27622
27851
|
return;
|
|
27623
27852
|
}
|
|
27624
27853
|
detailEl.innerHTML = renderSkillDetail(d.skill);
|
|
27854
|
+
if (typeof loadSkillSuppressionState === 'function') loadSkillSuppressionState(name);
|
|
27625
27855
|
} catch (e) {
|
|
27626
27856
|
detailEl.innerHTML = '<div style="padding:24px;color:var(--red);font-size:12px">Error: ' + esc(String(e)) + '</div>';
|
|
27627
27857
|
}
|
|
27628
27858
|
}
|
|
27629
27859
|
|
|
27860
|
+
// 1.18.127 — fetch the current suppression state and wire the checkbox.
|
|
27861
|
+
// Cached per call; the file is small enough that re-fetching on every
|
|
27862
|
+
// detail open is fine (and ensures consistency if the user just toggled
|
|
27863
|
+
// from another browser tab).
|
|
27864
|
+
async function loadSkillSuppressionState(skillName) {
|
|
27865
|
+
var checkbox = document.getElementById('skill-suppress-global');
|
|
27866
|
+
var status = document.getElementById('skill-suppress-status');
|
|
27867
|
+
if (!checkbox) return;
|
|
27868
|
+
try {
|
|
27869
|
+
var r = await apiFetch('/api/skills/suppressions');
|
|
27870
|
+
var d = await r.json();
|
|
27871
|
+
if (!r.ok || !d || !d.suppressions) return;
|
|
27872
|
+
var globalList = Array.isArray(d.suppressions.global) ? d.suppressions.global : [];
|
|
27873
|
+
checkbox.checked = globalList.indexOf(skillName) !== -1;
|
|
27874
|
+
if (status) {
|
|
27875
|
+
status.textContent = checkbox.checked ? 'Suppressed globally — runtime auto-match will skip this skill.' : '';
|
|
27876
|
+
}
|
|
27877
|
+
} catch (_) { /* non-fatal */ }
|
|
27878
|
+
}
|
|
27879
|
+
|
|
27880
|
+
async function toggleSkillSuppression(skillName, scope, suppressed) {
|
|
27881
|
+
var status = document.getElementById('skill-suppress-status');
|
|
27882
|
+
try {
|
|
27883
|
+
var r = await apiFetch('/api/skills/suppressions/' + encodeURIComponent(skillName), {
|
|
27884
|
+
method: 'PUT',
|
|
27885
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27886
|
+
body: JSON.stringify({ suppressed: !!suppressed, scope: scope }),
|
|
27887
|
+
});
|
|
27888
|
+
var d = await r.json();
|
|
27889
|
+
if (!r.ok) {
|
|
27890
|
+
toast(d.error || 'Failed to update suppression', 'error');
|
|
27891
|
+
return;
|
|
27892
|
+
}
|
|
27893
|
+
if (status) {
|
|
27894
|
+
status.textContent = suppressed ? 'Suppressed globally — runtime auto-match will skip this skill.' : '';
|
|
27895
|
+
}
|
|
27896
|
+
toast(suppressed ? 'Suppressed "' + skillName + '"' : 'Un-suppressed "' + skillName + '"', 'success');
|
|
27897
|
+
} catch (err) {
|
|
27898
|
+
toast('Failed: ' + err, 'error');
|
|
27899
|
+
}
|
|
27900
|
+
}
|
|
27901
|
+
|
|
27630
27902
|
// ── Skill detail pane ────────────────────────────────────────────────
|
|
27631
27903
|
// Renders a single skill in the right pane. Sections, in order:
|
|
27632
27904
|
// 1. Header (name + 3 badges + description + file path)
|
|
@@ -27668,6 +27940,17 @@ function renderSkillDetail(s) {
|
|
|
27668
27940
|
html += '<p style="font-size:12px;color:var(--text-muted);font-style:italic;margin:0">No description. Anthropic spec recommends adding one so the skill can be discovered by Claude.</p>';
|
|
27669
27941
|
}
|
|
27670
27942
|
html += '<div style="margin-top:10px;font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace">' + esc(s.filePath) + '</div>';
|
|
27943
|
+
// 1.18.127 — suppression toggle. Lets the user manually hide a skill
|
|
27944
|
+
// from auto-match retrieval without deleting it. Per-scope (global vs
|
|
27945
|
+
// per-agent). Lazy-loaded; rendered as a placeholder, populated by
|
|
27946
|
+
// loadSkillSuppressionState() right after the detail pane mounts.
|
|
27947
|
+
html += '<div id="skill-suppress-row" data-skill="' + esc(fm.name) + '" style="margin-top:14px;padding:10px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">';
|
|
27948
|
+
html += '<span style="font-size:12px;color:var(--text-secondary);font-weight:500">Suppress from auto-match:</span>';
|
|
27949
|
+
html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer">';
|
|
27950
|
+
html += '<input type="checkbox" id="skill-suppress-global" onchange="toggleSkillSuppression(\\x27' + jsStr(fm.name) + '\\x27, \\x27global\\x27, this.checked)"> globally';
|
|
27951
|
+
html += '</label>';
|
|
27952
|
+
html += '<span id="skill-suppress-status" style="font-size:11px;color:var(--text-muted);margin-left:auto"></span>';
|
|
27953
|
+
html += '</div>';
|
|
27671
27954
|
html += '</div>';
|
|
27672
27955
|
|
|
27673
27956
|
// ── 2. Validation warnings (if any)
|
|
@@ -28322,13 +28605,50 @@ async function loadSkillsCatalog() {
|
|
|
28322
28605
|
|
|
28323
28606
|
async function loadMcpCatalog() {
|
|
28324
28607
|
if (_mcpCatalog) return _mcpCatalog;
|
|
28608
|
+
// 1.18.128 — merge Composio toolkits into the picker. discoverMcpServers()
|
|
28609
|
+
// only sees Claude Desktop / Claude Code / Extensions / user-managed
|
|
28610
|
+
// config, but the runtime ALSO injects every connected Composio toolkit
|
|
28611
|
+
// via buildExtraMcpForRunAgent. The picker was blind to all that — users
|
|
28612
|
+
// would scroll and not see Gmail, Slack, Salesforce, etc., even though
|
|
28613
|
+
// those servers fire correctly when the cron runs. This fixes the picker
|
|
28614
|
+
// to match runtime reality.
|
|
28615
|
+
var servers = [];
|
|
28325
28616
|
try {
|
|
28326
28617
|
var r = await apiFetch('/api/mcp-servers');
|
|
28327
28618
|
var d = await r.json();
|
|
28328
|
-
|
|
28329
|
-
|
|
28330
|
-
|
|
28331
|
-
}
|
|
28619
|
+
servers = (d.servers || []).map(function(s) {
|
|
28620
|
+
return Object.assign({}, s, { _origin: s.source || 'config' });
|
|
28621
|
+
});
|
|
28622
|
+
} catch (_) { servers = []; }
|
|
28623
|
+
try {
|
|
28624
|
+
var rc = await apiFetch('/api/composio/toolkits');
|
|
28625
|
+
var dc = await rc.json();
|
|
28626
|
+
if (dc && dc.enabled !== false && Array.isArray(dc.toolkits)) {
|
|
28627
|
+
// Only show toolkits with at least one ACTIVE connection — those are
|
|
28628
|
+
// the ones the runtime can actually call. Auth-config-only toolkits
|
|
28629
|
+
// would fail tool calls, so showing them here would mislead.
|
|
28630
|
+
var connected = dc.toolkits.filter(function(t) {
|
|
28631
|
+
return Array.isArray(t.connections) && t.connections.some(function(c) { return c && c.status === 'ACTIVE'; });
|
|
28632
|
+
});
|
|
28633
|
+
var existingNames = new Set(servers.map(function(s) { return s.name; }));
|
|
28634
|
+
for (var i = 0; i < connected.length; i++) {
|
|
28635
|
+
var t = connected[i];
|
|
28636
|
+
if (existingNames.has(t.slug)) continue; // dedup — local config wins
|
|
28637
|
+
servers.push({
|
|
28638
|
+
name: t.slug,
|
|
28639
|
+
type: 'composio',
|
|
28640
|
+
description: t.description || (t.displayName + ' (via Composio)'),
|
|
28641
|
+
enabled: true,
|
|
28642
|
+
source: 'composio',
|
|
28643
|
+
_origin: 'composio',
|
|
28644
|
+
_displayName: t.displayName,
|
|
28645
|
+
_toolCount: t.toolCount,
|
|
28646
|
+
_connectionCount: (t.connections || []).filter(function(c) { return c && c.status === 'ACTIVE'; }).length,
|
|
28647
|
+
});
|
|
28648
|
+
}
|
|
28649
|
+
}
|
|
28650
|
+
} catch (_) { /* Composio not enabled / API down — picker still works */ }
|
|
28651
|
+
_mcpCatalog = { servers: servers };
|
|
28332
28652
|
return _mcpCatalog;
|
|
28333
28653
|
}
|
|
28334
28654
|
|
|
@@ -28373,14 +28693,50 @@ function renderSkillsPickerList() {
|
|
|
28373
28693
|
listEl.innerHTML = createRow + skills.slice(0, 50).map(function(s) {
|
|
28374
28694
|
var sel = _cronSelectedSkills.indexOf(s.name) !== -1;
|
|
28375
28695
|
var triggers = (s.triggers || []).slice(0, 4).join(', ');
|
|
28696
|
+
// 1.18.127 — pull preview metadata from the rich Skill record so the
|
|
28697
|
+
// picker shows what the user is about to pin: body line count, tool
|
|
28698
|
+
// count, useCount, lastUsed, and a stub warning if the body is < 5
|
|
28699
|
+
// lines (placeholder skill that shouldn't go to a critical task).
|
|
28700
|
+
var fm = s.frontmatter || {};
|
|
28701
|
+
var ext = (fm.clementine || {});
|
|
28702
|
+
var toolsAllow = (ext.tools && Array.isArray(ext.tools.allow)) ? ext.tools.allow : [];
|
|
28703
|
+
var bodyText = String(s.body || '');
|
|
28704
|
+
var bodyLines = bodyText ? bodyText.split('\\n').length : 0;
|
|
28705
|
+
var useCount = typeof ext.useCount === 'number' ? ext.useCount : 0;
|
|
28706
|
+
var lastUsedStr = ext.lastUsed || '';
|
|
28707
|
+
var lastUsedAgo = '';
|
|
28708
|
+
var ageDays = 999;
|
|
28709
|
+
if (lastUsedStr) {
|
|
28710
|
+
var lu = Date.parse(lastUsedStr);
|
|
28711
|
+
if (!isNaN(lu)) {
|
|
28712
|
+
ageDays = Math.floor((Date.now() - lu) / 86400000);
|
|
28713
|
+
lastUsedAgo = ageDays === 0 ? 'today' : ageDays === 1 ? 'yesterday' : ageDays + 'd ago';
|
|
28714
|
+
}
|
|
28715
|
+
}
|
|
28716
|
+
// Health pill: green (>=5 uses, fresh), yellow (untested), red (stale).
|
|
28717
|
+
var health, healthColor, healthLabel;
|
|
28718
|
+
if (useCount === 0) { health = 'untested'; healthColor = 'var(--yellow)'; healthLabel = 'never run'; }
|
|
28719
|
+
else if (useCount < 5) { health = 'untested'; healthColor = 'var(--yellow)'; healthLabel = useCount + 'x · light usage'; }
|
|
28720
|
+
else if (ageDays > 90) { health = 'stale'; healthColor = 'var(--red)'; healthLabel = useCount + 'x · stale (' + lastUsedAgo + ')'; }
|
|
28721
|
+
else { health = 'healthy'; healthColor = 'var(--green)'; healthLabel = useCount + 'x' + (lastUsedAgo ? ' · ' + lastUsedAgo : ''); }
|
|
28722
|
+
var stubWarn = bodyLines > 0 && bodyLines < 5
|
|
28723
|
+
? '<span style="color:var(--red);font-size:10px;margin-left:6px" title="Body is < 5 lines — likely a placeholder">⚠ stub</span>'
|
|
28724
|
+
: '';
|
|
28725
|
+
var metaPills = '<div class="cap-picker-row-meta" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:4px">'
|
|
28726
|
+
+ '<span style="color:var(--text-muted);font-size:11px">' + bodyLines + ' line' + (bodyLines === 1 ? '' : 's') + '</span>'
|
|
28727
|
+
+ (toolsAllow.length > 0 ? '<span style="color:var(--text-muted);font-size:11px" title="Tools the skill declares it needs">' + toolsAllow.length + ' tool' + (toolsAllow.length === 1 ? '' : 's') + '</span>' : '')
|
|
28728
|
+
+ '<span style="color:' + healthColor + ';font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em" title="Skill health based on useCount + lastUsed">● ' + healthLabel + '</span>'
|
|
28729
|
+
+ '</div>';
|
|
28376
28730
|
return '<div class="cap-picker-row' + (sel ? ' selected' : '') + '" onclick="addSkillToTrick(\\x27' + jsStr(s.name) + '\\x27)">'
|
|
28377
28731
|
+ '<div class="cap-picker-row-body">'
|
|
28378
28732
|
+ '<div class="cap-picker-row-title">' + esc(s.title || s.name) + ' '
|
|
28379
28733
|
+ '<span style="color:var(--text-muted);font-weight:normal;font-size:10px">' + esc(s.name) + '</span>'
|
|
28380
28734
|
+ (sel ? ' <span style="color:var(--accent);font-size:11px">✓ pinned</span>' : '')
|
|
28735
|
+
+ stubWarn
|
|
28381
28736
|
+ '</div>'
|
|
28382
28737
|
+ (s.description ? '<div class="cap-picker-row-desc">' + esc(s.description) + '</div>' : '')
|
|
28383
|
-
+
|
|
28738
|
+
+ metaPills
|
|
28739
|
+
+ (triggers ? '<div class="cap-picker-row-meta" style="margin-top:2px">triggers: ' + esc(triggers) + (s.triggers && s.triggers.length > 4 ? ' …' : '') + '</div>' : '')
|
|
28384
28740
|
+ '</div></div>';
|
|
28385
28741
|
}).join('');
|
|
28386
28742
|
}
|
|
@@ -28433,13 +28789,26 @@ function renderMcpPickerList() {
|
|
|
28433
28789
|
listEl.innerHTML = servers.slice(0, 50).map(function(s) {
|
|
28434
28790
|
var sel = _cronSelectedMcp.indexOf(s.name) !== -1;
|
|
28435
28791
|
var enabledTag = s.enabled === false ? ' <span style="color:var(--text-muted);font-size:10px">(disabled)</span>' : '';
|
|
28792
|
+
// 1.18.128 — distinct badge for Composio-sourced toolkits so users can
|
|
28793
|
+
// see at a glance which servers come from local config vs the
|
|
28794
|
+
// Composio account, plus a connection count for managed accounts.
|
|
28795
|
+
var sourceBadge = '';
|
|
28796
|
+
if (s._origin === 'composio') {
|
|
28797
|
+
var connTxt = s._connectionCount ? s._connectionCount + ' conn' : 'connected';
|
|
28798
|
+
sourceBadge = ' <span style="background:rgba(124,58,237,0.12);color:var(--purple);font-size:9px;padding:1px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em" title="Sourced from Composio account">composio</span>'
|
|
28799
|
+
+ ' <span style="color:var(--text-muted);font-size:10px">' + esc(connTxt) + '</span>';
|
|
28800
|
+
} else if (s.source && s.source !== 'auto-detected') {
|
|
28801
|
+
sourceBadge = ' <span style="background:var(--bg-tertiary);color:var(--text-muted);font-size:9px;padding:1px 6px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em">' + esc(s.source) + '</span>';
|
|
28802
|
+
}
|
|
28803
|
+
var displayName = s._displayName || s.name;
|
|
28436
28804
|
return '<div class="cap-picker-row mcp' + (sel ? ' selected' : '') + '" onclick="addMcpToTrick(\\x27' + jsStr(s.name) + '\\x27)">'
|
|
28437
28805
|
+ '<div class="cap-picker-row-body">'
|
|
28438
|
-
+ '<div class="cap-picker-row-title">' + esc(
|
|
28806
|
+
+ '<div class="cap-picker-row-title">' + esc(displayName)
|
|
28807
|
+
+ (displayName !== s.name ? ' <span style="color:var(--text-muted);font-weight:normal;font-size:10px">' + esc(s.name) + '</span>' : '')
|
|
28808
|
+
+ enabledTag + sourceBadge
|
|
28439
28809
|
+ (sel ? ' <span style="color:var(--purple);font-size:11px">✓ allowed</span>' : '')
|
|
28440
28810
|
+ '</div>'
|
|
28441
28811
|
+ (s.description ? '<div class="cap-picker-row-desc">' + esc(s.description) + '</div>' : '')
|
|
28442
|
-
+ (s.source ? '<div class="cap-picker-row-meta">source: ' + esc(s.source) + '</div>' : '')
|
|
28443
28812
|
+ '</div></div>';
|
|
28444
28813
|
}).join('');
|
|
28445
28814
|
}
|
|
@@ -29100,6 +29469,31 @@ async function enablePredictableFromBanner() {
|
|
|
29100
29469
|
}
|
|
29101
29470
|
}
|
|
29102
29471
|
|
|
29472
|
+
// 1.18.128 — open the cron modal pre-wired to a project. Called from the
|
|
29473
|
+
// "+ New task" button on each project card. Pre-fills cron-workdir and
|
|
29474
|
+
// suggests a name based on the project so the user only has to fill in
|
|
29475
|
+
// the prompt + schedule. The dropdown is populated by refreshProjects()
|
|
29476
|
+
// at page load, so the pre-filled value resolves cleanly to one of the
|
|
29477
|
+
// existing options.
|
|
29478
|
+
function openCronModalForProject(projectIdx) {
|
|
29479
|
+
var p = (typeof projectsData !== 'undefined' && Array.isArray(projectsData)) ? projectsData[projectIdx] : null;
|
|
29480
|
+
if (!p) { toast('Project not found.', 'error'); return; }
|
|
29481
|
+
openCreateCronModal();
|
|
29482
|
+
// Pre-set the project context. dropdown options were populated by
|
|
29483
|
+
// refreshProjects on page load, so the value matches one of them.
|
|
29484
|
+
var sel = document.getElementById('cron-workdir');
|
|
29485
|
+
if (sel) sel.value = p.path;
|
|
29486
|
+
// Suggest a task name based on the project — replaces non-slug chars
|
|
29487
|
+
// and truncates so the slug rule passes. User can override.
|
|
29488
|
+
var slugBase = (p.name || 'project').toLowerCase()
|
|
29489
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
29490
|
+
.replace(/^-+|-+$/g, '')
|
|
29491
|
+
.slice(0, 40);
|
|
29492
|
+
var nameInput = document.getElementById('cron-name');
|
|
29493
|
+
if (nameInput && !nameInput.value) nameInput.value = slugBase + '-task';
|
|
29494
|
+
toast('Project Context set to "' + (p.name || p.path) + '" — fill in the prompt and schedule.', 'info');
|
|
29495
|
+
}
|
|
29496
|
+
|
|
29103
29497
|
function openCreateCronModal(agentSlug) {
|
|
29104
29498
|
_cronAgentContext = agentSlug || '';
|
|
29105
29499
|
editingCronJob = null;
|
|
@@ -29381,12 +29775,46 @@ function renderCronPreview(d) {
|
|
|
29381
29775
|
if (!d.effectiveAllowedTools) {
|
|
29382
29776
|
html += '<div style="color:var(--text-muted);font-size:12px;font-style:italic">Inheriting from agent profile / SDK default — no per-trick tool restriction.</div>';
|
|
29383
29777
|
} else {
|
|
29384
|
-
|
|
29385
|
-
|
|
29778
|
+
// 1.18.127 — attribute each tool to the cron's own allowlist vs a
|
|
29779
|
+
// pinned-skill widening. The widenedFromSkills.tools list contains
|
|
29780
|
+
// entries that were ADDED by a skill on top of the cron's base list.
|
|
29781
|
+
var widenedTools = (d.widenedFromSkills && Array.isArray(d.widenedFromSkills.tools))
|
|
29782
|
+
? new Set(d.widenedFromSkills.tools) : new Set();
|
|
29783
|
+
html += '<div style="font-family:monospace;font-size:11px;line-height:1.7">';
|
|
29784
|
+
html += d.effectiveAllowedTools.map(function(t) {
|
|
29785
|
+
if (widenedTools.has(t)) {
|
|
29786
|
+
return '<span style="color:var(--purple)" title="Added by a pinned skill">' + esc(t) + ' <span style="font-style:italic;font-size:10px">(from skill)</span></span>';
|
|
29787
|
+
}
|
|
29788
|
+
return '<span style="color:var(--text-secondary)">' + esc(t) + '</span>';
|
|
29789
|
+
}).join(', ');
|
|
29386
29790
|
html += '</div>';
|
|
29791
|
+
if (widenedTools.size > 0) {
|
|
29792
|
+
html += '<div style="margin-top:8px;padding:8px 10px;border-radius:6px;background:var(--bg-tertiary);font-size:11px;color:var(--text-muted);line-height:1.5">'
|
|
29793
|
+
+ '<strong>Pinned-skill widening (1.18.125):</strong> a pinned skill\\'s <code>clementine.tools.allow</code> declaration added '
|
|
29794
|
+
+ widenedTools.size + ' tool' + (widenedTools.size === 1 ? '' : 's') + ' on top of this task\\'s base allowlist.'
|
|
29795
|
+
+ '</div>';
|
|
29796
|
+
}
|
|
29387
29797
|
}
|
|
29388
29798
|
html += '</div>';
|
|
29389
29799
|
|
|
29800
|
+
// Widened MCP servers from pinned-skill bodies (1.18.127)
|
|
29801
|
+
// Skills that reference mcp__server__tool tokens in their body implicitly
|
|
29802
|
+
// need that server live; surface those widenings here so the user sees
|
|
29803
|
+
// exactly which MCP servers got pulled in by the skills.
|
|
29804
|
+
if (d.widenedFromSkills && Array.isArray(d.widenedFromSkills.mcpServers) && d.widenedFromSkills.mcpServers.length > 0) {
|
|
29805
|
+
html += '<div class="preview-section">';
|
|
29806
|
+
html += '<h4>MCP servers widened by skill bodies</h4>';
|
|
29807
|
+
html += '<div style="font-size:12px;color:var(--text-secondary);line-height:1.6;margin-bottom:6px">';
|
|
29808
|
+
html += 'These MCP servers got attached because a pinned skill\\'s body references <code>mcp__server__tool</code> tokens.';
|
|
29809
|
+
html += '</div>';
|
|
29810
|
+
html += '<div>';
|
|
29811
|
+
for (var wj = 0; wj < d.widenedFromSkills.mcpServers.length; wj++) {
|
|
29812
|
+
html += '<span class="preview-chip pinned" style="color:var(--purple)" title="Pulled in by skill body MCP reference">' + esc(d.widenedFromSkills.mcpServers[wj]) + '</span>';
|
|
29813
|
+
}
|
|
29814
|
+
html += '</div>';
|
|
29815
|
+
html += '</div>';
|
|
29816
|
+
}
|
|
29817
|
+
|
|
29390
29818
|
// Built prompt (what the agent literally receives)
|
|
29391
29819
|
html += '<div class="preview-section">';
|
|
29392
29820
|
html += '<h4>Built prompt <span style="font-weight:normal;color:var(--text-muted)">(' + d.builtPrompt.length + ' chars — what the agent receives verbatim)</span></h4>';
|
|
@@ -34567,6 +34995,119 @@ function openStartUnleashedTask() {
|
|
|
34567
34995
|
toast('Open a scheduled task, set Mode to Unleashed, then run it.', 'info');
|
|
34568
34996
|
}
|
|
34569
34997
|
|
|
34998
|
+
// ── MEMORY.md inline editor (1.18.127) ──────────────────────────────
|
|
34999
|
+
// Three globals and four handlers. Globals are intentionally on window
|
|
35000
|
+
// (not module-scoped) so the inline onclick / onchange handlers in the
|
|
35001
|
+
// markup can reach them — this dashboard is one big inline-script SPA.
|
|
35002
|
+
window._memoryMdLoadedMtime = 0;
|
|
35003
|
+
window._memoryMdDirty = false;
|
|
35004
|
+
window._memoryMdSaving = false;
|
|
35005
|
+
|
|
35006
|
+
async function loadMemoryMdEditor() {
|
|
35007
|
+
var scopeEl = document.getElementById('memory-md-scope');
|
|
35008
|
+
var ed = document.getElementById('memory-md-editor');
|
|
35009
|
+
var statusEl = document.getElementById('memory-md-status');
|
|
35010
|
+
var saveBtn = document.getElementById('memory-md-save-btn');
|
|
35011
|
+
var pathEl = document.getElementById('memory-md-path');
|
|
35012
|
+
if (!ed || !statusEl || !saveBtn) return;
|
|
35013
|
+
|
|
35014
|
+
// Populate the scope dropdown once (global + every hired agent).
|
|
35015
|
+
if (scopeEl && scopeEl.options.length <= 1) {
|
|
35016
|
+
try {
|
|
35017
|
+
var ar = await apiFetch('/api/agents');
|
|
35018
|
+
var ad = await ar.json();
|
|
35019
|
+
var agents = (ad && Array.isArray(ad.agents)) ? ad.agents : [];
|
|
35020
|
+
for (var i = 0; i < agents.length; i++) {
|
|
35021
|
+
var slug = agents[i].slug || agents[i].name;
|
|
35022
|
+
if (!slug) continue;
|
|
35023
|
+
var opt = document.createElement('option');
|
|
35024
|
+
opt.value = slug;
|
|
35025
|
+
opt.textContent = (agents[i].name || slug) + ' (' + slug + ')';
|
|
35026
|
+
scopeEl.appendChild(opt);
|
|
35027
|
+
}
|
|
35028
|
+
} catch (_) { /* fallback: global only */ }
|
|
35029
|
+
}
|
|
35030
|
+
|
|
35031
|
+
var scope = scopeEl ? scopeEl.value : 'global';
|
|
35032
|
+
statusEl.textContent = 'Loading…';
|
|
35033
|
+
saveBtn.disabled = true;
|
|
35034
|
+
try {
|
|
35035
|
+
var url = '/api/memory/md' + (scope && scope !== 'global' ? '?agent=' + encodeURIComponent(scope) : '');
|
|
35036
|
+
var r = await apiFetch(url);
|
|
35037
|
+
var d = await r.json();
|
|
35038
|
+
if (!r.ok) {
|
|
35039
|
+
statusEl.textContent = 'Failed to load: ' + (d.error || r.status);
|
|
35040
|
+
return;
|
|
35041
|
+
}
|
|
35042
|
+
ed.value = d.content || '';
|
|
35043
|
+
window._memoryMdLoadedMtime = d.mtimeMs || 0;
|
|
35044
|
+
window._memoryMdDirty = false;
|
|
35045
|
+
if (pathEl) pathEl.textContent = d.filePath || '';
|
|
35046
|
+
var existsMsg = d.exists ? '' : ' (file not yet on disk — will be created on save)';
|
|
35047
|
+
statusEl.textContent = scope === 'global' ? 'Global MEMORY.md loaded' + existsMsg : 'MEMORY.md for ' + scope + ' loaded' + existsMsg;
|
|
35048
|
+
updateMemoryMdCounter();
|
|
35049
|
+
} catch (err) {
|
|
35050
|
+
statusEl.textContent = 'Failed to load: ' + (err && err.message || err);
|
|
35051
|
+
}
|
|
35052
|
+
}
|
|
35053
|
+
|
|
35054
|
+
function updateMemoryMdCounter() {
|
|
35055
|
+
var ed = document.getElementById('memory-md-editor');
|
|
35056
|
+
var counter = document.getElementById('memory-md-counter');
|
|
35057
|
+
if (!ed || !counter) return;
|
|
35058
|
+
var n = (ed.value || '').length;
|
|
35059
|
+
counter.textContent = n.toLocaleString() + ' chars' + (window._memoryMdDirty ? ' · unsaved' : '');
|
|
35060
|
+
}
|
|
35061
|
+
|
|
35062
|
+
function onMemoryMdInput() {
|
|
35063
|
+
window._memoryMdDirty = true;
|
|
35064
|
+
var saveBtn = document.getElementById('memory-md-save-btn');
|
|
35065
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
35066
|
+
updateMemoryMdCounter();
|
|
35067
|
+
}
|
|
35068
|
+
|
|
35069
|
+
async function saveMemoryMd() {
|
|
35070
|
+
if (window._memoryMdSaving) return;
|
|
35071
|
+
var ed = document.getElementById('memory-md-editor');
|
|
35072
|
+
var scopeEl = document.getElementById('memory-md-scope');
|
|
35073
|
+
var statusEl = document.getElementById('memory-md-status');
|
|
35074
|
+
var saveBtn = document.getElementById('memory-md-save-btn');
|
|
35075
|
+
if (!ed || !statusEl || !saveBtn) return;
|
|
35076
|
+
var scope = scopeEl ? scopeEl.value : 'global';
|
|
35077
|
+
window._memoryMdSaving = true;
|
|
35078
|
+
saveBtn.disabled = true;
|
|
35079
|
+
statusEl.textContent = 'Saving…';
|
|
35080
|
+
try {
|
|
35081
|
+
var url = '/api/memory/md' + (scope && scope !== 'global' ? '?agent=' + encodeURIComponent(scope) : '');
|
|
35082
|
+
var r = await apiFetch(url, {
|
|
35083
|
+
method: 'PUT',
|
|
35084
|
+
headers: { 'content-type': 'application/json' },
|
|
35085
|
+
body: JSON.stringify({ content: ed.value, expectedMtimeMs: window._memoryMdLoadedMtime }),
|
|
35086
|
+
});
|
|
35087
|
+
var d = await r.json();
|
|
35088
|
+
if (r.status === 409) {
|
|
35089
|
+
statusEl.textContent = 'Conflict: ' + (d.error || 'file changed on disk');
|
|
35090
|
+
toast('MEMORY.md was modified outside the dashboard. Click Reload to see the latest version.', 'warn');
|
|
35091
|
+
return;
|
|
35092
|
+
}
|
|
35093
|
+
if (!r.ok) {
|
|
35094
|
+
statusEl.textContent = 'Save failed: ' + (d.error || r.status);
|
|
35095
|
+
toast('Save failed: ' + (d.error || r.status), 'error');
|
|
35096
|
+
return;
|
|
35097
|
+
}
|
|
35098
|
+
window._memoryMdLoadedMtime = d.mtimeMs || 0;
|
|
35099
|
+
window._memoryMdDirty = false;
|
|
35100
|
+
statusEl.textContent = 'Saved · ' + new Date().toLocaleTimeString();
|
|
35101
|
+
updateMemoryMdCounter();
|
|
35102
|
+
toast('MEMORY.md saved', 'success');
|
|
35103
|
+
} catch (err) {
|
|
35104
|
+
statusEl.textContent = 'Save failed: ' + (err && err.message || err);
|
|
35105
|
+
toast('Save failed: ' + (err && err.message || err), 'error');
|
|
35106
|
+
} finally {
|
|
35107
|
+
window._memoryMdSaving = false;
|
|
35108
|
+
}
|
|
35109
|
+
}
|
|
35110
|
+
|
|
34570
35111
|
async function refreshMemoryHealth() {
|
|
34571
35112
|
var el = document.getElementById('memory-health-content');
|
|
34572
35113
|
if (!el) return;
|
|
@@ -39491,6 +40032,27 @@ try {
|
|
|
39491
40032
|
toast('Daemon restarted \u2014 refreshing data...', 'info');
|
|
39492
40033
|
setTimeout(function() { refreshAll(); }, 1500);
|
|
39493
40034
|
}
|
|
40035
|
+
// 1.18.127 \u2014 auto-extraction visibility. Surface a small toast when
|
|
40036
|
+
// the background memory extractor writes a fact / note / task /
|
|
40037
|
+
// user_model slot. Respects a localStorage "silent learning" toggle.
|
|
40038
|
+
if (evt.type === 'memory_extracted') {
|
|
40039
|
+
try {
|
|
40040
|
+
if (localStorage.getItem('clem-silent-learning') === '1') return;
|
|
40041
|
+
var d = evt.data || {};
|
|
40042
|
+
var summary = d.summary || '';
|
|
40043
|
+
var label = '';
|
|
40044
|
+
switch (d.toolName) {
|
|
40045
|
+
case 'memory_write': label = '\ud83d\udcdd Noted'; break;
|
|
40046
|
+
case 'note_create': label = '\ud83d\udcc4 Note created'; break;
|
|
40047
|
+
case 'note_take': label = '\ud83d\udcdd Note saved'; break;
|
|
40048
|
+
case 'task_add': label = '\u2705 Task added'; break;
|
|
40049
|
+
case 'user_model': label = '\ud83e\udde0 Updated user model'; break;
|
|
40050
|
+
default: label = '\ud83d\udcdd Memory updated';
|
|
40051
|
+
}
|
|
40052
|
+
var msg = summary ? label + ': ' + summary : label;
|
|
40053
|
+
toast(msg, 'info');
|
|
40054
|
+
} catch (e) { /* non-fatal */ }
|
|
40055
|
+
}
|
|
39494
40056
|
if (evt.type === 'builder') {
|
|
39495
40057
|
try { _handleBuilderEvent(evt.data); } catch(e) { /* non-fatal */ }
|
|
39496
40058
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clementine-agent",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.128",
|
|
4
4
|
"description": "Clementine — Personal AI Assistant (TypeScript)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"postinstall": "node scripts/postinstall.js 2>/dev/null || true"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
31
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.137",
|
|
32
32
|
"@anthropic-ai/sdk": "^0.91.0",
|
|
33
33
|
"@composio/claude-agent-sdk": "^0.8.1",
|
|
34
34
|
"@composio/core": "^0.8.1",
|