clementine-agent 1.18.114 → 1.18.117
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/run-agent.js +23 -1
- package/dist/cli/dashboard.js +473 -48
- package/package.json +1 -1
package/dist/agent/run-agent.js
CHANGED
|
@@ -128,6 +128,28 @@ function buildRunAgentEnv() {
|
|
|
128
128
|
return env;
|
|
129
129
|
}
|
|
130
130
|
const logger = pino({ name: 'clementine.run-agent' });
|
|
131
|
+
/**
|
|
132
|
+
* Map a sessionKey to the CLEMENTINE_INTERACTION_SOURCE value the MCP
|
|
133
|
+
* subprocess will read. Mirrors `inferInteractionSource` in assistant.ts
|
|
134
|
+
* (kept inline here to avoid a circular import — assistant.ts already
|
|
135
|
+
* imports from this module). Owner-DM-only admin tools like
|
|
136
|
+
* refresh_tool_inventory / allow_tool / env_set check for exactly
|
|
137
|
+
* 'owner-dm', so the previous hardcoded 'interactive' value broke them
|
|
138
|
+
* for every chat session routed through runAgent.
|
|
139
|
+
*/
|
|
140
|
+
function interactionSourceForSession(sessionKey, source) {
|
|
141
|
+
if (source === 'cron' || source === 'heartbeat')
|
|
142
|
+
return 'autonomous';
|
|
143
|
+
if (!sessionKey)
|
|
144
|
+
return 'autonomous';
|
|
145
|
+
if (sessionKey.startsWith('discord:member'))
|
|
146
|
+
return 'member-channel';
|
|
147
|
+
if (sessionKey.startsWith('discord:channel:'))
|
|
148
|
+
return 'owner-channel';
|
|
149
|
+
if (sessionKey.includes(':'))
|
|
150
|
+
return 'owner-dm';
|
|
151
|
+
return 'autonomous';
|
|
152
|
+
}
|
|
131
153
|
// Last-resort fallbacks for callers that pass NO maxBudgetUsd. The
|
|
132
154
|
// production callers (`runAgent` from gateway/router, runAgentCron,
|
|
133
155
|
// runAgentHeartbeat) read `BUDGET.*` from src/config.ts — which is
|
|
@@ -229,7 +251,7 @@ export async function runAgent(prompt, opts) {
|
|
|
229
251
|
...subprocessEnv,
|
|
230
252
|
CLEMENTINE_HOME: BASE_DIR,
|
|
231
253
|
...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
|
|
232
|
-
CLEMENTINE_INTERACTION_SOURCE:
|
|
254
|
+
CLEMENTINE_INTERACTION_SOURCE: interactionSourceForSession(opts.sessionKey, source),
|
|
233
255
|
},
|
|
234
256
|
},
|
|
235
257
|
...(opts.extraMcpServers ?? {}),
|
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 });
|
|
10178
|
+
}
|
|
10179
|
+
catch (err) {
|
|
10180
|
+
res.status(500).json({ error: String(err) });
|
|
10181
|
+
}
|
|
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' });
|
|
10138
10235
|
}
|
|
10139
10236
|
catch (err) {
|
|
10140
10237
|
res.status(500).json({ error: String(err) });
|
|
10141
10238
|
}
|
|
10142
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">
|
|
@@ -24351,11 +24483,30 @@ function renderScheduledTaskCard(task) {
|
|
|
24351
24483
|
var runOrCancelBtn = isRunning
|
|
24352
24484
|
? '<button class="btn-sm secondary btn-danger" onclick="cancelCronRun(\\x27' + safeName + '\\x27)" title="Stop this in-flight run (SIGTERM)">Cancel</button>'
|
|
24353
24485
|
: '<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>';
|
|
24486
|
+
// 1.18.115 — preview line. Prefer task.description (a future-proof
|
|
24487
|
+
// dedicated field), then strip leading TOOL RESTRICTIONS / FORBIDDEN
|
|
24488
|
+
// boilerplate (purely a runtime allowlist, not what the task does), then
|
|
24489
|
+
// grab the first real sentence so the card reads as "what this task is
|
|
24490
|
+
// for" rather than "wall of LLM scaffolding."
|
|
24491
|
+
function _taskPreview(t) {
|
|
24492
|
+
if (t.description && String(t.description).trim()) return String(t.description).trim();
|
|
24493
|
+
var p = String(t.prompt || '').trim();
|
|
24494
|
+
// Strip the canonical tool-restriction preamble (matches the user's
|
|
24495
|
+
// TOOL RESTRICTIONS — MANDATORY... block up to the first paragraph
|
|
24496
|
+
// that doesn't start with a numbered restriction line).
|
|
24497
|
+
var stripped = p.replace(/^TOOL RESTRICTIONS[\\s\\S]*?(?=\\n\\n[A-Z][^.]*\\.|\\n\\n\\w)/i, '').trim();
|
|
24498
|
+
if (!stripped) stripped = p;
|
|
24499
|
+
// First sentence or first line, whichever is shorter. Falls back to
|
|
24500
|
+
// the first 200 chars if nothing punctuates.
|
|
24501
|
+
var firstLine = stripped.split('\\n').map(function(s){ return s.trim(); }).find(function(s){ return s.length > 0; }) || '';
|
|
24502
|
+
var firstSentence = (firstLine.match(/^[^.!?]+[.!?]/) || [firstLine])[0];
|
|
24503
|
+
return (firstSentence || stripped).slice(0, 200);
|
|
24504
|
+
}
|
|
24354
24505
|
return '<div class="' + cardCls + '" style="' + style + '">'
|
|
24355
24506
|
+ '<div class="task-card-header"><strong>' + esc(task.displayName || task.name) + '</strong>'
|
|
24356
24507
|
+ '<label class="toggle-switch"><input type="checkbox"' + (enabled ? ' checked' : '') + ' onchange="toggleCronJob(\\x27' + safeName + '\\x27)"><span class="toggle-slider"></span></label></div>'
|
|
24357
24508
|
+ '<div class="task-card-schedule">' + operationScheduleHtml(task.schedule) + '</div>'
|
|
24358
|
-
+ '<div class="task-card-prompt">' + esc(task
|
|
24509
|
+
+ '<div class="task-card-prompt">' + esc(_taskPreview(task)) + '</div>'
|
|
24359
24510
|
+ renderTrickCapabilityStrip(task)
|
|
24360
24511
|
+ '<div class="task-card-status">' + lastRunHtml + '</div>'
|
|
24361
24512
|
+ renderTrickTagChips(task)
|
|
@@ -25809,15 +25960,24 @@ async function refreshCron() {
|
|
|
25809
25960
|
var visibleRunning = ownerScoped ? (ops.runningNow || []).filter(function(i) { return buildOpsOwnerMatches(i.owner || ''); }) : (ops.runningNow || []);
|
|
25810
25961
|
var ownerFilter = getBuildOwnerFilter();
|
|
25811
25962
|
|
|
25812
|
-
// PRD §12 / 1.18.88: Health Strip
|
|
25813
|
-
//
|
|
25814
|
-
//
|
|
25963
|
+
// PRD §12 / 1.18.88: Health Strip — kept always-visible at the top.
|
|
25964
|
+
// 7 KPI tiles (24h runs, success rate, cost, p50/p95 latency, running,
|
|
25965
|
+
// top failure). The runs payload from /api/cron/runs (already fetched
|
|
25966
|
+
// alongside ops) feeds the metrics. Render an empty shell first;
|
|
25967
|
+
// refreshHealthStrip fills it in.
|
|
25815
25968
|
var html = '<div id="health-strip" class="health-strip"></div>';
|
|
25816
|
-
//
|
|
25817
|
-
//
|
|
25818
|
-
//
|
|
25819
|
-
//
|
|
25820
|
-
|
|
25969
|
+
// 1.18.115 — collapse the cost/latency/reliability/activity mini-cards
|
|
25970
|
+
// into a <details> block. The Health Strip already covers what most
|
|
25971
|
+
// users want at a glance; the 4 mini-dashboards are for deeper
|
|
25972
|
+
// observability and don't need to be the second thing on the page.
|
|
25973
|
+
// Closed by default; users who want them flip it open once and the
|
|
25974
|
+
// browser remembers via the [open] attribute persistence pattern.
|
|
25975
|
+
html += '<details class="mini-dashboards-toggle" style="margin:14px 0 4px">'
|
|
25976
|
+
+ '<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">'
|
|
25977
|
+
+ '<span style="font-size:11px">▸</span> Show cost / latency / reliability mini-dashboards'
|
|
25978
|
+
+ '</summary>'
|
|
25979
|
+
+ '<div id="mini-dashboards" class="mini-dashboards" style="margin-top:10px"></div>'
|
|
25980
|
+
+ '</details>';
|
|
25821
25981
|
|
|
25822
25982
|
// ── Zone 1 — Running now (promoted to top, primary "what's live" view) ──
|
|
25823
25983
|
if (visibleRunning.length > 0) {
|
|
@@ -26748,6 +26908,250 @@ async function migrateAllLegacySkills() {
|
|
|
26748
26908
|
} catch (err) { toast('Bulk migration failed: ' + err, 'error'); }
|
|
26749
26909
|
}
|
|
26750
26910
|
|
|
26911
|
+
// 1.18.115 — tiny inline Markdown renderer for the Skills detail pane.
|
|
26912
|
+
// Handles the subset SKILL.md bodies use in practice: headers, bold,
|
|
26913
|
+
// italic, inline-code, fenced code blocks, ordered + unordered lists,
|
|
26914
|
+
// paragraphs, line breaks. Pulling in marked or markdown-it would balloon
|
|
26915
|
+
// the served bundle for ~80 lines of regex. Output is always escaped
|
|
26916
|
+
// first, then re-styled.
|
|
26917
|
+
//
|
|
26918
|
+
// NOTE: regex patterns containing backtick characters are constructed via
|
|
26919
|
+
// new RegExp(string) instead of regex literals — raw backticks would
|
|
26920
|
+
// otherwise close the surrounding TypeScript template literal that
|
|
26921
|
+
// builds the served HTML.
|
|
26922
|
+
function renderMarkdown(src) {
|
|
26923
|
+
if (!src) return '';
|
|
26924
|
+
// Escape every char first; we never inject raw HTML from the body.
|
|
26925
|
+
var s = String(src)
|
|
26926
|
+
.replace(/&/g, '&')
|
|
26927
|
+
.replace(/</g, '<')
|
|
26928
|
+
.replace(/>/g, '>')
|
|
26929
|
+
.replace(/"/g, '"');
|
|
26930
|
+
// Fenced code blocks (preserve interior verbatim — no inline markup
|
|
26931
|
+
// applied inside). Use a placeholder map so subsequent regex passes
|
|
26932
|
+
// don't munge the contents. Fence delimiter is three backticks.
|
|
26933
|
+
var BACKTICK = String.fromCharCode(96);
|
|
26934
|
+
var fences = [];
|
|
26935
|
+
var fenceRe = new RegExp(BACKTICK + BACKTICK + BACKTICK + '([a-z0-9_-]*)\\n?([\\s\\S]*?)' + BACKTICK + BACKTICK + BACKTICK, 'gi');
|
|
26936
|
+
s = s.replace(fenceRe, function(_, lang, code) {
|
|
26937
|
+
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>');
|
|
26938
|
+
return '\\u0000FENCE_' + (fences.length - 1) + '\\u0000';
|
|
26939
|
+
});
|
|
26940
|
+
// Inline code — single backticks
|
|
26941
|
+
var inlineRe = new RegExp(BACKTICK + '([^' + BACKTICK + '\\n]+)' + BACKTICK, 'g');
|
|
26942
|
+
s = s.replace(inlineRe, '<code style="background:var(--bg-tertiary);padding:1px 6px;border-radius:3px;font-size:0.92em">$1</code>');
|
|
26943
|
+
// Bold first (greedier double-star) so it doesn't get eaten by single-star italic.
|
|
26944
|
+
s = s.replace(/\\*\\*([^*\\n][^*]*?)\\*\\*/g, '<strong>$1</strong>');
|
|
26945
|
+
s = s.replace(/(^|[^*])\\*([^*\\n][^*]*?)\\*/g, '$1<em>$2</em>');
|
|
26946
|
+
// Headers — process line-by-line so we don't accidentally match across
|
|
26947
|
+
// paragraphs. Also collect lists into <ul>/<ol> blocks.
|
|
26948
|
+
var lines = s.split('\\n');
|
|
26949
|
+
var out = [];
|
|
26950
|
+
var listKind = null; // 'ul' | 'ol' | null
|
|
26951
|
+
var listItems = [];
|
|
26952
|
+
function flushList() {
|
|
26953
|
+
if (!listKind) return;
|
|
26954
|
+
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 + '>');
|
|
26955
|
+
listKind = null; listItems = [];
|
|
26956
|
+
}
|
|
26957
|
+
function paraStart() { out.push('<p style="margin:8px 0">'); }
|
|
26958
|
+
function paraEnd() {
|
|
26959
|
+
// Close the last <p> if there is one open.
|
|
26960
|
+
var last = out.length - 1;
|
|
26961
|
+
if (last >= 0 && out[last].endsWith('</p>')) return;
|
|
26962
|
+
if (last >= 0 && out[last] === '<p style="margin:8px 0">') { out.pop(); return; }
|
|
26963
|
+
if (last >= 0 && !/^<(h\\d|ul|ol|pre|hr|details)/.test(out[last])) {
|
|
26964
|
+
// We're mid-paragraph — emit the close tag.
|
|
26965
|
+
out.push('</p>');
|
|
26966
|
+
}
|
|
26967
|
+
}
|
|
26968
|
+
var paraOpen = false;
|
|
26969
|
+
function closePara() { if (paraOpen) { out.push('</p>'); paraOpen = false; } }
|
|
26970
|
+
function openPara() { if (!paraOpen) { out.push('<p style="margin:8px 0">'); paraOpen = true; } }
|
|
26971
|
+
for (var i = 0; i < lines.length; i++) {
|
|
26972
|
+
var line = lines[i];
|
|
26973
|
+
// Restore fence placeholders as their own block.
|
|
26974
|
+
var fenceMatch = line.match(/\\u0000FENCE_(\\d+)\\u0000/);
|
|
26975
|
+
if (fenceMatch) { closePara(); flushList(); out.push(fences[Number(fenceMatch[1])]); continue; }
|
|
26976
|
+
// Headers
|
|
26977
|
+
var hMatch = line.match(/^(#{1,6})\\s+(.+)$/);
|
|
26978
|
+
if (hMatch) {
|
|
26979
|
+
closePara(); flushList();
|
|
26980
|
+
var level = hMatch[1].length;
|
|
26981
|
+
var sizes = { 1: '1.4em', 2: '1.2em', 3: '1.05em', 4: '1em', 5: '0.95em', 6: '0.9em' };
|
|
26982
|
+
out.push('<h' + level + ' style="margin:14px 0 6px;font-size:' + sizes[level] + ';font-weight:600;color:var(--text-primary)">' + hMatch[2] + '</h' + level + '>');
|
|
26983
|
+
continue;
|
|
26984
|
+
}
|
|
26985
|
+
// Unordered list — dash, star, or plus prefix
|
|
26986
|
+
var ulMatch = line.match(/^\\s*[-*+]\\s+(.+)$/);
|
|
26987
|
+
if (ulMatch) {
|
|
26988
|
+
closePara();
|
|
26989
|
+
if (listKind && listKind !== 'ul') flushList();
|
|
26990
|
+
listKind = 'ul';
|
|
26991
|
+
listItems.push(ulMatch[1]);
|
|
26992
|
+
continue;
|
|
26993
|
+
}
|
|
26994
|
+
// Ordered list — 1., 2., etc.
|
|
26995
|
+
var olMatch = line.match(/^\\s*\\d+\\.\\s+(.+)$/);
|
|
26996
|
+
if (olMatch) {
|
|
26997
|
+
closePara();
|
|
26998
|
+
if (listKind && listKind !== 'ol') flushList();
|
|
26999
|
+
listKind = 'ol';
|
|
27000
|
+
listItems.push(olMatch[1]);
|
|
27001
|
+
continue;
|
|
27002
|
+
}
|
|
27003
|
+
// Blank line — paragraph break
|
|
27004
|
+
if (!line.trim()) { closePara(); flushList(); continue; }
|
|
27005
|
+
// Hr
|
|
27006
|
+
if (/^---+$/.test(line.trim())) { closePara(); flushList(); out.push('<hr style="border:none;border-top:1px solid var(--border);margin:12px 0">'); continue; }
|
|
27007
|
+
// Default — wrap as paragraph text
|
|
27008
|
+
flushList();
|
|
27009
|
+
openPara();
|
|
27010
|
+
// Append to current paragraph with a soft break between consecutive
|
|
27011
|
+
// text lines (markdown convention: lines in a paragraph join with space).
|
|
27012
|
+
var top = out.length - 1;
|
|
27013
|
+
if (out[top] === '<p style="margin:8px 0">') {
|
|
27014
|
+
out[top] = '<p style="margin:8px 0">' + line;
|
|
27015
|
+
} else {
|
|
27016
|
+
out[top] = out[top] + ' ' + line;
|
|
27017
|
+
}
|
|
27018
|
+
}
|
|
27019
|
+
closePara(); flushList();
|
|
27020
|
+
return out.join('\\n');
|
|
27021
|
+
}
|
|
27022
|
+
|
|
27023
|
+
// 1.18.115 — Skill creation modal. Until now there was no UI to make a
|
|
27024
|
+
// new skill; users had to mkdir + write SKILL.md by hand. Modal collects
|
|
27025
|
+
// name (Anthropic regex enforced client-side), description, body, and an
|
|
27026
|
+
// optional comma-separated tools.allow allowlist; POSTs to a new endpoint
|
|
27027
|
+
// that calls skill-store.parseSkillFolder/write under the hood.
|
|
27028
|
+
function openCreateSkillModal() { _openSkillModal({ mode: 'create' }); }
|
|
27029
|
+
function openEditSkillModal(name) { _openSkillModal({ mode: 'edit', name: name }); }
|
|
27030
|
+
|
|
27031
|
+
async function _openSkillModal(opts) {
|
|
27032
|
+
opts = opts || {};
|
|
27033
|
+
var existing = null;
|
|
27034
|
+
if (opts.mode === 'edit' && opts.name) {
|
|
27035
|
+
try {
|
|
27036
|
+
var r = await apiFetch('/api/skills/' + encodeURIComponent(opts.name));
|
|
27037
|
+
if (r.ok) existing = await r.json();
|
|
27038
|
+
} catch (e) { toast('Failed to load skill: ' + e, 'error'); return; }
|
|
27039
|
+
}
|
|
27040
|
+
var fm = (existing && existing.frontmatter) || {};
|
|
27041
|
+
var ext = fm.clementine || {};
|
|
27042
|
+
var nameVal = fm.name || '';
|
|
27043
|
+
var titleVal = fm.title || '';
|
|
27044
|
+
var descVal = fm.description || '';
|
|
27045
|
+
var bodyVal = (existing && existing.body) || '';
|
|
27046
|
+
var toolsVal = (ext.tools && Array.isArray(ext.tools.allow)) ? ext.tools.allow.join(', ') : '';
|
|
27047
|
+
var modal = document.getElementById('skill-edit-modal');
|
|
27048
|
+
if (!modal) {
|
|
27049
|
+
modal = document.createElement('div');
|
|
27050
|
+
modal.id = 'skill-edit-modal';
|
|
27051
|
+
modal.className = 'modal-overlay';
|
|
27052
|
+
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';
|
|
27053
|
+
modal.innerHTML =
|
|
27054
|
+
'<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)">'
|
|
27055
|
+
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border)">'
|
|
27056
|
+
+ '<h3 id="skill-modal-title" style="margin:0;font-size:15px;font-weight:600">New skill</h3>'
|
|
27057
|
+
+ '<button onclick="closeSkillModal()" style="background:none;border:none;font-size:18px;color:var(--text-muted);cursor:pointer;padding:0 4px;line-height:1">✕</button>'
|
|
27058
|
+
+ '</div>'
|
|
27059
|
+
+ '<div style="flex:1;overflow-y:auto;padding:18px 22px">'
|
|
27060
|
+
+ '<input type="hidden" id="skill-modal-original-name">'
|
|
27061
|
+
+ '<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>'
|
|
27062
|
+
+ '<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">'
|
|
27063
|
+
+ '<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>'
|
|
27064
|
+
+ '<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">'
|
|
27065
|
+
+ '<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>'
|
|
27066
|
+
+ '<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>'
|
|
27067
|
+
+ '<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>'
|
|
27068
|
+
+ '<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">'
|
|
27069
|
+
+ '<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>'
|
|
27070
|
+
+ '<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>'
|
|
27071
|
+
+ '<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>'
|
|
27072
|
+
+ '</div>'
|
|
27073
|
+
+ '<div style="display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--border);background:var(--bg-secondary)">'
|
|
27074
|
+
+ '<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>'
|
|
27075
|
+
+ '<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>'
|
|
27076
|
+
+ '</div>'
|
|
27077
|
+
+ '</div>';
|
|
27078
|
+
document.body.appendChild(modal);
|
|
27079
|
+
}
|
|
27080
|
+
document.getElementById('skill-modal-title').textContent = opts.mode === 'edit' ? 'Edit skill: ' + nameVal : 'New skill';
|
|
27081
|
+
document.getElementById('skill-modal-original-name').value = opts.mode === 'edit' ? nameVal : '';
|
|
27082
|
+
document.getElementById('skill-modal-name').value = nameVal;
|
|
27083
|
+
document.getElementById('skill-modal-name').disabled = opts.mode === 'edit';
|
|
27084
|
+
document.getElementById('skill-modal-title').nextElementSibling; // no-op
|
|
27085
|
+
document.getElementById('skill-modal-title').value = titleVal;
|
|
27086
|
+
document.getElementById('skill-modal-desc').value = descVal;
|
|
27087
|
+
document.getElementById('skill-modal-tools').value = toolsVal;
|
|
27088
|
+
document.getElementById('skill-modal-body').value = bodyVal;
|
|
27089
|
+
var errEl = document.getElementById('skill-modal-error');
|
|
27090
|
+
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
27091
|
+
modal.style.display = 'flex';
|
|
27092
|
+
document.getElementById('skill-modal-name').focus();
|
|
27093
|
+
}
|
|
27094
|
+
|
|
27095
|
+
function closeSkillModal() {
|
|
27096
|
+
var m = document.getElementById('skill-edit-modal');
|
|
27097
|
+
if (m) m.style.display = 'none';
|
|
27098
|
+
}
|
|
27099
|
+
|
|
27100
|
+
async function saveSkillFromModal() {
|
|
27101
|
+
var name = (document.getElementById('skill-modal-name')?.value || '').trim();
|
|
27102
|
+
var title = (document.getElementById('skill-modal-title')?.value || '').trim();
|
|
27103
|
+
var desc = (document.getElementById('skill-modal-desc')?.value || '').trim();
|
|
27104
|
+
var toolsRaw = (document.getElementById('skill-modal-tools')?.value || '').trim();
|
|
27105
|
+
var body = (document.getElementById('skill-modal-body')?.value || '');
|
|
27106
|
+
var originalName = (document.getElementById('skill-modal-original-name')?.value || '').trim();
|
|
27107
|
+
var errEl = document.getElementById('skill-modal-error');
|
|
27108
|
+
function fail(msg) {
|
|
27109
|
+
if (errEl) { errEl.textContent = msg; errEl.style.display = ''; }
|
|
27110
|
+
else toast(msg, 'error');
|
|
27111
|
+
}
|
|
27112
|
+
if (!name) return fail('Name is required.');
|
|
27113
|
+
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.');
|
|
27114
|
+
if (!desc) return fail('Description is required (used by Claude to decide when to apply this skill).');
|
|
27115
|
+
if (desc.length > 1024) return fail('Description must be ≤ 1024 chars (Anthropic spec).');
|
|
27116
|
+
if (!body.trim()) return fail('Procedure body is required.');
|
|
27117
|
+
var tools = toolsRaw ? toolsRaw.split(',').map(function(s){ return s.trim(); }).filter(Boolean) : [];
|
|
27118
|
+
var saveBtn = document.getElementById('skill-modal-save');
|
|
27119
|
+
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; }
|
|
27120
|
+
try {
|
|
27121
|
+
var endpoint = originalName ? '/api/skills/' + encodeURIComponent(originalName) : '/api/skills';
|
|
27122
|
+
var method = originalName ? 'PUT' : 'POST';
|
|
27123
|
+
var r = await apiFetch(endpoint, {
|
|
27124
|
+
method: method,
|
|
27125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27126
|
+
body: JSON.stringify({ name: name, title: title || undefined, description: desc, tools: tools, body: body }),
|
|
27127
|
+
});
|
|
27128
|
+
if (!r.ok) {
|
|
27129
|
+
var d = await r.json().catch(function(){ return {}; });
|
|
27130
|
+
return fail(d.error || ('Save failed: HTTP ' + r.status));
|
|
27131
|
+
}
|
|
27132
|
+
closeSkillModal();
|
|
27133
|
+
toast(originalName ? 'Skill updated' : 'Skill created', 'success');
|
|
27134
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
27135
|
+
// Auto-open the freshly-saved skill so the user sees their work.
|
|
27136
|
+
if (typeof showSkillDetail === 'function') showSkillDetail(name);
|
|
27137
|
+
} catch (err) { fail('Save failed: ' + err); }
|
|
27138
|
+
finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Save skill'; } }
|
|
27139
|
+
}
|
|
27140
|
+
|
|
27141
|
+
async function confirmDeleteSkill(name) {
|
|
27142
|
+
if (!confirm('Delete skill "' + name + '"? The folder will be removed; the .md.bak (if present) is preserved.')) return;
|
|
27143
|
+
try {
|
|
27144
|
+
var r = await apiFetch('/api/skills/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
27145
|
+
if (!r.ok) {
|
|
27146
|
+
var d = await r.json().catch(function(){ return {}; });
|
|
27147
|
+
toast(d.error || 'Delete failed', 'error');
|
|
27148
|
+
return;
|
|
27149
|
+
}
|
|
27150
|
+
toast('Skill "' + name + '" deleted', 'success');
|
|
27151
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
27152
|
+
} catch (err) { toast('Delete failed: ' + err, 'error'); }
|
|
27153
|
+
}
|
|
27154
|
+
|
|
26751
27155
|
async function refreshSkillsPage() {
|
|
26752
27156
|
var listEl = document.getElementById('skills-list');
|
|
26753
27157
|
var detailEl = document.getElementById('skills-detail');
|
|
@@ -27027,7 +27431,10 @@ function renderSkillDetail(s) {
|
|
|
27027
27431
|
html += renderSkillSection('Usage', '<div style="font-size:12px;color:var(--text-secondary)">' + ubits.map(esc).join(' · ') + '</div>');
|
|
27028
27432
|
}
|
|
27029
27433
|
|
|
27030
|
-
// ── 7. Procedure body (
|
|
27434
|
+
// ── 7. Procedure body — rendered as markdown (1.18.115). Prior shipped
|
|
27435
|
+
// raw <pre> source which read like config, not procedure. Headers/lists/
|
|
27436
|
+
// bold/code now render visually. Line counter still appears so authors
|
|
27437
|
+
// know if they're approaching Anthropic's ≤500-line guidance.
|
|
27031
27438
|
if (s.body && s.body.trim()) {
|
|
27032
27439
|
var bodyClass = bodyLines > 500 ? 'color:var(--yellow)' : 'color:var(--text-muted)';
|
|
27033
27440
|
html += '<div style="margin-top:18px">';
|
|
@@ -27035,10 +27442,17 @@ function renderSkillDetail(s) {
|
|
|
27035
27442
|
html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:500">Procedure</div>';
|
|
27036
27443
|
html += '<div style="font-size:10px;' + bodyClass + ';font-family:\\x27JetBrains Mono\\x27,monospace">' + bodyLines + ' / 500 lines</div>';
|
|
27037
27444
|
html += '</div>';
|
|
27038
|
-
html += '<
|
|
27445
|
+
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
27446
|
html += '</div>';
|
|
27040
27447
|
}
|
|
27041
27448
|
|
|
27449
|
+
// ── 8. Action footer (1.18.115) — Edit + Delete + Open file. The pane
|
|
27450
|
+
// was read-only; users had to leave the dashboard to edit anything.
|
|
27451
|
+
html += '<div style="margin-top:24px;display:flex;gap:8px;flex-wrap:wrap">';
|
|
27452
|
+
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>';
|
|
27453
|
+
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>';
|
|
27454
|
+
html += '</div>';
|
|
27455
|
+
|
|
27042
27456
|
// ── 8. Schema-specific footer
|
|
27043
27457
|
if (s.schemaVersion === 'legacy') {
|
|
27044
27458
|
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 +28030,20 @@ function renderSkillsPickerList() {
|
|
|
27616
28030
|
return hay.indexOf(q) !== -1;
|
|
27617
28031
|
});
|
|
27618
28032
|
}
|
|
28033
|
+
// 1.18.115 — "+ Create new skill" affordance pinned to the top of the
|
|
28034
|
+
// picker. Lets users author a skill in-flight without losing their cron
|
|
28035
|
+
// edit; the new skill appears in the list right after the modal closes.
|
|
28036
|
+
var createRow = '<div class="cap-picker-row" style="border:1px dashed var(--accent);background:transparent" onclick="openCreateSkillModal()">'
|
|
28037
|
+
+ '<div class="cap-picker-row-body">'
|
|
28038
|
+
+ '<div class="cap-picker-row-title" style="color:var(--accent);font-weight:600">+ Create new skill</div>'
|
|
28039
|
+
+ '<div class="cap-picker-row-desc">Author a fresh skill — opens the editor without leaving this task.</div>'
|
|
28040
|
+
+ '</div>'
|
|
28041
|
+
+ '</div>';
|
|
27619
28042
|
if (skills.length === 0) {
|
|
27620
|
-
listEl.innerHTML = '<div class="cap-picker-empty-state">' + (q ? 'No matches.' : 'No skills available.') + '</div>';
|
|
28043
|
+
listEl.innerHTML = createRow + '<div class="cap-picker-empty-state">' + (q ? 'No matches.' : 'No skills available.') + '</div>';
|
|
27621
28044
|
return;
|
|
27622
28045
|
}
|
|
27623
|
-
listEl.innerHTML = skills.slice(0, 50).map(function(s) {
|
|
28046
|
+
listEl.innerHTML = createRow + skills.slice(0, 50).map(function(s) {
|
|
27624
28047
|
var sel = _cronSelectedSkills.indexOf(s.name) !== -1;
|
|
27625
28048
|
var triggers = (s.triggers || []).slice(0, 4).join(', ');
|
|
27626
28049
|
return '<div class="cap-picker-row' + (sel ? ' selected' : '') + '" onclick="addSkillToTrick(\\x27' + jsStr(s.name) + '\\x27)">'
|
|
@@ -28300,19 +28723,21 @@ function onPredictableChange() {
|
|
|
28300
28723
|
function renderCronLegacyBanner(job) {
|
|
28301
28724
|
var host = document.getElementById('cron-legacy-banner-host');
|
|
28302
28725
|
if (!host) return;
|
|
28303
|
-
//
|
|
28304
|
-
//
|
|
28726
|
+
// Predictable jobs skip the tip entirely; legacy jobs get a non-alarming
|
|
28727
|
+
// suggestion. The previous wording ("OUTPUT MAY NOT MATCH WHAT YOU SEE
|
|
28728
|
+
// HERE") read as "the editor is lying to you" and made every legacy task
|
|
28729
|
+
// feel broken. It isn't — runs work fine. The toggle below explains the
|
|
28730
|
+
// trade-off; this is just a one-click shortcut for the common upgrade.
|
|
28305
28731
|
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
28732
|
host.innerHTML =
|
|
28310
|
-
'<div class="cron-banner
|
|
28311
|
-
+ '<
|
|
28312
|
-
|
|
28313
|
-
|
|
28314
|
-
|
|
28315
|
-
|
|
28733
|
+
'<div class="cron-banner info">'
|
|
28734
|
+
+ '<div style="display:flex;align-items:flex-start;gap:10px">'
|
|
28735
|
+
+ '<span style="font-size:14px;line-height:1.2;flex-shrink:0">💡</span>'
|
|
28736
|
+
+ '<div style="flex:1;min-width:0">'
|
|
28737
|
+
+ '<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:2px">Strict mode available</div>'
|
|
28738
|
+
+ '<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>'
|
|
28739
|
+
+ '</div>'
|
|
28740
|
+
+ '<button class="btn-primary btn-sm" onclick="enablePredictableFromBanner()" style="flex-shrink:0;font-size:11px;padding:5px 12px">Switch on</button>'
|
|
28316
28741
|
+ '</div>'
|
|
28317
28742
|
+ '</div>';
|
|
28318
28743
|
}
|