clementine-agent 1.0.96 → 1.0.97

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.
@@ -27,23 +27,40 @@ export type FixOperation = {
27
27
  op: 'remove';
28
28
  field: string;
29
29
  };
30
+ /** CRON.md frontmatter edit (the original auto-apply shape). */
31
+ export interface AutoApplyCron {
32
+ kind?: 'cron';
33
+ agentSlug?: string;
34
+ operations: FixOperation[];
35
+ }
36
+ /** Write a YAML rule to ~/.clementine/advisor-rules/user/<ruleId>.yaml */
37
+ export interface AutoApplyAdvisorRule {
38
+ kind: 'advisor-rule';
39
+ ruleId: string;
40
+ yamlContent: string;
41
+ }
42
+ /** Write a markdown override to ~/.clementine/prompt-overrides/... */
43
+ export interface AutoApplyPromptOverride {
44
+ kind: 'prompt-override';
45
+ scope: 'global' | 'agent' | 'job';
46
+ scopeKey?: string;
47
+ content: string;
48
+ }
49
+ export type AutoApply = AutoApplyCron | AutoApplyAdvisorRule | AutoApplyPromptOverride;
30
50
  export interface Diagnosis {
31
51
  rootCause: string;
32
52
  confidence: 'high' | 'medium' | 'low';
33
53
  proposedFix: {
34
- type: 'config_change' | 'prompt_change' | 'agent_scope' | 'disable' | 'credential_refresh' | 'escalate_to_owner';
54
+ type: 'config_change' | 'prompt_change' | 'agent_scope' | 'disable' | 'credential_refresh' | 'advisor_rule' | 'prompt_override' | 'escalate_to_owner';
35
55
  details: string;
36
56
  diff?: string;
37
57
  /**
38
- * When present, the fix can be applied with one click via the
39
- * /api/cron/broken-jobs/:jobName/apply-fix endpoint. Operations are
40
- * silently filtered against EDITABLE_FIELDS a proposal that mixes
41
- * safe and unsafe edits gets the unsafe ones dropped.
58
+ * When present, the fix can be applied with one click via the dashboard's
59
+ * apply-fix endpoint. Three shapes (kind=cron|advisor-rule|prompt-override).
60
+ * Each kind has its own validator that runs in sanitizeAutoApply before
61
+ * the proposal is cached, and again in fix-applier before any write.
42
62
  */
43
- autoApply?: {
44
- agentSlug?: string;
45
- operations: FixOperation[];
46
- };
63
+ autoApply?: AutoApply;
47
64
  };
48
65
  riskLevel: 'low' | 'medium' | 'high';
49
66
  generatedAt: string;
@@ -173,28 +173,43 @@ function buildPrompt(broken, jobDef, agentProfile, recentRuns) {
173
173
  '',
174
174
  '## Auto-apply contract',
175
175
  '',
176
- 'When (and ONLY when) the fix is a simple edit to one of these scalar fields — tier, mode, max_hours, max_turns, max_retries, enabled, agentSlug, work_dir, model, always_deliver, after, timeout_ms also populate `proposedFix.autoApply`. The owner can one-click approve it from the dashboard.',
176
+ 'When the fix is mechanical set or remove a known scalar field, write a small advisor rule, or add prompt guidanceALSO populate `proposedFix.autoApply`. The owner can one-click approve it. There are three KINDS of auto-apply, pick the one that matches:',
177
177
  '',
178
- 'For multi-line fields (prompt, pre_check, context, success_criteria), or for credential refreshes, or any change you are not very confident about: OMIT autoApply entirely. The owner will handle those manually.',
178
+ '### kind: "cron" (default edit CRON.md frontmatter)',
179
+ 'Use for: tier, mode, max_hours, max_turns, max_retries, enabled, agentSlug, work_dir, model, always_deliver, after, timeout_ms.',
180
+ 'Shape: { "kind": "cron", "agentSlug"?: "...", "operations": [...] }',
181
+ 'Operations: { "op": "set", "field": "<name>", "value": <scalar> } or { "op": "remove", "field": "<name>" }.',
182
+ 'If the job is agent-scoped (job name has ":"), set agentSlug to the prefix.',
183
+ 'Examples:',
184
+ '- Remove unleashed + companion + cap turns: { "kind": "cron", "operations": [{"op":"remove","field":"mode"}, {"op":"remove","field":"max_hours"}, {"op":"set","field":"max_turns","value":25}] }',
185
+ '- Bump maxTurns: { "kind": "cron", "operations": [{"op":"set","field":"max_turns","value":10}] }',
179
186
  '',
180
- `If the job is agent-scoped (job name includes ":"), set autoApply.agentSlug to the part BEFORE the colon. Otherwise omit it (global CRON.md).`,
187
+ '### kind: "advisor-rule" (write a YAML rule to ~/.clementine/advisor-rules/user/)',
188
+ 'Use when the fix is a behavioral rule that should affect ALL jobs matching some scope, not just one cron job. Examples: "for unleashed jobs, never bump maxTurns" or "for ross-the-sdr, always set timeout to 900s on max_turns errors".',
189
+ 'Shape: { "kind": "advisor-rule", "ruleId": "kebab-case-id", "yamlContent": "<full yaml body>" }',
190
+ 'The YAML body must be a valid advisor rule (schemaVersion: 1, id, description, priority, when, then). User rules at priority 100+ override builtins of the same id.',
191
+ 'Example:',
192
+ '{ "kind": "advisor-rule", "ruleId": "ross-aggressive-timeout", "yamlContent": "schemaVersion: 1\\nid: ross-aggressive-timeout\\ndescription: Bump timeout for ross\\npriority: 105\\nappliesTo:\\n agentSlug: ross-the-sdr\\nwhen:\\n - kind: recentTimeoutHits\\n window: 5\\n atLeast: 1\\nthen:\\n - kind: bumpTimeoutMs\\n multiplier: 2.0" }',
181
193
  '',
182
- 'Operations use the shape { "op": "set", "field": "<name>", "value": <scalar> } or { "op": "remove", "field": "<name>" }. Values are strings, numbers, or booleans.',
194
+ '### kind: "prompt-override" (write a markdown file to ~/.clementine/prompt-overrides/)',
195
+ 'Use when the fix is "give the LLM more guidance for this job/agent". Examples: a job consistently misses an edge case, an agent needs a reminder about output format.',
196
+ 'Shape: { "kind": "prompt-override", "scope": "job"|"agent"|"global", "scopeKey": "<job or agent name>", "content": "<markdown body>" }',
197
+ 'For scope=global, omit scopeKey. For scope=agent, scopeKey is the agent slug. For scope=job, scopeKey is the BARE job name (no agent prefix).',
198
+ 'Example:',
199
+ '{ "kind": "prompt-override", "scope": "job", "scopeKey": "market-leader-followup", "content": "If the inbox query returns 0 rows, batch the duplicate-task cleanup in groups of 50 using bash heredoc loops. Do not enumerate task IDs in the prompt." }',
183
200
  '',
184
- 'Examples:',
185
- '- Remove unleashed mode + its companion: operations: [{"op":"remove","field":"mode"}, {"op":"remove","field":"max_hours"}, {"op":"set","field":"max_turns","value":25}]',
186
- '- Scope a broken global job to Ross\'s profile: operations: [{"op":"set","field":"agentSlug","value":"ross-the-sdr"}]',
187
- '- Bump maxTurns on an under-resourced job: operations: [{"op":"set","field":"max_turns","value":10}]',
201
+ '## When NOT to use autoApply',
202
+ 'For credential refreshes, multi-line CRON.md edits beyond the scalar allowlist, or any change you are not confident about: OMIT autoApply entirely. The owner will handle those manually.',
188
203
  '',
189
204
  '## Output schema (JSON only, no markdown fences):',
190
205
  '{',
191
- ' "rootCause": "1-2 sentences explaining WHY the job is failing, referencing specific fields or error patterns from the CURRENT config",',
206
+ ' "rootCause": "1-2 sentences explaining WHY the job is failing",',
192
207
  ' "confidence": "high|medium|low",',
193
208
  ' "proposedFix": {',
194
- ' "type": "config_change|prompt_change|agent_scope|disable|credential_refresh|escalate_to_owner",',
195
- ' "details": "prose description of the fix, citing the exact field(s) to change",',
196
- ' "diff": "optional: exact before/after diff",',
197
- ' "autoApply": "optional: { agentSlug?, operations: [...] } — ONLY for simple scalar-field edits on the allowlist"',
209
+ ' "type": "config_change|prompt_change|agent_scope|disable|credential_refresh|advisor_rule|prompt_override|escalate_to_owner",',
210
+ ' "details": "prose description of the fix",',
211
+ ' "diff": "optional: before/after diff",',
212
+ ' "autoApply": "optional: one of the three shapes above"',
198
213
  ' },',
199
214
  ' "riskLevel": "low|medium|high"',
200
215
  '}',
@@ -230,32 +245,42 @@ function parseResponse(raw) {
230
245
  }
231
246
  }
232
247
  /**
233
- * Strictly validate and filter autoApply. Drops ops on non-allowlisted fields
234
- * silently (rather than rejecting the whole diagnosis). Returns null if
235
- * nothing valid remains.
248
+ * Strictly validate and filter autoApply. Dispatches on `kind` (default 'cron'
249
+ * for back-compat). Returns null if validation fails for the chosen kind.
236
250
  */
237
251
  function sanitizeAutoApply(raw) {
238
252
  if (!raw || typeof raw !== 'object')
239
253
  return null;
240
254
  const obj = raw;
255
+ const kind = typeof obj.kind === 'string' ? obj.kind : 'cron';
256
+ if (kind === 'cron')
257
+ return sanitizeAutoApplyCron(obj);
258
+ if (kind === 'advisor-rule')
259
+ return sanitizeAutoApplyAdvisorRule(obj);
260
+ if (kind === 'prompt-override')
261
+ return sanitizeAutoApplyPromptOverride(obj);
262
+ return null;
263
+ }
264
+ function sanitizeAutoApplyCron(raw) {
265
+ const obj = raw;
241
266
  if (!Array.isArray(obj.operations))
242
267
  return null;
243
268
  const operations = [];
244
269
  for (const op of obj.operations) {
245
270
  if (!op || typeof op !== 'object')
246
271
  continue;
247
- const raw = op;
248
- if (typeof raw.field !== 'string')
272
+ const r = op;
273
+ if (typeof r.field !== 'string')
249
274
  continue;
250
- if (!EDITABLE_FIELDS.has(raw.field))
275
+ if (!EDITABLE_FIELDS.has(r.field))
251
276
  continue;
252
- if (raw.op === 'remove') {
253
- operations.push({ op: 'remove', field: raw.field });
277
+ if (r.op === 'remove') {
278
+ operations.push({ op: 'remove', field: r.field });
254
279
  }
255
- else if (raw.op === 'set') {
256
- const v = raw.value;
280
+ else if (r.op === 'set') {
281
+ const v = r.value;
257
282
  if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
258
- operations.push({ op: 'set', field: raw.field, value: v });
283
+ operations.push({ op: 'set', field: r.field, value: v });
259
284
  }
260
285
  }
261
286
  }
@@ -264,7 +289,46 @@ function sanitizeAutoApply(raw) {
264
289
  const agentSlug = typeof obj.agentSlug === 'string' && /^[a-z0-9-]+$/i.test(obj.agentSlug)
265
290
  ? obj.agentSlug
266
291
  : undefined;
267
- return agentSlug ? { agentSlug, operations } : { operations };
292
+ return { kind: 'cron', operations, ...(agentSlug ? { agentSlug } : {}) };
293
+ }
294
+ function sanitizeAutoApplyAdvisorRule(raw) {
295
+ const obj = raw;
296
+ if (typeof obj.ruleId !== 'string' || !obj.ruleId.trim())
297
+ return null;
298
+ if (!/^[a-z0-9-]+$/.test(obj.ruleId))
299
+ return null; // safe filename
300
+ if (typeof obj.yamlContent !== 'string' || !obj.yamlContent.trim())
301
+ return null;
302
+ if (obj.yamlContent.length > 10_000)
303
+ return null; // sanity bound
304
+ return {
305
+ kind: 'advisor-rule',
306
+ ruleId: obj.ruleId,
307
+ yamlContent: obj.yamlContent,
308
+ };
309
+ }
310
+ function sanitizeAutoApplyPromptOverride(raw) {
311
+ const obj = raw;
312
+ if (obj.scope !== 'global' && obj.scope !== 'agent' && obj.scope !== 'job')
313
+ return null;
314
+ if (typeof obj.content !== 'string' || !obj.content.trim())
315
+ return null;
316
+ if (obj.content.length > 20_000)
317
+ return null; // sanity bound
318
+ if (obj.scope === 'global') {
319
+ return { kind: 'prompt-override', scope: 'global', content: obj.content };
320
+ }
321
+ // agent or job — require scopeKey, validate as safe filename
322
+ if (typeof obj.scopeKey !== 'string' || !obj.scopeKey)
323
+ return null;
324
+ if (!/^[a-zA-Z0-9_:-]+$/.test(obj.scopeKey))
325
+ return null;
326
+ return {
327
+ kind: 'prompt-override',
328
+ scope: obj.scope,
329
+ scopeKey: obj.scopeKey,
330
+ content: obj.content,
331
+ };
268
332
  }
269
333
  /**
270
334
  * Diagnose one broken job. Returns a cached diagnosis if one exists and is
@@ -25,28 +25,7 @@ export interface BrokenJob {
25
25
  circuitBreakerEngagedAt: string | null;
26
26
  lastAdvisorOpinion: string | null;
27
27
  /** Populated asynchronously by the diagnostic agent when available. */
28
- diagnosis?: {
29
- rootCause: string;
30
- confidence: 'high' | 'medium' | 'low';
31
- proposedFix: {
32
- type: string;
33
- details: string;
34
- diff?: string;
35
- autoApply?: {
36
- agentSlug?: string;
37
- operations: Array<{
38
- op: 'set';
39
- field: string;
40
- value: string | number | boolean;
41
- } | {
42
- op: 'remove';
43
- field: string;
44
- }>;
45
- };
46
- };
47
- riskLevel: 'low' | 'medium' | 'high';
48
- generatedAt: string;
49
- };
28
+ diagnosis?: import('./failure-diagnostics.js').Diagnosis;
50
29
  }
51
30
  /**
52
31
  * Compute the current set of broken jobs by scanning all run logs.
@@ -11,7 +11,7 @@
11
11
  * Every apply writes a .bak next to the CRON.md and appends to an audit
12
12
  * log before touching the file.
13
13
  */
14
- import { type FixOperation } from './failure-diagnostics.js';
14
+ import { type AutoApply, type FixOperation } from './failure-diagnostics.js';
15
15
  export interface ApplyResult {
16
16
  ok: boolean;
17
17
  message: string;
@@ -20,15 +20,19 @@ export interface ApplyResult {
20
20
  skippedOps?: FixOperation[];
21
21
  diff?: string;
22
22
  }
23
+ export interface ApplyOptions {
24
+ dryRun?: boolean;
25
+ /** Override BASE_DIR for advisor-rule and prompt-override write paths. Tests only. */
26
+ baseDir?: string;
27
+ }
23
28
  /**
24
- * Apply a proposed fix to the right CRON.md file. Idempotent with respect
25
- * to already-applied ops (remove on a missing field is a no-op, set on a
26
- * matching value is a no-op).
29
+ * Apply a proposed fix to the right target. Dispatches on autoApply.kind:
30
+ * 'cron' — edit CRON.md frontmatter (the original path)
31
+ * 'advisor-rule' — write a YAML rule under ~/.clementine/advisor-rules/user/
32
+ * 'prompt-override' — write a markdown override under ~/.clementine/prompt-overrides/
33
+ *
34
+ * Each path has its own backup/audit. All idempotent: re-applying the same
35
+ * fix produces the same on-disk state.
27
36
  */
28
- export declare function applyFix(jobName: string, autoApply: {
29
- agentSlug?: string;
30
- operations: FixOperation[];
31
- }, opts?: {
32
- dryRun?: boolean;
33
- }): ApplyResult;
37
+ export declare function applyFix(jobName: string, autoApply: AutoApply, opts?: ApplyOptions): ApplyResult;
34
38
  //# sourceMappingURL=fix-applier.d.ts.map
@@ -11,11 +11,12 @@
11
11
  * Every apply writes a .bak next to the CRON.md and appends to an audit
12
12
  * log before touching the file.
13
13
  */
14
- import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
14
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import pino from 'pino';
17
+ import yaml from 'js-yaml';
17
18
  import { AGENTS_DIR, BASE_DIR, CRON_FILE } from '../config.js';
18
- import { EDITABLE_FIELDS } from './failure-diagnostics.js';
19
+ import { EDITABLE_FIELDS, } from './failure-diagnostics.js';
19
20
  const logger = pino({ name: 'clementine.fix-applier' });
20
21
  const AUDIT_FILE = path.join(BASE_DIR, 'cron', 'fix-applier.log');
21
22
  /**
@@ -232,11 +233,26 @@ function findBlockEnd(lines, start) {
232
233
  return lines.length;
233
234
  }
234
235
  /**
235
- * Apply a proposed fix to the right CRON.md file. Idempotent with respect
236
- * to already-applied ops (remove on a missing field is a no-op, set on a
237
- * matching value is a no-op).
236
+ * Apply a proposed fix to the right target. Dispatches on autoApply.kind:
237
+ * 'cron' — edit CRON.md frontmatter (the original path)
238
+ * 'advisor-rule' — write a YAML rule under ~/.clementine/advisor-rules/user/
239
+ * 'prompt-override' — write a markdown override under ~/.clementine/prompt-overrides/
240
+ *
241
+ * Each path has its own backup/audit. All idempotent: re-applying the same
242
+ * fix produces the same on-disk state.
238
243
  */
239
244
  export function applyFix(jobName, autoApply, opts = {}) {
245
+ // Default 'cron' for back-compat with old AutoApplyCron objects without kind.
246
+ const kind = autoApply.kind ?? 'cron';
247
+ if (kind === 'cron')
248
+ return applyCronFix(jobName, autoApply, opts);
249
+ if (kind === 'advisor-rule')
250
+ return applyAdvisorRuleFix(jobName, autoApply, opts);
251
+ if (kind === 'prompt-override')
252
+ return applyPromptOverrideFix(jobName, autoApply, opts);
253
+ return { ok: false, message: `Unknown autoApply.kind: ${String(kind)}` };
254
+ }
255
+ function applyCronFix(jobName, autoApply, opts) {
240
256
  const cronFile = resolveCronFile(jobName, autoApply);
241
257
  if (!cronFile) {
242
258
  return { ok: false, message: `No CRON.md found for ${jobName}` };
@@ -280,6 +296,7 @@ export function applyFix(jobName, autoApply, opts = {}) {
280
296
  const newContent = newLines.join('\n');
281
297
  writeFileSync(cronFile, newContent);
282
298
  appendAudit({
299
+ kind: 'cron',
283
300
  jobName,
284
301
  file: cronFile,
285
302
  applied,
@@ -296,6 +313,108 @@ export function applyFix(jobName, autoApply, opts = {}) {
296
313
  diff,
297
314
  };
298
315
  }
316
+ // ── Advisor rule writer ──────────────────────────────────────────────
317
+ function userRulesDir(baseDir) {
318
+ return path.join(baseDir, 'advisor-rules', 'user');
319
+ }
320
+ function applyAdvisorRuleFix(jobName, autoApply, opts) {
321
+ const targetDir = userRulesDir(opts.baseDir ?? BASE_DIR);
322
+ // Validate that the YAML parses and has the minimum schema shape. Don't
323
+ // require full Phase 2 zod validation here — the loader will reject invalid
324
+ // rules at read time and the next reload — but catch obvious malformed input.
325
+ let parsed;
326
+ try {
327
+ parsed = yaml.load(autoApply.yamlContent);
328
+ }
329
+ catch (err) {
330
+ return { ok: false, message: `Invalid YAML in advisor-rule body: ${String(err)}` };
331
+ }
332
+ if (!parsed || typeof parsed !== 'object') {
333
+ return { ok: false, message: 'advisor-rule yamlContent did not parse as a YAML object' };
334
+ }
335
+ const r = parsed;
336
+ if (r.schemaVersion !== 1) {
337
+ return { ok: false, message: 'advisor-rule must declare schemaVersion: 1' };
338
+ }
339
+ if (typeof r.id !== 'string' || r.id !== autoApply.ruleId) {
340
+ return { ok: false, message: `advisor-rule yamlContent id must match ruleId="${autoApply.ruleId}"` };
341
+ }
342
+ if (!Array.isArray(r.when) || !Array.isArray(r.then)) {
343
+ return { ok: false, message: 'advisor-rule must have when[] and then[] arrays' };
344
+ }
345
+ const targetPath = path.join(targetDir, `${autoApply.ruleId}.yaml`);
346
+ const diff = `+ advisor-rule ${autoApply.ruleId} → ${targetPath}`;
347
+ if (opts.dryRun) {
348
+ return { ok: true, message: 'Dry run: advisor-rule would be written', file: targetPath, diff };
349
+ }
350
+ try {
351
+ mkdirSync(targetDir, { recursive: true });
352
+ const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
353
+ writeFileSync(tmp, autoApply.yamlContent);
354
+ renameSync(tmp, targetPath);
355
+ }
356
+ catch (err) {
357
+ return { ok: false, message: `Failed to write advisor rule: ${String(err)}` };
358
+ }
359
+ appendAudit({ kind: 'advisor-rule', jobName, file: targetPath, ruleId: autoApply.ruleId, diff });
360
+ logger.info({ jobName, ruleId: autoApply.ruleId, file: targetPath }, 'Applied advisor-rule fix');
361
+ return {
362
+ ok: true,
363
+ message: `Wrote advisor rule ${autoApply.ruleId} (hot-reloads on next eval)`,
364
+ file: targetPath,
365
+ diff,
366
+ };
367
+ }
368
+ // ── Prompt override writer ───────────────────────────────────────────
369
+ function promptOverridesDir(baseDir) {
370
+ return path.join(baseDir, 'prompt-overrides');
371
+ }
372
+ function applyPromptOverrideFix(jobName, autoApply, opts) {
373
+ const root = promptOverridesDir(opts.baseDir ?? BASE_DIR);
374
+ // Resolve target path from scope.
375
+ let targetPath;
376
+ if (autoApply.scope === 'global') {
377
+ targetPath = path.join(root, '_global.md');
378
+ }
379
+ else {
380
+ if (!autoApply.scopeKey) {
381
+ return { ok: false, message: `prompt-override scope=${autoApply.scope} requires scopeKey` };
382
+ }
383
+ if (/[\/\\\.]/.test(autoApply.scopeKey)) {
384
+ return { ok: false, message: 'prompt-override scopeKey cannot contain "/", "\\", or "."' };
385
+ }
386
+ const sub = autoApply.scope === 'agent' ? 'agents' : 'jobs';
387
+ targetPath = path.join(root, sub, `${autoApply.scopeKey}.md`);
388
+ }
389
+ const diff = `+ prompt-override ${autoApply.scope}${autoApply.scopeKey ? `:${autoApply.scopeKey}` : ''} → ${targetPath}`;
390
+ if (opts.dryRun) {
391
+ return { ok: true, message: 'Dry run: prompt-override would be written', file: targetPath, diff };
392
+ }
393
+ try {
394
+ mkdirSync(path.dirname(targetPath), { recursive: true });
395
+ const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
396
+ writeFileSync(tmp, autoApply.content);
397
+ renameSync(tmp, targetPath);
398
+ }
399
+ catch (err) {
400
+ return { ok: false, message: `Failed to write prompt override: ${String(err)}` };
401
+ }
402
+ appendAudit({
403
+ kind: 'prompt-override',
404
+ jobName,
405
+ file: targetPath,
406
+ scope: autoApply.scope,
407
+ scopeKey: autoApply.scopeKey,
408
+ diff,
409
+ });
410
+ logger.info({ jobName, scope: autoApply.scope, scopeKey: autoApply.scopeKey, file: targetPath }, 'Applied prompt-override fix');
411
+ return {
412
+ ok: true,
413
+ message: `Wrote prompt override ${autoApply.scope}${autoApply.scopeKey ? `:${autoApply.scopeKey}` : ''}`,
414
+ file: targetPath,
415
+ diff,
416
+ };
417
+ }
299
418
  function appendAudit(entry) {
300
419
  try {
301
420
  mkdirSync(path.dirname(AUDIT_FILE), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.96",
3
+ "version": "1.0.97",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",