clementine-agent 1.18.143 → 1.18.144

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.
@@ -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 (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(target)) {
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 || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(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
- const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
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
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) {
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
@@ -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;
@@ -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
- const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
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');
@@ -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 readdirSync(AGENTS_DIR, { withFileTypes: true }).filter(d => d.isDirectory() && !d.name.startsWith('_')).map(d => d.name)) {
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 = readdirSync(agentsBase, { withFileTypes: true })
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.143",
3
+ "version": "1.18.144",
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.137",
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",