clementine-agent 1.18.107 → 1.18.108
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 +41 -35
- package/dist/agent/skill-store.js +339 -174
- package/dist/cli/dashboard.js +158 -52
- package/dist/types.d.ts +82 -48
- package/package.json +1 -1
|
@@ -1,57 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill store — Phase A
|
|
2
|
+
* Skill store — Phase A / A.5 of the Skills-First redesign.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* disk; Phase B adds editing + testing; Phase C wires runtime invocation.
|
|
4
|
+
* Anthropic-compatible skill loader. Walks two skill directories and
|
|
5
|
+
* accepts both layouts:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. **Folder form** (Anthropic spec): <dir>/<skill-name>/SKILL.md
|
|
8
|
+
* A capital-S SKILL.md is the entry point. Sibling .md files and
|
|
9
|
+
* a scripts/ subdirectory are surfaced as bundled files.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 2. **Flat form** (Clementine legacy): <dir>/<skill-name>.md
|
|
12
|
+
* A single .md file with frontmatter + body. Bundled files
|
|
13
|
+
* unsupported. Used by the 12 pre-redesign skill files we already
|
|
14
|
+
* have on disk.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `legacy` and the dashboard shows a migration badge.
|
|
16
|
+
* Discovery directories (per-project wins on name collision):
|
|
17
|
+
* - global: $CLEMENTINE_HOME/vault/00-System/skills/
|
|
18
|
+
* - per-project: <work_dir>/.clementine/skills/
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Phase A is read-only. Phase B adds editing + a "Test this skill"
|
|
21
|
+
* runner. Phase C wires runtime invocation. Phase E migrates legacy
|
|
22
|
+
* crons → folder-form skills.
|
|
24
23
|
*/
|
|
25
|
-
import type { Skill, SkillScope, CronJobDefinition } from '../types.js';
|
|
24
|
+
import type { Skill, SkillScope, SkillValidationWarning, CronJobDefinition } from '../types.js';
|
|
25
|
+
/** Run Anthropic-spec validations on a parsed skill. Errors are spec
|
|
26
|
+
* violations (skill would be rejected by the Anthropic API); warnings
|
|
27
|
+
* are best-practice hints (still loadable). Findings render in the
|
|
28
|
+
* dashboard's detail pane. */
|
|
29
|
+
export declare function validateSkill(skill: Skill): SkillValidationWarning[];
|
|
26
30
|
interface ParseResult {
|
|
27
31
|
skill: Skill;
|
|
28
32
|
/** Set when the file existed but couldn't be parsed (bad YAML, etc.).
|
|
29
|
-
* We still surface the
|
|
30
|
-
* can
|
|
33
|
+
* We still surface the skill with synthesized frontmatter so the
|
|
34
|
+
* dashboard can render the offending file with an error banner. */
|
|
31
35
|
parseError?: string;
|
|
32
36
|
}
|
|
33
|
-
/** Parse a
|
|
34
|
-
* frontmatter is malformed — the dashboard renders the parse error
|
|
35
|
-
* in-pane so the user can fix it without leaving the UI. */
|
|
37
|
+
/** Parse a flat-form skill file (single .md). */
|
|
36
38
|
export declare function parseSkillFile(filePath: string, scope: SkillScope): ParseResult;
|
|
39
|
+
/** Parse a folder-form skill (Anthropic spec: <name>/SKILL.md plus optional
|
|
40
|
+
* bundled files). The folder name is the canonical skill identifier. */
|
|
41
|
+
export declare function parseSkillFolder(folderPath: string, scope: SkillScope): ParseResult;
|
|
37
42
|
export interface ListSkillsOptions {
|
|
38
|
-
/** Optional per-project work_dir to
|
|
39
|
-
*
|
|
43
|
+
/** Optional per-project work_dir to scan. Per-project skills shadow
|
|
44
|
+
* global ones with the same identifier. */
|
|
40
45
|
projectWorkDir?: string;
|
|
41
|
-
/** Optional cron jobs
|
|
42
|
-
* usedByTriggers field on each skill via the existing skills[] array
|
|
43
|
-
* on CronJobDefinition (Phase A's join). */
|
|
46
|
+
/** Optional cron jobs for the usedByTriggers join (via skills[]). */
|
|
44
47
|
jobs?: CronJobDefinition[];
|
|
45
48
|
}
|
|
46
|
-
/** Top-level discovery API.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
+
/** Top-level discovery API. Merges global + per-project pools, with
|
|
50
|
+
* per-project taking precedence. Populates usedByTriggers when jobs
|
|
51
|
+
* are passed. Returned list is sorted alphabetically by name. */
|
|
49
52
|
export declare function listSkills(opts?: ListSkillsOptions): Skill[];
|
|
50
|
-
/** Get
|
|
51
|
-
*
|
|
53
|
+
/** Get one skill by name, applying per-project precedence. Returns
|
|
54
|
+
* null when neither pool has the skill. */
|
|
52
55
|
export declare function getSkill(name: string, opts?: ListSkillsOptions): Skill | null;
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
56
|
+
/** Read one bundled file's contents — used by Phase B's preview pane.
|
|
57
|
+
* Defends against directory traversal: rejects paths that escape the
|
|
58
|
+
* skill folder. */
|
|
59
|
+
export declare function readBundledFile(skill: Skill, relPath: string): string | null;
|
|
60
|
+
/** Diagnostics for the dashboard — expose where the loader looked. */
|
|
55
61
|
export declare function _skillDirsForDiagnostics(workDir?: string): {
|
|
56
62
|
global: string;
|
|
57
63
|
project: string | null;
|
|
@@ -1,125 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill store — Phase A
|
|
2
|
+
* Skill store — Phase A / A.5 of the Skills-First redesign.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* disk; Phase B adds editing + testing; Phase C wires runtime invocation.
|
|
4
|
+
* Anthropic-compatible skill loader. Walks two skill directories and
|
|
5
|
+
* accepts both layouts:
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. **Folder form** (Anthropic spec): <dir>/<skill-name>/SKILL.md
|
|
8
|
+
* A capital-S SKILL.md is the entry point. Sibling .md files and
|
|
9
|
+
* a scripts/ subdirectory are surfaced as bundled files.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 2. **Flat form** (Clementine legacy): <dir>/<skill-name>.md
|
|
12
|
+
* A single .md file with frontmatter + body. Bundled files
|
|
13
|
+
* unsupported. Used by the 12 pre-redesign skill files we already
|
|
14
|
+
* have on disk.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `legacy` and the dashboard shows a migration badge.
|
|
16
|
+
* Discovery directories (per-project wins on name collision):
|
|
17
|
+
* - global: $CLEMENTINE_HOME/vault/00-System/skills/
|
|
18
|
+
* - per-project: <work_dir>/.clementine/skills/
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Phase A is read-only. Phase B adds editing + a "Test this skill"
|
|
21
|
+
* runner. Phase C wires runtime invocation. Phase E migrates legacy
|
|
22
|
+
* crons → folder-form skills.
|
|
24
23
|
*/
|
|
25
24
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
26
25
|
import os from 'node:os';
|
|
27
26
|
import path from 'node:path';
|
|
28
27
|
import matter from 'gray-matter';
|
|
29
|
-
|
|
28
|
+
// ── Path resolution (lazy — reads CLEMENTINE_HOME on each call) ──────
|
|
30
29
|
function globalSkillsDir() {
|
|
31
30
|
const base = process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
32
31
|
return path.join(base, 'vault', '00-System', 'skills');
|
|
33
32
|
}
|
|
34
|
-
/** Resolve a per-project skills directory. Returns null if work_dir is
|
|
35
|
-
* empty or doesn't have a .clementine/skills/ child. */
|
|
36
33
|
function projectSkillsDir(workDir) {
|
|
37
34
|
if (!workDir)
|
|
38
35
|
return null;
|
|
39
36
|
const dir = path.join(workDir, '.clementine', 'skills');
|
|
40
37
|
return existsSync(dir) ? dir : null;
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
// ── Anthropic spec validations ────────────────────────────────────────
|
|
40
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
41
|
+
const RESERVED_NAMES = new Set(['anthropic', 'claude']);
|
|
42
|
+
const NAME_MAX_LEN = 64;
|
|
43
|
+
const DESCRIPTION_MAX_LEN = 1024;
|
|
44
|
+
const BODY_LINE_LIMIT_WARN = 500;
|
|
45
|
+
/** Run Anthropic-spec validations on a parsed skill. Errors are spec
|
|
46
|
+
* violations (skill would be rejected by the Anthropic API); warnings
|
|
47
|
+
* are best-practice hints (still loadable). Findings render in the
|
|
48
|
+
* dashboard's detail pane. */
|
|
49
|
+
export function validateSkill(skill) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const fm = skill.frontmatter;
|
|
52
|
+
// Name validation (Anthropic spec).
|
|
53
|
+
if (!fm.name) {
|
|
54
|
+
out.push({ severity: 'error', field: 'name', message: 'name is required' });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (fm.name.length > NAME_MAX_LEN) {
|
|
58
|
+
out.push({ severity: 'error', field: 'name', message: `name exceeds ${NAME_MAX_LEN} chars (got ${fm.name.length})` });
|
|
59
|
+
}
|
|
60
|
+
if (!NAME_PATTERN.test(fm.name)) {
|
|
61
|
+
out.push({ severity: 'error', field: 'name', message: 'name must be lowercase letters, numbers, and hyphens only' });
|
|
62
|
+
}
|
|
63
|
+
const lower = fm.name.toLowerCase();
|
|
64
|
+
if (RESERVED_NAMES.has(lower) || lower.includes('anthropic') || lower.includes('claude')) {
|
|
65
|
+
// Anthropic forbids these words anywhere in the name.
|
|
66
|
+
out.push({ severity: 'error', field: 'name', message: 'name cannot contain reserved words "anthropic" or "claude"' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Description validation (Anthropic spec).
|
|
70
|
+
if (!fm.description || !fm.description.trim()) {
|
|
71
|
+
out.push({ severity: 'warning', field: 'description', message: 'description is required by Anthropic spec — add one so the skill can be discovered' });
|
|
72
|
+
}
|
|
73
|
+
else if (fm.description.length > DESCRIPTION_MAX_LEN) {
|
|
74
|
+
out.push({ severity: 'error', field: 'description', message: `description exceeds ${DESCRIPTION_MAX_LEN} chars (got ${fm.description.length})` });
|
|
75
|
+
}
|
|
76
|
+
else if (/<\w+/i.test(fm.description)) {
|
|
77
|
+
out.push({ severity: 'error', field: 'description', message: 'description cannot contain XML tags' });
|
|
78
|
+
}
|
|
79
|
+
// Body length (best-practice hint).
|
|
80
|
+
const bodyLines = (skill.body.match(/\n/g)?.length ?? 0) + 1;
|
|
81
|
+
if (bodyLines > BODY_LINE_LIMIT_WARN) {
|
|
82
|
+
out.push({ severity: 'warning', field: 'body', message: `body is ${bodyLines} lines — Anthropic recommends under ${BODY_LINE_LIMIT_WARN}; split into bundled files (FORMS.md, reference.md, etc.)` });
|
|
83
|
+
}
|
|
84
|
+
// Layout hint: flat skills can't bundle scripts or sibling references.
|
|
85
|
+
if (skill.layout === 'flat' && skill.schemaVersion !== 'legacy') {
|
|
86
|
+
out.push({ severity: 'warning', field: 'layout', message: 'consider folder form (<skill-name>/SKILL.md) so you can bundle scripts and reference files later' });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
60
89
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
// ── Frontmatter parsing ───────────────────────────────────────────────
|
|
91
|
+
/** Detect which of the three schema variants this frontmatter is.
|
|
92
|
+
*
|
|
93
|
+
* - 'clementine' if the `clementine:` namespace is present
|
|
94
|
+
* - 'legacy' if any of the pre-redesign top-level fields are present
|
|
95
|
+
* (title / triggers / toolsUsed / useCount) AND no clementine
|
|
96
|
+
* - 'anthropic' otherwise (just name + description, the canonical case)
|
|
97
|
+
*/
|
|
98
|
+
function detectSchemaVersion(raw) {
|
|
99
|
+
if (raw.clementine && typeof raw.clementine === 'object' && !Array.isArray(raw.clementine)) {
|
|
100
|
+
return 'clementine';
|
|
101
|
+
}
|
|
102
|
+
const legacyMarkers = ['title', 'triggers', 'toolsUsed', 'useCount', 'source'];
|
|
103
|
+
if (legacyMarkers.some((k) => k in raw))
|
|
104
|
+
return 'legacy';
|
|
105
|
+
return 'anthropic';
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// on it. Users wanting a friendly display string can set `title:`
|
|
81
|
-
// instead, which Phase B's editor surfaces as the heading.
|
|
82
|
-
name: fileBasename,
|
|
83
|
-
};
|
|
84
|
-
if (typeof raw.description === 'string')
|
|
85
|
-
fm.description = raw.description;
|
|
86
|
-
// v1 inputs — JSON Schema map keyed by field name.
|
|
87
|
-
if (raw.inputs && typeof raw.inputs === 'object' && !Array.isArray(raw.inputs)) {
|
|
88
|
-
fm.inputs = raw.inputs;
|
|
107
|
+
function coerceClementineExtensions(raw) {
|
|
108
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
109
|
+
return undefined;
|
|
110
|
+
const r = raw;
|
|
111
|
+
const out = {};
|
|
112
|
+
if (r.inputs && typeof r.inputs === 'object' && !Array.isArray(r.inputs)) {
|
|
113
|
+
out.inputs = r.inputs;
|
|
89
114
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const t = raw.tools;
|
|
115
|
+
if (r.tools && typeof r.tools === 'object' && !Array.isArray(r.tools)) {
|
|
116
|
+
const t = r.tools;
|
|
93
117
|
const policy = {};
|
|
94
118
|
if (Array.isArray(t.allow))
|
|
95
119
|
policy.allow = t.allow.map(String);
|
|
96
120
|
if (Array.isArray(t.deny))
|
|
97
121
|
policy.deny = t.deny.map(String);
|
|
98
122
|
if (policy.allow || policy.deny)
|
|
99
|
-
|
|
123
|
+
out.tools = policy;
|
|
100
124
|
}
|
|
101
|
-
if (Array.isArray(
|
|
102
|
-
|
|
103
|
-
.filter((d) => !!d && typeof d === 'object')
|
|
104
|
-
.map((d) => ({
|
|
105
|
-
kind: String(d.kind || 'unknown'),
|
|
106
|
-
purpose: String(d.purpose || ''),
|
|
107
|
-
}));
|
|
125
|
+
if (Array.isArray(r.dataSources)) {
|
|
126
|
+
out.dataSources = r.dataSources
|
|
127
|
+
.filter((d) => !!d && typeof d === 'object' && !Array.isArray(d))
|
|
128
|
+
.map((d) => ({ kind: String(d.kind || 'unknown'), purpose: String(d.purpose || '') }));
|
|
108
129
|
}
|
|
109
|
-
if (Array.isArray(
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
const s =
|
|
130
|
+
if (Array.isArray(r.stateKeys))
|
|
131
|
+
out.stateKeys = r.stateKeys.map(String);
|
|
132
|
+
if (r.success && typeof r.success === 'object' && !Array.isArray(r.success)) {
|
|
133
|
+
const s = r.success;
|
|
113
134
|
const success = {};
|
|
114
135
|
if (s.schema && typeof s.schema === 'object')
|
|
115
136
|
success.schema = s.schema;
|
|
116
137
|
if (typeof s.criterion === 'string')
|
|
117
138
|
success.criterion = s.criterion;
|
|
118
139
|
if (success.schema || success.criterion)
|
|
119
|
-
|
|
140
|
+
out.success = success;
|
|
120
141
|
}
|
|
121
|
-
if (
|
|
122
|
-
const l =
|
|
142
|
+
if (r.limits && typeof r.limits === 'object' && !Array.isArray(r.limits)) {
|
|
143
|
+
const l = r.limits;
|
|
123
144
|
const limits = {};
|
|
124
145
|
if (typeof l.maxTurns === 'number')
|
|
125
146
|
limits.maxTurns = l.maxTurns;
|
|
@@ -128,19 +149,31 @@ function coerceFrontmatter(raw, fileBasename) {
|
|
|
128
149
|
if (typeof l.timeoutSeconds === 'number')
|
|
129
150
|
limits.timeoutSeconds = l.timeoutSeconds;
|
|
130
151
|
if (Object.keys(limits).length > 0)
|
|
131
|
-
|
|
152
|
+
out.limits = limits;
|
|
132
153
|
}
|
|
133
|
-
if (typeof
|
|
134
|
-
|
|
135
|
-
if (typeof
|
|
136
|
-
|
|
137
|
-
if (typeof
|
|
138
|
-
|
|
139
|
-
if (typeof
|
|
140
|
-
|
|
141
|
-
if (typeof
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
if (typeof r.version === 'number')
|
|
155
|
+
out.version = r.version;
|
|
156
|
+
if (typeof r.createdAt === 'string')
|
|
157
|
+
out.createdAt = r.createdAt;
|
|
158
|
+
if (typeof r.updatedAt === 'string')
|
|
159
|
+
out.updatedAt = r.updatedAt;
|
|
160
|
+
if (typeof r.lastUsed === 'string')
|
|
161
|
+
out.lastUsed = r.lastUsed;
|
|
162
|
+
if (typeof r.lastTestPass === 'string')
|
|
163
|
+
out.lastTestPass = r.lastTestPass;
|
|
164
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
165
|
+
}
|
|
166
|
+
/** Coerce raw YAML object → SkillFrontmatter. Filename always wins for
|
|
167
|
+
* identity (frontmatter `name:` is ignored to prevent two skills from
|
|
168
|
+
* colliding); we treat the YAML name as a display alias only. */
|
|
169
|
+
function coerceFrontmatter(raw, fileBasename) {
|
|
170
|
+
const fm = { name: fileBasename };
|
|
171
|
+
if (typeof raw.description === 'string')
|
|
172
|
+
fm.description = raw.description;
|
|
173
|
+
const ext = coerceClementineExtensions(raw.clementine);
|
|
174
|
+
if (ext)
|
|
175
|
+
fm.clementine = ext;
|
|
176
|
+
// Legacy top-level fields preserved for the migration UI.
|
|
144
177
|
if (typeof raw.title === 'string')
|
|
145
178
|
fm.title = raw.title;
|
|
146
179
|
if (Array.isArray(raw.triggers))
|
|
@@ -153,58 +186,175 @@ function coerceFrontmatter(raw, fileBasename) {
|
|
|
153
186
|
fm.useCount = raw.useCount;
|
|
154
187
|
return fm;
|
|
155
188
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
// ── File / folder helpers ─────────────────────────────────────────────
|
|
190
|
+
function isLoadableSkillFile(name) {
|
|
191
|
+
if (name.startsWith('.'))
|
|
192
|
+
return false;
|
|
193
|
+
if (!name.endsWith('.md'))
|
|
194
|
+
return false;
|
|
195
|
+
if (name.endsWith('.bak'))
|
|
196
|
+
return false;
|
|
197
|
+
if (name.endsWith('.bak.md'))
|
|
198
|
+
return false;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
function classifyBundledFile(relPath) {
|
|
202
|
+
if (relPath.endsWith('.md'))
|
|
203
|
+
return 'markdown';
|
|
204
|
+
if (relPath.startsWith('scripts/'))
|
|
205
|
+
return 'script';
|
|
206
|
+
if (/\.(py|js|ts|sh|rb)$/.test(relPath))
|
|
207
|
+
return 'script';
|
|
208
|
+
return 'other';
|
|
209
|
+
}
|
|
210
|
+
/** Walk a skill folder (recursively shallow — one level into scripts/)
|
|
211
|
+
* and return non-SKILL.md files as bundled artifacts. Skips hidden
|
|
212
|
+
* files, .bak duplicates, and the SKILL.md itself. */
|
|
213
|
+
function discoverBundledFiles(skillFolder) {
|
|
214
|
+
const out = [];
|
|
215
|
+
const walk = (dir, relPrefix) => {
|
|
216
|
+
let entries;
|
|
217
|
+
try {
|
|
218
|
+
entries = readdirSync(dir);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (entry.startsWith('.'))
|
|
225
|
+
continue;
|
|
226
|
+
const abs = path.join(dir, entry);
|
|
227
|
+
let st;
|
|
228
|
+
try {
|
|
229
|
+
st = statSync(abs);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const rel = relPrefix ? `${relPrefix}/${entry}` : entry;
|
|
235
|
+
if (st.isDirectory()) {
|
|
236
|
+
// Only descend one level — avoids surprise when a skill bundles
|
|
237
|
+
// node_modules or similar. Convention: scripts/, reference/.
|
|
238
|
+
if (relPrefix)
|
|
239
|
+
continue;
|
|
240
|
+
walk(abs, rel);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!st.isFile())
|
|
244
|
+
continue;
|
|
245
|
+
// Skip the entry-point file itself + .bak duplicates.
|
|
246
|
+
if (rel === 'SKILL.md')
|
|
247
|
+
continue;
|
|
248
|
+
if (entry.endsWith('.bak') || entry.endsWith('.bak.md'))
|
|
249
|
+
continue;
|
|
250
|
+
out.push({
|
|
251
|
+
relPath: rel,
|
|
252
|
+
absPath: abs,
|
|
253
|
+
kind: classifyBundledFile(rel),
|
|
254
|
+
sizeBytes: st.size,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
walk(skillFolder, '');
|
|
259
|
+
// Sort deterministically: top-level files first, then scripts/, then
|
|
260
|
+
// alphabetical within each group.
|
|
261
|
+
out.sort((a, b) => {
|
|
262
|
+
const aTop = !a.relPath.includes('/');
|
|
263
|
+
const bTop = !b.relPath.includes('/');
|
|
264
|
+
if (aTop !== bTop)
|
|
265
|
+
return aTop ? -1 : 1;
|
|
266
|
+
return a.relPath.localeCompare(b.relPath);
|
|
267
|
+
});
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function emptySkill(filePath, basename, scope, layout) {
|
|
271
|
+
return {
|
|
272
|
+
frontmatter: { name: basename },
|
|
273
|
+
body: '',
|
|
274
|
+
filePath,
|
|
275
|
+
scope,
|
|
276
|
+
layout,
|
|
277
|
+
schemaVersion: 'legacy',
|
|
278
|
+
bundledFiles: [],
|
|
279
|
+
usedByTriggers: [],
|
|
280
|
+
validation: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** Parse a flat-form skill file (single .md). */
|
|
159
284
|
export function parseSkillFile(filePath, scope) {
|
|
160
|
-
const basename =
|
|
285
|
+
const basename = path.basename(filePath, '.md');
|
|
161
286
|
let raw;
|
|
162
287
|
try {
|
|
163
288
|
raw = readFileSync(filePath, 'utf-8');
|
|
164
289
|
}
|
|
165
290
|
catch (err) {
|
|
166
|
-
return {
|
|
167
|
-
skill: emptySkill(filePath, basename, scope),
|
|
168
|
-
parseError: 'failed to read: ' + String(err),
|
|
169
|
-
};
|
|
291
|
+
return { skill: emptySkill(filePath, basename, scope, 'flat'), parseError: 'failed to read: ' + String(err) };
|
|
170
292
|
}
|
|
171
293
|
let parsed;
|
|
172
294
|
try {
|
|
173
295
|
parsed = matter(raw);
|
|
174
296
|
}
|
|
175
297
|
catch (err) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
parseError: 'YAML parse error: ' + String(err),
|
|
179
|
-
};
|
|
298
|
+
const skill = { ...emptySkill(filePath, basename, scope, 'flat'), body: raw };
|
|
299
|
+
return { skill, parseError: 'YAML parse error: ' + String(err) };
|
|
180
300
|
}
|
|
181
301
|
const data = parsed.data;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
},
|
|
302
|
+
const skill = {
|
|
303
|
+
frontmatter: coerceFrontmatter(data, basename),
|
|
304
|
+
body: parsed.content || '',
|
|
305
|
+
filePath,
|
|
306
|
+
scope,
|
|
307
|
+
layout: 'flat',
|
|
308
|
+
schemaVersion: detectSchemaVersion(data),
|
|
309
|
+
bundledFiles: [],
|
|
310
|
+
usedByTriggers: [],
|
|
311
|
+
validation: [],
|
|
193
312
|
};
|
|
313
|
+
skill.validation = validateSkill(skill);
|
|
314
|
+
return { skill };
|
|
194
315
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
316
|
+
/** Parse a folder-form skill (Anthropic spec: <name>/SKILL.md plus optional
|
|
317
|
+
* bundled files). The folder name is the canonical skill identifier. */
|
|
318
|
+
export function parseSkillFolder(folderPath, scope) {
|
|
319
|
+
const basename = path.basename(folderPath);
|
|
320
|
+
const entryPoint = path.join(folderPath, 'SKILL.md');
|
|
321
|
+
if (!existsSync(entryPoint)) {
|
|
322
|
+
return {
|
|
323
|
+
skill: emptySkill(entryPoint, basename, scope, 'folder'),
|
|
324
|
+
parseError: 'no SKILL.md in folder',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
let raw;
|
|
328
|
+
try {
|
|
329
|
+
raw = readFileSync(entryPoint, 'utf-8');
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
return { skill: emptySkill(entryPoint, basename, scope, 'folder'), parseError: 'failed to read SKILL.md: ' + String(err) };
|
|
333
|
+
}
|
|
334
|
+
let parsed;
|
|
335
|
+
try {
|
|
336
|
+
parsed = matter(raw);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const skill = { ...emptySkill(entryPoint, basename, scope, 'folder'), body: raw };
|
|
340
|
+
return { skill, parseError: 'YAML parse error in SKILL.md: ' + String(err) };
|
|
341
|
+
}
|
|
342
|
+
const data = parsed.data;
|
|
343
|
+
const skill = {
|
|
344
|
+
frontmatter: coerceFrontmatter(data, basename),
|
|
345
|
+
body: parsed.content || '',
|
|
346
|
+
filePath: entryPoint,
|
|
200
347
|
scope,
|
|
201
|
-
|
|
348
|
+
layout: 'folder',
|
|
349
|
+
schemaVersion: detectSchemaVersion(data),
|
|
350
|
+
bundledFiles: discoverBundledFiles(folderPath),
|
|
202
351
|
usedByTriggers: [],
|
|
352
|
+
validation: [],
|
|
203
353
|
};
|
|
354
|
+
skill.validation = validateSkill(skill);
|
|
355
|
+
return { skill };
|
|
204
356
|
}
|
|
205
|
-
|
|
206
|
-
* so callers can immediately render them. Tolerates missing dirs and
|
|
207
|
-
* unreadable files — best-effort. */
|
|
357
|
+
// ── Discovery (top-level API) ────────────────────────────────────────
|
|
208
358
|
function listSkillsInDir(dir, scope) {
|
|
209
359
|
if (!existsSync(dir))
|
|
210
360
|
return [];
|
|
@@ -217,24 +367,34 @@ function listSkillsInDir(dir, scope) {
|
|
|
217
367
|
}
|
|
218
368
|
const out = [];
|
|
219
369
|
for (const entry of entries) {
|
|
220
|
-
if (
|
|
370
|
+
if (entry.startsWith('.'))
|
|
221
371
|
continue;
|
|
222
372
|
const fullPath = path.join(dir, entry);
|
|
373
|
+
let st;
|
|
223
374
|
try {
|
|
224
|
-
|
|
225
|
-
if (!stat.isFile())
|
|
226
|
-
continue;
|
|
375
|
+
st = statSync(fullPath);
|
|
227
376
|
}
|
|
228
377
|
catch {
|
|
229
378
|
continue;
|
|
230
379
|
}
|
|
231
|
-
|
|
380
|
+
if (st.isDirectory()) {
|
|
381
|
+
// Folder-form skill. Must contain SKILL.md (case-sensitive,
|
|
382
|
+
// Anthropic spec). Skip folders that don't (e.g. accidental
|
|
383
|
+
// dirs like 'auto/' that exist in current vault).
|
|
384
|
+
if (!existsSync(path.join(fullPath, 'SKILL.md')))
|
|
385
|
+
continue;
|
|
386
|
+
out.push(parseSkillFolder(fullPath, scope).skill);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (st.isFile() && isLoadableSkillFile(entry)) {
|
|
390
|
+
out.push(parseSkillFile(fullPath, scope).skill);
|
|
391
|
+
}
|
|
232
392
|
}
|
|
233
393
|
return out;
|
|
234
394
|
}
|
|
235
|
-
/** Top-level discovery API.
|
|
236
|
-
*
|
|
237
|
-
*
|
|
395
|
+
/** Top-level discovery API. Merges global + per-project pools, with
|
|
396
|
+
* per-project taking precedence. Populates usedByTriggers when jobs
|
|
397
|
+
* are passed. Returned list is sorted alphabetically by name. */
|
|
238
398
|
export function listSkills(opts = {}) {
|
|
239
399
|
const globalSkills = listSkillsInDir(globalSkillsDir(), 'global');
|
|
240
400
|
const projectSkills = opts.projectWorkDir
|
|
@@ -243,14 +403,11 @@ export function listSkills(opts = {}) {
|
|
|
243
403
|
return pdir ? listSkillsInDir(pdir, 'project') : [];
|
|
244
404
|
})()
|
|
245
405
|
: [];
|
|
246
|
-
// Build a map keyed by basename so per-project entries override global.
|
|
247
406
|
const merged = new Map();
|
|
248
407
|
for (const s of globalSkills)
|
|
249
408
|
merged.set(s.frontmatter.name, s);
|
|
250
409
|
for (const s of projectSkills)
|
|
251
410
|
merged.set(s.frontmatter.name, s);
|
|
252
|
-
// Used-by join from cron jobs' skills[] array. Same skill referenced by
|
|
253
|
-
// multiple jobs accumulates them in order.
|
|
254
411
|
if (opts.jobs && opts.jobs.length > 0) {
|
|
255
412
|
for (const job of opts.jobs) {
|
|
256
413
|
if (!Array.isArray(job.skills))
|
|
@@ -262,52 +419,60 @@ export function listSkills(opts = {}) {
|
|
|
262
419
|
}
|
|
263
420
|
}
|
|
264
421
|
}
|
|
265
|
-
// Sorted alphabetically — predictable rendering, no need for the
|
|
266
|
-
// dashboard to re-sort. Per-project always sorts at the same key as
|
|
267
|
-
// the global version it replaced.
|
|
268
422
|
return [...merged.values()].sort((a, b) => a.frontmatter.name.localeCompare(b.frontmatter.name));
|
|
269
423
|
}
|
|
270
|
-
/** Get
|
|
271
|
-
*
|
|
424
|
+
/** Get one skill by name, applying per-project precedence. Returns
|
|
425
|
+
* null when neither pool has the skill. */
|
|
272
426
|
export function getSkill(name, opts = {}) {
|
|
273
|
-
// Per-project first (precedence).
|
|
427
|
+
// Per-project first (precedence), then global.
|
|
428
|
+
const tryDir = (dir, scope) => {
|
|
429
|
+
const folder = path.join(dir, name);
|
|
430
|
+
if (existsSync(folder) && existsSync(path.join(folder, 'SKILL.md'))) {
|
|
431
|
+
return parseSkillFolder(folder, scope).skill;
|
|
432
|
+
}
|
|
433
|
+
const flat = path.join(dir, name + '.md');
|
|
434
|
+
if (existsSync(flat)) {
|
|
435
|
+
return parseSkillFile(flat, scope).skill;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
};
|
|
439
|
+
let skill = null;
|
|
274
440
|
if (opts.projectWorkDir) {
|
|
275
441
|
const pdir = projectSkillsDir(opts.projectWorkDir);
|
|
276
|
-
if (pdir)
|
|
277
|
-
|
|
278
|
-
if (existsSync(candidate)) {
|
|
279
|
-
const result = parseSkillFile(candidate, 'project');
|
|
280
|
-
if (opts.jobs)
|
|
281
|
-
result.skill.usedByTriggers = jobsUsing(name, opts.jobs);
|
|
282
|
-
return result.skill;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
442
|
+
if (pdir)
|
|
443
|
+
skill = tryDir(pdir, 'project');
|
|
285
444
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
445
|
+
if (!skill)
|
|
446
|
+
skill = tryDir(globalSkillsDir(), 'global');
|
|
447
|
+
if (skill && opts.jobs) {
|
|
448
|
+
for (const j of opts.jobs) {
|
|
449
|
+
if (Array.isArray(j.skills) && j.skills.includes(name))
|
|
450
|
+
skill.usedByTriggers.push(j.name);
|
|
451
|
+
}
|
|
293
452
|
}
|
|
294
|
-
return
|
|
453
|
+
return skill;
|
|
295
454
|
}
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
455
|
+
/** Read one bundled file's contents — used by Phase B's preview pane.
|
|
456
|
+
* Defends against directory traversal: rejects paths that escape the
|
|
457
|
+
* skill folder. */
|
|
458
|
+
export function readBundledFile(skill, relPath) {
|
|
459
|
+
if (skill.layout !== 'folder')
|
|
460
|
+
return null;
|
|
461
|
+
const skillFolder = path.dirname(skill.filePath);
|
|
462
|
+
const absPath = path.resolve(skillFolder, relPath);
|
|
463
|
+
if (!absPath.startsWith(skillFolder + path.sep) && absPath !== skillFolder)
|
|
464
|
+
return null;
|
|
465
|
+
if (!existsSync(absPath))
|
|
466
|
+
return null;
|
|
467
|
+
try {
|
|
468
|
+
return readFileSync(absPath, 'utf-8');
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
302
472
|
}
|
|
303
|
-
return out;
|
|
304
473
|
}
|
|
305
|
-
/**
|
|
306
|
-
* dashboard's diagnostics surface. */
|
|
474
|
+
/** Diagnostics for the dashboard — expose where the loader looked. */
|
|
307
475
|
export function _skillDirsForDiagnostics(workDir) {
|
|
308
|
-
return {
|
|
309
|
-
global: globalSkillsDir(),
|
|
310
|
-
project: projectSkillsDir(workDir) ?? null,
|
|
311
|
-
};
|
|
476
|
+
return { global: globalSkillsDir(), project: projectSkillsDir(workDir) ?? null };
|
|
312
477
|
}
|
|
313
478
|
//# sourceMappingURL=skill-store.js.map
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -26872,6 +26872,29 @@ function onSkillsSearch(value) {
|
|
|
26872
26872
|
renderSkillsList();
|
|
26873
26873
|
}
|
|
26874
26874
|
|
|
26875
|
+
// ── Skill list pane ──────────────────────────────────────────────────
|
|
26876
|
+
// Renders the left-rail skill list. Each card shows two compact badges:
|
|
26877
|
+
// - schemaBadge: ANTHROPIC (green) / CLEMENTINE (purple) / LEGACY (yellow)
|
|
26878
|
+
// - layoutBadge: FOLDER (subtle accent) — only shown when folder-form
|
|
26879
|
+
// Plus optional PROJECT scope badge. Below: description preview, used-by
|
|
26880
|
+
// count, and a small ✗ when validation has any errors.
|
|
26881
|
+
function _skillSchemaBadge(version) {
|
|
26882
|
+
if (version === 'anthropic') {
|
|
26883
|
+
return '<span style="font-size:9px;background:var(--green)20;color:var(--green);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Vanilla Anthropic frontmatter (name + description only)">ANTHROPIC</span>';
|
|
26884
|
+
}
|
|
26885
|
+
if (version === 'clementine') {
|
|
26886
|
+
return '<span style="font-size:9px;background:#8b5cf620;color:#8b5cf6;padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Anthropic-compatible + clementine: extensions">CLEMENTINE</span>';
|
|
26887
|
+
}
|
|
26888
|
+
return '<span style="font-size:9px;background:var(--yellow)20;color:var(--yellow);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Pre-redesign flat frontmatter — Phase B will help migrate">LEGACY</span>';
|
|
26889
|
+
}
|
|
26890
|
+
|
|
26891
|
+
function _skillLayoutBadge(layout) {
|
|
26892
|
+
if (layout === 'folder') {
|
|
26893
|
+
return '<span style="font-size:9px;background:var(--accent)15;color:var(--accent);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Folder form (Anthropic spec): SKILL.md plus optional bundled files">FOLDER</span>';
|
|
26894
|
+
}
|
|
26895
|
+
return '';
|
|
26896
|
+
}
|
|
26897
|
+
|
|
26875
26898
|
function renderSkillsList() {
|
|
26876
26899
|
var listEl = document.getElementById('skills-list');
|
|
26877
26900
|
if (!listEl) return;
|
|
@@ -26879,7 +26902,7 @@ function renderSkillsList() {
|
|
|
26879
26902
|
listEl.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:12px;text-align:center;line-height:1.5">'
|
|
26880
26903
|
+ (_skillsState.query
|
|
26881
26904
|
? 'No skills match <strong>' + esc(_skillsState.query) + '</strong>.'
|
|
26882
|
-
: 'No skills yet.
|
|
26905
|
+
: 'No skills yet. Drop a folder with <code>SKILL.md</code> inside, or a flat <code>.md</code> file, into <code>~/.clementine/vault/00-System/skills/</code>. Phase B will add a <strong>+ New skill</strong> button.')
|
|
26883
26906
|
+ '</div>';
|
|
26884
26907
|
return;
|
|
26885
26908
|
}
|
|
@@ -26889,21 +26912,21 @@ function renderSkillsList() {
|
|
|
26889
26912
|
var fm = s.frontmatter || {};
|
|
26890
26913
|
var isSelected = s.frontmatter.name === _skillsState.selectedName;
|
|
26891
26914
|
var bg = isSelected ? 'var(--bg-tertiary)' : 'transparent';
|
|
26892
|
-
|
|
26893
|
-
var
|
|
26894
|
-
? '<span style="font-size:9px;background:var(--green)20;color:var(--green);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em">V1</span>'
|
|
26895
|
-
: '<span style="font-size:9px;background:var(--yellow)20;color:var(--yellow);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Legacy frontmatter — Phase B will migrate this">LEGACY</span>';
|
|
26915
|
+
var schemaBadge = _skillSchemaBadge(s.schemaVersion);
|
|
26916
|
+
var layoutBadge = _skillLayoutBadge(s.layout);
|
|
26896
26917
|
var scopeBadge = s.scope === 'project'
|
|
26897
26918
|
? '<span style="font-size:9px;background:var(--blue)20;color:var(--blue);padding:1px 6px;border-radius:3px;font-weight:600;letter-spacing:0.04em" title="Loaded from per-project .clementine/skills/">PROJECT</span>'
|
|
26898
26919
|
: '';
|
|
26920
|
+
var hasErrors = Array.isArray(s.validation) && s.validation.some(function(v) { return v.severity === 'error'; });
|
|
26921
|
+
var errorMark = hasErrors ? '<span style="color:var(--red);font-size:11px;font-weight:600" title="Has validation errors">⚠</span>' : '';
|
|
26899
26922
|
var usedCount = (s.usedByTriggers || []).length;
|
|
26900
26923
|
var displayName = fm.title || fm.name;
|
|
26901
26924
|
var desc = (fm.description || '').slice(0, 100);
|
|
26902
26925
|
if (fm.description && fm.description.length > 100) desc += '…';
|
|
26903
26926
|
html += '<div onclick="showSkillDetail(\\x27' + jsStr(fm.name) + '\\x27)" style="padding:12px 14px;border-bottom:1px solid var(--border);cursor:pointer;background:' + bg + ';transition:background 0.1s" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27' + bg + '\\x27">'
|
|
26904
|
-
+ '<div style="display:flex;align-items:center;gap:
|
|
26905
|
-
+ schemaBadge + scopeBadge
|
|
26906
|
-
+ '<span style="font-weight:500;font-size:13px;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1" title="' + esc(displayName) + '">' + esc(displayName) + '</span>'
|
|
26927
|
+
+ '<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;flex-wrap:wrap">'
|
|
26928
|
+
+ schemaBadge + layoutBadge + scopeBadge + errorMark
|
|
26929
|
+
+ '<span style="font-weight:500;font-size:13px;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0" title="' + esc(displayName) + '">' + esc(displayName) + '</span>'
|
|
26907
26930
|
+ '</div>'
|
|
26908
26931
|
+ (desc ? '<div style="font-size:11px;color:var(--text-muted);line-height:1.4;margin-bottom:4px">' + esc(desc) + '</div>' : '')
|
|
26909
26932
|
+ '<div style="font-size:10px;color:var(--text-muted)">'
|
|
@@ -26933,72 +26956,117 @@ async function showSkillDetail(name) {
|
|
|
26933
26956
|
}
|
|
26934
26957
|
}
|
|
26935
26958
|
|
|
26959
|
+
// ── Skill detail pane ────────────────────────────────────────────────
|
|
26960
|
+
// Renders a single skill in the right pane. Sections, in order:
|
|
26961
|
+
// 1. Header (name + 3 badges + description + file path)
|
|
26962
|
+
// 2. Validation warnings (red errors / yellow warnings) when present
|
|
26963
|
+
// 3. Used-by triggers
|
|
26964
|
+
// 4. Bundled files (folder-form only) — sibling .md and scripts/
|
|
26965
|
+
// 5. Clementine extensions (inputs / tools / dataSources / stateKeys /
|
|
26966
|
+
// success / limits) — only when frontmatter.clementine is present
|
|
26967
|
+
// 6. Legacy fields (triggers / toolsUsed / useCount / lastUsed)
|
|
26968
|
+
// 7. Procedure body (with line counter "X/500")
|
|
26969
|
+
// 8. Schema-specific footer note (legacy → migration hint)
|
|
26936
26970
|
function renderSkillDetail(s) {
|
|
26937
26971
|
var fm = s.frontmatter || {};
|
|
26972
|
+
var ext = fm.clementine || {};
|
|
26938
26973
|
var displayName = fm.title || fm.name;
|
|
26974
|
+
var bodyLines = (s.body || '').split('\\n').length;
|
|
26939
26975
|
var html = '<div style="padding:24px 28px">';
|
|
26940
|
-
|
|
26976
|
+
|
|
26977
|
+
// ── 1. Header
|
|
26941
26978
|
html += '<div style="margin-bottom:18px">';
|
|
26942
|
-
html += '<div style="display:flex;align-items:center;gap:
|
|
26979
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">';
|
|
26943
26980
|
html += '<h2 style="margin:0;font-size:20px;font-weight:600;color:var(--text-primary)">' + esc(displayName) + '</h2>';
|
|
26944
|
-
html += s.schemaVersion
|
|
26945
|
-
|
|
26946
|
-
|
|
26981
|
+
html += _skillSchemaBadge(s.schemaVersion).replace('font-size:9px', 'font-size:10px').replace('padding:1px 6px', 'padding:2px 8px');
|
|
26982
|
+
if (s.layout === 'folder') {
|
|
26983
|
+
html += '<span style="font-size:10px;background:var(--accent)15;color:var(--accent);padding:2px 8px;border-radius:4px;font-weight:600;letter-spacing:0.04em" title="Folder layout (Anthropic spec)">FOLDER</span>';
|
|
26984
|
+
} else {
|
|
26985
|
+
html += '<span style="font-size:10px;background:var(--bg-tertiary);color:var(--text-muted);padding:2px 8px;border-radius:4px;font-weight:600;letter-spacing:0.04em" title="Single-file Clementine legacy layout">FLAT</span>';
|
|
26986
|
+
}
|
|
26947
26987
|
if (s.scope === 'project') {
|
|
26948
26988
|
html += '<span style="font-size:10px;background:var(--blue)20;color:var(--blue);padding:2px 8px;border-radius:4px;font-weight:600">PROJECT-SCOPED</span>';
|
|
26949
26989
|
}
|
|
26950
|
-
if (typeof
|
|
26951
|
-
html += '<span style="font-size:10px;color:var(--text-muted)">v' + esc(
|
|
26990
|
+
if (typeof ext.version === 'number') {
|
|
26991
|
+
html += '<span style="font-size:10px;color:var(--text-muted);font-weight:500">v' + esc(ext.version) + '</span>';
|
|
26952
26992
|
}
|
|
26953
26993
|
html += '</div>';
|
|
26954
26994
|
if (fm.description) {
|
|
26955
26995
|
html += '<p style="font-size:13px;color:var(--text-secondary);line-height:1.5;margin:0">' + esc(fm.description) + '</p>';
|
|
26996
|
+
} else {
|
|
26997
|
+
html += '<p style="font-size:12px;color:var(--text-muted);font-style:italic;margin:0">No description. Anthropic spec recommends adding one so the skill can be discovered by Claude.</p>';
|
|
26956
26998
|
}
|
|
26957
|
-
html += '<div style="margin-top:
|
|
26958
|
-
+ 'File: <code style="font-size:10px">' + esc(s.filePath) + '</code>'
|
|
26959
|
-
+ '</div>';
|
|
26999
|
+
html += '<div style="margin-top:10px;font-size:11px;color:var(--text-muted);font-family:\\x27JetBrains Mono\\x27,monospace">' + esc(s.filePath) + '</div>';
|
|
26960
27000
|
html += '</div>';
|
|
26961
27001
|
|
|
26962
|
-
//
|
|
27002
|
+
// ── 2. Validation warnings (if any)
|
|
27003
|
+
if (Array.isArray(s.validation) && s.validation.length > 0) {
|
|
27004
|
+
var errors = s.validation.filter(function(v) { return v.severity === 'error'; });
|
|
27005
|
+
var warnings = s.validation.filter(function(v) { return v.severity === 'warning'; });
|
|
27006
|
+
if (errors.length > 0) {
|
|
27007
|
+
html += '<div style="margin-bottom:18px;padding:12px 14px;background:rgba(239,68,68,0.08);border:1px solid var(--red);border-radius:6px">';
|
|
27008
|
+
html += '<div style="font-size:11px;color:var(--red);text-transform:uppercase;letter-spacing:0.04em;font-weight:600;margin-bottom:6px">' + errors.length + ' error' + (errors.length === 1 ? '' : 's') + ' — Anthropic spec violation</div>';
|
|
27009
|
+
html += '<ul style="margin:0;padding-left:18px;font-size:12px;line-height:1.5;color:var(--text-secondary)">';
|
|
27010
|
+
for (var ei = 0; ei < errors.length; ei++) {
|
|
27011
|
+
html += '<li><strong style="color:var(--red)">' + esc(errors[ei].field) + ':</strong> ' + esc(errors[ei].message) + '</li>';
|
|
27012
|
+
}
|
|
27013
|
+
html += '</ul></div>';
|
|
27014
|
+
}
|
|
27015
|
+
if (warnings.length > 0) {
|
|
27016
|
+
html += '<div style="margin-bottom:18px;padding:12px 14px;background:rgba(245,158,11,0.08);border:1px solid var(--yellow);border-radius:6px">';
|
|
27017
|
+
html += '<div style="font-size:11px;color:var(--yellow);text-transform:uppercase;letter-spacing:0.04em;font-weight:600;margin-bottom:6px">' + warnings.length + ' suggestion' + (warnings.length === 1 ? '' : 's') + '</div>';
|
|
27018
|
+
html += '<ul style="margin:0;padding-left:18px;font-size:12px;line-height:1.5;color:var(--text-secondary)">';
|
|
27019
|
+
for (var wi = 0; wi < warnings.length; wi++) {
|
|
27020
|
+
html += '<li><strong>' + esc(warnings[wi].field) + ':</strong> ' + esc(warnings[wi].message) + '</li>';
|
|
27021
|
+
}
|
|
27022
|
+
html += '</ul></div>';
|
|
27023
|
+
}
|
|
27024
|
+
}
|
|
27025
|
+
|
|
27026
|
+
// ── 3. Used-by triggers
|
|
26963
27027
|
if (Array.isArray(s.usedByTriggers) && s.usedByTriggers.length > 0) {
|
|
26964
27028
|
html += '<div style="margin-bottom:18px;padding:12px 14px;background:var(--bg-tertiary);border-radius:6px">';
|
|
26965
27029
|
html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px;font-weight:500">Used by ' + s.usedByTriggers.length + ' trigger' + (s.usedByTriggers.length === 1 ? '' : 's') + '</div>';
|
|
26966
27030
|
html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
|
|
26967
|
-
for (var
|
|
26968
|
-
html += '<span style="font-size:11px;background:var(--bg-secondary);padding:2px 8px;border-radius:4px;border:1px solid var(--border);color:var(--text-secondary)">' + esc(s.usedByTriggers[
|
|
27031
|
+
for (var ui = 0; ui < s.usedByTriggers.length; ui++) {
|
|
27032
|
+
html += '<span style="font-size:11px;background:var(--bg-secondary);padding:2px 8px;border-radius:4px;border:1px solid var(--border);color:var(--text-secondary)">' + esc(s.usedByTriggers[ui]) + '</span>';
|
|
26969
27033
|
}
|
|
26970
27034
|
html += '</div></div>';
|
|
26971
27035
|
}
|
|
26972
27036
|
|
|
26973
|
-
//
|
|
26974
|
-
if (
|
|
26975
|
-
html += renderSkillSection('
|
|
27037
|
+
// ── 4. Bundled files (folder-form only)
|
|
27038
|
+
if (s.layout === 'folder' && Array.isArray(s.bundledFiles) && s.bundledFiles.length > 0) {
|
|
27039
|
+
html += renderSkillSection('Bundled files (' + s.bundledFiles.length + ')', renderSkillBundledFiles(s.bundledFiles));
|
|
27040
|
+
}
|
|
27041
|
+
|
|
27042
|
+
// ── 5. Clementine extensions
|
|
27043
|
+
if (ext.inputs && Object.keys(ext.inputs).length > 0) {
|
|
27044
|
+
html += renderSkillSection('Inputs', renderSkillInputs(ext.inputs));
|
|
26976
27045
|
}
|
|
26977
|
-
if (
|
|
26978
|
-
html += renderSkillSection('Tools', renderSkillTools(
|
|
27046
|
+
if (ext.tools && ((ext.tools.allow && ext.tools.allow.length) || (ext.tools.deny && ext.tools.deny.length))) {
|
|
27047
|
+
html += renderSkillSection('Tools', renderSkillTools(ext.tools));
|
|
26979
27048
|
}
|
|
26980
|
-
if (Array.isArray(
|
|
26981
|
-
html += renderSkillSection('Data sources', renderSkillDataSources(
|
|
27049
|
+
if (Array.isArray(ext.dataSources) && ext.dataSources.length > 0) {
|
|
27050
|
+
html += renderSkillSection('Data sources', renderSkillDataSources(ext.dataSources));
|
|
26982
27051
|
}
|
|
26983
|
-
if (Array.isArray(
|
|
26984
|
-
html += renderSkillSection('State keys', '<div style="display:flex;flex-wrap:wrap;gap:4px">' +
|
|
27052
|
+
if (Array.isArray(ext.stateKeys) && ext.stateKeys.length > 0) {
|
|
27053
|
+
html += renderSkillSection('State keys', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + ext.stateKeys.map(function(k) { return '<code style="font-size:11px;background:var(--bg-tertiary);padding:2px 6px;border-radius:3px">' + esc(k) + '</code>'; }).join('') + '</div>');
|
|
26985
27054
|
}
|
|
26986
|
-
if (
|
|
27055
|
+
if (ext.success && (ext.success.criterion || ext.success.schema)) {
|
|
26987
27056
|
var sc = '';
|
|
26988
|
-
if (
|
|
26989
|
-
if (
|
|
27057
|
+
if (ext.success.criterion) sc += '<div style="font-size:12px;line-height:1.5;color:var(--text-secondary);margin-bottom:8px"><em>Criterion:</em> ' + esc(ext.success.criterion) + '</div>';
|
|
27058
|
+
if (ext.success.schema) sc += '<details><summary style="cursor:pointer;font-size:11px;color:var(--text-muted)">Schema</summary><pre style="font-size:11px;background:var(--bg-tertiary);padding:10px;border-radius:6px;margin-top:6px;overflow:auto">' + esc(JSON.stringify(ext.success.schema, null, 2)) + '</pre></details>';
|
|
26990
27059
|
html += renderSkillSection('Success criterion', sc);
|
|
26991
27060
|
}
|
|
26992
|
-
if (
|
|
26993
|
-
var
|
|
26994
|
-
|
|
26995
|
-
if (
|
|
26996
|
-
if (
|
|
26997
|
-
if (
|
|
26998
|
-
if (bits.length) html += renderSkillSection('Limits', '<div style="font-size:12px;color:var(--text-secondary)">' + bits.map(esc).join(' · ') + '</div>');
|
|
27061
|
+
if (ext.limits) {
|
|
27062
|
+
var lbits = [];
|
|
27063
|
+
if (ext.limits.maxTurns) lbits.push('max ' + ext.limits.maxTurns + ' turns');
|
|
27064
|
+
if (ext.limits.maxBudgetUsd) lbits.push('$' + ext.limits.maxBudgetUsd + ' budget');
|
|
27065
|
+
if (ext.limits.timeoutSeconds) lbits.push(ext.limits.timeoutSeconds + 's timeout');
|
|
27066
|
+
if (lbits.length) html += renderSkillSection('Limits', '<div style="font-size:12px;color:var(--text-secondary)">' + lbits.map(esc).join(' · ') + '</div>');
|
|
26999
27067
|
}
|
|
27000
27068
|
|
|
27001
|
-
// Legacy fields (preserved for
|
|
27069
|
+
// ── 6. Legacy fields (preserved for migration UI in Phase B)
|
|
27002
27070
|
if (Array.isArray(fm.triggers) && fm.triggers.length > 0) {
|
|
27003
27071
|
html += renderSkillSection('Triggers (legacy NLP phrases)',
|
|
27004
27072
|
'<div style="display:flex;flex-wrap:wrap;gap:4px">'
|
|
@@ -27012,27 +27080,35 @@ function renderSkillDetail(s) {
|
|
|
27012
27080
|
+ fm.toolsUsed.map(function(t) { return '<code style="font-size:11px;background:var(--bg-tertiary);padding:2px 6px;border-radius:3px">' + esc(t) + '</code>'; }).join('')
|
|
27013
27081
|
+ '</div>');
|
|
27014
27082
|
}
|
|
27015
|
-
if (fm.useCount ||
|
|
27016
|
-
var
|
|
27017
|
-
if (fm.useCount)
|
|
27018
|
-
if (
|
|
27019
|
-
html += renderSkillSection('Usage', '<div style="font-size:12px;color:var(--text-secondary)">' +
|
|
27083
|
+
if (fm.useCount || ext.lastUsed) {
|
|
27084
|
+
var ubits = [];
|
|
27085
|
+
if (fm.useCount) ubits.push('Used ' + fm.useCount + ' times');
|
|
27086
|
+
if (ext.lastUsed) ubits.push('Last: ' + new Date(ext.lastUsed).toLocaleString());
|
|
27087
|
+
html += renderSkillSection('Usage', '<div style="font-size:12px;color:var(--text-secondary)">' + ubits.map(esc).join(' · ') + '</div>');
|
|
27020
27088
|
}
|
|
27021
27089
|
|
|
27022
|
-
//
|
|
27023
|
-
// proper markdown renderer; Phase A keeps it simple to ship.
|
|
27090
|
+
// ── 7. Procedure body (with line counter)
|
|
27024
27091
|
if (s.body && s.body.trim()) {
|
|
27092
|
+
var bodyClass = bodyLines > 500 ? 'color:var(--yellow)' : 'color:var(--text-muted)';
|
|
27025
27093
|
html += '<div style="margin-top:18px">';
|
|
27026
|
-
html += '<div style="
|
|
27094
|
+
html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">';
|
|
27095
|
+
html += '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;font-weight:500">Procedure</div>';
|
|
27096
|
+
html += '<div style="font-size:10px;' + bodyClass + ';font-family:\\x27JetBrains Mono\\x27,monospace">' + bodyLines + ' / 500 lines</div>';
|
|
27097
|
+
html += '</div>';
|
|
27027
27098
|
html += '<pre style="font-size:12px;line-height:1.55;background:var(--bg-tertiary);padding:14px 16px;border-radius:6px;white-space:pre-wrap;word-break:break-word;font-family:inherit;border:1px solid var(--border);max-height:500px;overflow:auto">' + esc(s.body) + '</pre>';
|
|
27028
27099
|
html += '</div>';
|
|
27029
27100
|
}
|
|
27030
27101
|
|
|
27102
|
+
// ── 8. Schema-specific footer
|
|
27031
27103
|
if (s.schemaVersion === 'legacy') {
|
|
27032
|
-
html += '<div style="margin-top:24px;padding:12px 14px;background:
|
|
27104
|
+
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">'
|
|
27033
27105
|
+ '<strong style="color:var(--yellow)">Legacy schema.</strong> '
|
|
27034
|
-
+ 'This skill uses the pre-redesign frontmatter shape (
|
|
27035
|
-
+ '
|
|
27106
|
+
+ 'This skill uses the pre-redesign frontmatter shape. Phase B will offer a one-click migration to the Anthropic-compatible format (name + description top-level; cron-tailored fields under <code>clementine:</code>).'
|
|
27107
|
+
+ '</div>';
|
|
27108
|
+
} else if (s.schemaVersion === 'anthropic' && s.layout === 'flat') {
|
|
27109
|
+
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">'
|
|
27110
|
+
+ '<strong style="color:var(--blue)">Anthropic-compatible.</strong> '
|
|
27111
|
+
+ 'This skill matches the official Anthropic spec exactly. Consider promoting it to <strong>folder form</strong> (<code>' + esc(fm.name) + '/SKILL.md</code>) so you can bundle reference files and scripts later.'
|
|
27036
27112
|
+ '</div>';
|
|
27037
27113
|
}
|
|
27038
27114
|
|
|
@@ -27040,6 +27116,36 @@ function renderSkillDetail(s) {
|
|
|
27040
27116
|
return html;
|
|
27041
27117
|
}
|
|
27042
27118
|
|
|
27119
|
+
// PRD § Skills-First Phase A.5 / 1.18.108: bundled file tree.
|
|
27120
|
+
// Renders the folder's sibling files + scripts/ contents grouped by kind.
|
|
27121
|
+
// Each row: icon (📄 markdown / ⚙ script / · other), relPath, size.
|
|
27122
|
+
function renderSkillBundledFiles(files) {
|
|
27123
|
+
var iconFor = function(kind) {
|
|
27124
|
+
if (kind === 'markdown') return '📄';
|
|
27125
|
+
if (kind === 'script') return '⚙';
|
|
27126
|
+
return '·';
|
|
27127
|
+
};
|
|
27128
|
+
var fmt = function(bytes) {
|
|
27129
|
+
if (bytes < 1024) return bytes + ' B';
|
|
27130
|
+
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
|
27131
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
27132
|
+
};
|
|
27133
|
+
var html = '<div style="font-family:\\x27JetBrains Mono\\x27,monospace;font-size:11px;background:var(--bg-tertiary);border-radius:6px;padding:10px 14px">';
|
|
27134
|
+
for (var i = 0; i < files.length; i++) {
|
|
27135
|
+
var f = files[i];
|
|
27136
|
+
var indentStyle = f.relPath.indexOf('/') !== -1 ? 'padding-left:20px' : '';
|
|
27137
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;' + indentStyle + '">'
|
|
27138
|
+
+ '<span style="color:var(--text-secondary)">'
|
|
27139
|
+
+ '<span style="display:inline-block;width:18px;color:var(--text-muted)">' + iconFor(f.kind) + '</span>'
|
|
27140
|
+
+ esc(f.relPath)
|
|
27141
|
+
+ '</span>'
|
|
27142
|
+
+ '<span style="font-size:10px;color:var(--text-muted)">' + esc(fmt(f.sizeBytes)) + '</span>'
|
|
27143
|
+
+ '</div>';
|
|
27144
|
+
}
|
|
27145
|
+
html += '</div>';
|
|
27146
|
+
return html;
|
|
27147
|
+
}
|
|
27148
|
+
|
|
27043
27149
|
function renderSkillSection(title, body) {
|
|
27044
27150
|
return '<div style="margin-bottom:18px">'
|
|
27045
27151
|
+ '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:8px;font-weight:500">' + esc(title) + '</div>'
|
package/dist/types.d.ts
CHANGED
|
@@ -309,13 +309,18 @@ export interface AgentHeartbeatState {
|
|
|
309
309
|
*/
|
|
310
310
|
lastTickKind?: 'acted' | 'quiet' | 'silent' | 'override';
|
|
311
311
|
}
|
|
312
|
-
/**
|
|
312
|
+
/** Where a skill was loaded from. Per-project skills shadow global. */
|
|
313
313
|
export type SkillScope = 'global' | 'project';
|
|
314
|
-
/**
|
|
315
|
-
*
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
/** Three states the dashboard surfaces as badges:
|
|
315
|
+
* 'anthropic' — only `name` + `description` (vanilla Anthropic spec)
|
|
316
|
+
* 'clementine' — has the `clementine:` namespace with extensions
|
|
317
|
+
* 'legacy' — pre-redesign flat frontmatter (title/triggers/toolsUsed) */
|
|
318
|
+
export type SkillSchemaVersion = 'anthropic' | 'clementine' | 'legacy';
|
|
319
|
+
/** Whether the on-disk layout is a folder-with-SKILL.md (Anthropic spec)
|
|
320
|
+
* or a single .md file (Clementine flat legacy). New skills should be
|
|
321
|
+
* created in folder form so they can grow bundled files later. */
|
|
322
|
+
export type SkillLayout = 'folder' | 'flat';
|
|
323
|
+
/** A typed skill input — backed by JSON Schema. Used in `clementine.inputs`. */
|
|
319
324
|
export interface SkillInputSchema {
|
|
320
325
|
type?: 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object';
|
|
321
326
|
description?: string;
|
|
@@ -330,96 +335,125 @@ export interface SkillInputSchema {
|
|
|
330
335
|
properties?: Record<string, SkillInputSchema>;
|
|
331
336
|
required?: string[];
|
|
332
337
|
}
|
|
333
|
-
/** Declarative entry describing where a skill reads data from.
|
|
334
|
-
* in the dashboard's per-skill detail pane and in the Tools & MCP "used
|
|
335
|
-
* by" join. Free-form by design — different skills declare different
|
|
336
|
-
* data shapes. */
|
|
338
|
+
/** Declarative entry describing where a skill reads data from. */
|
|
337
339
|
export interface SkillDataSource {
|
|
338
|
-
/**
|
|
340
|
+
/** Loose identifier — 'outlook', 'memory', 'vault', 'cli', 'mcp:ElevenLabs'. */
|
|
339
341
|
kind: string;
|
|
340
|
-
/** One-line human description
|
|
342
|
+
/** One-line human description of what gets read. */
|
|
341
343
|
purpose: string;
|
|
342
344
|
}
|
|
343
|
-
/** Tool allowlist + denylist
|
|
344
|
-
*
|
|
345
|
-
* even if the trigger tries to override. */
|
|
345
|
+
/** Tool allowlist + denylist. Deny wins on conflict. Phase C runtime
|
|
346
|
+
* refuses tools not in `allow`, even when a trigger tries to override. */
|
|
346
347
|
export interface SkillToolPolicy {
|
|
347
348
|
allow?: string[];
|
|
348
349
|
deny?: string[];
|
|
349
350
|
}
|
|
350
|
-
/** Success criterion
|
|
351
|
-
* structured_output) or criterion (free-text Haiku evaluator). Both =
|
|
352
|
-
* both must pass. Mirrors the existing CronJobDefinition shape. */
|
|
351
|
+
/** Success criterion — JSON Schema (ajv-validated) and/or free-text. */
|
|
353
352
|
export interface SkillSuccess {
|
|
354
353
|
schema?: SkillInputSchema;
|
|
355
354
|
criterion?: string;
|
|
356
355
|
}
|
|
357
|
-
/** Per-skill caps. A trigger can tighten
|
|
356
|
+
/** Per-skill caps. A trigger can tighten but never loosen. */
|
|
358
357
|
export interface SkillLimits {
|
|
359
358
|
maxTurns?: number;
|
|
360
359
|
maxBudgetUsd?: number;
|
|
361
360
|
timeoutSeconds?: number;
|
|
362
361
|
}
|
|
363
|
-
/**
|
|
364
|
-
*
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
name: string;
|
|
368
|
-
/** One-line description (matches today's `description:` field). */
|
|
369
|
-
description?: string;
|
|
362
|
+
/** Clementine-specific extensions. All optional. Lives under the
|
|
363
|
+
* `clementine:` key in the YAML frontmatter so an Anthropic skill that
|
|
364
|
+
* doesn't have these stays valid. */
|
|
365
|
+
export interface ClementineSkillExtensions {
|
|
370
366
|
/** Typed parameters the skill accepts at invocation time. */
|
|
371
367
|
inputs?: Record<string, SkillInputSchema>;
|
|
372
368
|
/** Tool allowlist + denylist enforced by the runtime. */
|
|
373
369
|
tools?: SkillToolPolicy;
|
|
374
370
|
/** Where the skill reads data from — purely declarative. */
|
|
375
371
|
dataSources?: SkillDataSource[];
|
|
376
|
-
/**
|
|
372
|
+
/** state.* keys this skill owns. Others can't touch them. */
|
|
377
373
|
stateKeys?: string[];
|
|
378
374
|
/** Success criterion — schema and/or free-text evaluator. */
|
|
379
375
|
success?: SkillSuccess;
|
|
380
376
|
/** Caps the trigger can tighten but never loosen. */
|
|
381
377
|
limits?: SkillLimits;
|
|
382
|
-
/**
|
|
383
|
-
* doesn't increment. */
|
|
378
|
+
/** Skill version — bumped on Publish in Phase B. */
|
|
384
379
|
version?: number;
|
|
385
|
-
/** Timestamps
|
|
380
|
+
/** Timestamps Clementine captures. */
|
|
386
381
|
createdAt?: string;
|
|
387
382
|
updatedAt?: string;
|
|
388
383
|
lastUsed?: string;
|
|
389
|
-
/** Last
|
|
390
|
-
* (Phase B). Phase A reads but doesn't write. */
|
|
384
|
+
/** Last successful "Test this skill" run (Phase B). */
|
|
391
385
|
lastTestPass?: string;
|
|
392
|
-
|
|
386
|
+
}
|
|
387
|
+
/** Parsed frontmatter. Anthropic-canonical fields are top-level; our
|
|
388
|
+
* extensions live under `clementine`. Legacy fields (title/triggers/
|
|
389
|
+
* toolsUsed/useCount) are also top-level — they're what the existing
|
|
390
|
+
* pre-redesign skills already use and we keep them readable. */
|
|
391
|
+
export interface SkillFrontmatter {
|
|
392
|
+
/** Skill identifier. Filename is canonical; this field is honored when
|
|
393
|
+
* set but not required. Anthropic spec: max 64 chars, lowercase letters
|
|
394
|
+
* + numbers + hyphens, no XML, no reserved words ('anthropic'/'claude'). */
|
|
395
|
+
name: string;
|
|
396
|
+
/** What the skill does AND when to use it (third person). Anthropic
|
|
397
|
+
* spec: non-empty, max 1024 chars, no XML tags. */
|
|
398
|
+
description?: string;
|
|
399
|
+
/** Optional namespace for cron-tailored fields. Absent on vanilla
|
|
400
|
+
* Anthropic skills; present on Clementine-extended skills. */
|
|
401
|
+
clementine?: ClementineSkillExtensions;
|
|
402
|
+
/** Legacy: human-friendly display title. Falls back to `name` when absent. */
|
|
393
403
|
title?: string;
|
|
394
|
-
/** Legacy: NLP-style trigger phrases
|
|
395
|
-
* these against incoming chat messages. */
|
|
404
|
+
/** Legacy: NLP-style trigger phrases for chat-message matching. */
|
|
396
405
|
triggers?: string[];
|
|
397
406
|
/** Legacy: 'manual' / 'auto' / 'imported' — provenance label. */
|
|
398
407
|
source?: string;
|
|
399
|
-
/** Legacy: tools observed during runs. Informational, not
|
|
408
|
+
/** Legacy: tools observed during runs. Informational, not enforced. */
|
|
400
409
|
toolsUsed?: string[];
|
|
401
|
-
/** Legacy: incrementing counter of
|
|
410
|
+
/** Legacy: incrementing counter of runs that invoked the skill. */
|
|
402
411
|
useCount?: number;
|
|
403
412
|
}
|
|
413
|
+
/** A bundled file inside a folder-form skill. `kind` distinguishes
|
|
414
|
+
* loadable markdown from executable scripts so the dashboard can
|
|
415
|
+
* render them differently. */
|
|
416
|
+
export interface SkillBundledFile {
|
|
417
|
+
/** Path relative to the skill folder (e.g. 'FORMS.md', 'scripts/extract.py'). */
|
|
418
|
+
relPath: string;
|
|
419
|
+
/** Absolute path on disk. */
|
|
420
|
+
absPath: string;
|
|
421
|
+
/** Loose categorization for rendering: markdown reference vs script vs other. */
|
|
422
|
+
kind: 'markdown' | 'script' | 'other';
|
|
423
|
+
/** File size in bytes — surfaced as "X KB" in the dashboard. */
|
|
424
|
+
sizeBytes: number;
|
|
425
|
+
}
|
|
426
|
+
/** A validation finding for a skill — surfaced in the dashboard so
|
|
427
|
+
* authors can see what to fix. Severity 'error' indicates a spec
|
|
428
|
+
* violation (rejected by Anthropic API); 'warning' is a best-practice
|
|
429
|
+
* hint (still loadable). */
|
|
430
|
+
export interface SkillValidationWarning {
|
|
431
|
+
severity: 'error' | 'warning';
|
|
432
|
+
field: 'name' | 'description' | 'body' | 'frontmatter' | 'layout';
|
|
433
|
+
message: string;
|
|
434
|
+
}
|
|
404
435
|
/** Resolved skill record — frontmatter + body + computed extras the
|
|
405
|
-
* dashboard surfaces
|
|
436
|
+
* dashboard surfaces. */
|
|
406
437
|
export interface Skill {
|
|
407
|
-
/**
|
|
438
|
+
/** Parsed frontmatter (or synthesized when none / unparseable). */
|
|
408
439
|
frontmatter: SkillFrontmatter;
|
|
409
|
-
/** Markdown body of the skill
|
|
440
|
+
/** Markdown body of the skill (the actual procedure). */
|
|
410
441
|
body: string;
|
|
411
|
-
/** Absolute path to the
|
|
442
|
+
/** Absolute path to the entry-point file. For folder-form skills this
|
|
443
|
+
* points at <folder>/SKILL.md. For flat skills it points at <name>.md. */
|
|
412
444
|
filePath: string;
|
|
413
|
-
/**
|
|
414
|
-
* override. Per-project wins on name collision. */
|
|
445
|
+
/** Where this skill was loaded from (global vs per-project). */
|
|
415
446
|
scope: SkillScope;
|
|
416
|
-
/**
|
|
417
|
-
|
|
447
|
+
/** Folder-form (Anthropic-spec) vs flat-file (Clementine legacy). */
|
|
448
|
+
layout: SkillLayout;
|
|
449
|
+
/** Anthropic / clementine / legacy — drives the schema badge. */
|
|
418
450
|
schemaVersion: SkillSchemaVersion;
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
451
|
+
/** Sibling .md files + scripts/ contents (only populated for folder-form). */
|
|
452
|
+
bundledFiles: SkillBundledFile[];
|
|
453
|
+
/** Used-by join: cron jobs that reference this skill via skills[]. */
|
|
422
454
|
usedByTriggers: string[];
|
|
455
|
+
/** Validation findings — populated lazily so listSkills stays cheap. */
|
|
456
|
+
validation: SkillValidationWarning[];
|
|
423
457
|
}
|
|
424
458
|
export interface CronJobDefinition {
|
|
425
459
|
name: string;
|