clementine-agent 1.18.108 → 1.18.110
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/skill-store.d.ts +20 -0
- package/dist/agent/skill-store.js +177 -1
- package/dist/cli/dashboard.js +155 -1
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -57,6 +57,26 @@ export declare function getSkill(name: string, opts?: ListSkillsOptions): Skill
|
|
|
57
57
|
* Defends against directory traversal: rejects paths that escape the
|
|
58
58
|
* skill folder. */
|
|
59
59
|
export declare function readBundledFile(skill: Skill, relPath: string): string | null;
|
|
60
|
+
export interface MigrationResult {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
/** Path to the new SKILL.md created. Undefined when ok=false. */
|
|
63
|
+
newSkillPath?: string;
|
|
64
|
+
/** Path the original .md was moved to (.bak). Undefined when ok=false. */
|
|
65
|
+
backupPath?: string;
|
|
66
|
+
/** Set when ok=false — what went wrong. */
|
|
67
|
+
error?: string;
|
|
68
|
+
/** Set when the loader found nothing to migrate (already folder-form). */
|
|
69
|
+
alreadyMigrated?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** Migrate one legacy flat skill to Anthropic-compatible folder form.
|
|
72
|
+
* Idempotent: a folder-form skill is reported as alreadyMigrated. */
|
|
73
|
+
export declare function migrateLegacySkill(name: string): MigrationResult;
|
|
74
|
+
/** Migrate every legacy skill in the global pool. Returns per-skill
|
|
75
|
+
* results so the dashboard can render a summary banner. */
|
|
76
|
+
export declare function migrateAllLegacySkills(): {
|
|
77
|
+
migrated: MigrationResult[];
|
|
78
|
+
skipped: MigrationResult[];
|
|
79
|
+
};
|
|
60
80
|
/** Diagnostics for the dashboard — expose where the loader looked. */
|
|
61
81
|
export declare function _skillDirsForDiagnostics(workDir?: string): {
|
|
62
82
|
global: string;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* runner. Phase C wires runtime invocation. Phase E migrates legacy
|
|
22
22
|
* crons → folder-form skills.
|
|
23
23
|
*/
|
|
24
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import os from 'node:os';
|
|
26
26
|
import path from 'node:path';
|
|
27
27
|
import matter from 'gray-matter';
|
|
@@ -161,6 +161,17 @@ function coerceClementineExtensions(raw) {
|
|
|
161
161
|
out.lastUsed = r.lastUsed;
|
|
162
162
|
if (typeof r.lastTestPass === 'string')
|
|
163
163
|
out.lastTestPass = r.lastTestPass;
|
|
164
|
+
// Legacy Clementine concepts preserved through migration.
|
|
165
|
+
if (Array.isArray(r.triggers))
|
|
166
|
+
out.triggers = r.triggers.map(String);
|
|
167
|
+
if (typeof r.source === 'string')
|
|
168
|
+
out.source = r.source;
|
|
169
|
+
if (typeof r.useCount === 'number')
|
|
170
|
+
out.useCount = r.useCount;
|
|
171
|
+
if (typeof r.migratedFrom === 'string')
|
|
172
|
+
out.migratedFrom = r.migratedFrom;
|
|
173
|
+
if (typeof r.migratedAt === 'string')
|
|
174
|
+
out.migratedAt = r.migratedAt;
|
|
164
175
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
165
176
|
}
|
|
166
177
|
/** Coerce raw YAML object → SkillFrontmatter. Filename always wins for
|
|
@@ -471,6 +482,171 @@ export function readBundledFile(skill, relPath) {
|
|
|
471
482
|
return null;
|
|
472
483
|
}
|
|
473
484
|
}
|
|
485
|
+
// ── Legacy → folder-form migration ───────────────────────────────────
|
|
486
|
+
//
|
|
487
|
+
// Converts a flat-form legacy skill (`<name>.md` with title/triggers/
|
|
488
|
+
// toolsUsed/useCount frontmatter) into Anthropic-compatible folder form
|
|
489
|
+
// (`<name>/SKILL.md` with name+description top-level + clementine: namespace
|
|
490
|
+
// for the migration metadata).
|
|
491
|
+
//
|
|
492
|
+
// Preserves everything:
|
|
493
|
+
// - title (legacy display name) → moves to top-level `title` (still readable)
|
|
494
|
+
// - triggers (NLP phrases) → clementine.triggers (preserved for reference)
|
|
495
|
+
// - toolsUsed → clementine.tools.allow (informational → enforced allowlist)
|
|
496
|
+
// - useCount + lastUsed → clementine.useCount + clementine.lastUsed
|
|
497
|
+
// - source → clementine.source
|
|
498
|
+
// - body → unchanged, written to <folder>/SKILL.md
|
|
499
|
+
//
|
|
500
|
+
// The original `<name>.md` is renamed to `<name>.md.bak` (kept on disk for
|
|
501
|
+
// rollback). The bak is filtered out by isLoadableSkillFile so the dashboard
|
|
502
|
+
// won't show it twice.
|
|
503
|
+
import { renameSync } from 'node:fs';
|
|
504
|
+
/** Migrate one legacy flat skill to Anthropic-compatible folder form.
|
|
505
|
+
* Idempotent: a folder-form skill is reported as alreadyMigrated. */
|
|
506
|
+
export function migrateLegacySkill(name) {
|
|
507
|
+
if (!name)
|
|
508
|
+
return { ok: false, error: 'name required' };
|
|
509
|
+
const dir = globalSkillsDir();
|
|
510
|
+
const flat = path.join(dir, name + '.md');
|
|
511
|
+
const folder = path.join(dir, name);
|
|
512
|
+
// Already folder-form → no-op.
|
|
513
|
+
if (existsSync(folder) && existsSync(path.join(folder, 'SKILL.md'))) {
|
|
514
|
+
return { ok: true, alreadyMigrated: true, newSkillPath: path.join(folder, 'SKILL.md') };
|
|
515
|
+
}
|
|
516
|
+
// Source file must exist.
|
|
517
|
+
if (!existsSync(flat))
|
|
518
|
+
return { ok: false, error: `legacy skill file not found: ${flat}` };
|
|
519
|
+
// Folder must NOT exist (collides with the rename target).
|
|
520
|
+
if (existsSync(folder))
|
|
521
|
+
return { ok: false, error: `target folder already exists: ${folder}` };
|
|
522
|
+
// Read + parse the legacy file.
|
|
523
|
+
let raw;
|
|
524
|
+
try {
|
|
525
|
+
raw = readFileSync(flat, 'utf-8');
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
return { ok: false, error: 'failed to read source: ' + String(err) };
|
|
529
|
+
}
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = matter(raw);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
return { ok: false, error: 'failed to parse YAML in source: ' + String(err) };
|
|
536
|
+
}
|
|
537
|
+
const data = parsed.data;
|
|
538
|
+
const body = parsed.content || '';
|
|
539
|
+
// Build the new frontmatter:
|
|
540
|
+
// - name (always the filename)
|
|
541
|
+
// - description (preserved from legacy)
|
|
542
|
+
// - title (preserved as display alias)
|
|
543
|
+
// - clementine.* — bucket all the legacy-only fields here
|
|
544
|
+
const newFm = {
|
|
545
|
+
name,
|
|
546
|
+
};
|
|
547
|
+
if (typeof data.description === 'string' && data.description.trim())
|
|
548
|
+
newFm.description = data.description;
|
|
549
|
+
if (typeof data.title === 'string' && data.title.trim() && data.title !== data.description) {
|
|
550
|
+
newFm.title = data.title;
|
|
551
|
+
}
|
|
552
|
+
const clementine = {};
|
|
553
|
+
if (Array.isArray(data.triggers) && data.triggers.length > 0) {
|
|
554
|
+
clementine.triggers = data.triggers.map(String);
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(data.toolsUsed) && data.toolsUsed.length > 0) {
|
|
557
|
+
// Convert informational toolsUsed → enforced tools.allow. Authors can
|
|
558
|
+
// tighten by editing in Phase B.
|
|
559
|
+
clementine.tools = { allow: data.toolsUsed.map(String) };
|
|
560
|
+
}
|
|
561
|
+
if (typeof data.source === 'string' && data.source.trim())
|
|
562
|
+
clementine.source = data.source;
|
|
563
|
+
if (typeof data.useCount === 'number')
|
|
564
|
+
clementine.useCount = data.useCount;
|
|
565
|
+
if (typeof data.lastUsed === 'string')
|
|
566
|
+
clementine.lastUsed = data.lastUsed;
|
|
567
|
+
if (typeof data.createdAt === 'string')
|
|
568
|
+
clementine.createdAt = data.createdAt;
|
|
569
|
+
if (typeof data.updatedAt === 'string')
|
|
570
|
+
clementine.updatedAt = data.updatedAt;
|
|
571
|
+
// Stamp the migration provenance so future tooling can see where this
|
|
572
|
+
// came from.
|
|
573
|
+
clementine.migratedFrom = path.basename(flat);
|
|
574
|
+
clementine.migratedAt = new Date().toISOString();
|
|
575
|
+
clementine.version = 1;
|
|
576
|
+
if (Object.keys(clementine).length > 0)
|
|
577
|
+
newFm.clementine = clementine;
|
|
578
|
+
// Serialize: gray-matter handles YAML output. matter.stringify takes
|
|
579
|
+
// (content, data) → returns the full file with frontmatter.
|
|
580
|
+
const newContent = matter.stringify(body.startsWith('\n') ? body : '\n' + body, newFm);
|
|
581
|
+
// Create the folder + write SKILL.md.
|
|
582
|
+
try {
|
|
583
|
+
mkdirSync(folder, { recursive: true });
|
|
584
|
+
writeFileSync(path.join(folder, 'SKILL.md'), newContent);
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
return { ok: false, error: 'failed to write new SKILL.md: ' + String(err) };
|
|
588
|
+
}
|
|
589
|
+
// Move the original to .bak so the loader stops surfacing it. We use
|
|
590
|
+
// <name>.md.bak which isLoadableSkillFile already filters out.
|
|
591
|
+
const backupPath = flat + '.bak';
|
|
592
|
+
try {
|
|
593
|
+
renameSync(flat, backupPath);
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
// Roll back the folder we just created so we don't leave the user in
|
|
597
|
+
// a half-migrated state with both shapes for the same name.
|
|
598
|
+
try {
|
|
599
|
+
const fs = require('node:fs');
|
|
600
|
+
fs.unlinkSync(path.join(folder, 'SKILL.md'));
|
|
601
|
+
fs.rmdirSync(folder);
|
|
602
|
+
}
|
|
603
|
+
catch { /* best-effort */ }
|
|
604
|
+
return { ok: false, error: 'failed to rename original to .bak: ' + String(err) };
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
ok: true,
|
|
608
|
+
newSkillPath: path.join(folder, 'SKILL.md'),
|
|
609
|
+
backupPath,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/** Migrate every legacy skill in the global pool. Returns per-skill
|
|
613
|
+
* results so the dashboard can render a summary banner. */
|
|
614
|
+
export function migrateAllLegacySkills() {
|
|
615
|
+
// Walk the dir directly so we don't trigger a full parse + validation.
|
|
616
|
+
const dir = globalSkillsDir();
|
|
617
|
+
const migrated = [];
|
|
618
|
+
const skipped = [];
|
|
619
|
+
if (!existsSync(dir))
|
|
620
|
+
return { migrated, skipped };
|
|
621
|
+
let entries;
|
|
622
|
+
try {
|
|
623
|
+
entries = readdirSync(dir);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return { migrated, skipped };
|
|
627
|
+
}
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
if (!isLoadableSkillFile(entry))
|
|
630
|
+
continue;
|
|
631
|
+
const name = entry.replace(/\.md$/, '');
|
|
632
|
+
// Quickly check it's actually legacy — avoid migrating an Anthropic-
|
|
633
|
+
// form flat file that the user explicitly authored without a folder.
|
|
634
|
+
const filePath = path.join(dir, entry);
|
|
635
|
+
let isLegacy = false;
|
|
636
|
+
try {
|
|
637
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
638
|
+
const data = matter(raw).data;
|
|
639
|
+
isLegacy = ['title', 'triggers', 'toolsUsed', 'useCount', 'source'].some((k) => k in data);
|
|
640
|
+
}
|
|
641
|
+
catch { /* leave isLegacy=false */ }
|
|
642
|
+
if (!isLegacy) {
|
|
643
|
+
skipped.push({ ok: true, alreadyMigrated: true, newSkillPath: filePath });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
migrated.push(migrateLegacySkill(name));
|
|
647
|
+
}
|
|
648
|
+
return { migrated, skipped };
|
|
649
|
+
}
|
|
474
650
|
/** Diagnostics for the dashboard — expose where the loader looked. */
|
|
475
651
|
export function _skillDirsForDiagnostics(workDir) {
|
|
476
652
|
return { global: globalSkillsDir(), project: projectSkillsDir(workDir) ?? null };
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -4647,6 +4647,49 @@ export async function cmdDashboard(opts) {
|
|
|
4647
4647
|
res.status(500).json({ ok: false, error: String(err) });
|
|
4648
4648
|
}
|
|
4649
4649
|
});
|
|
4650
|
+
// ── Skill migration (legacy .md → folder/SKILL.md) ─────────────────
|
|
4651
|
+
// Two endpoints: per-skill and bulk. Both wrap migrateLegacySkill /
|
|
4652
|
+
// migrateAllLegacySkills from skill-store.ts. The original .md is
|
|
4653
|
+
// renamed to .md.bak so the migration is rollback-able by hand.
|
|
4654
|
+
app.post('/api/skills/:name/migrate', async (req, res) => {
|
|
4655
|
+
try {
|
|
4656
|
+
const name = req.params.name;
|
|
4657
|
+
if (!name) {
|
|
4658
|
+
res.status(400).json({ ok: false, error: 'name required' });
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
const { migrateLegacySkill } = await import('../agent/skill-store.js');
|
|
4662
|
+
const result = migrateLegacySkill(name);
|
|
4663
|
+
if (!result.ok) {
|
|
4664
|
+
res.status(409).json(result);
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4667
|
+
res.json(result);
|
|
4668
|
+
}
|
|
4669
|
+
catch (err) {
|
|
4670
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
app.post('/api/skills/migrate-all', async (_req, res) => {
|
|
4674
|
+
try {
|
|
4675
|
+
const { migrateAllLegacySkills } = await import('../agent/skill-store.js');
|
|
4676
|
+
const result = migrateAllLegacySkills();
|
|
4677
|
+
const failed = result.migrated.filter((m) => !m.ok);
|
|
4678
|
+
const succeeded = result.migrated.filter((m) => m.ok && !m.alreadyMigrated);
|
|
4679
|
+
res.json({
|
|
4680
|
+
ok: true,
|
|
4681
|
+
totalChecked: result.migrated.length + result.skipped.length,
|
|
4682
|
+
migratedCount: succeeded.length,
|
|
4683
|
+
failedCount: failed.length,
|
|
4684
|
+
skippedCount: result.skipped.length,
|
|
4685
|
+
migrated: result.migrated,
|
|
4686
|
+
skipped: result.skipped,
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
catch (err) {
|
|
4690
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
4691
|
+
}
|
|
4692
|
+
});
|
|
4650
4693
|
app.get('/api/cron/drafts', async (_req, res) => {
|
|
4651
4694
|
try {
|
|
4652
4695
|
const { listDraftNames } = await import('../agent/draft-store.js');
|
|
@@ -25125,6 +25168,11 @@ var _runListState = {
|
|
|
25125
25168
|
filterTrigger: 'all', // 'all' | manual | scheduled | after | api | webhook (PRD §6 Run.trigger)
|
|
25126
25169
|
filterText: '', // free-text task name match
|
|
25127
25170
|
activeView: 'failures-24h', // PRD §5.3: which Saved View chip is lit
|
|
25171
|
+
// PRD §5.3 Phase 5.3c / 1.18.100: column sort. Default = startedAt
|
|
25172
|
+
// desc (newest first), what most operators want when triaging.
|
|
25173
|
+
// Restored in 1.18.109 after the parallel agent's edits dropped these.
|
|
25174
|
+
sortBy: 'startedAt', // 'startedAt' | 'durationMs' | 'totalCostUsd' | 'jobName'
|
|
25175
|
+
sortDir: 'desc', // 'asc' | 'desc'
|
|
25128
25176
|
data: [], // raw runs from /api/cron/runs
|
|
25129
25177
|
};
|
|
25130
25178
|
|
|
@@ -25191,10 +25239,59 @@ function _runListSaveView() {
|
|
|
25191
25239
|
filterTrigger: _runListState.filterTrigger,
|
|
25192
25240
|
filterText: _runListState.filterText,
|
|
25193
25241
|
activeView: _runListState.activeView,
|
|
25242
|
+
sortBy: _runListState.sortBy,
|
|
25243
|
+
sortDir: _runListState.sortDir,
|
|
25194
25244
|
}));
|
|
25195
25245
|
} catch (e) { /* ignore */ }
|
|
25196
25246
|
}
|
|
25197
25247
|
|
|
25248
|
+
// PRD §5.3 Phase 5.3c / 1.18.100: column sort click handler. Cycle:
|
|
25249
|
+
// click new column → sort by it desc
|
|
25250
|
+
// click same column → flip direction
|
|
25251
|
+
// sort is independent of saved view so users can flip it freely
|
|
25252
|
+
// without dropping out of an active built-in view.
|
|
25253
|
+
//
|
|
25254
|
+
// (Re-added in 1.18.109 after the parallel agent's edits stripped these
|
|
25255
|
+
// functions and broke the Run list — the renderer references them via
|
|
25256
|
+
// onclick but they had no definitions on disk.)
|
|
25257
|
+
function setRunListSort(col) {
|
|
25258
|
+
if (_runListState.sortBy === col) {
|
|
25259
|
+
_runListState.sortDir = _runListState.sortDir === 'asc' ? 'desc' : 'asc';
|
|
25260
|
+
} else {
|
|
25261
|
+
_runListState.sortBy = col;
|
|
25262
|
+
_runListState.sortDir = col === 'jobName' ? 'asc' : 'desc';
|
|
25263
|
+
}
|
|
25264
|
+
_runListSaveView();
|
|
25265
|
+
var panel = document.getElementById('panel-runs');
|
|
25266
|
+
if (panel) panel.innerHTML = renderRunListBody(_runListState.data);
|
|
25267
|
+
}
|
|
25268
|
+
|
|
25269
|
+
// Comparator factory for Array.sort. Stable falsy handling: rows missing
|
|
25270
|
+
// the field sort to the end regardless of direction so the list doesn't
|
|
25271
|
+
// lead with a confusing block of "—" rows.
|
|
25272
|
+
function _runListComparator(by, dir) {
|
|
25273
|
+
var sign = dir === 'asc' ? 1 : -1;
|
|
25274
|
+
return function(a, b) {
|
|
25275
|
+
var av = a[by];
|
|
25276
|
+
var bv = b[by];
|
|
25277
|
+
var aMissing = av == null || av === '';
|
|
25278
|
+
var bMissing = bv == null || bv === '';
|
|
25279
|
+
if (aMissing && bMissing) return 0;
|
|
25280
|
+
if (aMissing) return 1;
|
|
25281
|
+
if (bMissing) return -1;
|
|
25282
|
+
if (by === 'startedAt') {
|
|
25283
|
+
av = new Date(av).getTime();
|
|
25284
|
+
bv = new Date(bv).getTime();
|
|
25285
|
+
} else if (by === 'jobName') {
|
|
25286
|
+
av = String(av).toLowerCase();
|
|
25287
|
+
bv = String(bv).toLowerCase();
|
|
25288
|
+
}
|
|
25289
|
+
if (av < bv) return -1 * sign;
|
|
25290
|
+
if (av > bv) return 1 * sign;
|
|
25291
|
+
return 0;
|
|
25292
|
+
};
|
|
25293
|
+
}
|
|
25294
|
+
|
|
25198
25295
|
function applyRunListView(id) {
|
|
25199
25296
|
if (id === 'custom') { _runListState.activeView = 'custom'; _runListSaveView(); return; }
|
|
25200
25297
|
var view = null;
|
|
@@ -26823,6 +26920,44 @@ var _skillsState = {
|
|
|
26823
26920
|
query: '', // search input value
|
|
26824
26921
|
};
|
|
26825
26922
|
|
|
26923
|
+
// ── Migration handlers (Phase A.6 / 1.18.110) ────────────────────────
|
|
26924
|
+
// migrateOneSkill: per-skill migration from the detail-pane footer.
|
|
26925
|
+
// migrateAllLegacySkills: bulk migration from the list-pane banner.
|
|
26926
|
+
// Both use POST endpoints in dashboard.ts and refresh the page on success.
|
|
26927
|
+
|
|
26928
|
+
async function migrateOneSkill(name) {
|
|
26929
|
+
if (!confirm('Migrate "' + name + '" to folder form? The original .md file will be renamed to .md.bak (preserved for rollback).')) return;
|
|
26930
|
+
try {
|
|
26931
|
+
var r = await apiFetch('/api/skills/' + encodeURIComponent(name) + '/migrate', { method: 'POST' });
|
|
26932
|
+
var d = await r.json().catch(function() { return {}; });
|
|
26933
|
+
if (!r.ok || d.ok === false) {
|
|
26934
|
+
toast(d.error || ('Migration failed (HTTP ' + r.status + ')'), 'error');
|
|
26935
|
+
return;
|
|
26936
|
+
}
|
|
26937
|
+
toast(d.alreadyMigrated ? 'Already in folder form.' : 'Migrated. Folder created at ' + (d.newSkillPath || ''), 'success');
|
|
26938
|
+
// Reload the skills list so the new layout reflects.
|
|
26939
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
26940
|
+
if (name && typeof showSkillDetail === 'function') showSkillDetail(name);
|
|
26941
|
+
} catch (err) { toast('Migration failed: ' + err, 'error'); }
|
|
26942
|
+
}
|
|
26943
|
+
|
|
26944
|
+
async function migrateAllLegacySkills() {
|
|
26945
|
+
if (!confirm('Migrate ALL legacy skills to folder form? Each .md file becomes <name>/SKILL.md, originals preserved as .bak.')) return;
|
|
26946
|
+
try {
|
|
26947
|
+
var r = await apiFetch('/api/skills/migrate-all', { method: 'POST' });
|
|
26948
|
+
var d = await r.json().catch(function() { return {}; });
|
|
26949
|
+
if (!r.ok || d.ok === false) {
|
|
26950
|
+
toast(d.error || ('Bulk migration failed (HTTP ' + r.status + ')'), 'error');
|
|
26951
|
+
return;
|
|
26952
|
+
}
|
|
26953
|
+
var msg = 'Migrated ' + d.migratedCount + ' skill' + (d.migratedCount === 1 ? '' : 's')
|
|
26954
|
+
+ (d.failedCount > 0 ? ' (' + d.failedCount + ' failed)' : '')
|
|
26955
|
+
+ (d.skippedCount > 0 ? ' (' + d.skippedCount + ' already in folder form)' : '');
|
|
26956
|
+
toast(msg, d.failedCount > 0 ? 'error' : 'success');
|
|
26957
|
+
if (typeof refreshSkillsPage === 'function') await refreshSkillsPage();
|
|
26958
|
+
} catch (err) { toast('Bulk migration failed: ' + err, 'error'); }
|
|
26959
|
+
}
|
|
26960
|
+
|
|
26826
26961
|
async function refreshSkillsPage() {
|
|
26827
26962
|
var listEl = document.getElementById('skills-list');
|
|
26828
26963
|
var detailEl = document.getElementById('skills-detail');
|
|
@@ -26907,6 +27042,21 @@ function renderSkillsList() {
|
|
|
26907
27042
|
return;
|
|
26908
27043
|
}
|
|
26909
27044
|
var html = '';
|
|
27045
|
+
// PRD § Skills-First Phase A.6 / 1.18.110: legacy migration banner.
|
|
27046
|
+
// Counts how many flat-form legacy skills exist; shows a one-click
|
|
27047
|
+
// "Migrate all" button when ≥1 found. Banner disappears after they
|
|
27048
|
+
// all become folder form. Per-skill migration also lives in the
|
|
27049
|
+
// detail pane footer for individual control.
|
|
27050
|
+
var legacyCount = _skillsState.data.filter(function(s) {
|
|
27051
|
+
return s.layout === 'flat' && s.schemaVersion === 'legacy';
|
|
27052
|
+
}).length;
|
|
27053
|
+
if (legacyCount > 0 && !_skillsState.query) {
|
|
27054
|
+
html += '<div style="padding:12px 14px;background:rgba(245,158,11,0.10);border-bottom:1px solid var(--yellow);font-size:11px;line-height:1.5;color:var(--text-secondary)">'
|
|
27055
|
+
+ '<div style="font-weight:600;color:var(--yellow);margin-bottom:6px">' + legacyCount + ' legacy skill' + (legacyCount === 1 ? '' : 's') + ' to migrate</div>'
|
|
27056
|
+
+ '<div style="margin-bottom:8px">Convert flat <code>.md</code> files to folder form (<code>name/SKILL.md</code>) so you can bundle templates + scripts + reference docs.</div>'
|
|
27057
|
+
+ '<button class="btn-sm btn-primary" onclick="migrateAllLegacySkills()" style="font-size:11px;padding:4px 10px">Migrate all to folder form</button>'
|
|
27058
|
+
+ '</div>';
|
|
27059
|
+
}
|
|
26910
27060
|
for (var i = 0; i < _skillsState.filtered.length; i++) {
|
|
26911
27061
|
var s = _skillsState.filtered[i];
|
|
26912
27062
|
var fm = s.frontmatter || {};
|
|
@@ -27103,7 +27253,11 @@ function renderSkillDetail(s) {
|
|
|
27103
27253
|
if (s.schemaVersion === 'legacy') {
|
|
27104
27254
|
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">'
|
|
27105
27255
|
+ '<strong style="color:var(--yellow)">Legacy schema.</strong> '
|
|
27106
|
-
+ 'This skill uses the pre-redesign frontmatter shape.
|
|
27256
|
+
+ 'This skill uses the pre-redesign frontmatter shape. '
|
|
27257
|
+
+ 'Click <strong>Migrate to folder form</strong> below to convert it to <code>' + esc(fm.name) + '/SKILL.md</code> with the cron-tailored fields (triggers, toolsUsed, etc.) moved under a <code>clementine:</code> namespace. The original is preserved as a <code>.bak</code> file for rollback.'
|
|
27258
|
+
+ '<div style="margin-top:10px">'
|
|
27259
|
+
+ '<button class="btn-sm btn-primary" onclick="migrateOneSkill(\\x27' + jsStr(fm.name) + '\\x27)" style="font-size:11px;padding:4px 12px">Migrate to folder form →</button>'
|
|
27260
|
+
+ '</div>'
|
|
27107
27261
|
+ '</div>';
|
|
27108
27262
|
} else if (s.schemaVersion === 'anthropic' && s.layout === 'flat') {
|
|
27109
27263
|
html += '<div style="margin-top:24px;padding:12px 14px;background:rgba(59,130,246,0.06);border:1px solid var(--blue);border-radius:6px;font-size:12px;color:var(--text-secondary);line-height:1.5">'
|
package/dist/types.d.ts
CHANGED
|
@@ -383,6 +383,20 @@ export interface ClementineSkillExtensions {
|
|
|
383
383
|
lastUsed?: string;
|
|
384
384
|
/** Last successful "Test this skill" run (Phase B). */
|
|
385
385
|
lastTestPass?: string;
|
|
386
|
+
/** Legacy: NLP-style trigger phrases the pre-redesign chat router used
|
|
387
|
+
* to match incoming messages against this skill. Preserved for the
|
|
388
|
+
* migration UI; not enforced. */
|
|
389
|
+
triggers?: string[];
|
|
390
|
+
/** Legacy: 'manual' / 'auto' / 'imported' — provenance label on the
|
|
391
|
+
* pre-redesign skills. */
|
|
392
|
+
source?: string;
|
|
393
|
+
/** Legacy: incrementing counter of runs that invoked the skill. */
|
|
394
|
+
useCount?: number;
|
|
395
|
+
/** Filename the original legacy skill was migrated from. Helps the
|
|
396
|
+
* migration UI show what came from where. */
|
|
397
|
+
migratedFrom?: string;
|
|
398
|
+
/** ISO timestamp of when the migration ran. */
|
|
399
|
+
migratedAt?: string;
|
|
386
400
|
}
|
|
387
401
|
/** Parsed frontmatter. Anthropic-canonical fields are top-level; our
|
|
388
402
|
* extensions live under `clementine`. Legacy fields (title/triggers/
|