clementine-agent 1.18.155 → 1.18.157
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 +291 -27
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -0
- package/vault/00-System/skills/skill-creator/LICENSE.txt +202 -0
- package/vault/00-System/skills/skill-creator/SKILL.md +485 -0
- package/vault/00-System/skills/skill-creator/agents/analyzer.md +274 -0
- package/vault/00-System/skills/skill-creator/agents/comparator.md +202 -0
- package/vault/00-System/skills/skill-creator/agents/grader.md +223 -0
- package/vault/00-System/skills/skill-creator/assets/eval_review.html +146 -0
- package/vault/00-System/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/vault/00-System/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/vault/00-System/skills/skill-creator/references/schemas.md +430 -0
- package/vault/00-System/skills/skill-creator/scripts/__init__.py +0 -0
- package/vault/00-System/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/vault/00-System/skills/skill-creator/scripts/generate_report.py +326 -0
- package/vault/00-System/skills/skill-creator/scripts/improve_description.py +247 -0
- package/vault/00-System/skills/skill-creator/scripts/package_skill.py +136 -0
- package/vault/00-System/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/vault/00-System/skills/skill-creator/scripts/run_eval.py +310 -0
- package/vault/00-System/skills/skill-creator/scripts/run_loop.py +328 -0
- package/vault/00-System/skills/skill-creator/scripts/utils.py +47 -0
package/dist/cli/dashboard.js
CHANGED
|
@@ -5136,21 +5136,33 @@ export async function cmdDashboard(opts) {
|
|
|
5136
5136
|
res.status(500).json({ ok: false, error: String(err) });
|
|
5137
5137
|
}
|
|
5138
5138
|
});
|
|
5139
|
-
app.post('/api/cron/migrate-all-to-skills', async (
|
|
5139
|
+
app.post('/api/cron/migrate-all-to-skills', async (req, res) => {
|
|
5140
5140
|
try {
|
|
5141
|
-
const { findMatchingSkill } = await import('../agent/cron-migrator.js');
|
|
5142
|
-
const { listSkills } = await import('../agent/skill-store.js');
|
|
5141
|
+
const { findMatchingSkill, generateDescription } = await import('../agent/cron-migrator.js');
|
|
5142
|
+
const { listSkills, writeSkill } = await import('../agent/skill-store.js');
|
|
5143
5143
|
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5144
5144
|
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5145
|
+
// 1.18.156 — `createMissing` defaults true so the bulk path actually
|
|
5146
|
+
// converts every legacy cron (auto-generates a folder-form skill from
|
|
5147
|
+
// the cron prompt when no match exists). The user can still pass
|
|
5148
|
+
// `{createMissing: false}` if they only want safe matches. .bak
|
|
5149
|
+
// backups still happen either way.
|
|
5150
|
+
const body = (req.body ?? {});
|
|
5151
|
+
const createMissing = body.createMissing !== false;
|
|
5145
5152
|
const skills = listSkills();
|
|
5146
5153
|
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5147
5154
|
const legacy = allJobs.filter(j => j.source !== 'scheduled-skill');
|
|
5148
5155
|
const migrated = [];
|
|
5149
5156
|
const skipped = [];
|
|
5157
|
+
// Track names we've just created so subsequent jobs in the loop don't
|
|
5158
|
+
// bail with "skill not in catalog" before listSkills() refreshes.
|
|
5159
|
+
const createdThisPass = new Set();
|
|
5160
|
+
const inCatalog = (n) => createdThisPass.has(n) || skills.some(s => s.frontmatter.name === n);
|
|
5150
5161
|
// Group by source CRON.md so we open + parse + write each file once.
|
|
5151
5162
|
const byFile = new Map();
|
|
5152
5163
|
for (const job of legacy) {
|
|
5153
5164
|
let skillName = null;
|
|
5165
|
+
let created = false;
|
|
5154
5166
|
if (job.skills && job.skills.length > 0) {
|
|
5155
5167
|
skillName = job.skills.find(s => skills.some(sk => sk.frontmatter.name === s)) ?? null;
|
|
5156
5168
|
}
|
|
@@ -5160,10 +5172,49 @@ export async function cmdDashboard(opts) {
|
|
|
5160
5172
|
skillName = m.frontmatter.name;
|
|
5161
5173
|
}
|
|
5162
5174
|
if (!skillName) {
|
|
5163
|
-
|
|
5164
|
-
|
|
5175
|
+
if (!createMissing) {
|
|
5176
|
+
skipped.push({ name: job.name, reason: 'no skill match (set createMissing=true to auto-create)' });
|
|
5177
|
+
continue;
|
|
5178
|
+
}
|
|
5179
|
+
// Auto-create a skill from the cron's prompt. Mirrors the per-row
|
|
5180
|
+
// /migrate-with-skill-creation endpoint logic.
|
|
5181
|
+
const candidate = job.name
|
|
5182
|
+
.toLowerCase().replace(/[_\s]+/g, '-').replace(/[^a-z0-9-]/g, '')
|
|
5183
|
+
.replace(/-+/g, '-').replace(/^-+|-+$/g, '')
|
|
5184
|
+
.replace(/-(cron|task|job)$/, '').slice(0, 64);
|
|
5185
|
+
if (!candidate || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(candidate) || inCatalog(candidate)) {
|
|
5186
|
+
skipped.push({ name: job.name, reason: `cannot derive a unique kebab-case name (candidate: "${candidate}")` });
|
|
5187
|
+
continue;
|
|
5188
|
+
}
|
|
5189
|
+
const cronPrompt = String(job.prompt || '').trim();
|
|
5190
|
+
const desc = generateDescription(cronPrompt, null);
|
|
5191
|
+
const triggerPhrase = job.name.replace(/-/g, ' ');
|
|
5192
|
+
const description = `${desc} Use when this scheduled task fires (cron: ${job.schedule}) or when the user mentions "${triggerPhrase}".`.slice(0, 1024);
|
|
5193
|
+
try {
|
|
5194
|
+
writeSkill({
|
|
5195
|
+
name: candidate,
|
|
5196
|
+
description,
|
|
5197
|
+
body: [
|
|
5198
|
+
`# ${candidate.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}`,
|
|
5199
|
+
'', '## Instructions', '',
|
|
5200
|
+
cronPrompt || '(prompt not captured — fill in)',
|
|
5201
|
+
'', '## When to use', '',
|
|
5202
|
+
`Auto-fired on schedule \`${job.schedule}\`. Also matches user prompts about "${triggerPhrase}".`,
|
|
5203
|
+
'',
|
|
5204
|
+
].join('\n'),
|
|
5205
|
+
source: 'imported',
|
|
5206
|
+
...(job.agentSlug ? { agentSlug: job.agentSlug } : {}),
|
|
5207
|
+
});
|
|
5208
|
+
createdThisPass.add(candidate);
|
|
5209
|
+
skillName = candidate;
|
|
5210
|
+
created = true;
|
|
5211
|
+
}
|
|
5212
|
+
catch (writeErr) {
|
|
5213
|
+
skipped.push({ name: job.name, reason: `skill auto-create failed: ${String(writeErr)}` });
|
|
5214
|
+
continue;
|
|
5215
|
+
}
|
|
5165
5216
|
}
|
|
5166
|
-
if (!
|
|
5217
|
+
if (!inCatalog(skillName)) {
|
|
5167
5218
|
skipped.push({ name: job.name, reason: `skill "${skillName}" not in catalog` });
|
|
5168
5219
|
continue;
|
|
5169
5220
|
}
|
|
@@ -5173,7 +5224,7 @@ export async function cmdDashboard(opts) {
|
|
|
5173
5224
|
enabled: job.enabled !== false,
|
|
5174
5225
|
agentSlug: job.agentSlug ?? null,
|
|
5175
5226
|
});
|
|
5176
|
-
migrated.push({ name: job.name, skillName });
|
|
5227
|
+
migrated.push({ name: job.name, skillName, ...(created ? { created: true } : {}) });
|
|
5177
5228
|
const { cronFile, bareJobName } = resolveJobCronFile(job.name);
|
|
5178
5229
|
if (!byFile.has(cronFile))
|
|
5179
5230
|
byFile.set(cronFile, { jobsToRemove: new Set(), cronFile });
|
|
@@ -5217,6 +5268,136 @@ export async function cmdDashboard(opts) {
|
|
|
5217
5268
|
res.status(500).json({ ok: false, error: String(err) });
|
|
5218
5269
|
}
|
|
5219
5270
|
});
|
|
5271
|
+
// 1.18.156 — Per-row "create skill from this cron prompt + schedule it"
|
|
5272
|
+
// path. The user's legacy crons usually have no matching skill (so the
|
|
5273
|
+
// existing /api/cron/:job/migrate-to-skill aborts with "Create a skill
|
|
5274
|
+
// first"). This endpoint closes that gap by writing a folder-form
|
|
5275
|
+
// SKILL.md from the cron's prompt, then performing the schedule wire-up
|
|
5276
|
+
// in one shot. Anthropic-canonical: name = kebab-case, description has
|
|
5277
|
+
// WHAT + WHEN + first-sentence trigger phrase, body is the cron prompt
|
|
5278
|
+
// verbatim. The user can immediately refine via the Builder afterward.
|
|
5279
|
+
app.post('/api/cron/:job/migrate-with-skill-creation', async (req, res) => {
|
|
5280
|
+
try {
|
|
5281
|
+
const jobName = req.params.job;
|
|
5282
|
+
if (!jobName) {
|
|
5283
|
+
res.status(400).json({ ok: false, error: 'job name required' });
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
const proposedName = String((req.body && req.body.skillName) || '').trim();
|
|
5287
|
+
const { generateDescription } = await import('../agent/cron-migrator.js');
|
|
5288
|
+
const { writeSkill, listSkills } = await import('../agent/skill-store.js');
|
|
5289
|
+
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5290
|
+
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5291
|
+
const { cronFile, bareJobName } = resolveJobCronFile(jobName);
|
|
5292
|
+
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5293
|
+
const target = allJobs.find(j => String(j.name).toLowerCase() === jobName.toLowerCase());
|
|
5294
|
+
if (!target)
|
|
5295
|
+
return res.status(404).json({ ok: false, error: `job "${jobName}" not found` });
|
|
5296
|
+
if (target.source === 'scheduled-skill') {
|
|
5297
|
+
return res.status(400).json({ ok: false, error: 'already a scheduled skill' });
|
|
5298
|
+
}
|
|
5299
|
+
// Slugify proposed name (or fall back to the job name) to Anthropic's
|
|
5300
|
+
// kebab-case convention. Strip "cron"/"task" suffixes that don't add
|
|
5301
|
+
// meaning; cap at 64 chars per the SKILL.md spec.
|
|
5302
|
+
const rawName = proposedName || jobName;
|
|
5303
|
+
const skillName = rawName
|
|
5304
|
+
.toLowerCase()
|
|
5305
|
+
.replace(/[_\s]+/g, '-')
|
|
5306
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
5307
|
+
.replace(/-+/g, '-')
|
|
5308
|
+
.replace(/^-+|-+$/g, '')
|
|
5309
|
+
.replace(/-(cron|task|job)$/, '')
|
|
5310
|
+
.slice(0, 64);
|
|
5311
|
+
if (!skillName || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(skillName)) {
|
|
5312
|
+
return res.status(400).json({ ok: false, error: `cannot derive a valid kebab-case skill name from "${rawName}"` });
|
|
5313
|
+
}
|
|
5314
|
+
// Collision check.
|
|
5315
|
+
const existing = listSkills();
|
|
5316
|
+
if (existing.some(s => s.frontmatter.name === skillName)) {
|
|
5317
|
+
return res.status(409).json({
|
|
5318
|
+
ok: false,
|
|
5319
|
+
error: `Skill "${skillName}" already exists. Pass a different skillName or use /migrate-to-skill to bind to it.`,
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
// Build an Anthropic-canonical SKILL.md. Description follows the
|
|
5323
|
+
// WHAT + WHEN pattern from the PDF (page 11): start with what the
|
|
5324
|
+
// cron does, add a trigger phrase derived from the cron name.
|
|
5325
|
+
const cronPrompt = String(target.prompt || '').trim();
|
|
5326
|
+
const cleanedDesc = generateDescription(cronPrompt, null);
|
|
5327
|
+
const triggerPhrase = jobName.replace(/-/g, ' ');
|
|
5328
|
+
const description = `${cleanedDesc} Use when this scheduled task fires (cron: ${target.schedule}) or when the user mentions "${triggerPhrase}".`.slice(0, 1024);
|
|
5329
|
+
const body = [
|
|
5330
|
+
`# ${skillName.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}`,
|
|
5331
|
+
'',
|
|
5332
|
+
'## Instructions',
|
|
5333
|
+
'',
|
|
5334
|
+
cronPrompt || '(prompt not captured — fill in)',
|
|
5335
|
+
'',
|
|
5336
|
+
'## When to use',
|
|
5337
|
+
'',
|
|
5338
|
+
`Auto-fired on schedule \`${target.schedule}\`. Also matches user prompts about "${triggerPhrase}".`,
|
|
5339
|
+
'',
|
|
5340
|
+
].join('\n');
|
|
5341
|
+
let writeResult;
|
|
5342
|
+
try {
|
|
5343
|
+
writeResult = writeSkill({
|
|
5344
|
+
name: skillName,
|
|
5345
|
+
description,
|
|
5346
|
+
body,
|
|
5347
|
+
source: 'imported',
|
|
5348
|
+
...(target.agentSlug ? { agentSlug: target.agentSlug } : {}),
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
catch (writeErr) {
|
|
5352
|
+
return res.status(500).json({ ok: false, error: `skill write failed: ${String(writeErr)}` });
|
|
5353
|
+
}
|
|
5354
|
+
// Now wire the schedule pointing at the new skill.
|
|
5355
|
+
const entry = setSchedule(skillName, {
|
|
5356
|
+
schedule: target.schedule,
|
|
5357
|
+
enabled: target.enabled !== false,
|
|
5358
|
+
agentSlug: target.agentSlug ?? null,
|
|
5359
|
+
});
|
|
5360
|
+
// Remove the legacy cron entry. Same .bak + write-back pattern as the
|
|
5361
|
+
// canonical /migrate-to-skill endpoint.
|
|
5362
|
+
const bakPath = cronFile + '.bak';
|
|
5363
|
+
try {
|
|
5364
|
+
writeFileSync(bakPath, readFileSync(cronFile, 'utf-8'));
|
|
5365
|
+
}
|
|
5366
|
+
catch { /* best-effort */ }
|
|
5367
|
+
const { parsed, jobs } = readCronFileAt(cronFile);
|
|
5368
|
+
const idx = jobs.findIndex(j => String(j.name).toLowerCase() === bareJobName.toLowerCase());
|
|
5369
|
+
if (idx >= 0) {
|
|
5370
|
+
jobs.splice(idx, 1);
|
|
5371
|
+
writeCronFileAt(cronFile, parsed, jobs);
|
|
5372
|
+
}
|
|
5373
|
+
try {
|
|
5374
|
+
const gw = await getGateway();
|
|
5375
|
+
const sched = gw.cronScheduler;
|
|
5376
|
+
if (sched && typeof sched.reloadJobs === 'function')
|
|
5377
|
+
sched.reloadJobs();
|
|
5378
|
+
}
|
|
5379
|
+
catch { /* best-effort */ }
|
|
5380
|
+
try {
|
|
5381
|
+
broadcastEvent({ type: 'skill_created', data: { skillName, fromCron: jobName } });
|
|
5382
|
+
}
|
|
5383
|
+
catch { /* non-fatal */ }
|
|
5384
|
+
try {
|
|
5385
|
+
broadcastEvent({ type: 'cron_deleted', data: { job: jobName, source: 'cron-md', migrated: true } });
|
|
5386
|
+
}
|
|
5387
|
+
catch { /* non-fatal */ }
|
|
5388
|
+
res.json({
|
|
5389
|
+
ok: true,
|
|
5390
|
+
skillName,
|
|
5391
|
+
scheduleEntry: entry,
|
|
5392
|
+
skillPath: writeResult.filePath,
|
|
5393
|
+
bakPath,
|
|
5394
|
+
message: `Created skill "${skillName}" and scheduled it on ${target.schedule}. Cron entry removed (backup: ${bakPath}).`,
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
catch (err) {
|
|
5398
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
5399
|
+
}
|
|
5400
|
+
});
|
|
5220
5401
|
app.post('/api/cron/migrate-all', async (_req, res) => {
|
|
5221
5402
|
try {
|
|
5222
5403
|
const { migrateAllEligibleJobs } = await import('../agent/cron-migrator.js');
|
|
@@ -21199,6 +21380,15 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
21199
21380
|
</div>
|
|
21200
21381
|
</div>
|
|
21201
21382
|
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
21383
|
+
<!-- 1.18.157 — Build with skill-creator. The official Anthropic
|
|
21384
|
+
skill-creator skill is bundled in the vault and auto-discovered.
|
|
21385
|
+
This button opens the chat with a prefilled prompt that matches
|
|
21386
|
+
its triggers. The skill walks the user through Anthropic-canonical
|
|
21387
|
+
skill creation conversationally — quality linting, trigger phrase
|
|
21388
|
+
suggestions, iteration loop, all built in. -->
|
|
21389
|
+
<button class="btn-secondary" onclick="askClementineWith('Help me build a new skill using skill-creator. Walk me through it conversationally.')" style="font-size:13px;padding:8px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px" title="Walk through Anthropic's official skill-creator skill in chat — generates an Anthropic-canonical SKILL.md from a description, then iterates with you on triggers + body.">
|
|
21390
|
+
<span style="font-size:14px;line-height:1">✨</span> Build with skill-creator
|
|
21391
|
+
</button>
|
|
21202
21392
|
<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">
|
|
21203
21393
|
<span style="font-size:14px;line-height:1">+</span> New skill
|
|
21204
21394
|
</button>
|
|
@@ -25498,42 +25688,82 @@ function renderScheduledTaskCard(task) {
|
|
|
25498
25688
|
}
|
|
25499
25689
|
|
|
25500
25690
|
// 1.18.132 — Phase 3 migrator: convert one legacy cron → scheduled skill.
|
|
25501
|
-
//
|
|
25502
|
-
//
|
|
25691
|
+
// 1.18.156 — End-to-end migration: try /migrate-to-skill first; if the
|
|
25692
|
+
// backend bails with "no skill match" (the common case for the user's
|
|
25693
|
+
// 15 legacy crons), surface a confirm to CREATE the skill from the cron's
|
|
25694
|
+
// prompt and schedule it in one shot via /migrate-with-skill-creation.
|
|
25695
|
+
// Replaces the silent-failure UX where the button did nothing visible.
|
|
25503
25696
|
async function migrateCronToSkillFromCard(jobName) {
|
|
25504
25697
|
if (!confirm('Convert "' + jobName + '" to a scheduled skill?\\n\\n' +
|
|
25505
|
-
'This writes a thin entry in ~/.clementine/schedules.json
|
|
25506
|
-
'and removes the legacy fat-cron entry from CRON.md. '
|
|
25507
|
-
'A .bak backup is left so you can restore if anything looks wrong.')) return;
|
|
25698
|
+
'This writes a thin entry in ~/.clementine/schedules.json pointing to a skill, ' +
|
|
25699
|
+
'and removes the legacy fat-cron entry from CRON.md. A .bak backup is left.')) return;
|
|
25508
25700
|
try {
|
|
25509
25701
|
var r = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/migrate-to-skill', { method: 'POST' });
|
|
25510
25702
|
var d = await r.json();
|
|
25511
|
-
if (
|
|
25512
|
-
|
|
25513
|
-
|
|
25703
|
+
if (r.ok) {
|
|
25704
|
+
toast('Migrated "' + jobName + '" → scheduled skill "' + d.skillName + '"', 'success');
|
|
25705
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25706
|
+
return;
|
|
25707
|
+
}
|
|
25708
|
+
// Common path: no matching skill exists. Offer to auto-create one.
|
|
25709
|
+
var msg = d.error || '';
|
|
25710
|
+
if (msg.indexOf('No skill match') >= 0 || msg.indexOf('not in catalog') >= 0) {
|
|
25711
|
+
var proceed = confirm(
|
|
25712
|
+
'No matching skill found for "' + jobName + '".\\n\\n' +
|
|
25713
|
+
'Want me to create a folder-form skill from this cron\\x27s prompt and schedule it?\\n\\n' +
|
|
25714
|
+
'• The skill is named after the cron (kebab-case)\\n' +
|
|
25715
|
+
'• Description follows Anthropic\\x27s WHAT + WHEN format\\n' +
|
|
25716
|
+
'• The cron prompt becomes the skill body — refine it later in the Builder\\n' +
|
|
25717
|
+
'• The legacy cron is removed (with .bak backup)'
|
|
25718
|
+
);
|
|
25719
|
+
if (!proceed) { toast('Migration cancelled — no skill created', 'info'); return; }
|
|
25720
|
+
var r2 = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/migrate-with-skill-creation', { method: 'POST' });
|
|
25721
|
+
var d2 = await r2.json();
|
|
25722
|
+
if (!r2.ok) { toast(d2.error || 'Skill creation + migration failed', 'error'); return; }
|
|
25723
|
+
toast('Created skill "' + d2.skillName + '" + scheduled it. Refine in the Builder when ready.', 'success');
|
|
25724
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25725
|
+
if (typeof refreshSkills === 'function') refreshSkills();
|
|
25726
|
+
return;
|
|
25727
|
+
}
|
|
25728
|
+
toast(msg || 'Migration failed', 'error');
|
|
25514
25729
|
} catch (err) {
|
|
25515
25730
|
toast('Failed: ' + err, 'error');
|
|
25516
25731
|
}
|
|
25517
25732
|
}
|
|
25518
25733
|
|
|
25519
|
-
// Bulk migrator
|
|
25520
|
-
// that has
|
|
25734
|
+
// 1.18.156 — Bulk migrator. Default behavior auto-creates a folder-form
|
|
25735
|
+
// skill for any cron that has no matching skill (createMissing=true),
|
|
25736
|
+
// because the user's 15 legacy crons typically have no match — without
|
|
25737
|
+
// auto-create, the bulk button silently skips everything. .bak backups
|
|
25738
|
+
// preserve every original CRON.md so rollback is one cp away.
|
|
25521
25739
|
async function migrateAllCronsToSkills() {
|
|
25522
|
-
if (!confirm('Convert ALL
|
|
25523
|
-
'
|
|
25524
|
-
'
|
|
25525
|
-
'no
|
|
25740
|
+
if (!confirm('Convert ALL legacy crons to scheduled skills?\\n\\n' +
|
|
25741
|
+
'For each cron:\\n' +
|
|
25742
|
+
'• If a matching skill exists → bind to it\\n' +
|
|
25743
|
+
'• If no matching skill → auto-create one (kebab-case, Anthropic format) from the cron prompt\\n\\n' +
|
|
25744
|
+
'CRON.md files get .bak backups. Refine generated skills in the Builder afterwards.')) return;
|
|
25526
25745
|
try {
|
|
25527
|
-
var r = await apiFetch('/api/cron/migrate-all-to-skills', {
|
|
25746
|
+
var r = await apiFetch('/api/cron/migrate-all-to-skills', {
|
|
25747
|
+
method: 'POST',
|
|
25748
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25749
|
+
body: JSON.stringify({ createMissing: true }),
|
|
25750
|
+
});
|
|
25528
25751
|
var d = await r.json();
|
|
25529
25752
|
if (!r.ok) { toast(d.error || 'Bulk migration failed', 'error'); return; }
|
|
25530
|
-
var migrated =
|
|
25753
|
+
var migrated = d.migrated || [];
|
|
25754
|
+
var created = migrated.filter(function(m) { return m.created; }).length;
|
|
25755
|
+
var bound = migrated.length - created;
|
|
25531
25756
|
var skipped = (d.skipped || []).length;
|
|
25532
|
-
var
|
|
25757
|
+
var parts = [];
|
|
25758
|
+
if (bound > 0) parts.push(bound + ' bound to existing skill' + (bound === 1 ? '' : 's'));
|
|
25759
|
+
if (created > 0) parts.push(created + ' new skill' + (created === 1 ? '' : 's') + ' created');
|
|
25760
|
+
var msg = 'Migrated ' + migrated.length + ' cron' + (migrated.length === 1 ? '' : 's')
|
|
25761
|
+
+ (parts.length ? ' (' + parts.join(', ') + ')' : '');
|
|
25533
25762
|
if (skipped > 0) msg += '. ' + skipped + ' skipped — see console for reasons.';
|
|
25534
|
-
toast(msg, migrated > 0 ? 'success' : 'info');
|
|
25763
|
+
toast(msg, migrated.length > 0 ? 'success' : 'info');
|
|
25535
25764
|
if (skipped > 0 && d.skipped) console.warn('[migrate-all-to-skills] skipped:', d.skipped);
|
|
25536
25765
|
if (typeof refreshCron === 'function') refreshCron();
|
|
25766
|
+
if (typeof refreshSkills === 'function') refreshSkills();
|
|
25537
25767
|
} catch (err) {
|
|
25538
25768
|
toast('Failed: ' + err, 'error');
|
|
25539
25769
|
}
|
|
@@ -28844,10 +29074,22 @@ async function _openSkillModal(opts) {
|
|
|
28844
29074
|
+ '<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>'
|
|
28845
29075
|
+ '<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">'
|
|
28846
29076
|
+ '<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:4px">'
|
|
28847
|
-
+ '<label style="font-size:12px;color:var(--text-secondary);font-weight:500">Description <span style="color:var(--text-muted)">(what this skill does
|
|
29077
|
+
+ '<label style="font-size:12px;color:var(--text-secondary);font-weight:500">Description <span style="color:var(--text-muted)">(what this skill does + when Claude should run it)</span></label>'
|
|
28848
29078
|
+ '<span id="skill-modal-desc-counter" style="font-size:10px;color:var(--text-muted);font-variant-numeric:tabular-nums">0 / 1024 chars</span>'
|
|
28849
29079
|
+ '</div>'
|
|
28850
|
-
|
|
29080
|
+
// 1.18.157 — inline guidance from Anthropic Skill Building Guide page 11.
|
|
29081
|
+
// The description field is the most important part of a skill (it's
|
|
29082
|
+
// the "first level of progressive disclosure" — the only thing always
|
|
29083
|
+
// loaded). Bad descriptions = skill never triggers (or triggers on
|
|
29084
|
+
// everything). Below the field: WHAT + WHEN format reminder + an
|
|
29085
|
+
// anchor link to skill-creator if they want it written for them.
|
|
29086
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;line-height:1.5;padding:6px 10px;background:rgba(255,141,0,0.04);border-left:2px solid var(--accent);border-radius:3px">'
|
|
29087
|
+
+ '<strong style="color:var(--text-secondary);font-weight:600">Format:</strong> '
|
|
29088
|
+
+ '<code style="font-size:10px;background:var(--bg-secondary);padding:1px 4px;border-radius:3px">[WHAT it does] + [WHEN to use it] + [trigger phrases]</code>'
|
|
29089
|
+
+ ' · under 1024 chars · no <code style="font-size:10px">< ></code> · '
|
|
29090
|
+
+ '<a href="javascript:void(0)" onclick="askClementineWith(\\x27Use skill-creator to help me write a great Anthropic-canonical description for the skill I am building.\\x27)" style="color:var(--accent);text-decoration:none">use skill-creator</a>'
|
|
29091
|
+
+ '</div>'
|
|
29092
|
+
+ '<textarea id="skill-modal-desc" rows="2" oninput="updateSkillModalCounters()" placeholder="Example: Analyzes Outlook emails and drafts triage replies. Use when user asks to triage email or mentions inbox cleanup." 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>'
|
|
28851
29093
|
+ '<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>'
|
|
28852
29094
|
+ '<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">'
|
|
28853
29095
|
+ '<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:4px">'
|
|
@@ -37748,6 +37990,28 @@ async function refreshHomeDigest() {
|
|
|
37748
37990
|
}
|
|
37749
37991
|
|
|
37750
37992
|
// ── Home chat FAB + panel ────────────────────────────────────────
|
|
37993
|
+
// 1.18.157 — Open the chat panel with a prefilled prompt and (optionally)
|
|
37994
|
+
// auto-send. Used by surfaces like the Skills page "Build with skill-creator"
|
|
37995
|
+
// button. The chat side reads the input value so we just set + dispatch.
|
|
37996
|
+
function askClementineWith(prompt, opts) {
|
|
37997
|
+
var autoSend = opts && opts.autoSend !== false; // default true
|
|
37998
|
+
toggleHomeChat(true);
|
|
37999
|
+
setTimeout(function() {
|
|
38000
|
+
var input = document.getElementById('chat-input');
|
|
38001
|
+
if (!input) return;
|
|
38002
|
+
input.value = prompt;
|
|
38003
|
+
input.focus();
|
|
38004
|
+
if (autoSend) {
|
|
38005
|
+
// Some chat sends listen on Enter keydown; others have a button.
|
|
38006
|
+
// Try button first, fall back to dispatching Enter.
|
|
38007
|
+
var btn = document.querySelector('#home-chat-panel button[type="submit"], #home-chat-panel button.btn-send, #home-chat-send');
|
|
38008
|
+
if (btn) { btn.click(); return; }
|
|
38009
|
+
var ev = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true });
|
|
38010
|
+
input.dispatchEvent(ev);
|
|
38011
|
+
}
|
|
38012
|
+
}, 120);
|
|
38013
|
+
}
|
|
38014
|
+
|
|
37751
38015
|
function toggleHomeChat(forceOpen) {
|
|
37752
38016
|
var fab = document.getElementById('home-chat-fab');
|
|
37753
38017
|
var panel = document.getElementById('home-chat-panel');
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -103,6 +103,32 @@ if (existsSync(srcVault)) {
|
|
|
103
103
|
if (copied > 0) {
|
|
104
104
|
console.log(`Initialized ${copied} default vault files in ${dstVault}`);
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
// 1.18.157 — bundle Anthropic-shipped first-party skills (currently
|
|
108
|
+
// just `skill-creator`). Recursive copy under skills/<name>/. We DO
|
|
109
|
+
// overwrite on every install so users always get the latest version
|
|
110
|
+
// when they `npm i -g clementine-agent@latest`. Custom user skills
|
|
111
|
+
// are NOT touched — only the bundled-by-Anthropic ones whose name
|
|
112
|
+
// appears in BUNDLED_FIRST_PARTY_SKILLS.
|
|
113
|
+
const BUNDLED_FIRST_PARTY_SKILLS = ['skill-creator'];
|
|
114
|
+
const srcSkillsRoot = path.join(srcVault, 'skills');
|
|
115
|
+
const dstSkillsRoot = path.join(dstVault, 'skills');
|
|
116
|
+
if (existsSync(srcSkillsRoot)) {
|
|
117
|
+
mkdirSync(dstSkillsRoot, { recursive: true });
|
|
118
|
+
let installedSkills = 0;
|
|
119
|
+
for (const skillName of BUNDLED_FIRST_PARTY_SKILLS) {
|
|
120
|
+
const srcSkill = path.join(srcSkillsRoot, skillName);
|
|
121
|
+
const dstSkill = path.join(dstSkillsRoot, skillName);
|
|
122
|
+
if (!existsSync(srcSkill)) continue;
|
|
123
|
+
try {
|
|
124
|
+
cpSync(srcSkill, dstSkill, { recursive: true });
|
|
125
|
+
installedSkills++;
|
|
126
|
+
} catch { /* skip — keep going for other skills */ }
|
|
127
|
+
}
|
|
128
|
+
if (installedSkills > 0) {
|
|
129
|
+
console.log(`Bundled ${installedSkills} first-party skill(s) into ${dstSkillsRoot}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
// ── Step 4: Optional local embedding model prefetch ─────────────────
|