clementine-agent 1.18.143 → 1.18.145
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/self-improve.js +3 -2
- package/dist/agent/skill-store.d.ts +6 -0
- package/dist/agent/skill-store.js +7 -1
- package/dist/cli/routes/workflows.js +2 -1
- package/dist/gateway/scheduler-state.d.ts +16 -0
- package/dist/gateway/scheduler-state.js +26 -0
- package/dist/tools/brain-tools.d.ts +15 -0
- package/dist/tools/brain-tools.js +113 -0
- package/dist/tools/mcp-server.js +2 -0
- package/dist/tools/schedule-tools.d.ts +17 -0
- package/dist/tools/schedule-tools.js +105 -0
- package/dist/tools/shared.d.ts +9 -0
- package/dist/tools/shared.js +21 -0
- package/dist/tools/skill-tools.js +4 -1
- package/dist/tools/team-tools.js +3 -5
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ import pino from 'pino';
|
|
|
17
17
|
import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
|
|
18
18
|
import { listAllGoals } from '../tools/shared.js';
|
|
19
19
|
import { MemoryStore } from '../memory/store.js';
|
|
20
|
+
import { ANTHROPIC_SKILL_NAME_PATTERN } from './skill-store.js';
|
|
20
21
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
21
22
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
22
23
|
const DEFAULT_CONFIG = {
|
|
@@ -2196,7 +2197,7 @@ export function validateProposal(area, target, proposedChange) {
|
|
|
2196
2197
|
// present + non-empty, no XML tags in description, no Anthropic-
|
|
2197
2198
|
// reserved words in name. Reuses the centralized validator that
|
|
2198
2199
|
// dashboard + MCP + auto-extract all share.
|
|
2199
|
-
if (
|
|
2200
|
+
if (!ANTHROPIC_SKILL_NAME_PATTERN.test(target)) {
|
|
2200
2201
|
return { valid: false, error: `skill target must be a valid slug (got "${target}")` };
|
|
2201
2202
|
}
|
|
2202
2203
|
let parsed;
|
|
@@ -2210,7 +2211,7 @@ export function validateProposal(area, target, proposedChange) {
|
|
|
2210
2211
|
const name = typeof fm.name === 'string' ? fm.name : '';
|
|
2211
2212
|
const description = typeof fm.description === 'string' ? fm.description : '';
|
|
2212
2213
|
const body = parsed.content || '';
|
|
2213
|
-
if (!name ||
|
|
2214
|
+
if (!name || !ANTHROPIC_SKILL_NAME_PATTERN.test(name)) {
|
|
2214
2215
|
return { valid: false, error: 'skill frontmatter "name" missing or invalid slug' };
|
|
2215
2216
|
}
|
|
2216
2217
|
if (name !== target) {
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
* crons → folder-form skills.
|
|
23
23
|
*/
|
|
24
24
|
import type { Skill, SkillScope, SkillValidationWarning, CronJobDefinition } from '../types.js';
|
|
25
|
+
/**
|
|
26
|
+
* Anthropic skill slug regex. Exported (1.18.144) so other modules
|
|
27
|
+
* (self-improve, migration tooling) don't drift their own copies.
|
|
28
|
+
* Lowercase letters/digits/dashes, must start with [a-z0-9], ≤64 chars.
|
|
29
|
+
*/
|
|
30
|
+
export declare const ANTHROPIC_SKILL_NAME_PATTERN: RegExp;
|
|
25
31
|
/** Run Anthropic-spec validations on a parsed skill. Errors are spec
|
|
26
32
|
* violations (skill would be rejected by the Anthropic API); warnings
|
|
27
33
|
* are best-practice hints (still loadable). Findings render in the
|
|
@@ -37,7 +37,13 @@ function projectSkillsDir(workDir) {
|
|
|
37
37
|
return existsSync(dir) ? dir : null;
|
|
38
38
|
}
|
|
39
39
|
// ── Anthropic spec validations ────────────────────────────────────────
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Anthropic skill slug regex. Exported (1.18.144) so other modules
|
|
42
|
+
* (self-improve, migration tooling) don't drift their own copies.
|
|
43
|
+
* Lowercase letters/digits/dashes, must start with [a-z0-9], ≤64 chars.
|
|
44
|
+
*/
|
|
45
|
+
export const ANTHROPIC_SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
46
|
+
const NAME_PATTERN = ANTHROPIC_SKILL_NAME_PATTERN;
|
|
41
47
|
const RESERVED_NAMES = new Set(['anthropic', 'claude']);
|
|
42
48
|
const NAME_MAX_LEN = 64;
|
|
43
49
|
const DESCRIPTION_MAX_LEN = 1024;
|
|
@@ -116,7 +116,8 @@ export function workflowsRouter(deps) {
|
|
|
116
116
|
const { wf, agentSlug } = candidates[0];
|
|
117
117
|
// Slugify the workflow name to the Anthropic regex
|
|
118
118
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
119
|
-
|
|
119
|
+
const { ANTHROPIC_SKILL_NAME_PATTERN } = await import('../../agent/skill-store.js');
|
|
120
|
+
if (!ANTHROPIC_SKILL_NAME_PATTERN.test(slug)) {
|
|
120
121
|
res.status(400).json({ ok: false, error: `Workflow name "${name}" cannot be slugified to Anthropic regex` });
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
@@ -33,6 +33,22 @@
|
|
|
33
33
|
* missing fields, drop invalid values).
|
|
34
34
|
*/
|
|
35
35
|
export declare function loadStateFile<T>(filePath: string, defaultValue: T, validator?: (raw: unknown) => T): T;
|
|
36
|
+
/**
|
|
37
|
+
* 1.18.144 — Generic JSON file reader for non-scheduler call sites.
|
|
38
|
+
*
|
|
39
|
+
* Same try/parse/fallback shape as loadStateFile but with a `silent`
|
|
40
|
+
* option for callers who expect missing files to be common (e.g. a
|
|
41
|
+
* tool reading an optional config) and don't want every miss to log.
|
|
42
|
+
*
|
|
43
|
+
* Six+ files were inlining `try { JSON.parse(readFileSync(p)) } catch
|
|
44
|
+
* { return default }` before this. They now share one well-tested
|
|
45
|
+
* helper, which means the next file that needs to read a JSON sidecar
|
|
46
|
+
* doesn't add a seventh variant.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadJsonFile<T>(filePath: string, defaultValue: T, opts?: {
|
|
49
|
+
silent?: boolean;
|
|
50
|
+
validator?: (raw: unknown) => T;
|
|
51
|
+
}): T;
|
|
36
52
|
export interface SaveStateOptions {
|
|
37
53
|
/**
|
|
38
54
|
* If true, write to `<file>.tmp` then rename — guarantees the on-disk
|
|
@@ -48,6 +48,32 @@ export function loadStateFile(filePath, defaultValue, validator) {
|
|
|
48
48
|
return defaultValue;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 1.18.144 — Generic JSON file reader for non-scheduler call sites.
|
|
53
|
+
*
|
|
54
|
+
* Same try/parse/fallback shape as loadStateFile but with a `silent`
|
|
55
|
+
* option for callers who expect missing files to be common (e.g. a
|
|
56
|
+
* tool reading an optional config) and don't want every miss to log.
|
|
57
|
+
*
|
|
58
|
+
* Six+ files were inlining `try { JSON.parse(readFileSync(p)) } catch
|
|
59
|
+
* { return default }` before this. They now share one well-tested
|
|
60
|
+
* helper, which means the next file that needs to read a JSON sidecar
|
|
61
|
+
* doesn't add a seventh variant.
|
|
62
|
+
*/
|
|
63
|
+
export function loadJsonFile(filePath, defaultValue, opts = {}) {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(filePath))
|
|
66
|
+
return defaultValue;
|
|
67
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
68
|
+
return opts.validator ? opts.validator(raw) : raw;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!opts.silent) {
|
|
72
|
+
logger.warn({ err, filePath }, 'Failed to load JSON file — using default');
|
|
73
|
+
}
|
|
74
|
+
return defaultValue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
51
77
|
/**
|
|
52
78
|
* Write a JSON state file. Creates the parent directory if missing.
|
|
53
79
|
* Returns true on success, false on failure (always logs a warning
|
|
@@ -31,5 +31,20 @@ export interface BrainIngestFolderResult {
|
|
|
31
31
|
message: string;
|
|
32
32
|
}
|
|
33
33
|
export declare function ingestBrainRecords(slug: string, records: IngestRecordInput[]): Promise<BrainIngestFolderResult>;
|
|
34
|
+
export interface BrainSaveInput {
|
|
35
|
+
/** Either a URL (will be fetched) or raw text content. */
|
|
36
|
+
content: string;
|
|
37
|
+
/** Optional human-readable title. Inferred from <title> tag for HTML URLs. */
|
|
38
|
+
title?: string;
|
|
39
|
+
/** Logical bucket the record belongs to. Default: "chat-saves". */
|
|
40
|
+
slug?: string;
|
|
41
|
+
/** Stable id so repeat saves dedupe. Default: hash of content. */
|
|
42
|
+
externalId?: string;
|
|
43
|
+
/** Free-form tags carried in frontmatter for later filtering/recall. */
|
|
44
|
+
tags?: string[];
|
|
45
|
+
}
|
|
46
|
+
export declare function brainSave(input: BrainSaveInput): Promise<BrainIngestFolderResult & {
|
|
47
|
+
sourceType: 'url' | 'text';
|
|
48
|
+
}>;
|
|
34
49
|
export declare function registerBrainTools(server: McpServer): void;
|
|
35
50
|
//# sourceMappingURL=brain-tools.d.ts.map
|
|
@@ -125,7 +125,120 @@ export async function ingestBrainRecords(slug, records) {
|
|
|
125
125
|
message,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
// ── brain_save — one-shot ingestion from chat ─────────────────────────
|
|
129
|
+
//
|
|
130
|
+
// 1.18.145 — closes the chat-parity gap on the write side. The user
|
|
131
|
+
// can now say "save this article" or "ingest this URL" and the agent
|
|
132
|
+
// drives the same pipeline the dashboard's Seed tab uses, no manual
|
|
133
|
+
// record assembly required.
|
|
134
|
+
//
|
|
135
|
+
// Accepts either raw text or a URL. URLs are fetched + reasonably
|
|
136
|
+
// extracted (HTML→text via regex strip, JSON/markdown passthrough);
|
|
137
|
+
// for richer extraction the user should still use the dashboard's
|
|
138
|
+
// adapter pipeline (PDF/DOCX/CSV/etc. — they need parsers we don't
|
|
139
|
+
// want to import into every chat session).
|
|
140
|
+
const URL_LIKE = /^https?:\/\//i;
|
|
141
|
+
function looksLikeUrl(s) {
|
|
142
|
+
return URL_LIKE.test(s.trim());
|
|
143
|
+
}
|
|
144
|
+
async function fetchUrlText(url, timeoutMs = 20_000) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(url, {
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
headers: { 'User-Agent': 'Clementine/brain_save (compatible; +https://github.com/Natebreynolds/Clementine-AI-Assistant)' },
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok)
|
|
153
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
154
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
155
|
+
const raw = await res.text();
|
|
156
|
+
if (/json/i.test(contentType)) {
|
|
157
|
+
return { text: raw, contentType };
|
|
158
|
+
}
|
|
159
|
+
if (/html/i.test(contentType) || /^\s*<!DOCTYPE|^\s*<html/i.test(raw)) {
|
|
160
|
+
// Crude HTML→text. Good enough for "save this article" — anything
|
|
161
|
+
// fancier (Readability, Mercury) is a dashboard concern.
|
|
162
|
+
const titleMatch = raw.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
163
|
+
const title = titleMatch ? titleMatch[1].trim() : undefined;
|
|
164
|
+
const text = raw
|
|
165
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
166
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
167
|
+
.replace(/<[^>]+>/g, ' ')
|
|
168
|
+
.replace(/ /g, ' ')
|
|
169
|
+
.replace(/&/g, '&')
|
|
170
|
+
.replace(/</g, '<')
|
|
171
|
+
.replace(/>/g, '>')
|
|
172
|
+
.replace(/"/g, '"')
|
|
173
|
+
.replace(/'/g, "'")
|
|
174
|
+
.replace(/\s+/g, ' ')
|
|
175
|
+
.trim();
|
|
176
|
+
return { text, title, contentType };
|
|
177
|
+
}
|
|
178
|
+
// Plaintext, markdown, etc.
|
|
179
|
+
return { text: raw, contentType };
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function brainSave(input) {
|
|
186
|
+
const slug = sanitizeSlug(input.slug || 'chat-saves');
|
|
187
|
+
const isUrl = looksLikeUrl(input.content);
|
|
188
|
+
let text = input.content;
|
|
189
|
+
let title = input.title;
|
|
190
|
+
let urlContentType;
|
|
191
|
+
if (isUrl) {
|
|
192
|
+
const fetched = await fetchUrlText(input.content);
|
|
193
|
+
text = fetched.text;
|
|
194
|
+
title = title || fetched.title || input.content;
|
|
195
|
+
urlContentType = fetched.contentType;
|
|
196
|
+
}
|
|
197
|
+
if (!text || !text.trim()) {
|
|
198
|
+
throw new Error('brain_save: content is empty after fetch/extract');
|
|
199
|
+
}
|
|
200
|
+
// Stable id: caller-provided OR URL-as-id OR sha-style hash of text
|
|
201
|
+
const externalId = input.externalId
|
|
202
|
+
|| (isUrl ? input.content : fallbackExternalId(slug, 0, text));
|
|
203
|
+
const metadata = {
|
|
204
|
+
savedFromChat: true,
|
|
205
|
+
sourceType: isUrl ? 'url' : 'text',
|
|
206
|
+
};
|
|
207
|
+
if (isUrl)
|
|
208
|
+
metadata.url = input.content;
|
|
209
|
+
if (urlContentType)
|
|
210
|
+
metadata.contentType = urlContentType;
|
|
211
|
+
if (input.tags && input.tags.length)
|
|
212
|
+
metadata.tags = input.tags;
|
|
213
|
+
const result = await ingestBrainRecords(slug, [{
|
|
214
|
+
title: title || 'Untitled',
|
|
215
|
+
externalId,
|
|
216
|
+
content: text,
|
|
217
|
+
metadata,
|
|
218
|
+
}]);
|
|
219
|
+
return { ...result, sourceType: isUrl ? 'url' : 'text' };
|
|
220
|
+
}
|
|
128
221
|
export function registerBrainTools(server) {
|
|
222
|
+
server.tool('brain_save', 'Save a single piece of content (text or URL) to the brain right now. Use when the user says things like "remember this", "save this article", "ingest this URL", "add to memory". Routes through the same distillation pipeline the dashboard\'s Seed tab uses (chunking + LLM summary + vault note + memory index + knowledge graph). For batch ingestion or recurring feeds, see brain_ingest_folder + schedule_skill.', {
|
|
223
|
+
content: z.string().describe('Either a URL (fetched + text-extracted) or raw text content.'),
|
|
224
|
+
title: z.string().optional().describe('Optional human-readable title. Inferred from <title> tag for HTML URLs.'),
|
|
225
|
+
slug: z.string().optional().describe('Logical bucket (folder) the record lands in under 04-Ingest/<slug>/. Default: "chat-saves".'),
|
|
226
|
+
externalId: z.string().optional().describe('Stable id so repeat saves dedupe (e.g. URL, message id). Default: hash of content.'),
|
|
227
|
+
tags: z.array(z.string()).optional().describe('Free-form tags persisted in frontmatter for later filtering/recall.'),
|
|
228
|
+
}, async (input) => {
|
|
229
|
+
try {
|
|
230
|
+
const result = await brainSave(input);
|
|
231
|
+
const where = `04-Ingest/${result.slug}/`;
|
|
232
|
+
return textResult(`Saved to brain (${result.sourceType}): "${(input.title || input.content).slice(0, 80)}"\n` +
|
|
233
|
+
`Folder: ${where} · Pipeline: ${result.pipeline.recordsIn} in · ${result.pipeline.recordsWritten} written · ${result.pipeline.recordsSkipped} skipped` +
|
|
234
|
+
(result.pipeline.recordsFailed > 0 ? ` · ${result.pipeline.recordsFailed} failed` : ''));
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
logger.error({ err }, 'brain_save: failed');
|
|
239
|
+
return textResult(`brain_save failed: ${msg}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
129
242
|
server.tool('brain_ingest_folder', 'Ingest a batch of records into the brain under a named slug. Sends records directly into the distillation pipeline (chunking, LLM summarization, vault note write, memory indexing, knowledge graph write). Use at the end of Connector Feed cron jobs. Safe to re-run — records with the same externalId update the same distilled note.', {
|
|
130
243
|
slug: z.string().describe('Feed slug (matches 04-Ingest/<slug> folder). Lowercase, hyphen-separated.'),
|
|
131
244
|
records: z.array(z.object({
|
package/dist/tools/mcp-server.js
CHANGED
|
@@ -31,6 +31,7 @@ import { registerBackgroundTaskTools } from './background-task-tools.js';
|
|
|
31
31
|
import { registerDecisionReflectionTools } from './decision-reflection-tools.js';
|
|
32
32
|
import { registerBuilderTools } from './builder-tools.js';
|
|
33
33
|
import { registerSkillTools } from './skill-tools.js';
|
|
34
|
+
import { registerScheduleTools } from './schedule-tools.js';
|
|
34
35
|
// ── Server ──────────────────────────────────────────────────────────────
|
|
35
36
|
const serverName = (env['ASSISTANT_NAME'] ?? 'Clementine').toLowerCase() + '-tools';
|
|
36
37
|
const server = new McpServer({ name: serverName, version: '1.0.0' });
|
|
@@ -75,6 +76,7 @@ registerBackgroundTaskTools(scopedServer);
|
|
|
75
76
|
registerDecisionReflectionTools(scopedServer);
|
|
76
77
|
registerBuilderTools(scopedServer);
|
|
77
78
|
registerSkillTools(scopedServer);
|
|
79
|
+
registerScheduleTools(scopedServer);
|
|
78
80
|
// ── Main ────────────────────────────────────────────────────────────────
|
|
79
81
|
async function main() {
|
|
80
82
|
// Initialize memory store and run full sync on startup
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Schedule MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Thin chat-facing wrappers around src/agent/schedule-registry.ts so
|
|
5
|
+
* the agent can compose the full automation flow from natural language:
|
|
6
|
+
*
|
|
7
|
+
* user: "scrape Drive every 2 days for AI articles, save to memory"
|
|
8
|
+
* agent: 1. create_skill('drive-ai-scraper', { body: '...' })
|
|
9
|
+
* 2. schedule_skill('drive-ai-scraper', '0 9 *_/2 * *') (comment elides slash)
|
|
10
|
+
*
|
|
11
|
+
* 1.18.145 — closes the chat-parity gap on the automation side. Before
|
|
12
|
+
* this, the agent could create skills but had no way to schedule them
|
|
13
|
+
* recurringly without dropping to the dashboard.
|
|
14
|
+
*/
|
|
15
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
export declare function registerScheduleTools(server: McpServer): void;
|
|
17
|
+
//# sourceMappingURL=schedule-tools.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Schedule MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Thin chat-facing wrappers around src/agent/schedule-registry.ts so
|
|
5
|
+
* the agent can compose the full automation flow from natural language:
|
|
6
|
+
*
|
|
7
|
+
* user: "scrape Drive every 2 days for AI articles, save to memory"
|
|
8
|
+
* agent: 1. create_skill('drive-ai-scraper', { body: '...' })
|
|
9
|
+
* 2. schedule_skill('drive-ai-scraper', '0 9 *_/2 * *') (comment elides slash)
|
|
10
|
+
*
|
|
11
|
+
* 1.18.145 — closes the chat-parity gap on the automation side. Before
|
|
12
|
+
* this, the agent could create skills but had no way to schedule them
|
|
13
|
+
* recurringly without dropping to the dashboard.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import cron from 'node-cron';
|
|
17
|
+
import { listSchedules, getSchedule, setSchedule, removeSchedule, enableSchedule, } from '../agent/schedule-registry.js';
|
|
18
|
+
import { loadSkillByName } from '../agent/skill-extractor.js';
|
|
19
|
+
import { textResult, logger } from './shared.js';
|
|
20
|
+
export function registerScheduleTools(server) {
|
|
21
|
+
server.tool('schedule_skill', 'Schedule a skill to run automatically on a cron expression. Pair with create_skill to build "recurring brain feeds" or any other automation from chat. The skill must already exist in the catalog. Idempotent — calling twice for the same skill updates the existing schedule. Use enabled=false to pause without losing the schedule.', {
|
|
22
|
+
skillName: z.string().describe('The skill slug (must already exist in the catalog — call create_skill first if needed).'),
|
|
23
|
+
schedule: z.string().describe('Cron expression. Examples: "0 9 * * *" = daily 9am, "0 9 */2 * *" = every 2 days at 9am, "0 */4 * * *" = every 4 hours, "0 7 * * 1-5" = weekdays at 7am.'),
|
|
24
|
+
enabled: z.boolean().optional().describe('When false, schedule is saved but the runner skips it. Default: true.'),
|
|
25
|
+
agentSlug: z.string().nullable().optional().describe('Optional: run as a hired agent (e.g. "ross-the-sdr"). Default: null = Clementine.'),
|
|
26
|
+
}, async ({ skillName, schedule, enabled, agentSlug }) => {
|
|
27
|
+
// Validate cron expression up-front so the user gets a clear
|
|
28
|
+
// error before we touch the registry.
|
|
29
|
+
if (!cron.validate(schedule)) {
|
|
30
|
+
return textResult(`schedule_skill: "${schedule}" is not a valid cron expression. Try something like "0 9 * * *" (daily 9am).`);
|
|
31
|
+
}
|
|
32
|
+
// Validate the skill exists. Without this, the schedule would
|
|
33
|
+
// silently sit in the registry and the cron scheduler would skip
|
|
34
|
+
// it on every fire — confusing failure mode.
|
|
35
|
+
const skill = loadSkillByName(skillName, agentSlug ?? undefined);
|
|
36
|
+
if (!skill) {
|
|
37
|
+
return textResult(`schedule_skill: skill "${skillName}" not found${agentSlug ? ` (in agent "${agentSlug}" scope)` : ''}. ` +
|
|
38
|
+
`Create it first with create_skill, then schedule it.`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const entry = setSchedule(skillName, {
|
|
42
|
+
schedule,
|
|
43
|
+
enabled: enabled ?? true,
|
|
44
|
+
agentSlug: agentSlug ?? null,
|
|
45
|
+
});
|
|
46
|
+
logger.info({ skillName, schedule, enabled: entry.enabled, agentSlug: entry.agentSlug }, 'schedule_skill: scheduled');
|
|
47
|
+
return textResult(`Scheduled "${skillName}" to run on "${schedule}"` +
|
|
48
|
+
(entry.enabled ? '' : ' (DISABLED — flip enabled:true to start firing)') +
|
|
49
|
+
`. View on the Tasks page or call list_schedules to confirm.`);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
logger.error({ err, skillName }, 'schedule_skill: failed');
|
|
54
|
+
return textResult(`schedule_skill failed: ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.tool('list_schedules', 'List every scheduled skill and its cron expression. Returns the same data the dashboard\'s Tasks page shows for the SKILL-tagged rows.', {}, async () => {
|
|
58
|
+
try {
|
|
59
|
+
const entries = listSchedules();
|
|
60
|
+
if (entries.length === 0) {
|
|
61
|
+
return textResult('No scheduled skills yet. Use schedule_skill to create one.');
|
|
62
|
+
}
|
|
63
|
+
const lines = [`${entries.length} scheduled skill${entries.length === 1 ? '' : 's'}:`];
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
const status = e.enabled ? '✓' : '⏸';
|
|
66
|
+
const owner = e.agentSlug ? ` (as ${e.agentSlug})` : '';
|
|
67
|
+
lines.push(`${status} ${e.skillName} — ${e.schedule}${owner}`);
|
|
68
|
+
}
|
|
69
|
+
return textResult(lines.join('\n'));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
return textResult(`list_schedules failed: ${msg}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
server.tool('unschedule_skill', 'Remove a skill\'s schedule entirely. The skill stays in the catalog and can still be run manually or rescheduled later. Use pause_schedule (enabled:false via schedule_skill) for a temporary pause instead.', {
|
|
77
|
+
skillName: z.string().describe('The skill slug to unschedule.'),
|
|
78
|
+
}, async ({ skillName }) => {
|
|
79
|
+
const existing = getSchedule(skillName);
|
|
80
|
+
if (!existing) {
|
|
81
|
+
return textResult(`unschedule_skill: "${skillName}" wasn't scheduled — nothing to do.`);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
removeSchedule(skillName);
|
|
85
|
+
logger.info({ skillName }, 'unschedule_skill: removed');
|
|
86
|
+
return textResult(`Unscheduled "${skillName}". The skill stays in the catalog and can be run manually.`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return textResult(`unschedule_skill failed: ${msg}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.tool('pause_schedule', 'Pause or resume an existing scheduled skill without losing its cron expression. Equivalent to schedule_skill with enabled:false but doesn\'t require re-passing the schedule string.', {
|
|
94
|
+
skillName: z.string().describe('The skill slug.'),
|
|
95
|
+
enabled: z.boolean().describe('true = resume firing, false = pause.'),
|
|
96
|
+
}, async ({ skillName, enabled }) => {
|
|
97
|
+
const updated = enableSchedule(skillName, enabled);
|
|
98
|
+
if (!updated) {
|
|
99
|
+
return textResult(`pause_schedule: "${skillName}" isn't scheduled. Use schedule_skill to create the schedule first.`);
|
|
100
|
+
}
|
|
101
|
+
logger.info({ skillName, enabled }, 'pause_schedule: updated');
|
|
102
|
+
return textResult(`${enabled ? 'Resumed' : 'Paused'} "${skillName}" (${updated.schedule}).`);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=schedule-tools.js.map
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -676,6 +676,15 @@ export declare function agentTasksFile(slug: string | null): string;
|
|
|
676
676
|
export declare function agentWorkingMemoryFile(slug: string | null): string;
|
|
677
677
|
export declare function agentGoalsDir(slug: string | null): string;
|
|
678
678
|
export declare function agentDailyNotesDir(slug: string | null): string;
|
|
679
|
+
/**
|
|
680
|
+
* 1.18.144 — Single source of truth for "all currently-hired agent
|
|
681
|
+
* slugs." Five+ files used to inline the same readdirSync + filter
|
|
682
|
+
* pattern (skipping leading-underscore directories like _archive).
|
|
683
|
+
*
|
|
684
|
+
* Returns slugs sorted alphabetically. Returns [] when AGENTS_DIR
|
|
685
|
+
* doesn't exist or can't be read.
|
|
686
|
+
*/
|
|
687
|
+
export declare function listAgentSlugs(): string[];
|
|
679
688
|
export type GoalRecord = {
|
|
680
689
|
id: string;
|
|
681
690
|
title: string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -80,6 +80,27 @@ export function agentDailyNotesDir(slug) {
|
|
|
80
80
|
return DAILY_NOTES_DIR;
|
|
81
81
|
return path.join(AGENTS_DIR, slug, 'daily-notes');
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 1.18.144 — Single source of truth for "all currently-hired agent
|
|
85
|
+
* slugs." Five+ files used to inline the same readdirSync + filter
|
|
86
|
+
* pattern (skipping leading-underscore directories like _archive).
|
|
87
|
+
*
|
|
88
|
+
* Returns slugs sorted alphabetically. Returns [] when AGENTS_DIR
|
|
89
|
+
* doesn't exist or can't be read.
|
|
90
|
+
*/
|
|
91
|
+
export function listAgentSlugs() {
|
|
92
|
+
if (!existsSync(AGENTS_DIR))
|
|
93
|
+
return [];
|
|
94
|
+
try {
|
|
95
|
+
return readdirSync(AGENTS_DIR, { withFileTypes: true })
|
|
96
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
|
|
97
|
+
.map((d) => d.name)
|
|
98
|
+
.sort();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
/** Return the directory where a goal owned by `owner` should live. */
|
|
84
105
|
export function goalDirForOwner(owner) {
|
|
85
106
|
if (!owner || owner === 'clementine')
|
|
@@ -25,7 +25,10 @@ import { VAULT_DIR, textResult, logger } from './shared.js';
|
|
|
25
25
|
// 1.18.124 — name regex is the only validator skill-tools still uses
|
|
26
26
|
// directly (for update_skill's pre-flight slug check). All other
|
|
27
27
|
// validations + the file write live in skill-store.writeSkill.
|
|
28
|
-
|
|
28
|
+
// 1.18.144 — pulled the regex from skill-store's exported canonical
|
|
29
|
+
// constant so all skill-name validation now traces to one source.
|
|
30
|
+
import { ANTHROPIC_SKILL_NAME_PATTERN } from '../agent/skill-store.js';
|
|
31
|
+
const NAME_PATTERN = ANTHROPIC_SKILL_NAME_PATTERN;
|
|
29
32
|
const DESCRIPTION_MAX_LEN = 1024;
|
|
30
33
|
function globalSkillsDir() {
|
|
31
34
|
return path.join(VAULT_DIR, '00-System', 'skills');
|
package/dist/tools/team-tools.js
CHANGED
|
@@ -7,13 +7,14 @@ import path from 'node:path';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { ACTIVE_AGENT_SLUG, AGENTS_DIR, BASE_DIR, DELEGATIONS_BASE, TEAM_COMMS_LOG, env, logger, parseTasks, textResult, } from './shared.js';
|
|
9
9
|
import { todayISO } from '../gateway/cron-scheduler.js';
|
|
10
|
+
import { listAgentSlugs } from './shared.js';
|
|
10
11
|
async function loadTeamAgents() {
|
|
11
12
|
const matterMod = await import('gray-matter');
|
|
12
13
|
const agents = [];
|
|
13
14
|
const seen = new Set();
|
|
14
15
|
if (existsSync(AGENTS_DIR)) {
|
|
15
16
|
try {
|
|
16
|
-
for (const slug of
|
|
17
|
+
for (const slug of listAgentSlugs()) {
|
|
17
18
|
const agentFile = path.join(AGENTS_DIR, slug, 'agent.md');
|
|
18
19
|
if (!existsSync(agentFile))
|
|
19
20
|
continue;
|
|
@@ -349,10 +350,7 @@ export function registerTeamTools(server) {
|
|
|
349
350
|
const agentsBase = AGENTS_DIR;
|
|
350
351
|
if (!existsSync(agentsBase))
|
|
351
352
|
return textResult('No agents found.');
|
|
352
|
-
const agentSlugs =
|
|
353
|
-
.filter(d => d.isDirectory())
|
|
354
|
-
.map(d => d.name)
|
|
355
|
-
.filter(n => !agent || n === agent);
|
|
353
|
+
const agentSlugs = listAgentSlugs().filter(n => !agent || n === agent);
|
|
356
354
|
if (!agentSlugs.length)
|
|
357
355
|
return textResult('No agents found.');
|
|
358
356
|
const matterMod = await import('gray-matter');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clementine-agent",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.145",
|
|
4
4
|
"description": "Clementine — Personal AI Assistant (TypeScript)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"postinstall": "node scripts/postinstall.js 2>/dev/null || true"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
31
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.138",
|
|
32
32
|
"@anthropic-ai/sdk": "^0.91.0",
|
|
33
33
|
"@composio/claude-agent-sdk": "^0.8.1",
|
|
34
34
|
"@composio/core": "^0.8.1",
|