clementine-agent 1.8.1 → 1.8.2

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.
@@ -26,19 +26,6 @@
26
26
  */
27
27
  export interface TriggerFile {
28
28
  jobName: string;
29
- /**
30
- * Bare job name (without `{agentSlug}:` prefix). Set by cron-scheduler
31
- * for agent-scoped jobs so the loop can look the job up in
32
- * agents/{agentSlug}/CRON.md. Optional for backward compat with
33
- * triggers written before this field existed.
34
- */
35
- bareName?: string;
36
- /**
37
- * Owning agent slug, set by cron-scheduler. When present, the loop
38
- * applies fixes to vault/00-System/agents/{agentSlug}/CRON.md instead
39
- * of the central CRON.md. Falls back to scanning if absent (older triggers).
40
- */
41
- agentSlug?: string;
42
29
  consecutiveErrors: number;
43
30
  recentErrors: string[];
44
31
  triggeredAt: string;
@@ -74,11 +61,6 @@ export interface SelfImproveLoopOptions {
74
61
  triggersDir?: string;
75
62
  pendingDir?: string;
76
63
  cronPath?: string;
77
- /**
78
- * Override the agents root (vault/00-System/agents). When a trigger
79
- * has agentSlug, the loop reads/writes `${agentsDir}/${agentSlug}/CRON.md`.
80
- */
81
- agentsDir?: string;
82
64
  /**
83
65
  * Disable the fs.watch event-driven path. Tests use this so they can
84
66
  * call tick() directly without racing the watcher.
@@ -91,7 +73,6 @@ export declare class SelfImproveLoop {
91
73
  private readonly triggersDir;
92
74
  private readonly pendingDir;
93
75
  private readonly cronPath;
94
- private readonly agentsDir;
95
76
  private readonly dispatcher;
96
77
  private readonly watchEnabled;
97
78
  private timer;
@@ -28,7 +28,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, wr
28
28
  import path from 'node:path';
29
29
  import matter from 'gray-matter';
30
30
  import pino from 'pino';
31
- import { AGENTS_DIR, BASE_DIR, SYSTEM_DIR } from '../config.js';
31
+ import { BASE_DIR, SYSTEM_DIR } from '../config.js';
32
32
  const logger = pino({ name: 'clementine.self-improve-loop' });
33
33
  /**
34
34
  * Fallback tick interval. The loop is primarily event-driven via fs.watch
@@ -46,7 +46,6 @@ const WATCH_DEBOUNCE_MS = 2000;
46
46
  const TRIGGERS_DIR = path.join(BASE_DIR, 'self-improve', 'triggers');
47
47
  const PENDING_CHANGES_DIR = path.join(BASE_DIR, 'self-improve', 'pending-changes');
48
48
  const CRON_PATH = path.join(SYSTEM_DIR, 'CRON.md');
49
- const AGENTS_ROOT = AGENTS_DIR;
50
49
  // ── Pattern recognition ──────────────────────────────────────────────
51
50
  const PATTERNS = [
52
51
  {
@@ -109,108 +108,36 @@ export function classifyFailure(recentErrors) {
109
108
  description: 'Unrecognized failure pattern. Owner needs to inspect the trigger file.',
110
109
  };
111
110
  }
112
- function readJobsFromFile(cronPath) {
111
+ function loadCronJob(jobName, cronPath) {
113
112
  if (!existsSync(cronPath))
114
113
  return null;
115
114
  const raw = readFileSync(cronPath, 'utf-8');
116
115
  const parsed = matter(raw);
117
116
  const jobs = (parsed.data.jobs ?? []);
118
- return { raw, parsed, jobs };
119
- }
120
- function readAgentSlug(job) {
121
- if (typeof job.agentSlug === 'string')
122
- return job.agentSlug;
123
- if (typeof job.agent_slug === 'string')
124
- return job.agent_slug;
125
- return undefined;
126
- }
127
- /**
128
- * Locate a job's frontmatter entry in either the central CRON.md or an
129
- * agent-scoped CRON.md. Search priority:
130
- *
131
- * 1. If trigger.agentSlug is set, look in agents/{slug}/CRON.md by bareName.
132
- * 2. Otherwise look in central CRON.md by exact name.
133
- * 3. Fall back to scanning agents/* for the bareName (covers older triggers
134
- * that lack agentSlug — the cron-scheduler-prefixed jobName like
135
- * `slug:name` lets us recover the slug).
136
- */
137
- function loadCronJob(trigger, cronPath, agentsDir) {
138
- const explicitSlug = trigger.agentSlug;
139
- const bare = trigger.bareName ?? (explicitSlug && trigger.jobName.startsWith(`${explicitSlug}:`)
140
- ? trigger.jobName.slice(explicitSlug.length + 1)
141
- : trigger.jobName);
142
- // 1. Agent-scoped file when slug is known
143
- if (explicitSlug) {
144
- const agentCronPath = path.join(agentsDir, explicitSlug, 'CRON.md');
145
- const file = readJobsFromFile(agentCronPath);
146
- if (file) {
147
- const job = file.jobs.find((j) => String(j.name ?? '') === bare);
148
- if (job) {
149
- return {
150
- agentSlug: explicitSlug,
151
- cronPath: agentCronPath,
152
- bareName: bare,
153
- job,
154
- raw: file.raw,
155
- parsed: file.parsed,
156
- };
157
- }
158
- }
159
- }
160
- // 2. Central CRON.md by full jobName (handles globally-defined jobs and
161
- // legacy jobs tagged with agentSlug field directly in the central file)
162
- const central = readJobsFromFile(cronPath);
163
- if (central) {
164
- const job = central.jobs.find((j) => String(j.name ?? '') === trigger.jobName);
165
- if (job) {
166
- return {
167
- agentSlug: explicitSlug ?? readAgentSlug(job),
168
- cronPath,
169
- bareName: String(job.name ?? ''),
170
- job,
171
- raw: central.raw,
172
- parsed: central.parsed,
173
- };
174
- }
175
- }
176
- // 3. Recover via scan: trigger jobName follows `{slug}:{bareName}` for
177
- // agent-scoped jobs even when older triggers omit agentSlug.
178
- if (!explicitSlug && trigger.jobName.includes(':')) {
179
- const [slug, ...rest] = trigger.jobName.split(':');
180
- const inferredBare = rest.join(':');
181
- if (slug && inferredBare) {
182
- const agentCronPath = path.join(agentsDir, slug, 'CRON.md');
183
- const file = readJobsFromFile(agentCronPath);
184
- if (file) {
185
- const job = file.jobs.find((j) => String(j.name ?? '') === inferredBare);
186
- if (job) {
187
- return {
188
- agentSlug: slug,
189
- cronPath: agentCronPath,
190
- bareName: inferredBare,
191
- job,
192
- raw: file.raw,
193
- parsed: file.parsed,
194
- };
195
- }
196
- }
197
- }
198
- }
199
- return null;
117
+ const job = jobs.find((j) => String(j.name ?? '') === jobName);
118
+ if (!job)
119
+ return null;
120
+ const agentSlug = typeof job.agentSlug === 'string' ? job.agentSlug : (typeof job.agent_slug === 'string' ? job.agent_slug : undefined);
121
+ return { agentSlug, job, raw, parsed };
200
122
  }
201
123
  /**
202
- * Apply the recipe's mutator to the job's frontmatter and write the CRON.md
203
- * (central or agent-scoped, whichever the lookup resolved to) back atomically.
204
- * Returns true if a change was actually written.
124
+ * Apply the recipe's mutator to the job's frontmatter and write CRON.md
125
+ * back atomically. Returns true if a change was actually written.
205
126
  */
206
- function applyCronEdit(lookup, recipe) {
127
+ function applyCronEdit(jobName, recipe, cronPath) {
207
128
  if (!recipe.apply)
208
129
  return false;
130
+ const lookup = loadCronJob(jobName, cronPath);
131
+ if (!lookup) {
132
+ logger.warn({ jobName }, 'Job not found in CRON.md — cannot apply fix');
133
+ return false;
134
+ }
209
135
  const changed = recipe.apply(lookup.job);
210
136
  if (!changed)
211
137
  return false;
138
+ // Re-stringify with the existing content body preserved.
212
139
  const updated = matter.stringify(lookup.parsed.content, lookup.parsed.data);
213
- writeFileSync(lookup.cronPath, updated);
140
+ writeFileSync(cronPath, updated);
214
141
  return true;
215
142
  }
216
143
  function writePendingChange(record, dir) {
@@ -225,7 +152,6 @@ export class SelfImproveLoop {
225
152
  triggersDir;
226
153
  pendingDir;
227
154
  cronPath;
228
- agentsDir;
229
155
  dispatcher;
230
156
  watchEnabled;
231
157
  timer = null;
@@ -239,7 +165,6 @@ export class SelfImproveLoop {
239
165
  this.triggersDir = opts.triggersDir ?? TRIGGERS_DIR;
240
166
  this.pendingDir = opts.pendingDir ?? PENDING_CHANGES_DIR;
241
167
  this.cronPath = opts.cronPath ?? CRON_PATH;
242
- this.agentsDir = opts.agentsDir ?? AGENTS_ROOT;
243
168
  this.watchEnabled = opts.disableWatch !== true;
244
169
  }
245
170
  start() {
@@ -361,32 +286,23 @@ export class SelfImproveLoop {
361
286
  }
362
287
  async processOne(trigger, counts) {
363
288
  const recipe = classifyFailure(trigger.recentErrors);
364
- const lookup = loadCronJob(trigger, this.cronPath, this.agentsDir);
365
- const agentSlug = trigger.agentSlug ?? lookup?.agentSlug;
289
+ const lookup = loadCronJob(trigger.jobName, this.cronPath);
290
+ const agentSlug = lookup?.agentSlug;
366
291
  if (recipe.category === 'safe-cron-config') {
367
- if (!lookup) {
368
- // Job vanished from CRON files (renamed/deleted). Nothing to fix.
369
- counts.noop++;
370
- logger.warn({ jobName: trigger.jobName, agentSlug }, 'Job not found in any CRON.md — cannot apply fix');
371
- return;
372
- }
373
- const applied = applyCronEdit(lookup, recipe);
292
+ const applied = applyCronEdit(trigger.jobName, recipe, this.cronPath);
374
293
  if (applied) {
375
294
  counts.applied++;
376
- const where = lookup.agentSlug
377
- ? `\`agents/${lookup.agentSlug}/CRON.md\``
378
- : '`CRON.md`';
379
295
  await this.notifyAgent(agentSlug, [
380
296
  `🔧 **Auto-fixed** \`${trigger.jobName}\` after ${trigger.consecutiveErrors} consecutive failures.`,
381
297
  '',
382
298
  recipe.description,
383
299
  '',
384
- `Edit applied to ${where}. I'll watch the next run to confirm it lands cleanly.`,
300
+ 'I\'ll watch the next run to confirm it lands cleanly.',
385
301
  ].join('\n'));
386
302
  }
387
303
  else {
388
304
  counts.noop++;
389
- logger.info({ jobName: trigger.jobName, agentSlug }, 'Fix recipe applied is already in place — trigger removed without further action');
305
+ logger.info({ jobName: trigger.jobName }, 'Fix recipe applied is already in place — trigger removed without further action');
390
306
  }
391
307
  return;
392
308
  }
@@ -32,5 +32,24 @@ export declare function cmdBrowserEnable(): Promise<void>;
32
32
  */
33
33
  export declare function maybePromptBrowserHarness(): Promise<void>;
34
34
  export declare function cmdBrowserDisable(): Promise<void>;
35
+ /**
36
+ * Non-interactive connect — meant for callers that aren't a TTY (MCP tool,
37
+ * daemon-internal callers). Returns a structured result instead of prompting
38
+ * or printing decorative output. Caller decides how to surface failures.
39
+ *
40
+ * Behavior:
41
+ * - CDP already up → { ok: true, alreadyConnected: true }
42
+ * - No Chrome running → launch with flag, poll, return result
43
+ * - Chrome running without flag → if allowQuitChrome=false, refuse with
44
+ * a clear message; if true, quit + relaunch (DESTRUCTIVE — closes tabs).
45
+ */
46
+ export declare function runConnectNonInteractive(opts?: {
47
+ allowQuitChrome?: boolean;
48
+ }): Promise<{
49
+ ok: boolean;
50
+ message: string;
51
+ alreadyConnected?: boolean;
52
+ needsForceQuit?: boolean;
53
+ }>;
35
54
  export declare function cmdBrowserConnect(): Promise<void>;
36
55
  //# sourceMappingURL=browser.d.ts.map
@@ -354,11 +354,85 @@ export async function cmdBrowserDisable() {
354
354
  console.log();
355
355
  }
356
356
  /**
357
- * Core connect logic quits any running Chrome and relaunches with
358
- * --remote-debugging-port=9222 so browser-harness can connect.
357
+ * Non-interactive connect — meant for callers that aren't a TTY (MCP tool,
358
+ * daemon-internal callers). Returns a structured result instead of prompting
359
+ * or printing decorative output. Caller decides how to surface failures.
359
360
  *
360
- * Returns true when CDP is reachable on :9222 at the end, false otherwise.
361
- * Never calls process.exit so it's safe to call from the auto-prompt flow.
361
+ * Behavior:
362
+ * - CDP already up { ok: true, alreadyConnected: true }
363
+ * - No Chrome running → launch with flag, poll, return result
364
+ * - Chrome running without flag → if allowQuitChrome=false, refuse with
365
+ * a clear message; if true, quit + relaunch (DESTRUCTIVE — closes tabs).
366
+ */
367
+ export async function runConnectNonInteractive(opts = {}) {
368
+ if (await probeCdp()) {
369
+ return { ok: true, alreadyConnected: true, message: 'Already connected — Chrome is running with remote debugging on :9222.' };
370
+ }
371
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
372
+ return {
373
+ ok: false,
374
+ message: 'Auto-connect is only supported on macOS and Linux. Launch Chrome manually with --remote-debugging-port=9222.',
375
+ };
376
+ }
377
+ if (isChromeRunning() && !opts.allowQuitChrome) {
378
+ return {
379
+ ok: false,
380
+ needsForceQuit: true,
381
+ message: 'Chrome is running without remote debugging. Connecting requires quitting Chrome and relaunching with --remote-debugging-port=9222 (this closes your current Chrome windows). Re-run with force_quit=true to proceed, or quit Chrome yourself first and call this again.',
382
+ };
383
+ }
384
+ if (isChromeRunning() && opts.allowQuitChrome) {
385
+ try {
386
+ if (process.platform === 'darwin') {
387
+ execSync('osascript -e \'tell application "Google Chrome" to quit\'', { stdio: 'pipe' });
388
+ }
389
+ else {
390
+ try {
391
+ execSync('pkill -TERM -x "google-chrome|chromium|chrome"', { stdio: 'pipe' });
392
+ }
393
+ catch { /* ok */ }
394
+ }
395
+ for (let i = 0; i < 15; i++) {
396
+ if (!isChromeRunning())
397
+ break;
398
+ await new Promise(r => setTimeout(r, 300));
399
+ }
400
+ }
401
+ catch {
402
+ return { ok: false, message: 'Failed to quit Chrome. Quit it manually and try again.' };
403
+ }
404
+ }
405
+ try {
406
+ if (process.platform === 'darwin') {
407
+ execSync('open -na "Google Chrome" --args --remote-debugging-port=9222', { stdio: 'pipe' });
408
+ }
409
+ else {
410
+ const candidates = ['google-chrome', 'chromium', 'chrome'];
411
+ const bin = candidates.find(commandExists);
412
+ if (!bin) {
413
+ return { ok: false, message: 'No Chrome / Chromium binary found in PATH.' };
414
+ }
415
+ execSync(`nohup ${bin} --remote-debugging-port=9222 >/dev/null 2>&1 &`, { stdio: 'pipe' });
416
+ }
417
+ }
418
+ catch (e) {
419
+ return { ok: false, message: `Failed to launch Chrome: ${String(e).slice(0, 200)}` };
420
+ }
421
+ for (let i = 0; i < 24; i++) {
422
+ await new Promise(r => setTimeout(r, 250));
423
+ if (await probeCdp()) {
424
+ return { ok: true, message: 'Connected — Chrome is running with remote debugging on :9222.' };
425
+ }
426
+ }
427
+ return {
428
+ ok: false,
429
+ message: 'Chrome launched, but CDP socket isn\'t responding yet. Check that Chrome started successfully, then verify with: curl http://localhost:9222/json/version',
430
+ };
431
+ }
432
+ /**
433
+ * Interactive CLI connect — wraps runConnectNonInteractive with TTY prompts
434
+ * and decorative output. Used by `clementine browser connect` and the auto-
435
+ * prompt flow.
362
436
  */
363
437
  async function runConnect(opts = {}) {
364
438
  // 1. Already connected? Done.
@@ -1184,27 +1184,19 @@ export class CronScheduler {
1184
1184
  if (advice.shouldEscalate) {
1185
1185
  this.logAdvisorEvent('escalation', job.name, advice.escalationReason ?? 'Escalated to unleashed');
1186
1186
  }
1187
- // Write targeted self-improvement trigger when consecutive errors are high.
1188
- // Include agentSlug + bareName so the self-improve loop can locate jobs
1189
- // defined in per-agent CRON.md files (vault/00-System/agents/{slug}/CRON.md)
1190
- // rather than only the central one.
1187
+ // Write targeted self-improvement trigger when consecutive errors are high
1191
1188
  if (consErrors >= 3) {
1192
1189
  try {
1193
1190
  const triggerDir = path.join(BASE_DIR, 'self-improve', 'triggers');
1194
1191
  mkdirSync(triggerDir, { recursive: true });
1195
1192
  const triggerPath = path.join(triggerDir, `${job.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
1196
- const bareName = job.agentSlug && job.name.startsWith(`${job.agentSlug}:`)
1197
- ? job.name.slice(job.agentSlug.length + 1)
1198
- : job.name;
1199
1193
  writeFileSync(triggerPath, JSON.stringify({
1200
1194
  jobName: job.name,
1201
- bareName,
1202
- agentSlug: job.agentSlug,
1203
1195
  consecutiveErrors: consErrors,
1204
1196
  recentErrors: this.runLog.readRecent(job.name, 3).map(e => e.error?.slice(0, 200)),
1205
1197
  triggeredAt: new Date().toISOString(),
1206
1198
  }, null, 2));
1207
- logger.info({ job: job.name, agentSlug: job.agentSlug, consErrors }, 'Wrote self-improvement trigger for failing job');
1199
+ logger.info({ job: job.name, consErrors }, 'Wrote self-improvement trigger for failing job');
1208
1200
  }
1209
1201
  catch { /* non-fatal */ }
1210
1202
  }
@@ -1873,5 +1873,13 @@ export function registerAdminTools(server) {
1873
1873
  logger.info({ jobName: job_name, runCount: updated.runCount }, 'Cron progress saved');
1874
1874
  return textResult(`Progress saved for "${job_name}" (run #${updated.runCount}). ${(completedItems?.length ?? 0)} items completed, ${(updated.pendingItems?.length ?? 0)} pending.`);
1875
1875
  });
1876
+ // ── Browser harness — chat-driven Chrome connect ────────────────────
1877
+ server.tool('browser_connect', 'Connect Chrome to the browser harness via CDP. Idempotent — if Chrome is already running with remote debugging on :9222 this is a no-op. If no Chrome is running, launches Chrome with --remote-debugging-port=9222. If Chrome is running normally without the flag, refuses unless force_quit=true (which closes the user\'s open tabs). Use this so the user can connect from any chat channel without dropping to the terminal.', {
1878
+ force_quit: z.boolean().optional().describe('If true, quit any running Chrome before relaunching with the debug flag. DESTRUCTIVE — closes the user\'s open tabs. Only set after the user has explicitly confirmed they want this. Defaults to false.'),
1879
+ }, async ({ force_quit }) => {
1880
+ const { runConnectNonInteractive } = await import('../cli/browser.js');
1881
+ const result = await runConnectNonInteractive({ allowQuitChrome: !!force_quit });
1882
+ return textResult(result.message);
1883
+ });
1876
1884
  }
1877
1885
  //# sourceMappingURL=admin-tools.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",