clementine-agent 1.18.131 → 1.18.132
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/channels/discord.js +95 -0
- package/dist/cli/dashboard.js +251 -2
- package/dist/gateway/heartbeat-scheduler.js +20 -1
- package/package.json +1 -1
package/dist/channels/discord.js
CHANGED
|
@@ -34,6 +34,14 @@ const slashCommands = [
|
|
|
34
34
|
.addChoices({ name: 'List jobs', value: 'list' }, { name: 'Run a job', value: 'run' }, { name: 'Enable a job', value: 'enable' }, { name: 'Disable a job', value: 'disable' }))
|
|
35
35
|
.addStringOption(o => o.setName('job').setDescription('Job name (for run/enable/disable)').setAutocomplete(true)),
|
|
36
36
|
new SlashCommandBuilder().setName('heartbeat').setDescription('Run heartbeat check manually'),
|
|
37
|
+
// 1.18.132 — Phase 3: /skill is the on-demand sibling to scheduled
|
|
38
|
+
// skills. List the catalog or fire any skill once from anywhere
|
|
39
|
+
// Discord can reach. Uses cmdCronRun's catalog fallback (1.18.129)
|
|
40
|
+
// so unscheduled skills work too.
|
|
41
|
+
new SlashCommandBuilder().setName('skill').setDescription('List or run a skill on demand')
|
|
42
|
+
.addStringOption(o => o.setName('action').setDescription('Action').setRequired(true)
|
|
43
|
+
.addChoices({ name: 'List skills', value: 'list' }, { name: 'Run a skill', value: 'run' }))
|
|
44
|
+
.addStringOption(o => o.setName('name').setDescription('Skill name (for run)').setAutocomplete(true)),
|
|
37
45
|
new SlashCommandBuilder().setName('tools').setDescription('List available MCP tools'),
|
|
38
46
|
new SlashCommandBuilder().setName('toolset').setDescription('Set this chat tool mode')
|
|
39
47
|
.addStringOption(o => o.setName('mode').setDescription('Tool mode').setRequired(true)
|
|
@@ -1190,6 +1198,23 @@ export async function startDiscord(gateway, heartbeat, cronScheduler, dispatcher
|
|
|
1190
1198
|
.slice(0, 25);
|
|
1191
1199
|
await interaction.respond(filtered.map(name => ({ name, value: name })));
|
|
1192
1200
|
}
|
|
1201
|
+
else if (interaction.commandName === 'skill') {
|
|
1202
|
+
// 1.18.132 — autocomplete from the live skill catalog (every
|
|
1203
|
+
// skill, scheduled or not). cmdCronRun handles either case.
|
|
1204
|
+
const focused = interaction.options.getFocused().toLowerCase();
|
|
1205
|
+
try {
|
|
1206
|
+
const { listSkills } = await import('../agent/skill-store.js');
|
|
1207
|
+
const skills = listSkills();
|
|
1208
|
+
const filtered = skills
|
|
1209
|
+
.map(s => s.frontmatter.name)
|
|
1210
|
+
.filter(name => name.toLowerCase().includes(focused))
|
|
1211
|
+
.slice(0, 25);
|
|
1212
|
+
await interaction.respond(filtered.map(name => ({ name, value: name })));
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
await interaction.respond([]);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1193
1218
|
return;
|
|
1194
1219
|
}
|
|
1195
1220
|
// ── Slash commands ───────────────────────────────────────────────
|
|
@@ -1373,6 +1398,76 @@ export async function startDiscord(gateway, heartbeat, cronScheduler, dispatcher
|
|
|
1373
1398
|
gateway.injectContext(sessionKey, `!cron run ${jobName}`, response);
|
|
1374
1399
|
return;
|
|
1375
1400
|
}
|
|
1401
|
+
// 1.18.132 — Phase 3: /skill list / /skill run <name>
|
|
1402
|
+
// List shows the full catalog (folder + flat) with descriptions.
|
|
1403
|
+
// Run fires the named skill via the same cronScheduler.runManual
|
|
1404
|
+
// path; cmdCronRun's catalog fallback (1.18.129) means unscheduled
|
|
1405
|
+
// skills still fire correctly. Output streams back to Discord.
|
|
1406
|
+
if (name === 'skill') {
|
|
1407
|
+
const action = cmd.options.getString('action', true);
|
|
1408
|
+
const skillName = cmd.options.getString('name') ?? '';
|
|
1409
|
+
if (action === 'list') {
|
|
1410
|
+
try {
|
|
1411
|
+
const { listSkills } = await import('../agent/skill-store.js');
|
|
1412
|
+
const skills = listSkills();
|
|
1413
|
+
if (skills.length === 0) {
|
|
1414
|
+
await cmd.reply('No skills found yet. Use the dashboard Skill Builder or `mcp__clementine-tools__create_skill` to author one.');
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const lines = skills.map(s => {
|
|
1418
|
+
const fm = s.frontmatter;
|
|
1419
|
+
const ext = fm.clementine ?? {};
|
|
1420
|
+
const useCount = typeof ext.useCount === 'number' ? ext.useCount : 0;
|
|
1421
|
+
const desc = (fm.description || '').slice(0, 100) + ((fm.description || '').length > 100 ? '…' : '');
|
|
1422
|
+
return `• \`${fm.name}\` — ${desc}${useCount > 0 ? ` (${useCount}×)` : ''}`;
|
|
1423
|
+
});
|
|
1424
|
+
const header = `**${skills.length} skill${skills.length === 1 ? '' : 's'} available** — fire any with \`/skill run <name>\`:\n\n`;
|
|
1425
|
+
const chunks = chunkText(header + lines.join('\n'), 1900);
|
|
1426
|
+
await cmd.reply(chunks[0]);
|
|
1427
|
+
for (let i = 1; i < chunks.length; i++)
|
|
1428
|
+
await cmd.followUp(chunks[i]);
|
|
1429
|
+
}
|
|
1430
|
+
catch (err) {
|
|
1431
|
+
await cmd.reply(`Failed to list skills: ${err}`);
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (action === 'run') {
|
|
1436
|
+
if (!skillName) {
|
|
1437
|
+
await cmd.reply('Specify a skill name. Try `/skill list` to see options.');
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
const { getSkill } = await import('../agent/skill-store.js');
|
|
1442
|
+
const skill = getSkill(skillName);
|
|
1443
|
+
if (!skill) {
|
|
1444
|
+
await cmd.reply(`Skill '${skillName}' not found. Try \`/skill list\`.`);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
// Re-uses the cron run pipeline. cmdCronRun (1.18.129) falls
|
|
1448
|
+
// back to the skill catalog when the name isn't a registered
|
|
1449
|
+
// cron job, so this works for both scheduled + unscheduled
|
|
1450
|
+
// skills.
|
|
1451
|
+
if (cronScheduler.isJobRunning(skillName)) {
|
|
1452
|
+
await cmd.reply(`Skill '${skillName}' is already running.`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
await cmd.deferReply();
|
|
1456
|
+
const response = await cronScheduler.runManual(skillName);
|
|
1457
|
+
const chunks = chunkText(response || `*(skill '${skillName}' completed — no output)*`, 1900);
|
|
1458
|
+
await cmd.editReply(chunks[0]);
|
|
1459
|
+
for (let i = 1; i < chunks.length; i++)
|
|
1460
|
+
await cmd.followUp(chunks[i]);
|
|
1461
|
+
gateway.injectContext(sessionKey, `!skill run ${skillName}`, response);
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
await cmd.followUp({ content: `Skill run failed: ${err}` }).catch(() => { });
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
await cmd.reply('Unknown action. Try `/skill list` or `/skill run <name>`.');
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1376
1471
|
// Workflow command
|
|
1377
1472
|
if (name === 'workflow') {
|
|
1378
1473
|
const action = cmd.options.getString('action', true);
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5066,6 +5066,189 @@ export async function cmdDashboard(opts) {
|
|
|
5066
5066
|
res.status(500).json({ ok: false, error: String(err) });
|
|
5067
5067
|
}
|
|
5068
5068
|
});
|
|
5069
|
+
// 1.18.132 — Phase 3: convert a legacy cron to a scheduled-skill
|
|
5070
|
+
// registry entry. Distinct from /api/cron/:job/migrate (which keeps
|
|
5071
|
+
// the cron in CRON.md but cleans it up). This endpoint REMOVES the
|
|
5072
|
+
// entry from CRON.md and writes it into ~/.clementine/schedules.json
|
|
5073
|
+
// — the Anthropic-pure target shape.
|
|
5074
|
+
//
|
|
5075
|
+
// Eligibility: the cron must already pin a skill OR have a matchable
|
|
5076
|
+
// skill name. We refuse to migrate a cron with no skill to fall back
|
|
5077
|
+
// on, because the schedules.json shape only references {skillName →
|
|
5078
|
+
// schedule}; it has no place for the cron's own prompt body. Users
|
|
5079
|
+
// with custom-prompt crons can either (a) create a skill from the
|
|
5080
|
+
// prompt first via the Builder, then re-run this, or (b) keep the
|
|
5081
|
+
// legacy cron format.
|
|
5082
|
+
app.post('/api/cron/:job/migrate-to-skill', async (req, res) => {
|
|
5083
|
+
try {
|
|
5084
|
+
const jobName = req.params.job;
|
|
5085
|
+
if (!jobName) {
|
|
5086
|
+
res.status(400).json({ ok: false, error: 'job name required' });
|
|
5087
|
+
return;
|
|
5088
|
+
}
|
|
5089
|
+
const { findMatchingSkill } = await import('../agent/cron-migrator.js');
|
|
5090
|
+
const { listSkills } = await import('../agent/skill-store.js');
|
|
5091
|
+
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5092
|
+
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5093
|
+
const { cronFile, bareJobName } = resolveJobCronFile(jobName);
|
|
5094
|
+
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5095
|
+
const target = allJobs.find(j => String(j.name).toLowerCase() === jobName.toLowerCase());
|
|
5096
|
+
if (!target)
|
|
5097
|
+
return res.status(404).json({ ok: false, error: `job "${jobName}" not found` });
|
|
5098
|
+
// Only migrate true legacy CRON.md jobs — scheduled skills aren't migration targets.
|
|
5099
|
+
if (target.source === 'scheduled-skill') {
|
|
5100
|
+
return res.status(400).json({ ok: false, error: 'already a scheduled skill' });
|
|
5101
|
+
}
|
|
5102
|
+
// Find the skill to bind. Prefer an explicit pin; fall back to
|
|
5103
|
+
// findMatchingSkill which uses prompt + name keyword overlap.
|
|
5104
|
+
let skillName = null;
|
|
5105
|
+
const skills = listSkills();
|
|
5106
|
+
if (target.skills && target.skills.length > 0) {
|
|
5107
|
+
// Use the first pinned skill that actually exists in the catalog.
|
|
5108
|
+
skillName = target.skills.find(s => skills.some(sk => sk.frontmatter.name === s)) ?? target.skills[0];
|
|
5109
|
+
}
|
|
5110
|
+
else {
|
|
5111
|
+
const m = findMatchingSkill(target, skills);
|
|
5112
|
+
if (m)
|
|
5113
|
+
skillName = m.frontmatter.name;
|
|
5114
|
+
}
|
|
5115
|
+
if (!skillName) {
|
|
5116
|
+
return res.status(400).json({
|
|
5117
|
+
ok: false,
|
|
5118
|
+
error: 'No skill match. Create a skill from this prompt in the Builder first, then retry.',
|
|
5119
|
+
});
|
|
5120
|
+
}
|
|
5121
|
+
// Sanity: a skill of this name must exist in the catalog (otherwise
|
|
5122
|
+
// the runtime will treat it as a missing pin).
|
|
5123
|
+
if (!skills.some(s => s.frontmatter.name === skillName)) {
|
|
5124
|
+
return res.status(400).json({ ok: false, error: `Pinned skill "${skillName}" not in catalog. Create it first.` });
|
|
5125
|
+
}
|
|
5126
|
+
// Write to schedules.json. Carry over agentSlug + enabled + schedule.
|
|
5127
|
+
const entry = setSchedule(skillName, {
|
|
5128
|
+
schedule: target.schedule,
|
|
5129
|
+
enabled: target.enabled !== false,
|
|
5130
|
+
agentSlug: target.agentSlug ?? null,
|
|
5131
|
+
});
|
|
5132
|
+
// Remove from CRON.md by bare name.
|
|
5133
|
+
const bakPath = cronFile + '.bak';
|
|
5134
|
+
try {
|
|
5135
|
+
writeFileSync(bakPath, readFileSync(cronFile, 'utf-8'));
|
|
5136
|
+
}
|
|
5137
|
+
catch { /* best-effort */ }
|
|
5138
|
+
const { parsed, jobs } = readCronFileAt(cronFile);
|
|
5139
|
+
const idx = jobs.findIndex(j => String(j.name).toLowerCase() === bareJobName.toLowerCase());
|
|
5140
|
+
if (idx < 0) {
|
|
5141
|
+
return res.status(404).json({ ok: false, error: `job not in CRON.md (looked for "${bareJobName}")` });
|
|
5142
|
+
}
|
|
5143
|
+
const removed = jobs.splice(idx, 1)[0];
|
|
5144
|
+
writeCronFileAt(cronFile, parsed, jobs);
|
|
5145
|
+
// Hot-reload + broadcast so the Tasks page updates without manual refresh.
|
|
5146
|
+
try {
|
|
5147
|
+
const gw = await getGateway();
|
|
5148
|
+
const sched = gw.cronScheduler;
|
|
5149
|
+
if (sched && typeof sched.reloadJobs === 'function')
|
|
5150
|
+
sched.reloadJobs();
|
|
5151
|
+
}
|
|
5152
|
+
catch { /* best-effort */ }
|
|
5153
|
+
try {
|
|
5154
|
+
broadcastEvent({ type: 'cron_deleted', data: { job: jobName, source: 'cron-md', migrated: true } });
|
|
5155
|
+
}
|
|
5156
|
+
catch { /* non-fatal */ }
|
|
5157
|
+
res.json({
|
|
5158
|
+
ok: true,
|
|
5159
|
+
skillName,
|
|
5160
|
+
scheduleEntry: entry,
|
|
5161
|
+
removedFrom: cronFile,
|
|
5162
|
+
bakPath,
|
|
5163
|
+
removedBareName: removed?.name,
|
|
5164
|
+
message: `Converted "${jobName}" → scheduled skill "${skillName}". CRON.md backup at ${bakPath}.`,
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
catch (err) {
|
|
5168
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
5169
|
+
}
|
|
5170
|
+
});
|
|
5171
|
+
app.post('/api/cron/migrate-all-to-skills', async (_req, res) => {
|
|
5172
|
+
try {
|
|
5173
|
+
const { findMatchingSkill } = await import('../agent/cron-migrator.js');
|
|
5174
|
+
const { listSkills } = await import('../agent/skill-store.js');
|
|
5175
|
+
const { setSchedule } = await import('../agent/schedule-registry.js');
|
|
5176
|
+
const { parseCronJobs, parseAgentCronJobs } = await import('../gateway/cron-scheduler.js');
|
|
5177
|
+
const skills = listSkills();
|
|
5178
|
+
const allJobs = [...parseCronJobs(), ...parseAgentCronJobs(path.join(VAULT_DIR, '00-System', 'agents'))];
|
|
5179
|
+
const legacy = allJobs.filter(j => j.source !== 'scheduled-skill');
|
|
5180
|
+
const migrated = [];
|
|
5181
|
+
const skipped = [];
|
|
5182
|
+
// Group by source CRON.md so we open + parse + write each file once.
|
|
5183
|
+
const byFile = new Map();
|
|
5184
|
+
for (const job of legacy) {
|
|
5185
|
+
let skillName = null;
|
|
5186
|
+
if (job.skills && job.skills.length > 0) {
|
|
5187
|
+
skillName = job.skills.find(s => skills.some(sk => sk.frontmatter.name === s)) ?? null;
|
|
5188
|
+
}
|
|
5189
|
+
if (!skillName) {
|
|
5190
|
+
const m = findMatchingSkill(job, skills);
|
|
5191
|
+
if (m)
|
|
5192
|
+
skillName = m.frontmatter.name;
|
|
5193
|
+
}
|
|
5194
|
+
if (!skillName) {
|
|
5195
|
+
skipped.push({ name: job.name, reason: 'no skill match' });
|
|
5196
|
+
continue;
|
|
5197
|
+
}
|
|
5198
|
+
if (!skills.some(s => s.frontmatter.name === skillName)) {
|
|
5199
|
+
skipped.push({ name: job.name, reason: `skill "${skillName}" not in catalog` });
|
|
5200
|
+
continue;
|
|
5201
|
+
}
|
|
5202
|
+
try {
|
|
5203
|
+
setSchedule(skillName, {
|
|
5204
|
+
schedule: job.schedule,
|
|
5205
|
+
enabled: job.enabled !== false,
|
|
5206
|
+
agentSlug: job.agentSlug ?? null,
|
|
5207
|
+
});
|
|
5208
|
+
migrated.push({ name: job.name, skillName });
|
|
5209
|
+
const { cronFile, bareJobName } = resolveJobCronFile(job.name);
|
|
5210
|
+
if (!byFile.has(cronFile))
|
|
5211
|
+
byFile.set(cronFile, { jobsToRemove: new Set(), cronFile });
|
|
5212
|
+
byFile.get(cronFile).jobsToRemove.add(bareJobName);
|
|
5213
|
+
}
|
|
5214
|
+
catch (err) {
|
|
5215
|
+
skipped.push({ name: job.name, reason: String(err) });
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
const touchedFiles = [];
|
|
5219
|
+
for (const { cronFile, jobsToRemove } of byFile.values()) {
|
|
5220
|
+
try {
|
|
5221
|
+
const bakPath = cronFile + '.bak';
|
|
5222
|
+
try {
|
|
5223
|
+
writeFileSync(bakPath, readFileSync(cronFile, 'utf-8'));
|
|
5224
|
+
}
|
|
5225
|
+
catch { /* best-effort */ }
|
|
5226
|
+
const { parsed, jobs } = readCronFileAt(cronFile);
|
|
5227
|
+
const filtered = jobs.filter(j => !jobsToRemove.has(String(j.name)));
|
|
5228
|
+
writeCronFileAt(cronFile, parsed, filtered);
|
|
5229
|
+
touchedFiles.push(cronFile);
|
|
5230
|
+
}
|
|
5231
|
+
catch (err) {
|
|
5232
|
+
skipped.push({ name: '(file write)', reason: `${cronFile}: ${err}` });
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
try {
|
|
5236
|
+
const gw = await getGateway();
|
|
5237
|
+
const sched = gw.cronScheduler;
|
|
5238
|
+
if (sched && typeof sched.reloadJobs === 'function')
|
|
5239
|
+
sched.reloadJobs();
|
|
5240
|
+
}
|
|
5241
|
+
catch { /* best-effort */ }
|
|
5242
|
+
try {
|
|
5243
|
+
broadcastEvent({ type: 'cron_migrated_to_skills', data: { migratedCount: migrated.length } });
|
|
5244
|
+
}
|
|
5245
|
+
catch { /* non-fatal */ }
|
|
5246
|
+
res.json({ ok: true, migrated, skipped, touchedFiles });
|
|
5247
|
+
}
|
|
5248
|
+
catch (err) {
|
|
5249
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
5250
|
+
}
|
|
5251
|
+
});
|
|
5069
5252
|
app.post('/api/cron/migrate-all', async (_req, res) => {
|
|
5070
5253
|
try {
|
|
5071
5254
|
const { migrateAllEligibleJobs } = await import('../agent/cron-migrator.js');
|
|
@@ -25276,10 +25459,53 @@ function renderScheduledTaskCard(task) {
|
|
|
25276
25459
|
+ '<button class="btn-sm secondary" data-trace-job="' + esc(task.name) + '" title="View execution trace">Trace</button>'
|
|
25277
25460
|
+ (defObj.source === 'scheduled-skill'
|
|
25278
25461
|
? '<button class="btn-sm secondary btn-danger" onclick="unscheduleSkillFromCard(\\x27' + safeName + '\\x27)" title="Remove the schedule (skill stays)">Unschedule</button>'
|
|
25279
|
-
: '<button class="btn-sm secondary
|
|
25462
|
+
: '<button class="btn-sm secondary" onclick="migrateCronToSkillFromCard(\\x27' + safeName + '\\x27)" title="Convert this legacy cron to a scheduled skill — Anthropic-pure format">→ Skill</button>'
|
|
25463
|
+
+ '<button class="btn-sm secondary btn-danger" onclick="confirmDeleteCron(\\x27' + safeName + '\\x27)" title="Delete task">Del</button>')
|
|
25280
25464
|
+ '</div></div>';
|
|
25281
25465
|
}
|
|
25282
25466
|
|
|
25467
|
+
// 1.18.132 — Phase 3 migrator: convert one legacy cron → scheduled skill.
|
|
25468
|
+
// Calls /api/cron/:job/migrate-to-skill which writes schedules.json and
|
|
25469
|
+
// removes the entry from CRON.md (with a .bak backup).
|
|
25470
|
+
async function migrateCronToSkillFromCard(jobName) {
|
|
25471
|
+
if (!confirm('Convert "' + jobName + '" to a scheduled skill?\\n\\n' +
|
|
25472
|
+
'This writes a thin entry in ~/.clementine/schedules.json that points to a skill, ' +
|
|
25473
|
+
'and removes the legacy fat-cron entry from CRON.md. ' +
|
|
25474
|
+
'A .bak backup is left so you can restore if anything looks wrong.')) return;
|
|
25475
|
+
try {
|
|
25476
|
+
var r = await apiFetch('/api/cron/' + encodeURIComponent(jobName) + '/migrate-to-skill', { method: 'POST' });
|
|
25477
|
+
var d = await r.json();
|
|
25478
|
+
if (!r.ok) { toast(d.error || 'Migration failed', 'error'); return; }
|
|
25479
|
+
toast('Migrated "' + jobName + '" → scheduled skill "' + d.skillName + '"', 'success');
|
|
25480
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25481
|
+
} catch (err) {
|
|
25482
|
+
toast('Failed: ' + err, 'error');
|
|
25483
|
+
}
|
|
25484
|
+
}
|
|
25485
|
+
|
|
25486
|
+
// Bulk migrator from the deprecation banner. Migrates every legacy cron
|
|
25487
|
+
// that has a matching skill; reports skipped ones with reasons.
|
|
25488
|
+
async function migrateAllCronsToSkills() {
|
|
25489
|
+
if (!confirm('Convert ALL eligible legacy crons to scheduled skills?\\n\\n' +
|
|
25490
|
+
'Each cron with a matched skill will become a thin schedules.json entry. ' +
|
|
25491
|
+
'CRON.md files get .bak backups. Crons without a skill match are skipped — ' +
|
|
25492
|
+
'no data loss.')) return;
|
|
25493
|
+
try {
|
|
25494
|
+
var r = await apiFetch('/api/cron/migrate-all-to-skills', { method: 'POST' });
|
|
25495
|
+
var d = await r.json();
|
|
25496
|
+
if (!r.ok) { toast(d.error || 'Bulk migration failed', 'error'); return; }
|
|
25497
|
+
var migrated = (d.migrated || []).length;
|
|
25498
|
+
var skipped = (d.skipped || []).length;
|
|
25499
|
+
var msg = 'Migrated ' + migrated + ' cron' + (migrated === 1 ? '' : 's') + ' to scheduled skills';
|
|
25500
|
+
if (skipped > 0) msg += '. ' + skipped + ' skipped — see console for reasons.';
|
|
25501
|
+
toast(msg, migrated > 0 ? 'success' : 'info');
|
|
25502
|
+
if (skipped > 0 && d.skipped) console.warn('[migrate-all-to-skills] skipped:', d.skipped);
|
|
25503
|
+
if (typeof refreshCron === 'function') refreshCron();
|
|
25504
|
+
} catch (err) {
|
|
25505
|
+
toast('Failed: ' + err, 'error');
|
|
25506
|
+
}
|
|
25507
|
+
}
|
|
25508
|
+
|
|
25283
25509
|
// 1.18.129 — replace the "+ New Task" tile with a small dropdown that
|
|
25284
25510
|
// nudges users toward the new "schedule a skill" path. Legacy cron
|
|
25285
25511
|
// option stays for backward compat / power users with hand-rolled
|
|
@@ -26817,7 +27043,30 @@ async function refreshCron() {
|
|
|
26817
27043
|
// placeholder; refreshCronCleanBanner() fetches /api/cron/migrate-preview
|
|
26818
27044
|
// async and fills this in only when there are eligible legacy jobs.
|
|
26819
27045
|
// The banner stays empty (zero visual noise) when the vault is clean.
|
|
26820
|
-
|
|
27046
|
+
// 1.18.132 — Phase 3: soft-deprecate banner. Counts legacy crons
|
|
27047
|
+
// (definition.source !== 'scheduled-skill') and surfaces a one-click
|
|
27048
|
+
// bulk migrator. Dismissable; persists in localStorage so it doesn't
|
|
27049
|
+
// nag on every refresh.
|
|
27050
|
+
var legacyCount = 0;
|
|
27051
|
+
try {
|
|
27052
|
+
legacyCount = (visibleTasks || []).filter(function(t) {
|
|
27053
|
+
return !(t.definition && t.definition.source === 'scheduled-skill');
|
|
27054
|
+
}).length;
|
|
27055
|
+
} catch (_) { /* defensive */ }
|
|
27056
|
+
var bannerHtml = '';
|
|
27057
|
+
var dismissed = localStorage.getItem('clem-skill-migrate-banner-dismissed') === '1';
|
|
27058
|
+
if (legacyCount > 0 && !dismissed) {
|
|
27059
|
+
bannerHtml = '<div style="background:rgba(124,58,237,0.08);border:1px solid var(--purple);border-radius:8px;padding:12px 14px;margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap">'
|
|
27060
|
+
+ '<span style="font-size:18px">⚡</span>'
|
|
27061
|
+
+ '<div style="flex:1;min-width:200px">'
|
|
27062
|
+
+ '<div style="font-size:13px;font-weight:500;color:var(--text-primary)">' + legacyCount + ' legacy cron task' + (legacyCount === 1 ? '' : 's') + ' can become scheduled skills</div>'
|
|
27063
|
+
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Skills are the Anthropic-pure unit of work. Scheduled-skill format is thinner, easier to maintain, and tasks can be reused on demand.</div>'
|
|
27064
|
+
+ '</div>'
|
|
27065
|
+
+ '<button class="btn-sm btn-primary" onclick="migrateAllCronsToSkills()" style="font-size:12px;padding:6px 12px">Migrate all eligible →</button>'
|
|
27066
|
+
+ '<button class="btn-sm" onclick="localStorage.setItem(\\x27clem-skill-migrate-banner-dismissed\\x27,\\x271\\x27);refreshCron()" title="Hide this banner" style="font-size:12px;padding:6px 10px">Dismiss</button>'
|
|
27067
|
+
+ '</div>';
|
|
27068
|
+
}
|
|
27069
|
+
var html = bannerHtml + '<div id="cron-migrate-banner-host"></div>';
|
|
26821
27070
|
html += '<div id="health-strip" class="health-strip"></div>';
|
|
26822
27071
|
// 1.18.115 — collapse the cost/latency/reliability/activity mini-cards
|
|
26823
27072
|
// into a <details> block. The Health Strip already covers what most
|
|
@@ -1032,12 +1032,31 @@ export class HeartbeatScheduler {
|
|
|
1032
1032
|
let response = null;
|
|
1033
1033
|
try {
|
|
1034
1034
|
const cronCall = buildInsightCheckCronCall(prompt);
|
|
1035
|
+
// 1.18.132 — fix the "Prompt is too long" loop. Insight-check is
|
|
1036
|
+
// an internal Haiku classifier ("should we proactively message
|
|
1037
|
+
// Nathan?") with a tightly-built prompt assembled by
|
|
1038
|
+
// gatherInsightSignals + buildInsightPrompt. Without predictable
|
|
1039
|
+
// mode the runtime ALSO injected MEMORY.md (8k cap) + team
|
|
1040
|
+
// comms + delegation queue + auto-matched skills (up to 4 skill
|
|
1041
|
+
// bodies). The user's MEMORY.md alone was ~80% of the budget;
|
|
1042
|
+
// adding even one auto-matched skill body tipped it past the
|
|
1043
|
+
// model's input window.
|
|
1044
|
+
//
|
|
1045
|
+
// Pass predictable: true + an explicit empty allowedTools list so
|
|
1046
|
+
// the SDK call is just: built-in claude_code system prompt +
|
|
1047
|
+
// our compact insight prompt. This is a classifier, not a working
|
|
1048
|
+
// agent; nothing in MEMORY.md or skill bodies is relevant to
|
|
1049
|
+
// urgency rating.
|
|
1035
1050
|
response = await this.gateway.handleCronJob(cronCall.jobName, cronCall.jobPrompt, cronCall.tier, cronCall.maxTurns, cronCall.model, undefined, // workDir
|
|
1036
1051
|
'standard', // mode (display only)
|
|
1037
1052
|
undefined, // maxHours
|
|
1038
1053
|
undefined, // timeoutMs
|
|
1039
1054
|
undefined, // successCriteria
|
|
1040
|
-
undefined
|
|
1055
|
+
undefined, // agentSlug
|
|
1056
|
+
undefined, // pinnedSkills
|
|
1057
|
+
[], // allowedTools — empty = no MCP injection
|
|
1058
|
+
[], // allowedMcpServers — empty = no MCP servers wired
|
|
1059
|
+
true);
|
|
1041
1060
|
this.runLog.append({
|
|
1042
1061
|
jobName: 'insight-check',
|
|
1043
1062
|
startedAt: icStartedAt.toISOString(),
|