clementine-agent 1.18.140 → 1.18.142
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/cli/dashboard.js +62 -10
- package/dist/cli/routes/workflows.js +99 -1
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -19130,7 +19130,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19130
19130
|
<button class="active" data-icon="layoutDashboard" onclick="switchTab('intelligence','overview')"><span class="icon-slot"></span> Overview</button>
|
|
19131
19131
|
<button data-icon="database" onclick="switchTab('intelligence','search')"><span class="icon-slot"></span> Chunks</button>
|
|
19132
19132
|
<button data-icon="upload" onclick="switchTab('intelligence','seed')"><span class="icon-slot"></span> Seed</button>
|
|
19133
|
-
<button data-icon="repeat" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span>
|
|
19133
|
+
<button data-icon="repeat" onclick="switchTab('intelligence','sources')"><span class="icon-slot"></span> Feeds</button>
|
|
19134
19134
|
<button data-icon="listChecks" onclick="switchTab('intelligence','runs')"><span class="icon-slot"></span> Runs</button>
|
|
19135
19135
|
<button data-icon="sparkles" onclick="switchTab('intelligence','graph')"><span class="icon-slot"></span> Knowledge</button>
|
|
19136
19136
|
<button data-icon="fileText" onclick="switchTab('intelligence','files')"><span class="icon-slot"></span> Memory</button>
|
|
@@ -19150,7 +19150,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19150
19150
|
</div>
|
|
19151
19151
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
19152
19152
|
<button class="btn-primary btn-sm" onclick="switchTab('intelligence','seed')"><span class="icon-slot" data-icon="upload"></span> Seed local data</button>
|
|
19153
|
-
<button class="btn-sm" onclick="switchTab('intelligence','sources')"><span class="icon-slot" data-icon="repeat"></span> Add
|
|
19153
|
+
<button class="btn-sm" onclick="switchTab('intelligence','sources')"><span class="icon-slot" data-icon="repeat"></span> Add a feed</button>
|
|
19154
19154
|
<button class="btn-sm" onclick="switchTab('intelligence','health')"><span class="icon-slot" data-icon="activity"></span> Verify health</button>
|
|
19155
19155
|
</div>
|
|
19156
19156
|
</div>
|
|
@@ -19411,9 +19411,18 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19411
19411
|
</div>
|
|
19412
19412
|
</div>
|
|
19413
19413
|
|
|
19414
|
-
<!-- Sources -->
|
|
19414
|
+
<!-- Feeds (formerly "Sources" tab — renamed 1.18.141 to match user mental model) -->
|
|
19415
19415
|
<div class="tab-pane" id="tab-intelligence-sources">
|
|
19416
19416
|
|
|
19417
|
+
<!-- Section header — explains what Feeds are vs. one-shot Seed uploads -->
|
|
19418
|
+
<div style="margin-bottom:14px">
|
|
19419
|
+
<div style="font-size:18px;font-weight:600;margin-bottom:4px">Feeds</div>
|
|
19420
|
+
<div style="color:var(--muted);font-size:13px;line-height:1.5">
|
|
19421
|
+
Auto-watched external sources Clementine polls on a schedule — Google Drive folders, connected apps via Composio, Claude Desktop connectors, REST endpoints. Each feed fetches records, dedupes against existing memory, and writes distilled notes to the brain.<br>
|
|
19422
|
+
<span style="opacity:0.85">For one-shot uploads (drop a file, choose a folder), use the <a href="#" onclick="switchTab('intelligence','seed');return false" style="text-decoration:underline">Seed</a> tab instead.</span>
|
|
19423
|
+
</div>
|
|
19424
|
+
</div>
|
|
19425
|
+
|
|
19417
19426
|
<!-- ═══ Auto-seed feeds (connected tools → cron → brain) ═══ -->
|
|
19418
19427
|
<div class="card" style="padding:16px;margin-bottom:16px">
|
|
19419
19428
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
@@ -19440,11 +19449,17 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19440
19449
|
</div>
|
|
19441
19450
|
</div>
|
|
19442
19451
|
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19452
|
+
<!-- Legacy direct-integration paths (REST polls + webhooks). De-emphasized 1.18.141 -->
|
|
19453
|
+
<!-- so the modern "Add feed" flow above stays the primary surface. The forms below -->
|
|
19454
|
+
<!-- are still wired (write to /api/brain/sources) for users who need raw HTTP plumbing. -->
|
|
19455
|
+
<details style="margin-bottom:16px">
|
|
19456
|
+
<summary style="color:var(--muted);font-size:13px;cursor:pointer;padding:8px 0">Advanced — manual integrations (REST polls, webhooks, credentials)</summary>
|
|
19457
|
+
<div style="display:flex;gap:8px;margin-top:8px;margin-bottom:8px;flex-wrap:wrap">
|
|
19458
|
+
<button class="btn-primary" onclick="brainShowPollForm()">+ Scheduled REST poll</button>
|
|
19459
|
+
<button class="btn-primary" onclick="brainShowWebhookForm()">+ Inbound webhook</button>
|
|
19460
|
+
<button class="btn" onclick="brainShowCredsForm()">🔑 Credentials</button>
|
|
19461
|
+
</div>
|
|
19462
|
+
</details>
|
|
19448
19463
|
|
|
19449
19464
|
<!-- Webhook form -->
|
|
19450
19465
|
<div id="brain-webhook-form" class="card" style="display:none;padding:16px;margin-bottom:16px">
|
|
@@ -39294,11 +39309,23 @@ async function refreshWorkflows() {
|
|
|
39294
39309
|
var r = await apiFetch('/api/workflows');
|
|
39295
39310
|
var data = await r.json();
|
|
39296
39311
|
var workflows = data.workflows || [];
|
|
39312
|
+
// 1.18.142 — Soft-deprecation banner. Always shown when there's at least
|
|
39313
|
+
// one workflow on disk, hidden when the user has fully migrated. New
|
|
39314
|
+
// users who never had workflows in the first place see a different empty
|
|
39315
|
+
// state pointing them at Skills.
|
|
39316
|
+
var banner = '';
|
|
39317
|
+
if (workflows.length > 0) {
|
|
39318
|
+
banner = '<div class="card" style="margin-bottom:14px;padding:12px 14px;background:var(--accent-glow);border-left:3px solid var(--accent)">' +
|
|
39319
|
+
'<div style="font-weight:600;margin-bottom:4px">Workflows are being phased out → Skills</div>' +
|
|
39320
|
+
'<div style="font-size:13px;color:var(--text-secondary);line-height:1.5">' +
|
|
39321
|
+
'Skills cover the same use cases with better composability and the new builder. Use the <strong>Migrate</strong> button on any workflow row below to convert it into a vanilla Anthropic skill folder. Your existing workflows keep firing until you migrate them — nothing breaks.' +
|
|
39322
|
+
'</div></div>';
|
|
39323
|
+
}
|
|
39297
39324
|
if (workflows.length === 0) {
|
|
39298
|
-
containers.forEach(function(c) { c.innerHTML = '<div class="empty-state">No workflows defined.
|
|
39325
|
+
containers.forEach(function(c) { c.innerHTML = '<div class="empty-state">No workflows defined. <strong>Skills are the way forward</strong> — create one from the Skills tab. Workflow .md files in vault/00-System/workflows/ still work for legacy setups.</div>'; });
|
|
39299
39326
|
return;
|
|
39300
39327
|
}
|
|
39301
|
-
var html =
|
|
39328
|
+
var html = banner;
|
|
39302
39329
|
workflows.forEach(function(wf) {
|
|
39303
39330
|
var triggerLabel = wf.trigger && wf.trigger.schedule ? describeCron(wf.trigger.schedule) || wf.trigger.schedule : 'Manual only';
|
|
39304
39331
|
var stepCount = wf.steps ? wf.steps.length : 0;
|
|
@@ -39309,6 +39336,7 @@ async function refreshWorkflows() {
|
|
|
39309
39336
|
html += '<span class="badge ' + (wf.enabled ? 'badge-green' : 'badge-gray') + '" style="font-size:10px">' + (wf.enabled ? 'Enabled' : 'Disabled') + '</span>';
|
|
39310
39337
|
html += '<button class="btn btn-sm" onclick="runWorkflow(\\x27' + esc(wf.name) + '\\x27)" style="font-size:10px;color:var(--green)">Run</button>';
|
|
39311
39338
|
html += '<button class="btn btn-sm" onclick="showWorkflowRuns(\\x27' + esc(wf.name) + '\\x27)" style="font-size:10px">History</button>';
|
|
39339
|
+
html += '<button class="btn btn-sm" onclick="migrateWorkflowToSkill(\\x27' + esc(wf.name) + '\\x27)" style="font-size:10px;color:var(--accent)" title="Convert this workflow into a vanilla Anthropic skill folder. Original is renamed to .md.migrated, kept on disk for rollback.">Migrate → Skill</button>';
|
|
39312
39340
|
html += '</div>';
|
|
39313
39341
|
html += '<div class="card-body">';
|
|
39314
39342
|
if (wf.description) html += '<div style="font-size:13px;color:var(--text-secondary);margin-bottom:8px">' + esc(wf.description) + '</div>';
|
|
@@ -39352,6 +39380,30 @@ async function runWorkflow(name) {
|
|
|
39352
39380
|
} catch(e) { toast(String(e), 'error'); }
|
|
39353
39381
|
}
|
|
39354
39382
|
|
|
39383
|
+
// 1.18.142 — Convert a workflow into a vanilla skill folder, then refresh
|
|
39384
|
+
// the list so the migrated workflow disappears (its .md is renamed to
|
|
39385
|
+
// .md.migrated server-side and parseAllWorkflows stops picking it up).
|
|
39386
|
+
async function migrateWorkflowToSkill(name) {
|
|
39387
|
+
if (!confirm('Migrate "' + name + '" into a skill folder?\n\n' +
|
|
39388
|
+
'• A new skill will be created at vault/00-System/skills/<slug>/SKILL.md\n' +
|
|
39389
|
+
'• The original workflow .md will be renamed to .md.migrated (kept for rollback)\n' +
|
|
39390
|
+
'• The workflow stops firing; the skill is ready to schedule from the Skills tab')) return;
|
|
39391
|
+
try {
|
|
39392
|
+
var r = await apiFetch('/api/workflows/' + encodeURIComponent(name) + '/migrate-to-skill', {
|
|
39393
|
+
method: 'POST',
|
|
39394
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39395
|
+
body: JSON.stringify({}),
|
|
39396
|
+
});
|
|
39397
|
+
var d = await r.json();
|
|
39398
|
+
if (d.ok) {
|
|
39399
|
+
toast('Migrated "' + name + '" → skill "' + (d.skill && d.skill.name || '?') + '"' + (d.warning ? ' (' + d.warning + ')' : ''));
|
|
39400
|
+
refreshWorkflows();
|
|
39401
|
+
} else {
|
|
39402
|
+
toast(d.error || 'Migration failed', 'error');
|
|
39403
|
+
}
|
|
39404
|
+
} catch(e) { toast(String(e), 'error'); }
|
|
39405
|
+
}
|
|
39406
|
+
|
|
39355
39407
|
async function showWorkflowRuns(name) {
|
|
39356
39408
|
var safeId = 'wf-runs-' + name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
39357
39409
|
var panel = document.getElementById(safeId);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import express from 'express';
|
|
6
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, renameSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
export function workflowsRouter(deps) {
|
|
9
9
|
const router = Router();
|
|
@@ -68,6 +68,104 @@ export function workflowsRouter(deps) {
|
|
|
68
68
|
res.status(500).json({ ok: false, error: String(e) });
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
|
+
/**
|
|
72
|
+
* 1.18.142 — Migrate a workflow into a vanilla Anthropic skill folder.
|
|
73
|
+
*
|
|
74
|
+
* Skills are subsuming workflows. To let users move at their own pace
|
|
75
|
+
* without breaking the workflow runtime, this endpoint converts ONE
|
|
76
|
+
* workflow at a time and renames the original .md → .md.migrated so
|
|
77
|
+
* the workflow runner stops picking it up but the file is still on
|
|
78
|
+
* disk for rollback.
|
|
79
|
+
*
|
|
80
|
+
* Conversion: workflow.name → skill name (slugified to Anthropic
|
|
81
|
+
* regex), workflow.description → skill description, raw workflow
|
|
82
|
+
* markdown body → skill procedure body (steps + synthesis prompt are
|
|
83
|
+
* preserved as documentation; runtime is the SDK reading the body).
|
|
84
|
+
* Frontmatter is rebuilt from scratch by writeSkill, so legacy
|
|
85
|
+
* workflow YAML doesn't leak into the new skill.
|
|
86
|
+
*/
|
|
87
|
+
router.post('/:name/migrate-to-skill', express.json(), async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const name = decodeURIComponent(req.params.name);
|
|
90
|
+
const { parseAllWorkflows } = await import('../../agent/workflow-runner.js');
|
|
91
|
+
const { writeSkill } = await import('../../agent/skill-store.js');
|
|
92
|
+
const matter = (await import('gray-matter')).default;
|
|
93
|
+
// Find the workflow across global + agent scopes
|
|
94
|
+
const candidates = [];
|
|
95
|
+
if (existsSync(workflowsDir)) {
|
|
96
|
+
for (const wf of parseAllWorkflows(workflowsDir)) {
|
|
97
|
+
if (wf.name === name)
|
|
98
|
+
candidates.push({ wf });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (existsSync(agentsBase)) {
|
|
102
|
+
for (const slug of readdirSync(agentsBase).filter(d => !d.startsWith('_'))) {
|
|
103
|
+
const wfDir = path.join(agentsBase, slug, 'workflows');
|
|
104
|
+
if (!existsSync(wfDir))
|
|
105
|
+
continue;
|
|
106
|
+
for (const wf of parseAllWorkflows(wfDir)) {
|
|
107
|
+
if (wf.name === name)
|
|
108
|
+
candidates.push({ wf, agentSlug: slug });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (candidates.length === 0) {
|
|
113
|
+
res.status(404).json({ ok: false, error: 'Workflow not found: ' + name });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { wf, agentSlug } = candidates[0];
|
|
117
|
+
// Slugify the workflow name to the Anthropic regex
|
|
118
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
119
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) {
|
|
120
|
+
res.status(400).json({ ok: false, error: `Workflow name "${name}" cannot be slugified to Anthropic regex` });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Build the skill body from the workflow's raw markdown body. The
|
|
124
|
+
// workflow runner's frontmatter is dropped — writeSkill rebuilds
|
|
125
|
+
// a clementine.* block from scratch. Steps and synthesis live as
|
|
126
|
+
// markdown for the SDK to read directly.
|
|
127
|
+
const raw = readFileSync(wf.sourceFile, 'utf-8');
|
|
128
|
+
const parsed = matter(raw);
|
|
129
|
+
const body = parsed.content.trim() || `Migrated from workflow "${name}". Add a procedure here.`;
|
|
130
|
+
const description = wf.description || `Migrated from workflow ${name}`;
|
|
131
|
+
let written;
|
|
132
|
+
try {
|
|
133
|
+
written = writeSkill({
|
|
134
|
+
name: slug,
|
|
135
|
+
title: wf.name,
|
|
136
|
+
description,
|
|
137
|
+
body,
|
|
138
|
+
source: 'imported',
|
|
139
|
+
agentSlug,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
res.status(409).json({ ok: false, error: String(err instanceof Error ? err.message : err) });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Rename the original .md → .md.migrated so parseAllWorkflows
|
|
147
|
+
// stops picking it up. File stays on disk for rollback.
|
|
148
|
+
const migratedPath = wf.sourceFile + '.migrated';
|
|
149
|
+
try {
|
|
150
|
+
renameSync(wf.sourceFile, migratedPath);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Skill is already written; surface the rename failure but don't
|
|
154
|
+
// roll back — the user can manually delete the .md if needed.
|
|
155
|
+
res.json({
|
|
156
|
+
ok: true,
|
|
157
|
+
skill: written,
|
|
158
|
+
warning: `Skill created but original workflow file rename failed: ${String(err)}`,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
broadcastEvent({ type: 'workflow_migrated', data: { workflowName: name, skillSlug: slug, agentSlug } });
|
|
163
|
+
res.json({ ok: true, skill: written, originalRenamedTo: migratedPath });
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
res.status(500).json({ ok: false, error: String(e) });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
71
169
|
router.get('/:name/runs', (_req, res) => {
|
|
72
170
|
try {
|
|
73
171
|
const name = decodeURIComponent(_req.params.name);
|