clementine-agent 1.18.109 → 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 +101 -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');
|
|
@@ -26877,6 +26920,44 @@ var _skillsState = {
|
|
|
26877
26920
|
query: '', // search input value
|
|
26878
26921
|
};
|
|
26879
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
|
+
|
|
26880
26961
|
async function refreshSkillsPage() {
|
|
26881
26962
|
var listEl = document.getElementById('skills-list');
|
|
26882
26963
|
var detailEl = document.getElementById('skills-detail');
|
|
@@ -26961,6 +27042,21 @@ function renderSkillsList() {
|
|
|
26961
27042
|
return;
|
|
26962
27043
|
}
|
|
26963
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
|
+
}
|
|
26964
27060
|
for (var i = 0; i < _skillsState.filtered.length; i++) {
|
|
26965
27061
|
var s = _skillsState.filtered[i];
|
|
26966
27062
|
var fm = s.frontmatter || {};
|
|
@@ -27157,7 +27253,11 @@ function renderSkillDetail(s) {
|
|
|
27157
27253
|
if (s.schemaVersion === 'legacy') {
|
|
27158
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">'
|
|
27159
27255
|
+ '<strong style="color:var(--yellow)">Legacy schema.</strong> '
|
|
27160
|
-
+ '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>'
|
|
27161
27261
|
+ '</div>';
|
|
27162
27262
|
} else if (s.schemaVersion === 'anthropic' && s.layout === 'flat') {
|
|
27163
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/
|