clementine-agent 1.18.116 → 1.18.118
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 +505 -60
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -10118,43 +10118,156 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
10118
10118
|
res.status(500).json({ error: String(err) });
|
|
10119
10119
|
}
|
|
10120
10120
|
});
|
|
10121
|
+
// POST /api/skills — create a new folder-form skill (1.18.115).
|
|
10122
|
+
// Anthropic-compatible: top-level `name` + `description` required; the
|
|
10123
|
+
// optional `tools` array goes under `clementine.tools.allow` so the
|
|
10124
|
+
// frontmatter parses cleanly in vanilla Claude Agent SDK while still
|
|
10125
|
+
// letting the cron runtime enforce the allowlist.
|
|
10121
10126
|
app.post('/api/skills', (req, res) => {
|
|
10122
10127
|
try {
|
|
10123
|
-
const { title, description,
|
|
10124
|
-
if (!
|
|
10125
|
-
res.status(400).json({ error: '
|
|
10128
|
+
const { name, title, description, body, tools } = req.body ?? {};
|
|
10129
|
+
if (!name || typeof name !== 'string') {
|
|
10130
|
+
res.status(400).json({ error: 'name is required' });
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
|
|
10134
|
+
res.status(400).json({ error: 'name must match ^[a-z0-9][a-z0-9-]{0,63}$ (Anthropic spec)' });
|
|
10135
|
+
return;
|
|
10136
|
+
}
|
|
10137
|
+
if (!description || typeof description !== 'string') {
|
|
10138
|
+
res.status(400).json({ error: 'description is required' });
|
|
10139
|
+
return;
|
|
10140
|
+
}
|
|
10141
|
+
if (description.length > 1024) {
|
|
10142
|
+
res.status(400).json({ error: 'description must be ≤ 1024 chars (Anthropic spec)' });
|
|
10143
|
+
return;
|
|
10144
|
+
}
|
|
10145
|
+
if (!body || typeof body !== 'string' || !body.trim()) {
|
|
10146
|
+
res.status(400).json({ error: 'body is required' });
|
|
10126
10147
|
return;
|
|
10127
10148
|
}
|
|
10128
10149
|
const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10129
10150
|
if (!existsSync(skillsDir))
|
|
10130
10151
|
mkdirSync(skillsDir, { recursive: true });
|
|
10131
|
-
const
|
|
10152
|
+
const folderPath = path.join(skillsDir, name);
|
|
10153
|
+
const entryPath = path.join(folderPath, 'SKILL.md');
|
|
10154
|
+
if (existsSync(entryPath)) {
|
|
10155
|
+
res.status(409).json({ error: 'Skill "' + name + '" already exists. Use PUT to update.' });
|
|
10156
|
+
return;
|
|
10157
|
+
}
|
|
10158
|
+
mkdirSync(folderPath, { recursive: true });
|
|
10132
10159
|
const now = new Date().toISOString();
|
|
10133
|
-
const
|
|
10160
|
+
const fm = { name, description };
|
|
10161
|
+
if (title && typeof title === 'string' && title.trim())
|
|
10162
|
+
fm.title = title.trim();
|
|
10163
|
+
const allowed = Array.isArray(tools) ? tools.map(String).map(s => s.trim()).filter(Boolean) : [];
|
|
10164
|
+
const clementineExt = {
|
|
10165
|
+
source: 'manual',
|
|
10166
|
+
useCount: 0,
|
|
10167
|
+
createdAt: now,
|
|
10168
|
+
updatedAt: now,
|
|
10169
|
+
version: 1,
|
|
10170
|
+
};
|
|
10171
|
+
if (allowed.length > 0)
|
|
10172
|
+
clementineExt.tools = { allow: allowed };
|
|
10173
|
+
fm.clementine = clementineExt;
|
|
10134
10174
|
const matterMod = require('gray-matter');
|
|
10135
|
-
const content = matterMod.stringify(
|
|
10136
|
-
writeFileSync(
|
|
10137
|
-
res.json({ ok: true, name });
|
|
10175
|
+
const content = matterMod.stringify(body.endsWith('\n') ? body : body + '\n', fm);
|
|
10176
|
+
writeFileSync(entryPath, content);
|
|
10177
|
+
res.json({ ok: true, name, layout: 'folder', filePath: entryPath });
|
|
10138
10178
|
}
|
|
10139
10179
|
catch (err) {
|
|
10140
10180
|
res.status(500).json({ error: String(err) });
|
|
10141
10181
|
}
|
|
10142
10182
|
});
|
|
10183
|
+
// PUT /api/skills/:name — update an existing skill (1.18.115).
|
|
10184
|
+
// Folder-aware: writes back to <name>/SKILL.md when the skill is in
|
|
10185
|
+
// folder layout, otherwise updates the flat <name>.md (preserves the
|
|
10186
|
+
// existing layout — we don't auto-migrate on edit; users go through the
|
|
10187
|
+
// Migrate flow when they're ready).
|
|
10188
|
+
app.put('/api/skills/:name', (req, res) => {
|
|
10189
|
+
try {
|
|
10190
|
+
const skillName = req.params.name;
|
|
10191
|
+
const { title, description, body, tools } = req.body ?? {};
|
|
10192
|
+
if (!description || typeof description !== 'string') {
|
|
10193
|
+
res.status(400).json({ error: 'description is required' });
|
|
10194
|
+
return;
|
|
10195
|
+
}
|
|
10196
|
+
if (description.length > 1024) {
|
|
10197
|
+
res.status(400).json({ error: 'description must be ≤ 1024 chars (Anthropic spec)' });
|
|
10198
|
+
return;
|
|
10199
|
+
}
|
|
10200
|
+
if (!body || typeof body !== 'string' || !body.trim()) {
|
|
10201
|
+
res.status(400).json({ error: 'body is required' });
|
|
10202
|
+
return;
|
|
10203
|
+
}
|
|
10204
|
+
const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10205
|
+
const folderEntry = path.join(skillsDir, skillName, 'SKILL.md');
|
|
10206
|
+
const flatEntry = path.join(skillsDir, skillName + '.md');
|
|
10207
|
+
const targetPath = existsSync(folderEntry) ? folderEntry : (existsSync(flatEntry) ? flatEntry : null);
|
|
10208
|
+
if (!targetPath) {
|
|
10209
|
+
res.status(404).json({ error: 'Skill "' + skillName + '" not found' });
|
|
10210
|
+
return;
|
|
10211
|
+
}
|
|
10212
|
+
// Preserve existing frontmatter (esp. clementine namespace fields like
|
|
10213
|
+
// useCount, createdAt, migration provenance) and only merge in updates.
|
|
10214
|
+
const matterMod = require('gray-matter');
|
|
10215
|
+
const existingRaw = readFileSync(targetPath, 'utf-8');
|
|
10216
|
+
const parsed = matterMod(existingRaw);
|
|
10217
|
+
const fm = { ...parsed.data };
|
|
10218
|
+
fm.name = skillName;
|
|
10219
|
+
fm.description = description;
|
|
10220
|
+
if (title && typeof title === 'string' && title.trim())
|
|
10221
|
+
fm.title = title.trim();
|
|
10222
|
+
else
|
|
10223
|
+
delete fm.title;
|
|
10224
|
+
const ext = (fm.clementine && typeof fm.clementine === 'object') ? fm.clementine : {};
|
|
10225
|
+
ext.updatedAt = new Date().toISOString();
|
|
10226
|
+
const allowed = Array.isArray(tools) ? tools.map(String).map(s => s.trim()).filter(Boolean) : [];
|
|
10227
|
+
if (allowed.length > 0)
|
|
10228
|
+
ext.tools = { ...(ext.tools || {}), allow: allowed };
|
|
10229
|
+
else if (ext.tools && typeof ext.tools === 'object')
|
|
10230
|
+
delete ext.tools.allow;
|
|
10231
|
+
fm.clementine = ext;
|
|
10232
|
+
const content = matterMod.stringify(body.endsWith('\n') ? body : body + '\n', fm);
|
|
10233
|
+
writeFileSync(targetPath, content);
|
|
10234
|
+
res.json({ ok: true, name: skillName, layout: targetPath === folderEntry ? 'folder' : 'flat' });
|
|
10235
|
+
}
|
|
10236
|
+
catch (err) {
|
|
10237
|
+
res.status(500).json({ error: String(err) });
|
|
10238
|
+
}
|
|
10239
|
+
});
|
|
10240
|
+
// DELETE /api/skills/:name — remove a skill (folder OR flat). 1.18.115:
|
|
10241
|
+
// updated to handle folder form. The .md.bak (if present) stays as a
|
|
10242
|
+
// rollback artefact unless the caller passes `?bak=clean`.
|
|
10143
10243
|
app.delete('/api/skills/:name', (req, res) => {
|
|
10144
10244
|
try {
|
|
10245
|
+
const skillName = req.params.name;
|
|
10145
10246
|
const skillsDir = path.join(VAULT_DIR, '00-System', 'skills');
|
|
10146
|
-
const
|
|
10147
|
-
|
|
10148
|
-
|
|
10247
|
+
const folderPath = path.join(skillsDir, skillName);
|
|
10248
|
+
const folderEntry = path.join(folderPath, 'SKILL.md');
|
|
10249
|
+
const flatEntry = path.join(skillsDir, skillName + '.md');
|
|
10250
|
+
const cleanBak = req.query.bak === 'clean';
|
|
10251
|
+
let removed = false;
|
|
10252
|
+
if (existsSync(folderEntry)) {
|
|
10253
|
+
// Folder form — remove the whole skill folder.
|
|
10254
|
+
rmSync(folderPath, { recursive: true, force: true });
|
|
10255
|
+
removed = true;
|
|
10256
|
+
}
|
|
10257
|
+
else if (existsSync(flatEntry)) {
|
|
10258
|
+
// Legacy flat form — remove the .md plus optional .files dir.
|
|
10259
|
+
unlinkSync(flatEntry);
|
|
10260
|
+
const filesDir = path.join(skillsDir, skillName + '.files');
|
|
10261
|
+
if (existsSync(filesDir))
|
|
10262
|
+
rmSync(filesDir, { recursive: true, force: true });
|
|
10263
|
+
removed = true;
|
|
10264
|
+
}
|
|
10265
|
+
if (!removed) {
|
|
10266
|
+
res.status(404).json({ error: 'Skill "' + skillName + '" not found' });
|
|
10149
10267
|
return;
|
|
10150
10268
|
}
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
const filesDir = path.join(skillsDir, `${req.params.name}.files`);
|
|
10154
|
-
if (existsSync(filesDir))
|
|
10155
|
-
rmSync(filesDir, { recursive: true, force: true });
|
|
10156
|
-
const bakPath = filePath.replace(/\.md$/, '.md.bak');
|
|
10157
|
-
if (existsSync(bakPath))
|
|
10269
|
+
const bakPath = path.join(skillsDir, skillName + '.md.bak');
|
|
10270
|
+
if (cleanBak && existsSync(bakPath))
|
|
10158
10271
|
unlinkSync(bakPath);
|
|
10159
10272
|
res.json({ ok: true });
|
|
10160
10273
|
}
|
|
@@ -16107,6 +16220,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
16107
16220
|
color: var(--green); font-size: 12px; font-weight: 700;
|
|
16108
16221
|
margin: 0 0 4px 0; text-transform: uppercase; letter-spacing: 0.4px;
|
|
16109
16222
|
}
|
|
16223
|
+
/* Soft info variant — used by the Strict-mode-available tip on legacy
|
|
16224
|
+
tasks. Quieter than .warn so it reads as a suggestion, not a defect. */
|
|
16225
|
+
.cron-banner.info {
|
|
16226
|
+
background: var(--bg-secondary);
|
|
16227
|
+
border: 1px solid var(--border);
|
|
16228
|
+
color: var(--text-primary);
|
|
16229
|
+
}
|
|
16110
16230
|
.cron-banner .banner-actions { margin-top: 10px; display: flex; gap: 8px; }
|
|
16111
16231
|
.cron-banner .banner-actions button {
|
|
16112
16232
|
font-size: 12px; padding: 6px 12px; border-radius: 6px; cursor: pointer;
|
|
@@ -17063,7 +17183,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
17063
17183
|
Tasks (default) + Tools & MCP catalog. Workflows still reachable
|
|
17064
17184
|
via deep-link ?tab=workflows for power users with existing
|
|
17065
17185
|
multi-step workflows. -->
|
|
17066
|
-
<div id="build-tabs" style="display:flex;gap:4px;padding:8px 18px 0;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0">
|
|
17186
|
+
<div id="build-tabs" style="display:flex;gap:4px;padding:8px 18px 0;background:var(--bg-secondary);border-bottom:1px solid var(--border);flex-shrink:0;align-items:flex-end">
|
|
17067
17187
|
<button class="build-tab-btn active" data-build-tab="crons" onclick="switchBuildTab('crons')" style="padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-primary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
|
|
17068
17188
|
<span style="margin-right:6px">📅</span>Tasks <span id="build-tab-cron-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
|
|
17069
17189
|
</button>
|
|
@@ -17076,6 +17196,11 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
17076
17196
|
<button class="build-tab-btn" data-build-tab="workflows" onclick="switchBuildTab('workflows')" style="display:none;padding:8px 14px;border-radius:6px 6px 0 0;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent">
|
|
17077
17197
|
<span style="margin-right:6px">🔧</span>Workflows <span id="build-tab-workflows-count" style="display:none;margin-left:4px;font-size:10px;background:var(--bg-tertiary);padding:1px 6px;border-radius:999px;color:var(--text-muted)">0</span>
|
|
17078
17198
|
</button>
|
|
17199
|
+
<!-- Spacer + primary "create" CTA. The Tasks/Runs/Tools tabs are above; this sits flush with them on the right so creation is one click from anywhere on the Tasks domain. -->
|
|
17200
|
+
<div style="flex:1"></div>
|
|
17201
|
+
<button class="btn-primary" onclick="openCreateCronModal()" style="margin-bottom:6px;font-size:13px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
|
17202
|
+
<span style="font-size:14px;line-height:1">+</span> New task
|
|
17203
|
+
</button>
|
|
17079
17204
|
</div>
|
|
17080
17205
|
<style>
|
|
17081
17206
|
.build-tab-btn.active { color:var(--accent) !important; border-bottom-color:var(--accent) !important; background:var(--bg-primary) !important; }
|
|
@@ -20173,11 +20298,18 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20173
20298
|
invocation lands in Phase C. The page is intentionally minimal —
|
|
20174
20299
|
we want users to see what's there, not be overwhelmed by 7 tiles. -->
|
|
20175
20300
|
<div class="page" id="page-skills">
|
|
20176
|
-
<div class="page-head">
|
|
20177
|
-
<div
|
|
20178
|
-
|
|
20179
|
-
<
|
|
20180
|
-
|
|
20301
|
+
<div class="page-head" style="display:flex;align-items:flex-start;justify-content:space-between;gap:18px">
|
|
20302
|
+
<div style="display:flex;align-items:flex-start;gap:14px;flex:1;min-width:0">
|
|
20303
|
+
<div class="icon icon-slot" data-icon="brain"></div>
|
|
20304
|
+
<div class="title-block">
|
|
20305
|
+
<h1>Skills</h1>
|
|
20306
|
+
<p class="desc">Reusable procedures. A <strong>skill</strong> is a recipe (Markdown body + tool allowlist + bundled docs). Tasks pin skills; chats can run them. One skill, many tasks.</p>
|
|
20307
|
+
</div>
|
|
20308
|
+
</div>
|
|
20309
|
+
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
20310
|
+
<button class="btn-primary" onclick="openCreateSkillModal()" style="font-size:13px;padding:8px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
|
20311
|
+
<span style="font-size:14px;line-height:1">+</span> New skill
|
|
20312
|
+
</button>
|
|
20181
20313
|
</div>
|
|
20182
20314
|
</div>
|
|
20183
20315
|
<div style="display:grid;grid-template-columns:380px 1fr;gap:18px;height:calc(100vh - 180px);min-height:500px">
|
|
@@ -20188,8 +20320,10 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20188
20320
|
<div id="skills-list" style="padding:6px"></div>
|
|
20189
20321
|
</div>
|
|
20190
20322
|
<div id="skills-detail-pane" style="overflow-y:auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);padding:0">
|
|
20191
|
-
<div id="skills-detail" style="padding:
|
|
20192
|
-
|
|
20323
|
+
<div id="skills-detail" style="padding:0;font-size:13px">
|
|
20324
|
+
<div style="padding:60px 24px;color:var(--text-muted);text-align:center;font-size:13px">
|
|
20325
|
+
Select a skill on the left to see its procedure, tools, and data sources.
|
|
20326
|
+
</div>
|
|
20193
20327
|
</div>
|
|
20194
20328
|
</div>
|
|
20195
20329
|
</div>
|
|
@@ -24341,7 +24475,14 @@ function renderScheduledTaskCard(task) {
|
|
|
24341
24475
|
if (task.maxRetries != null) badges += '<span class="badge badge-gray">' + esc(task.maxRetries) + ' retries</span>';
|
|
24342
24476
|
badges += operationUsageBadge(task.usage);
|
|
24343
24477
|
badges += '<span class="badge ' + (enabled ? 'badge-green' : 'badge-gray') + '">' + (enabled ? 'Enabled' : 'Disabled') + '</span>';
|
|
24344
|
-
|
|
24478
|
+
// 1.18.118 — only emit the health badge when it adds new information.
|
|
24479
|
+
// ok/idle are implicit when the green Enabled badge is showing, and
|
|
24480
|
+
// disabled would just duplicate the gray Disabled badge above.
|
|
24481
|
+
var hl = String(task.healthLabel || task.health || '').toLowerCase();
|
|
24482
|
+
if (hl && hl !== 'ok' && hl !== 'idle' && hl !== 'disabled') {
|
|
24483
|
+
var hlClass = (hl === 'broken' || hl === 'failed' || hl === 'timeout') ? 'badge-yellow' : (hl === 'running' ? 'badge-blue' : 'badge-gray');
|
|
24484
|
+
badges += '<span class="badge ' + hlClass + '">' + esc(task.healthLabel || task.health) + '</span>';
|
|
24485
|
+
}
|
|
24345
24486
|
var safeName = jsStr(task.name);
|
|
24346
24487
|
// PRD §10 / 1.18.91: when a task is mid-flight, Run Now is meaningless and
|
|
24347
24488
|
// would race against the concurrency lock; replace it with a Cancel button
|
|
@@ -24351,11 +24492,30 @@ function renderScheduledTaskCard(task) {
|
|
|
24351
24492
|
var runOrCancelBtn = isRunning
|
|
24352
24493
|
? '<button class="btn-sm secondary btn-danger" onclick="cancelCronRun(\\x27' + safeName + '\\x27)" title="Stop this in-flight run (SIGTERM)">Cancel</button>'
|
|
24353
24494
|
: '<button class="btn-sm secondary btn-success" onclick="apiPost(\\x27/api/cron/run/' + encodeURIComponent(task.name) + '\\x27)" title="Run this task once now">Run Now</button>';
|
|
24495
|
+
// 1.18.115 — preview line. Prefer task.description (a future-proof
|
|
24496
|
+
// dedicated field), then strip leading TOOL RESTRICTIONS / FORBIDDEN
|
|
24497
|
+
// boilerplate (purely a runtime allowlist, not what the task does), then
|
|
24498
|
+
// grab the first real sentence so the card reads as "what this task is
|
|
24499
|
+
// for" rather than "wall of LLM scaffolding."
|
|
24500
|
+
function _taskPreview(t) {
|
|
24501
|
+
if (t.description && String(t.description).trim()) return String(t.description).trim();
|
|
24502
|
+
var p = String(t.prompt || '').trim();
|
|
24503
|
+
// Strip the canonical tool-restriction preamble (matches the user's
|
|
24504
|
+
// TOOL RESTRICTIONS — MANDATORY... block up to the first paragraph
|
|
24505
|
+
// that doesn't start with a numbered restriction line).
|
|
24506
|
+
var stripped = p.replace(/^TOOL RESTRICTIONS[\\s\\S]*?(?=\\n\\n[A-Z][^.]*\\.|\\n\\n\\w)/i, '').trim();
|
|
24507
|
+
if (!stripped) stripped = p;
|
|
24508
|
+
// First sentence or first line, whichever is shorter. Falls back to
|
|
24509
|
+
// the first 200 chars if nothing punctuates.
|
|
24510
|
+
var firstLine = stripped.split('\\n').map(function(s){ return s.trim(); }).find(function(s){ return s.length > 0; }) || '';
|
|
24511
|
+
var firstSentence = (firstLine.match(/^[^.!?]+[.!?]/) || [firstLine])[0];
|
|
24512
|
+
return (firstSentence || stripped).slice(0, 200);
|
|
24513
|
+
}
|
|
24354
24514
|
return '<div class="' + cardCls + '" style="' + style + '">'
|
|
24355
24515
|
+ '<div class="task-card-header"><strong>' + esc(task.displayName || task.name) + '</strong>'
|
|
24356
24516
|
+ '<label class="toggle-switch"><input type="checkbox"' + (enabled ? ' checked' : '') + ' onchange="toggleCronJob(\\x27' + safeName + '\\x27)"><span class="toggle-slider"></span></label></div>'
|
|
24357
24517
|
+ '<div class="task-card-schedule">' + operationScheduleHtml(task.schedule) + '</div>'
|
|
24358
|
-
+ '<div class="task-card-prompt">' + esc(task
|
|
24518
|
+
+ '<div class="task-card-prompt">' + esc(_taskPreview(task)) + '</div>'
|
|
24359
24519
|
+ renderTrickCapabilityStrip(task)
|
|
24360
24520
|
+ '<div class="task-card-status">' + lastRunHtml + '</div>'
|
|
24361
24521
|
+ renderTrickTagChips(task)
|
|
@@ -25809,15 +25969,24 @@ async function refreshCron() {
|
|
|
25809
25969
|
var visibleRunning = ownerScoped ? (ops.runningNow || []).filter(function(i) { return buildOpsOwnerMatches(i.owner || ''); }) : (ops.runningNow || []);
|
|
25810
25970
|
var ownerFilter = getBuildOwnerFilter();
|
|
25811
25971
|
|
|
25812
|
-
// PRD §12 / 1.18.88: Health Strip
|
|
25813
|
-
//
|
|
25814
|
-
//
|
|
25972
|
+
// PRD §12 / 1.18.88: Health Strip — kept always-visible at the top.
|
|
25973
|
+
// 7 KPI tiles (24h runs, success rate, cost, p50/p95 latency, running,
|
|
25974
|
+
// top failure). The runs payload from /api/cron/runs (already fetched
|
|
25975
|
+
// alongside ops) feeds the metrics. Render an empty shell first;
|
|
25976
|
+
// refreshHealthStrip fills it in.
|
|
25815
25977
|
var html = '<div id="health-strip" class="health-strip"></div>';
|
|
25816
|
-
//
|
|
25817
|
-
//
|
|
25818
|
-
//
|
|
25819
|
-
//
|
|
25820
|
-
|
|
25978
|
+
// 1.18.115 — collapse the cost/latency/reliability/activity mini-cards
|
|
25979
|
+
// into a <details> block. The Health Strip already covers what most
|
|
25980
|
+
// users want at a glance; the 4 mini-dashboards are for deeper
|
|
25981
|
+
// observability and don't need to be the second thing on the page.
|
|
25982
|
+
// Closed by default; users who want them flip it open once and the
|
|
25983
|
+
// browser remembers via the [open] attribute persistence pattern.
|
|
25984
|
+
html += '<details class="mini-dashboards-toggle" style="margin:14px 0 4px">'
|
|
25985
|
+
+ '<summary style="font-size:12px;color:var(--text-muted);cursor:pointer;padding:6px 0;user-select:none;display:inline-flex;align-items:center;gap:6px">'
|
|
25986
|
+
+ '<span style="font-size:11px">▸</span> Show cost / latency / reliability mini-dashboards'
|
|
25987
|
+
+ '</summary>'
|
|
25988
|
+
+ '<div id="mini-dashboards" class="mini-dashboards" style="margin-top:10px"></div>'
|
|
25989
|
+
+ '</details>';
|
|
25821
25990
|
|
|
25822
25991
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
25823
25992
|
if (visibleRunning.length > 0) {
|
|
@@ -25826,18 +25995,13 @@ async function refreshCron() {
|
|
|
25826
25995
|
if (visibleRunning.length > 10) html += '<div class="empty-state" style="padding:18px;color:var(--text-muted);font-size:13px">Showing 10 of ' + visibleRunning.length + ' active runs. Use the Owner filter to narrow this list.</div>';
|
|
25827
25996
|
}
|
|
25828
25997
|
|
|
25829
|
-
// ──
|
|
25830
|
-
|
|
25831
|
-
|
|
25832
|
-
+ '<div class="task-grid">' + visibleAttention.slice(0, 12).map(renderAttentionCard).join('') + '</div>';
|
|
25833
|
-
if (visibleAttention.length > 12) html += '<div class="empty-state" style="padding:18px;color:var(--text-muted);font-size:13px">Showing 12 of ' + visibleAttention.length + ' items. Use the Owner filter to narrow this list.</div>';
|
|
25834
|
-
}
|
|
25835
|
-
|
|
25836
|
-
// ── Zone 2 — Your tasks (the main card grid) ──
|
|
25998
|
+
// ── Zone 2 — Your tasks (the main card grid; promoted above "Needs
|
|
25999
|
+
// attention" in 1.18.118 so the user sees their working tasks at
|
|
26000
|
+
// fold instead of having to scroll past 1,000+ px of error cards).
|
|
25837
26001
|
var filteredTasks = applyTrickFilter(visibleTasks, _trickFilter);
|
|
25838
26002
|
var filterPillsHtml = renderTrickFilterRow(visibleTasks, _trickFilter);
|
|
25839
26003
|
var taskCountLabel = (_trickFilter.kind ? filteredTasks.length + '/' + visibleTasks.length : visibleTasks.length) + ' task' + (visibleTasks.length === 1 ? '' : 's');
|
|
25840
|
-
html += operationSectionHeader('Your tasks', 'Recurring jobs Clementine runs for you. Tap any card to edit; the toggle on each card pauses or resumes it.', 'badge-blue', taskCountLabel,
|
|
26004
|
+
html += operationSectionHeader('Your tasks', 'Recurring jobs Clementine runs for you. Tap any card to edit; the toggle on each card pauses or resumes it.', 'badge-blue', taskCountLabel, visibleRunning.length > 0 ? '28px' : '0')
|
|
25841
26005
|
+ filterPillsHtml
|
|
25842
26006
|
+ '<div class="task-grid">';
|
|
25843
26007
|
if (filteredTasks.length === 0) {
|
|
@@ -25861,6 +26025,22 @@ async function refreshCron() {
|
|
|
25861
26025
|
html += '<div class="task-grid">' + visibleWorkflows.map(renderScheduledWorkflowCard).join('') + '</div>';
|
|
25862
26026
|
}
|
|
25863
26027
|
|
|
26028
|
+
// ── Needs attention — collapsed by default in 1.18.118. Was the
|
|
26029
|
+
// second thing on the page; pushed "Your tasks" 1,000+ px below
|
|
26030
|
+
// the fold. Still surfaced visibly via a yellow count chip in the
|
|
26031
|
+
// summary so users know it's there. Click to expand for triage.
|
|
26032
|
+
if (visibleAttention.length > 0) {
|
|
26033
|
+
html += '<details style="margin-top:28px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:0;overflow:hidden">'
|
|
26034
|
+
+ '<summary style="padding:12px 16px;cursor:pointer;display:flex;align-items:center;gap:10px;user-select:none">'
|
|
26035
|
+
+ '<span style="font-size:14px;font-weight:600;color:var(--text-primary)">Needs attention</span>'
|
|
26036
|
+
+ '<span class="badge badge-yellow">' + visibleAttention.length + ' review</span>'
|
|
26037
|
+
+ '<span style="font-size:11px;color:var(--text-muted);margin-left:6px">Broken scheduled tasks and failed runtime work that can waste tokens or silently stop.</span>'
|
|
26038
|
+
+ '</summary>'
|
|
26039
|
+
+ '<div style="padding:0 16px 16px"><div class="task-grid">' + visibleAttention.slice(0, 12).map(renderAttentionCard).join('') + '</div>';
|
|
26040
|
+
if (visibleAttention.length > 12) html += '<div class="empty-state" style="padding:18px;color:var(--text-muted);font-size:13px">Showing 12 of ' + visibleAttention.length + ' items. Use the Owner filter to narrow this list.</div>';
|
|
26041
|
+
html += '</div></details>';
|
|
26042
|
+
}
|
|
26043
|
+
|
|
25864
26044
|
// ── Zone 3 — Recent history (last 50 runs across all jobs) ──
|
|
25865
26045
|
html += operationSectionHeader('Recent history', 'The last 50 task runs across every job, newest first. Click any row to open the full trace.', 'badge-gray', historyData.length + ' run' + (historyData.length === 1 ? '' : 's'), '28px');
|
|
25866
26046
|
html += renderRecentHistoryList(historyData);
|
|
@@ -26748,6 +26928,250 @@ async function migrateAllLegacySkills() {
|
|
|
26748
26928
|
} catch (err) { toast('Bulk migration failed: ' + err, 'error'); }
|
|
26749
26929
|
}
|
|
26750
26930
|
|
|
26931
|
+
// 1.18.115 — tiny inline Markdown renderer for the Skills detail pane.
|
|
26932
|
+
// Handles the subset SKILL.md bodies use in practice: headers, bold,
|
|
26933
|
+
// italic, inline-code, fenced code blocks, ordered + unordered lists,
|
|
26934
|
+
// paragraphs, line breaks. Pulling in marked or markdown-it would balloon
|
|
26935
|
+
// the served bundle for ~80 lines of regex. Output is always escaped
|
|
26936
|
+
// first, then re-styled.
|
|
26937
|
+
//
|
|
26938
|
+
// NOTE: regex patterns containing backtick characters are constructed via
|
|
26939
|
+
// new RegExp(string) instead of regex literals — raw backticks would
|
|
26940
|
+
// otherwise close the surrounding TypeScript template literal that
|
|
26941
|
+
// builds the served HTML.
|
|
26942
|
+
function renderMarkdown(src) {
|
|
26943
|
+
if (!src) return '';
|
|
26944
|
+
// Escape every char first; we never inject raw HTML from the body.
|
|
26945
|
+
var s = String(src)
|
|
26946
|
+
.replace(/&/g, '&')
|
|
26947
|
+
.replace(/</g, '<')
|
|
26948
|
+
.replace(/>/g, '>')
|
|
26949
|
+
.replace(/"/g, '"');
|
|
26950
|
+
// Fenced code blocks (preserve interior verbatim — no inline markup
|
|
26951
|
+
// applied inside). Use a placeholder map so subsequent regex passes
|
|
26952
|
+
// don't munge the contents. Fence delimiter is three backticks.
|
|
26953
|
+
var BACKTICK = String.fromCharCode(96);
|
|
26954
|
+
var fences = [];
|
|
26955
|
+
var fenceRe = new RegExp(BACKTICK + BACKTICK + BACKTICK + '([a-z0-9_-]*)\\n?([\\s\\S]*?)' + BACKTICK + BACKTICK + BACKTICK, 'gi');
|
|
26956
|
+
s = s.replace(fenceRe, function(_, lang, code) {
|
|
26957
|
+
fences.push('<pre style="background:var(--bg-tertiary);padding:12px 14px;border-radius:6px;overflow:auto;font-size:12px;line-height:1.5;margin:8px 0;border:1px solid var(--border)"><code>' + code.replace(/\\n+$/, '') + '</code></pre>');
|
|
26958
|
+
return '\\u0000FENCE_' + (fences.length - 1) + '\\u0000';
|
|
26959
|
+
});
|
|
26960
|
+
// Inline code — single backticks
|
|
26961
|
+
var inlineRe = new RegExp(BACKTICK + '([^' + BACKTICK + '\\n]+)' + BACKTICK, 'g');
|
|
26962
|
+
s = s.replace(inlineRe, '<code style="background:var(--bg-tertiary);padding:1px 6px;border-radius:3px;font-size:0.92em">$1</code>');
|
|
26963
|
+
// Bold first (greedier double-star) so it doesn't get eaten by single-star italic.
|
|
26964
|
+
s = s.replace(/\\*\\*([^*\\n][^*]*?)\\*\\*/g, '<strong>$1</strong>');
|
|
26965
|
+
s = s.replace(/(^|[^*])\\*([^*\\n][^*]*?)\\*/g, '$1<em>$2</em>');
|
|
26966
|
+
// Headers — process line-by-line so we don't accidentally match across
|
|
26967
|
+
// paragraphs. Also collect lists into <ul>/<ol> blocks.
|
|
26968
|
+
var lines = s.split('\\n');
|
|
26969
|
+
var out = [];
|
|
26970
|
+
var listKind = null; // 'ul' | 'ol' | null
|
|
26971
|
+
var listItems = [];
|
|
26972
|
+
function flushList() {
|
|
26973
|
+
if (!listKind) return;
|
|
26974
|
+
out.push('<' + listKind + ' style="margin:6px 0 10px;padding-left:22px">' + listItems.map(function(li) { return '<li style="margin:2px 0">' + li + '</li>'; }).join('') + '</' + listKind + '>');
|
|
26975
|
+
listKind = null; listItems = [];
|
|
26976
|
+
}
|
|
26977
|
+
function paraStart() { out.push('<p style="margin:8px 0">'); }
|
|
26978
|
+
function paraEnd() {
|
|
26979
|
+
// Close the last <p> if there is one open.
|
|
26980
|
+
var last = out.length - 1;
|
|
26981
|
+
if (last >= 0 && out[last].endsWith('</p>')) return;
|
|
26982
|
+
if (last >= 0 && out[last] === '<p style="margin:8px 0">') { out.pop(); return; }
|
|
26983
|
+
if (last >= 0 && !/^<(h\\d|ul|ol|pre|hr|details)/.test(out[last])) {
|
|
26984
|
+
// We're mid-paragraph — emit the close tag.
|
|
26985
|
+
out.push('</p>');
|
|
26986
|
+
}
|
|
26987
|
+
}
|
|
26988
|
+
var paraOpen = false;
|
|
26989
|
+
function closePara() { if (paraOpen) { out.push('</p>'); paraOpen = false; } }
|
|
26990
|
+
function openPara() { if (!paraOpen) { out.push('<p style="margin:8px 0">'); paraOpen = true; } }
|
|
26991
|
+
for (var i = 0; i < lines.length; i++) {
|
|
26992
|
+
var line = lines[i];
|
|
26993
|
+
// Restore fence placeholders as their own block.
|
|
26994
|
+
var fenceMatch = line.match(/\\u0000FENCE_(\\d+)\\u0000/);
|
|
26995
|
+
if (fenceMatch) { closePara(); flushList(); out.push(fences[Number(fenceMatch[1])]); continue; }
|
|
26996
|
+
// Headers
|
|
26997
|
+
var hMatch = line.match(/^(#{1,6})\\s+(.+)$/);
|
|
26998
|
+
if (hMatch) {
|
|
26999
|
+
closePara(); flushList();
|
|
27000
|
+
var level = hMatch[1].length;
|
|
27001
|
+
var sizes = { 1: '1.4em', 2: '1.2em', 3: '1.05em', 4: '1em', 5: '0.95em', 6: '0.9em' };
|
|
27002
|
+
out.push('<h' + level + ' style="margin:14px 0 6px;font-size:' + sizes[level] + ';font-weight:600;color:var(--text-primary)">' + hMatch[2] + '</h' + level + '>');
|
|
27003
|
+
continue;
|
|
27004
|
+
}
|
|
27005
|
+
// Unordered list — dash, star, or plus prefix
|
|
27006
|
+
var ulMatch = line.match(/^\\s*[-*+]\\s+(.+)$/);
|
|
27007
|
+
if (ulMatch) {
|
|
27008
|
+
closePara();
|
|
27009
|
+
if (listKind && listKind !== 'ul') flushList();
|
|
27010
|
+
listKind = 'ul';
|
|
27011
|
+
listItems.push(ulMatch[1]);
|
|
27012
|
+
continue;
|
|
27013
|
+
}
|
|
27014
|
+
// Ordered list — 1., 2., etc.
|
|
27015
|
+
var olMatch = line.match(/^\\s*\\d+\\.\\s+(.+)$/);
|
|
27016
|
+
if (olMatch) {
|
|
27017
|
+
closePara();
|
|
27018
|
+
if (listKind && listKind !== 'ol') flushList();
|
|
27019
|
+
listKind = 'ol';
|
|
27020
|
+
listItems.push(olMatch[1]);
|
|
27021
|
+
continue;
|
|
27022
|
+
}
|
|
27023
|
+
// Blank line — paragraph break
|
|
27024
|
+
if (!line.trim()) { closePara(); flushList(); continue; }
|
|
27025
|
+
// Hr
|
|
27026
|
+
if (/^---+$/.test(line.trim())) { closePara(); flushList(); out.push('<hr style="border:none;border-top:1px solid var(--border);margin:12px 0">'); continue; }
|
|
27027
|
+
// Default — wrap as paragraph text
|
|
27028
|
+
flushList();
|
|
27029
|
+
openPara();
|
|
27030
|
+
// Append to current paragraph with a soft break between consecutive
|
|
27031
|
+
// text lines (markdown convention: lines in a paragraph join with space).
|
|
27032
|
+
var top = out.length - 1;
|
|
27033
|
+
if (out[top] === '<p style="margin:8px 0">') {
|
|
27034
|
+
out[top] = '<p style="margin:8px 0">' + line;
|
|
27035
|
+
} else {
|
|
27036
|
+
out[top] = out[top] + ' ' + line;
|
|
27037
|
+
}
|
|
27038
|
+
}
|
|
27039
|
+
closePara(); flushList();
|
|
27040
|
+
return out.join('\\n');
|
|
27041
|
+
}
|
|
27042
|
+
|
|
27043
|
+
// 1.18.115 — Skill creation modal. Until now there was no UI to make a
|
|
27044
|
+
// new skill; users had to mkdir + write SKILL.md by hand. Modal collects
|
|
27045
|
+
// name (Anthropic regex enforced client-side), description, body, and an
|
|
27046
|
+
// optional comma-separated tools.allow allowlist; POSTs to a new endpoint
|
|
27047
|
+
// that calls skill-store.parseSkillFolder/write under the hood.
|
|
27048
|
+
function openCreateSkillModal() { _openSkillModal({ mode: 'create' }); }
|
|
27049
|
+
function openEditSkillModal(name) { _openSkillModal({ mode: 'edit', name: name }); }
|
|
27050
|
+
|
|
27051
|
+
async function _openSkillModal(opts) {
|
|
27052
|
+
opts = opts || {};
|
|
27053
|
+
var existing = null;
|
|
27054
|
+
if (opts.mode === 'edit' && opts.name) {
|
|
27055
|
+
try {
|
|
27056
|
+
var r = await apiFetch('/api/skills/' + encodeURIComponent(opts.name));
|
|
27057
|
+
if (r.ok) existing = await r.json();
|
|
27058
|
+
} catch (e) { toast('Failed to load skill: ' + e, 'error'); return; }
|
|
27059
|
+
}
|
|
27060
|
+
var fm = (existing && existing.frontmatter) || {};
|
|
27061
|
+
var ext = fm.clementine || {};
|
|
27062
|
+
var nameVal = fm.name || '';
|
|
27063
|
+
var titleVal = fm.title || '';
|
|
27064
|
+
var descVal = fm.description || '';
|
|
27065
|
+
var bodyVal = (existing && existing.body) || '';
|
|
27066
|
+
var toolsVal = (ext.tools && Array.isArray(ext.tools.allow)) ? ext.tools.allow.join(', ') : '';
|
|
27067
|
+
var modal = document.getElementById('skill-edit-modal');
|
|
27068
|
+
if (!modal) {
|
|
27069
|
+
modal = document.createElement('div');
|
|
27070
|
+
modal.id = 'skill-edit-modal';
|
|
27071
|
+
modal.className = 'modal-overlay';
|
|
27072
|
+
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px';
|
|
27073
|
+
modal.innerHTML =
|
|
27074
|
+
'<div style="background:var(--bg-primary);border:1px solid var(--border);border-radius:10px;width:min(720px,95vw);max-height:90vh;display:flex;flex-direction:column;box-shadow:0 16px 48px rgba(0,0,0,0.35)">'
|
|
27075
|
+
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border)">'
|
|
27076
|
+
+ '<h3 id="skill-modal-title" style="margin:0;font-size:15px;font-weight:600">New skill</h3>'
|
|
27077
|
+
+ '<button onclick="closeSkillModal()" style="background:none;border:none;font-size:18px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1">✕</button>'
|
|
27078
|
+
+ '</div>'
|
|
27079
|
+
+ '<div style="flex:1;overflow-y:auto;padding:18px 22px">'
|
|
27080
|
+
+ '<input type="hidden" id="skill-modal-original-name">'
|
|
27081
|
+
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Name <span style="color:var(--text-muted)">(lowercase, dashes, max 64 chars)</span></label>'
|
|
27082
|
+
+ '<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">'
|
|
27083
|
+
+ '<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>'
|
|
27084
|
+
+ '<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">'
|
|
27085
|
+
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;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>'
|
|
27086
|
+
+ '<textarea id="skill-modal-desc" rows="2" 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>'
|
|
27087
|
+
+ '<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>'
|
|
27088
|
+
+ '<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">'
|
|
27089
|
+
+ '<label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px;font-weight:500">Procedure <span style="color:var(--text-muted)">(Markdown — the actual steps Claude follows)</span></label>'
|
|
27090
|
+
+ '<textarea id="skill-modal-body" rows="14" 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>'
|
|
27091
|
+
+ '<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>'
|
|
27092
|
+
+ '</div>'
|
|
27093
|
+
+ '<div style="display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--border);background:var(--bg-secondary)">'
|
|
27094
|
+
+ '<button onclick="closeSkillModal()" style="padding:7px 14px;font-size:13px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-primary);cursor:pointer">Cancel</button>'
|
|
27095
|
+
+ '<button id="skill-modal-save" onclick="saveSkillFromModal()" class="btn-primary" style="padding:7px 16px;font-size:13px;border:none;border-radius:6px;background:var(--accent);color:#fff;font-weight:500;cursor:pointer">Save skill</button>'
|
|
27096
|
+
+ '</div>'
|
|
27097
|
+
+ '</div>';
|
|
27098
|
+
document.body.appendChild(modal);
|
|
27099
|
+
}
|
|
27100
|
+
document.getElementById('skill-modal-title').textContent = opts.mode === 'edit' ? 'Edit skill: ' + nameVal : 'New skill';
|
|
27101
|
+
document.getElementById('skill-modal-original-name').value = opts.mode === 'edit' ? nameVal : '';
|
|
27102
|
+
document.getElementById('skill-modal-name').value = nameVal;
|
|
27103
|
+
document.getElementById('skill-modal-name').disabled = opts.mode === 'edit';
|
|
27104
|
+
document.getElementById('skill-modal-title').nextElementSibling; // no-op
|
|
27105
|
+
document.getElementById('skill-modal-title').value = titleVal;
|
|
27106
|
+
document.getElementById('skill-modal-desc').value = descVal;
|
|
27107
|
+
document.getElementById('skill-modal-tools').value = toolsVal;
|
|
27108
|
+
document.getElementById('skill-modal-body').value = bodyVal;
|
|
27109
|
+
var errEl = document.getElementById('skill-modal-error');
|
|
27110
|
+
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
27111
|
+
modal.style.display = 'flex';
|
|
27112
|
+
document.getElementById('skill-modal-name').focus();
|
|
27113
|
+
}
|
|
27114
|
+
|
|
27115
|
+
function closeSkillModal() {
|
|
27116
|
+
var m = document.getElementById('skill-edit-modal');
|
|
27117
|
+
if (m) m.style.display = 'none';
|
|
27118
|
+
}
|
|
27119
|
+
|
|
27120
|
+
async function saveSkillFromModal() {
|
|
27121
|
+
var name = (document.getElementById('skill-modal-name')?.value || '').trim();
|
|
27122
|
+
var title = (document.getElementById('skill-modal-title')?.value || '').trim();
|
|
27123
|
+
var desc = (document.getElementById('skill-modal-desc')?.value || '').trim();
|
|
27124
|
+
var toolsRaw = (document.getElementById('skill-modal-tools')?.value || '').trim();
|
|
27125
|
+
var body = (document.getElementById('skill-modal-body')?.value || '');
|
|
27126
|
+
var originalName = (document.getElementById('skill-modal-original-name')?.value || '').trim();
|
|
27127
|
+
var errEl = document.getElementById('skill-modal-error');
|
|
27128
|
+
function fail(msg) {
|
|
27129
|
+
if (errEl) { errEl.textContent = msg; errEl.style.display = ''; }
|
|
27130
|
+
else toast(msg, 'error');
|
|
27131
|
+
}
|
|
27132
|
+
if (!name) return fail('Name is required.');
|
|
27133
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) return fail('Name must be lowercase letters/digits/dashes, start with a letter or digit, max 64 chars.');
|
|
27134
|
+
if (!desc) return fail('Description is required (used by Claude to decide when to apply this skill).');
|
|
27135
|
+
if (desc.length > 1024) return fail('Description must be ≤ 1024 chars (Anthropic spec).');
|
|
27136
|
+
if (!body.trim()) return fail('Procedure body is required.');
|
|
27137
|
+
var tools = toolsRaw ? toolsRaw.split(',').map(function(s){ return s.trim(); }).filter(Boolean) : [];
|
|
27138
|
+
var saveBtn = document.getElementById('skill-modal-save');
|
|
27139
|
+
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; }
|
|
27140
|
+
try {
|
|
27141
|
+
var endpoint = originalName ? '/api/skills/' + encodeURIComponent(originalName) : '/api/skills';
|
|
27142
|
+
var method = originalName ? 'PUT' : 'POST';
|
|
27143
|
+
var r = await apiFetch(endpoint, {
|
|
27144
|
+
method: method,
|
|
27145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27146
|
+
body: JSON.stringify({ name: name, title: title || undefined, description: desc, tools: tools, body: body }),
|
|
27147
|
+
});
|
|
27148
|
+
if (!r.ok) {
|
|
27149
|
+
var d = await r.json().catch(function(){ return {}; });
|
|
27150
|
+
return fail(d.error || ('Save failed: HTTP ' + r.status));
|
|
27151
|
+
}
|
|
27152
|
+
closeSkillModal();
|
|
27153
|
+
toast(originalName ? 'Skill updated' : 'Skill created', 'success');
|
|
27154
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
27155
|
+
// Auto-open the freshly-saved skill so the user sees their work.
|
|
27156
|
+
if (typeof showSkillDetail === 'function') showSkillDetail(name);
|
|
27157
|
+
} catch (err) { fail('Save failed: ' + err); }
|
|
27158
|
+
finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Save skill'; } }
|
|
27159
|
+
}
|
|
27160
|
+
|
|
27161
|
+
async function confirmDeleteSkill(name) {
|
|
27162
|
+
if (!confirm('Delete skill "' + name + '"? The folder will be removed; the .md.bak (if present) is preserved.')) return;
|
|
27163
|
+
try {
|
|
27164
|
+
var r = await apiFetch('/api/skills/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
27165
|
+
if (!r.ok) {
|
|
27166
|
+
var d = await r.json().catch(function(){ return {}; });
|
|
27167
|
+
toast(d.error || 'Delete failed', 'error');
|
|
27168
|
+
return;
|
|
27169
|
+
}
|
|
27170
|
+
toast('Skill "' + name + '" deleted', 'success');
|
|
27171
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
27172
|
+
} catch (err) { toast('Delete failed: ' + err, 'error'); }
|
|
27173
|
+
}
|
|
27174
|
+
|
|
26751
27175
|
async function refreshSkillsPage() {
|
|
26752
27176
|
var listEl = document.getElementById('skills-list');
|
|
26753
27177
|
var detailEl = document.getElementById('skills-detail');
|
|
@@ -27027,7 +27451,10 @@ function renderSkillDetail(s) {
|
|
|
27027
27451
|
html += renderSkillSection('Usage', '<div style="font-size:12px;color:var(--text-secondary)">' + ubits.map(esc).join(' · ') + '</div>');
|
|
27028
27452
|
}
|
|
27029
27453
|
|
|
27030
|
-
// ── 7. Procedure body (
|
|
27454
|
+
// ── 7. Procedure body — rendered as markdown (1.18.115). Prior shipped
|
|
27455
|
+
// raw <pre> source which read like config, not procedure. Headers/lists/
|
|
27456
|
+
// bold/code now render visually. Line counter still appears so authors
|
|
27457
|
+
// know if they're approaching Anthropic's ≤500-line guidance.
|
|
27031
27458
|
if (s.body && s.body.trim()) {
|
|
27032
27459
|
var bodyClass = bodyLines > 500 ? 'color:var(--yellow)' : 'color:var(--text-muted)';
|
|
27033
27460
|
html += '<div style="margin-top:18px">';
|
|
@@ -27035,10 +27462,17 @@ function renderSkillDetail(s) {
|
|
|
27035
27462
|
html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:500">Procedure</div>';
|
|
27036
27463
|
html += '<div style="font-size:10px;' + bodyClass + ';font-family:\\x27JetBrains Mono\\x27,monospace">' + bodyLines + ' / 500 lines</div>';
|
|
27037
27464
|
html += '</div>';
|
|
27038
|
-
html += '<
|
|
27465
|
+
html += '<div class="skill-md" style="font-size:13px;line-height:1.6;color:var(--text-primary);background:var(--bg-secondary);padding:18px 22px;border-radius:8px;border:1px solid var(--border);max-height:560px;overflow:auto">' + renderMarkdown(s.body) + '</div>';
|
|
27039
27466
|
html += '</div>';
|
|
27040
27467
|
}
|
|
27041
27468
|
|
|
27469
|
+
// ── 8. Action footer (1.18.115) — Edit + Delete + Open file. The pane
|
|
27470
|
+
// was read-only; users had to leave the dashboard to edit anything.
|
|
27471
|
+
html += '<div style="margin-top:24px;display:flex;gap:8px;flex-wrap:wrap">';
|
|
27472
|
+
html += '<button class="btn-primary" onclick="openEditSkillModal(\\x27' + jsStr(fm.name) + '\\x27)" style="font-size:12px;padding:6px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:500;cursor:pointer">Edit skill</button>';
|
|
27473
|
+
html += '<button class="btn-sm" onclick="confirmDeleteSkill(\\x27' + jsStr(fm.name) + '\\x27)" style="font-size:12px;padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--red);cursor:pointer">Delete</button>';
|
|
27474
|
+
html += '</div>';
|
|
27475
|
+
|
|
27042
27476
|
// ── 8. Schema-specific footer
|
|
27043
27477
|
if (s.schemaVersion === 'legacy') {
|
|
27044
27478
|
html += '<div style="margin-top:24px;padding:12px 14px;background:rgba(245,158,11,0.08);border:1px solid var(--yellow);border-radius:6px;font-size:12px;color:var(--text-secondary);line-height:1.5">'
|
|
@@ -27616,11 +28050,20 @@ function renderSkillsPickerList() {
|
|
|
27616
28050
|
return hay.indexOf(q) !== -1;
|
|
27617
28051
|
});
|
|
27618
28052
|
}
|
|
28053
|
+
// 1.18.115 — "+ Create new skill" affordance pinned to the top of the
|
|
28054
|
+
// picker. Lets users author a skill in-flight without losing their cron
|
|
28055
|
+
// edit; the new skill appears in the list right after the modal closes.
|
|
28056
|
+
var createRow = '<div class="cap-picker-row" style="border:1px dashed var(--accent);background:transparent" onclick="openCreateSkillModal()">'
|
|
28057
|
+
+ '<div class="cap-picker-row-body">'
|
|
28058
|
+
+ '<div class="cap-picker-row-title" style="color:var(--accent);font-weight:600">+ Create new skill</div>'
|
|
28059
|
+
+ '<div class="cap-picker-row-desc">Author a fresh skill — opens the editor without leaving this task.</div>'
|
|
28060
|
+
+ '</div>'
|
|
28061
|
+
+ '</div>';
|
|
27619
28062
|
if (skills.length === 0) {
|
|
27620
|
-
listEl.innerHTML = '<div class="cap-picker-empty-state">' + (q ? 'No matches.' : 'No skills available.') + '</div>';
|
|
28063
|
+
listEl.innerHTML = createRow + '<div class="cap-picker-empty-state">' + (q ? 'No matches.' : 'No skills available.') + '</div>';
|
|
27621
28064
|
return;
|
|
27622
28065
|
}
|
|
27623
|
-
listEl.innerHTML = skills.slice(0, 50).map(function(s) {
|
|
28066
|
+
listEl.innerHTML = createRow + skills.slice(0, 50).map(function(s) {
|
|
27624
28067
|
var sel = _cronSelectedSkills.indexOf(s.name) !== -1;
|
|
27625
28068
|
var triggers = (s.triggers || []).slice(0, 4).join(', ');
|
|
27626
28069
|
return '<div class="cap-picker-row' + (sel ? ' selected' : '') + '" onclick="addSkillToTrick(\\x27' + jsStr(s.name) + '\\x27)">'
|
|
@@ -28300,19 +28743,21 @@ function onPredictableChange() {
|
|
|
28300
28743
|
function renderCronLegacyBanner(job) {
|
|
28301
28744
|
var host = document.getElementById('cron-legacy-banner-host');
|
|
28302
28745
|
if (!host) return;
|
|
28303
|
-
//
|
|
28304
|
-
//
|
|
28746
|
+
// Predictable jobs skip the tip entirely; legacy jobs get a non-alarming
|
|
28747
|
+
// suggestion. The previous wording ("OUTPUT MAY NOT MATCH WHAT YOU SEE
|
|
28748
|
+
// HERE") read as "the editor is lying to you" and made every legacy task
|
|
28749
|
+
// feel broken. It isn't — runs work fine. The toggle below explains the
|
|
28750
|
+
// trade-off; this is just a one-click shortcut for the common upgrade.
|
|
28305
28751
|
if (job && job.predictable === true) { host.innerHTML = ''; return; }
|
|
28306
|
-
var msg = (job && job.predictable === false)
|
|
28307
|
-
? "This task is set to legacy mode. At fire-time the runner injects MEMORY.md, recent team activity, the delegation queue, and auto-matches MCP servers based on prompt text — even if your prompt forbids them. The <strong>What will run</strong> tab shows what actually gets attached."
|
|
28308
|
-
: "This task was created before Predictable Mode existed. At fire-time the runner still injects MEMORY.md, recent team activity, and auto-matches MCP servers based on prompt text. Open the <strong>What will run</strong> tab to see what actually gets attached.";
|
|
28309
28752
|
host.innerHTML =
|
|
28310
|
-
'<div class="cron-banner
|
|
28311
|
-
+ '<
|
|
28312
|
-
|
|
28313
|
-
|
|
28314
|
-
|
|
28315
|
-
|
|
28753
|
+
'<div class="cron-banner info">'
|
|
28754
|
+
+ '<div style="display:flex;align-items:flex-start;gap:10px">'
|
|
28755
|
+
+ '<span style="font-size:14px;line-height:1.2;flex-shrink:0">💡</span>'
|
|
28756
|
+
+ '<div style="flex:1;min-width:0">'
|
|
28757
|
+
+ '<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:2px">Strict mode available</div>'
|
|
28758
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);line-height:1.45">Runs use only the prompt + pinned skills/MCP below — no auto-injected memory or team comms. More reproducible. <a href="javascript:void(0)" onclick="switchCronTab(\\x27preview\\x27)" style="color:var(--accent);text-decoration:none">See what runs today →</a></div>'
|
|
28759
|
+
+ '</div>'
|
|
28760
|
+
+ '<button class="btn-primary btn-sm" onclick="enablePredictableFromBanner()" style="flex-shrink:0;font-size:11px;padding:5px 12px">Switch on</button>'
|
|
28316
28761
|
+ '</div>'
|
|
28317
28762
|
+ '</div>';
|
|
28318
28763
|
}
|