clementine-agent 1.18.128 → 1.18.130
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/schedule-registry.d.ts +77 -0
- package/dist/agent/schedule-registry.js +147 -0
- package/dist/agent/skill-templates.d.ts +55 -0
- package/dist/agent/skill-templates.js +220 -0
- package/dist/cli/cron.js +30 -0
- package/dist/cli/dashboard.js +1210 -15
- package/dist/gateway/cron-scheduler.d.ts +13 -1
- package/dist/gateway/cron-scheduler.js +81 -17
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule registry — 1.18.129.
|
|
3
|
+
*
|
|
4
|
+
* The Anthropic-pure architectural shift: skills stay vanilla SKILL.md
|
|
5
|
+
* folders, scheduling lives in a separate tiny registry. Today's "fat
|
|
6
|
+
* cron" model in CRON.md duplicated ~70% of the skill schema (prompt,
|
|
7
|
+
* tools, MCP allowlists, work_dir, success criteria); this registry
|
|
8
|
+
* replaces all that with a thin {skillName → schedule} map.
|
|
9
|
+
*
|
|
10
|
+
* Storage: a single JSON file at `~/.clementine/schedules.json` with
|
|
11
|
+
* the shape:
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "morning-briefing": {
|
|
15
|
+
* "schedule": "0 7 * * 1-5",
|
|
16
|
+
* "enabled": true,
|
|
17
|
+
* "agentSlug": null,
|
|
18
|
+
* "addedAt": "2026-05-08T...",
|
|
19
|
+
* "lastModifiedAt": "2026-05-08T..."
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* The cron scheduler reads this alongside CRON.md and emits one
|
|
24
|
+
* CronJobDefinition per scheduled skill. The runtime path is unchanged
|
|
25
|
+
* — the skill body becomes the prompt via the existing buildSkillContext
|
|
26
|
+
* pipeline, no special case.
|
|
27
|
+
*
|
|
28
|
+
* Coexists with CRON.md indefinitely. Both formats run today; new work
|
|
29
|
+
* goes through this registry. Phase 3 ships the migrator that converts
|
|
30
|
+
* legacy crons to scheduled skills.
|
|
31
|
+
*/
|
|
32
|
+
export interface ScheduleEntry {
|
|
33
|
+
/** Skill slug — must match a skill in the catalog. The runtime auto-pins
|
|
34
|
+
* this skill, so its body becomes the prompt at fire-time. */
|
|
35
|
+
skillName: string;
|
|
36
|
+
/** Cron expression. Empty / falsy = no auto-fire (still appears on
|
|
37
|
+
* Tasks page so the user can edit it). */
|
|
38
|
+
schedule: string;
|
|
39
|
+
/** When false the scheduler skips it. Lets users pause without losing
|
|
40
|
+
* the schedule definition. */
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
/** When set, the skill runs as the named hired agent (Sasha, Ross,
|
|
43
|
+
* Nora, etc.). null/undefined = Clementine. Per-agent skills load
|
|
44
|
+
* from `agents/<slug>/skills/` first; the runtime resolves precedence. */
|
|
45
|
+
agentSlug?: string | null;
|
|
46
|
+
/** ISO timestamp of when the schedule was first created. */
|
|
47
|
+
addedAt?: string;
|
|
48
|
+
/** ISO timestamp of the last edit. */
|
|
49
|
+
lastModifiedAt?: string;
|
|
50
|
+
}
|
|
51
|
+
/** On-disk shape — keyed by skill name for O(1) lookup + simple merge. */
|
|
52
|
+
export type ScheduleFile = Record<string, Omit<ScheduleEntry, 'skillName'>>;
|
|
53
|
+
/**
|
|
54
|
+
* Read every schedule entry as a flat array. Each entry includes its
|
|
55
|
+
* skill name so callers don't have to reconstruct it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function listSchedules(): ScheduleEntry[];
|
|
58
|
+
/** Read one entry by skill name. Returns null when not scheduled. */
|
|
59
|
+
export declare function getSchedule(skillName: string): ScheduleEntry | null;
|
|
60
|
+
export interface SetScheduleInput {
|
|
61
|
+
schedule: string;
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
agentSlug?: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Upsert a schedule for a skill. New entries get `addedAt`; existing
|
|
67
|
+
* entries get `lastModifiedAt` updated. Returns the resulting entry so
|
|
68
|
+
* the dashboard can re-render without a re-fetch.
|
|
69
|
+
*/
|
|
70
|
+
export declare function setSchedule(skillName: string, input: SetScheduleInput): ScheduleEntry;
|
|
71
|
+
/** Drop the entry for a skill. No-op when nothing is scheduled. */
|
|
72
|
+
export declare function removeSchedule(skillName: string): void;
|
|
73
|
+
/** Toggle the enabled flag. Skill stays in the registry; just won't
|
|
74
|
+
* fire while disabled. Caller-side convenience to avoid re-passing
|
|
75
|
+
* schedule + agentSlug just to flip the boolean. */
|
|
76
|
+
export declare function enableSchedule(skillName: string, enabled: boolean): ScheduleEntry | null;
|
|
77
|
+
//# sourceMappingURL=schedule-registry.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule registry — 1.18.129.
|
|
3
|
+
*
|
|
4
|
+
* The Anthropic-pure architectural shift: skills stay vanilla SKILL.md
|
|
5
|
+
* folders, scheduling lives in a separate tiny registry. Today's "fat
|
|
6
|
+
* cron" model in CRON.md duplicated ~70% of the skill schema (prompt,
|
|
7
|
+
* tools, MCP allowlists, work_dir, success criteria); this registry
|
|
8
|
+
* replaces all that with a thin {skillName → schedule} map.
|
|
9
|
+
*
|
|
10
|
+
* Storage: a single JSON file at `~/.clementine/schedules.json` with
|
|
11
|
+
* the shape:
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "morning-briefing": {
|
|
15
|
+
* "schedule": "0 7 * * 1-5",
|
|
16
|
+
* "enabled": true,
|
|
17
|
+
* "agentSlug": null,
|
|
18
|
+
* "addedAt": "2026-05-08T...",
|
|
19
|
+
* "lastModifiedAt": "2026-05-08T..."
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* The cron scheduler reads this alongside CRON.md and emits one
|
|
24
|
+
* CronJobDefinition per scheduled skill. The runtime path is unchanged
|
|
25
|
+
* — the skill body becomes the prompt via the existing buildSkillContext
|
|
26
|
+
* pipeline, no special case.
|
|
27
|
+
*
|
|
28
|
+
* Coexists with CRON.md indefinitely. Both formats run today; new work
|
|
29
|
+
* goes through this registry. Phase 3 ships the migrator that converts
|
|
30
|
+
* legacy crons to scheduled skills.
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
// Resolve lazily on each call so test environments (which override
|
|
36
|
+
// CLEMENTINE_HOME inside beforeEach) see the fresh value rather than the
|
|
37
|
+
// value snapshot at module-load time. Mirrors skill-suppressions.ts.
|
|
38
|
+
function baseDir() {
|
|
39
|
+
return process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine');
|
|
40
|
+
}
|
|
41
|
+
function schedulesPath() {
|
|
42
|
+
return path.join(baseDir(), 'schedules.json');
|
|
43
|
+
}
|
|
44
|
+
function readFile() {
|
|
45
|
+
const filePath = schedulesPath();
|
|
46
|
+
if (!existsSync(filePath))
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
50
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
51
|
+
return {};
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
54
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
55
|
+
continue;
|
|
56
|
+
const e = entry;
|
|
57
|
+
// Tolerate partially-populated entries (a hand-edited file might
|
|
58
|
+
// have only `schedule` set). Default everything else.
|
|
59
|
+
out[name] = {
|
|
60
|
+
schedule: typeof e.schedule === 'string' ? e.schedule : '',
|
|
61
|
+
enabled: e.enabled !== false, // default true
|
|
62
|
+
agentSlug: typeof e.agentSlug === 'string' ? e.agentSlug : null,
|
|
63
|
+
addedAt: typeof e.addedAt === 'string' ? e.addedAt : undefined,
|
|
64
|
+
lastModifiedAt: typeof e.lastModifiedAt === 'string' ? e.lastModifiedAt : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Malformed JSON — never throw out of the registry. Worst case the
|
|
71
|
+
// user re-creates entries via the UI; their CRON.md jobs keep firing.
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeFile(data) {
|
|
76
|
+
const dir = baseDir();
|
|
77
|
+
if (!existsSync(dir))
|
|
78
|
+
mkdirSync(dir, { recursive: true });
|
|
79
|
+
// Sorted keys → deterministic on disk → friendly to git users who
|
|
80
|
+
// version-control their ~/.clementine/.
|
|
81
|
+
const sorted = {};
|
|
82
|
+
for (const k of Object.keys(data).sort())
|
|
83
|
+
sorted[k] = data[k];
|
|
84
|
+
writeFileSync(schedulesPath(), JSON.stringify(sorted, null, 2));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read every schedule entry as a flat array. Each entry includes its
|
|
88
|
+
* skill name so callers don't have to reconstruct it.
|
|
89
|
+
*/
|
|
90
|
+
export function listSchedules() {
|
|
91
|
+
const file = readFile();
|
|
92
|
+
return Object.entries(file).map(([skillName, entry]) => ({ skillName, ...entry }));
|
|
93
|
+
}
|
|
94
|
+
/** Read one entry by skill name. Returns null when not scheduled. */
|
|
95
|
+
export function getSchedule(skillName) {
|
|
96
|
+
const file = readFile();
|
|
97
|
+
const entry = file[skillName];
|
|
98
|
+
if (!entry)
|
|
99
|
+
return null;
|
|
100
|
+
return { skillName, ...entry };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Upsert a schedule for a skill. New entries get `addedAt`; existing
|
|
104
|
+
* entries get `lastModifiedAt` updated. Returns the resulting entry so
|
|
105
|
+
* the dashboard can re-render without a re-fetch.
|
|
106
|
+
*/
|
|
107
|
+
export function setSchedule(skillName, input) {
|
|
108
|
+
if (!skillName || typeof skillName !== 'string') {
|
|
109
|
+
throw new Error('setSchedule: skillName required');
|
|
110
|
+
}
|
|
111
|
+
const file = readFile();
|
|
112
|
+
const existing = file[skillName];
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const entry = {
|
|
115
|
+
schedule: input.schedule ?? '',
|
|
116
|
+
enabled: input.enabled !== false,
|
|
117
|
+
agentSlug: input.agentSlug ?? null,
|
|
118
|
+
addedAt: existing?.addedAt ?? now,
|
|
119
|
+
lastModifiedAt: now,
|
|
120
|
+
};
|
|
121
|
+
file[skillName] = entry;
|
|
122
|
+
writeFile(file);
|
|
123
|
+
return { skillName, ...entry };
|
|
124
|
+
}
|
|
125
|
+
/** Drop the entry for a skill. No-op when nothing is scheduled. */
|
|
126
|
+
export function removeSchedule(skillName) {
|
|
127
|
+
const file = readFile();
|
|
128
|
+
if (!(skillName in file))
|
|
129
|
+
return;
|
|
130
|
+
delete file[skillName];
|
|
131
|
+
writeFile(file);
|
|
132
|
+
}
|
|
133
|
+
/** Toggle the enabled flag. Skill stays in the registry; just won't
|
|
134
|
+
* fire while disabled. Caller-side convenience to avoid re-passing
|
|
135
|
+
* schedule + agentSlug just to flip the boolean. */
|
|
136
|
+
export function enableSchedule(skillName, enabled) {
|
|
137
|
+
const file = readFile();
|
|
138
|
+
const entry = file[skillName];
|
|
139
|
+
if (!entry)
|
|
140
|
+
return null;
|
|
141
|
+
entry.enabled = enabled;
|
|
142
|
+
entry.lastModifiedAt = new Date().toISOString();
|
|
143
|
+
file[skillName] = entry;
|
|
144
|
+
writeFile(file);
|
|
145
|
+
return { skillName, ...entry };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=schedule-registry.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill templates — 1.18.130 (Phase 2 / Skill Builder).
|
|
3
|
+
*
|
|
4
|
+
* Each template scaffolds a SKILL.md frontmatter + body skeleton plus
|
|
5
|
+
* (optionally) starter bundled files. Templates encode the five common
|
|
6
|
+
* skill archetypes the user pulled out of the Anthropic skills video:
|
|
7
|
+
*
|
|
8
|
+
* - **Orchestrator** — the meta-skill that routes work to other skills
|
|
9
|
+
* - **Scraper / Poller** — read external data, surface what's new
|
|
10
|
+
* - **Transformer** — mutate input → produce output (no side effects)
|
|
11
|
+
* - **Notifier** — send a message to Discord/Slack/email when X happens
|
|
12
|
+
* - **Conversational** — interactive multi-turn agent for one workflow
|
|
13
|
+
*
|
|
14
|
+
* The Builder asks the user to pick one when creating a new skill;
|
|
15
|
+
* the chosen template defines the starting body + suggested tools.allow.
|
|
16
|
+
* Authors edit from there. Templates aren't a runtime concept — they're
|
|
17
|
+
* just initial content the writeSkill() helper persists like any other
|
|
18
|
+
* skill.
|
|
19
|
+
*/
|
|
20
|
+
export interface SkillTemplate {
|
|
21
|
+
/** Stable id used by the picker. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Display name shown in the picker. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** One-line "use when" hint for the picker subtitle. */
|
|
26
|
+
hint: string;
|
|
27
|
+
/** Emoji shown next to the label — gives the picker visual texture. */
|
|
28
|
+
emoji: string;
|
|
29
|
+
/** Initial frontmatter description filled into the create modal.
|
|
30
|
+
* User can override before save; serves as a writing prompt. */
|
|
31
|
+
defaultDescription: string;
|
|
32
|
+
/** Initial Markdown body. Should follow the Anthropic procedure
|
|
33
|
+
* shape (numbered steps, clear inputs/outputs section) so authors
|
|
34
|
+
* start from a good pattern instead of a blank page. */
|
|
35
|
+
body: string;
|
|
36
|
+
/** Suggested clementine.tools.allow allowlist. The Builder pre-fills
|
|
37
|
+
* the tools chip list so authors see "this archetype usually needs
|
|
38
|
+
* Read + Bash + memory_write" right away. */
|
|
39
|
+
suggestedTools: string[];
|
|
40
|
+
/** Optional bundled files to drop alongside SKILL.md. Each entry is
|
|
41
|
+
* written via the same writeSkill folder as the entry-point file.
|
|
42
|
+
* Example: an Orchestrator template ships a templates/output.md
|
|
43
|
+
* scaffold; a Scraper ships a scripts/fetch.py stub. */
|
|
44
|
+
bundledFiles?: Array<{
|
|
45
|
+
relPath: string;
|
|
46
|
+
content: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export declare const SKILL_TEMPLATES: SkillTemplate[];
|
|
50
|
+
/** Lookup by id. */
|
|
51
|
+
export declare function getSkillTemplate(id: string): SkillTemplate | null;
|
|
52
|
+
/** Apply a template to a skill name — substitutes \`{{TITLE}}\` placeholders
|
|
53
|
+
* in the body with the user's display title. */
|
|
54
|
+
export declare function renderTemplateBody(template: SkillTemplate, displayTitle: string): string;
|
|
55
|
+
//# sourceMappingURL=skill-templates.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill templates — 1.18.130 (Phase 2 / Skill Builder).
|
|
3
|
+
*
|
|
4
|
+
* Each template scaffolds a SKILL.md frontmatter + body skeleton plus
|
|
5
|
+
* (optionally) starter bundled files. Templates encode the five common
|
|
6
|
+
* skill archetypes the user pulled out of the Anthropic skills video:
|
|
7
|
+
*
|
|
8
|
+
* - **Orchestrator** — the meta-skill that routes work to other skills
|
|
9
|
+
* - **Scraper / Poller** — read external data, surface what's new
|
|
10
|
+
* - **Transformer** — mutate input → produce output (no side effects)
|
|
11
|
+
* - **Notifier** — send a message to Discord/Slack/email when X happens
|
|
12
|
+
* - **Conversational** — interactive multi-turn agent for one workflow
|
|
13
|
+
*
|
|
14
|
+
* The Builder asks the user to pick one when creating a new skill;
|
|
15
|
+
* the chosen template defines the starting body + suggested tools.allow.
|
|
16
|
+
* Authors edit from there. Templates aren't a runtime concept — they're
|
|
17
|
+
* just initial content the writeSkill() helper persists like any other
|
|
18
|
+
* skill.
|
|
19
|
+
*/
|
|
20
|
+
export const SKILL_TEMPLATES = [
|
|
21
|
+
{
|
|
22
|
+
id: 'orchestrator',
|
|
23
|
+
label: 'Orchestrator',
|
|
24
|
+
hint: 'Routes work to other skills based on conditions. The "meta-skill" pattern from the Anthropic agent skills video.',
|
|
25
|
+
emoji: '🎯',
|
|
26
|
+
defaultDescription: 'Route incoming work through a sequence of decisions. For each item, pick the right downstream skill based on the conditions in the body.',
|
|
27
|
+
suggestedTools: ['Agent', 'Read'],
|
|
28
|
+
body: `# {{TITLE}}
|
|
29
|
+
|
|
30
|
+
This skill is an **orchestrator** — it doesn't do the work itself, it routes work to other skills based on conditions.
|
|
31
|
+
|
|
32
|
+
## Procedure
|
|
33
|
+
|
|
34
|
+
1. **Gather inputs.** State what you need to know to make routing decisions (e.g., query a data source, read state, check a flag).
|
|
35
|
+
|
|
36
|
+
2. **For each item, decide the path:**
|
|
37
|
+
- If <condition A> → invoke the **<skill-name-a>** skill with \`{key: value}\`
|
|
38
|
+
- If <condition B> → invoke the **<skill-name-b>** skill with \`{key: value}\`
|
|
39
|
+
- Otherwise → log to the daily note as "no match"
|
|
40
|
+
|
|
41
|
+
3. **Summarize.** Send a short Discord summary: how many items, which skills fired, any errors.
|
|
42
|
+
|
|
43
|
+
## Sub-skills referenced
|
|
44
|
+
|
|
45
|
+
- \`<skill-name-a>\` — describe what it does
|
|
46
|
+
- \`<skill-name-b>\` — describe what it does
|
|
47
|
+
|
|
48
|
+
## Tips for writing orchestrators
|
|
49
|
+
|
|
50
|
+
- Keep the routing logic in this body. Sub-skills should be pure functions that don't know they're being called from here.
|
|
51
|
+
- Pin every sub-skill to the same scheduled task so their bodies are loaded into context together.
|
|
52
|
+
- The Agent tool is what dispatches sub-skills — keep it in your tools.allow.
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'scraper',
|
|
57
|
+
label: 'Scraper / Poller',
|
|
58
|
+
hint: 'Read an external source, surface what is new since the last run.',
|
|
59
|
+
emoji: '🔍',
|
|
60
|
+
defaultDescription: 'Poll an external source on a schedule and surface what is new since the last run. Tracks state via the cron progress mechanism so it does not re-process old items.',
|
|
61
|
+
suggestedTools: ['Read', 'WebFetch', 'cron_progress_read', 'cron_progress_write'],
|
|
62
|
+
body: `# {{TITLE}}
|
|
63
|
+
|
|
64
|
+
Read an external data source on a schedule, surface only what's changed since the last run.
|
|
65
|
+
|
|
66
|
+
## Procedure
|
|
67
|
+
|
|
68
|
+
1. **Read state.** \`cron_progress_read\` to get \`{processed_ids: [...]}\` (default \`[]\`).
|
|
69
|
+
|
|
70
|
+
2. **Query the source.** Describe exactly what you fetch (URL, MCP tool call, file path).
|
|
71
|
+
|
|
72
|
+
3. **Filter to new items.** Compare each item's id against \`processed_ids\`. Skip ones already seen.
|
|
73
|
+
|
|
74
|
+
4. **For each new item:**
|
|
75
|
+
- <do the thing>
|
|
76
|
+
|
|
77
|
+
5. **Persist state.** \`cron_progress_write({processed_ids: [...]})\` with the union of old + new ids.
|
|
78
|
+
|
|
79
|
+
6. **Output.** If nothing was new, output \`__NOTHING__\`. Otherwise summarize what was processed.
|
|
80
|
+
|
|
81
|
+
## Inputs
|
|
82
|
+
|
|
83
|
+
- (declare any \`clementine.inputs\` parameters here so the agent reads them as inputs)
|
|
84
|
+
`,
|
|
85
|
+
bundledFiles: [
|
|
86
|
+
{
|
|
87
|
+
relPath: 'references/state-shape.md',
|
|
88
|
+
content: `# State shape\\n\\nThis skill reads/writes its state via \`cron_progress_*\`. Document the shape here so future-you remembers what each key means.\\n\\n\\\`\\\`\\\`json\\n{\\n "processed_ids": ["id1", "id2"],\\n "last_run_summary": "..."\\n}\\n\\\`\\\`\\\`\\n`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'transformer',
|
|
94
|
+
label: 'Transformer',
|
|
95
|
+
hint: 'Take input → produce output. Pure function, no side effects.',
|
|
96
|
+
emoji: '⚙',
|
|
97
|
+
defaultDescription: 'Pure transformation: receives input, produces output. No external side effects. Useful as a sub-skill called by an orchestrator.',
|
|
98
|
+
suggestedTools: ['Read', 'Write'],
|
|
99
|
+
body: `# {{TITLE}}
|
|
100
|
+
|
|
101
|
+
Transform input into output. No side effects — safe to call from any context.
|
|
102
|
+
|
|
103
|
+
## Inputs
|
|
104
|
+
|
|
105
|
+
Declare in \`clementine.inputs\`:
|
|
106
|
+
|
|
107
|
+
\\\`\\\`\\\`yaml
|
|
108
|
+
clementine:
|
|
109
|
+
inputs:
|
|
110
|
+
source_text:
|
|
111
|
+
type: string
|
|
112
|
+
description: The raw text to transform
|
|
113
|
+
required: true
|
|
114
|
+
style:
|
|
115
|
+
type: string
|
|
116
|
+
enum: [casual, formal, urgent]
|
|
117
|
+
default: casual
|
|
118
|
+
\\\`\\\`\\\`
|
|
119
|
+
|
|
120
|
+
## Procedure
|
|
121
|
+
|
|
122
|
+
1. **Read inputs.** All inputs are interpolated as \`{{ input_name }}\`.
|
|
123
|
+
|
|
124
|
+
2. **Transform.** Describe the transformation step by step.
|
|
125
|
+
|
|
126
|
+
3. **Output.** Return the transformed text only — no commentary, no narration.
|
|
127
|
+
|
|
128
|
+
## Output shape
|
|
129
|
+
|
|
130
|
+
\\\`\\\`\\\`
|
|
131
|
+
<one-line summary>
|
|
132
|
+
|
|
133
|
+
<transformed body>
|
|
134
|
+
\\\`\\\`\\\`
|
|
135
|
+
`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'notifier',
|
|
139
|
+
label: 'Notifier',
|
|
140
|
+
hint: 'Send a message somewhere when a condition fires.',
|
|
141
|
+
emoji: '📣',
|
|
142
|
+
defaultDescription: 'Send a message to Discord/Slack/email/SMS when a condition is met. Composes well with a Scraper that detects "something changed."',
|
|
143
|
+
suggestedTools: ['Read', 'discord_channel_send', 'slack_post_message'],
|
|
144
|
+
body: `# {{TITLE}}
|
|
145
|
+
|
|
146
|
+
Notify a destination when a condition fires.
|
|
147
|
+
|
|
148
|
+
## Procedure
|
|
149
|
+
|
|
150
|
+
1. **Check the condition.** Describe what triggers the notification (e.g., new audit row, daily digest ready, alert threshold crossed).
|
|
151
|
+
|
|
152
|
+
2. **Build the message.** Keep it short. Include:
|
|
153
|
+
- What happened
|
|
154
|
+
- Why it matters (one line)
|
|
155
|
+
- Action the recipient should take, if any
|
|
156
|
+
|
|
157
|
+
3. **Send via the right channel:**
|
|
158
|
+
- Use \`discord_channel_send\` for the team / yourself
|
|
159
|
+
- Use \`slack_post_message\` if the recipient prefers Slack
|
|
160
|
+
- Use \`mcp__gmail__send\` for external recipients
|
|
161
|
+
|
|
162
|
+
4. **Confirm delivery.** Output a one-liner like "Sent to #ops-alerts at 09:14."
|
|
163
|
+
|
|
164
|
+
## Inputs
|
|
165
|
+
|
|
166
|
+
\\\`\\\`\\\`yaml
|
|
167
|
+
clementine:
|
|
168
|
+
inputs:
|
|
169
|
+
destination:
|
|
170
|
+
type: string
|
|
171
|
+
enum: [discord-ops, slack-team, email-owner]
|
|
172
|
+
default: discord-ops
|
|
173
|
+
severity:
|
|
174
|
+
type: string
|
|
175
|
+
enum: [info, warning, urgent]
|
|
176
|
+
default: info
|
|
177
|
+
\\\`\\\`\\\`
|
|
178
|
+
`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'conversational',
|
|
182
|
+
label: 'Conversational',
|
|
183
|
+
hint: 'Multi-turn interactive agent for one specific workflow.',
|
|
184
|
+
emoji: '💬',
|
|
185
|
+
defaultDescription: 'Multi-turn conversational agent for a specific workflow (e.g., onboarding a new client, debugging a system). Maintains a focused context across the conversation.',
|
|
186
|
+
suggestedTools: ['Read', 'memory_read', 'memory_write', 'note_create'],
|
|
187
|
+
body: `# {{TITLE}}
|
|
188
|
+
|
|
189
|
+
Run a focused multi-turn conversation. The user comes to you with a specific goal; you guide them step by step until it's done.
|
|
190
|
+
|
|
191
|
+
## Procedure
|
|
192
|
+
|
|
193
|
+
1. **Open with intent confirmation.** "Sounds like you want to do X. Is that right?" Don't start tools until they confirm.
|
|
194
|
+
|
|
195
|
+
2. **Gather what you need.** Ask one question at a time, not a wall.
|
|
196
|
+
|
|
197
|
+
3. **Use tools sparingly.** Each tool call should be obvious and explainable in the next turn.
|
|
198
|
+
|
|
199
|
+
4. **Save what you learn.** Use \`memory_write\` for facts that should persist beyond this conversation.
|
|
200
|
+
|
|
201
|
+
5. **Close cleanly.** When done, summarize what was accomplished + any next steps for the user.
|
|
202
|
+
|
|
203
|
+
## Conversation principles
|
|
204
|
+
|
|
205
|
+
- Match the user's tone (casual / formal / urgent — read their first message)
|
|
206
|
+
- Never start a multi-step process without checking in first
|
|
207
|
+
- If the user changes direction mid-conversation, acknowledge + pivot — don't pretend they asked for what you started
|
|
208
|
+
`,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
/** Lookup by id. */
|
|
212
|
+
export function getSkillTemplate(id) {
|
|
213
|
+
return SKILL_TEMPLATES.find((t) => t.id === id) ?? null;
|
|
214
|
+
}
|
|
215
|
+
/** Apply a template to a skill name — substitutes \`{{TITLE}}\` placeholders
|
|
216
|
+
* in the body with the user's display title. */
|
|
217
|
+
export function renderTemplateBody(template, displayTitle) {
|
|
218
|
+
return template.body.replace(/\{\{TITLE\}\}/g, displayTitle);
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=skill-templates.js.map
|
package/dist/cli/cron.js
CHANGED
|
@@ -62,6 +62,36 @@ export async function cmdCronRun(jobName) {
|
|
|
62
62
|
process.env.CLEMENTINE_HOME = BASE_DIR;
|
|
63
63
|
const jobs = parseCronJobs();
|
|
64
64
|
let job = jobs.find((j) => j.name === jobName);
|
|
65
|
+
// 1.18.129 — Run-now fallback for skills that aren't on a schedule.
|
|
66
|
+
// The dashboard's "Run now" button on a skill detail pane calls this
|
|
67
|
+
// endpoint with the skill name. If the skill has no registry entry,
|
|
68
|
+
// synthesize a transient CronJobDefinition so the runtime can fire
|
|
69
|
+
// it once. The skill body becomes the prompt via buildSkillContext,
|
|
70
|
+
// same as scheduled-skill jobs.
|
|
71
|
+
if (!job) {
|
|
72
|
+
try {
|
|
73
|
+
const { getSkill } = await import('../agent/skill-store.js');
|
|
74
|
+
const skill = getSkill(jobName);
|
|
75
|
+
if (skill) {
|
|
76
|
+
const ext = (skill.frontmatter.clementine ?? {});
|
|
77
|
+
job = {
|
|
78
|
+
name: skill.frontmatter.name,
|
|
79
|
+
schedule: '0 0 1 1 *', // dummy — never auto-fires; run-now bypasses scheduling
|
|
80
|
+
prompt: '', // buildSkillContext injects the skill body
|
|
81
|
+
tier: 1,
|
|
82
|
+
enabled: true,
|
|
83
|
+
skills: [skill.frontmatter.name],
|
|
84
|
+
agentSlug: ext.agentSlug ?? undefined,
|
|
85
|
+
predictable: true,
|
|
86
|
+
source: 'scheduled-skill',
|
|
87
|
+
};
|
|
88
|
+
console.log(`(running skill "${jobName}" on demand — not yet scheduled)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Fall through to error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
65
95
|
if (!job) {
|
|
66
96
|
console.error(`Job not found: ${jobName}`);
|
|
67
97
|
console.error(`Available jobs: ${jobs.map((j) => j.name).join(', ') || '(none)'}`);
|