agenr 0.9.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2 (2026-02-26)
4
+
5
+ ### Fixed
6
+ - fix(consolidate): fragmented clustering produced duplicate canonical entries instead of a single winner (#249)
7
+ - Phase 1 now over-fetches neighbors (3x) when type-filtered to preserve same-type neighborhood coverage
8
+ - Added a new Phase 3 post-merge dedup pass to merge near-duplicate canonical entries created in the same run
9
+ - Phase 3 disables idempotency and only processes clusters that include entries created during the current run
10
+
11
+ ## 0.9.1 (2026-02-26)
12
+
13
+ ### Changed
14
+ - Renamed `agenr daemon` CLI command to `agenr watcher` - "watcher" better describes what it does
15
+ - `agenr daemon` still works as a hidden compatibility command
16
+ - Updated user-facing command output to say "watcher" instead of "daemon"
17
+
18
+ ### Internal
19
+ - Renamed `src/commands/daemon.ts` to `src/commands/watcher.ts`
20
+ - Renamed daemon command interfaces and exports from `Daemon*`/`runDaemon*` to `Watcher*`/`runWatcher*`
21
+
3
22
  ## 0.9.0 (2026-02-25)
4
23
 
5
24
  ### Features
package/dist/cli-main.js CHANGED
@@ -1040,7 +1040,7 @@ async function runResetCommand(options, deps) {
1040
1040
  return { exitCode: 0 };
1041
1041
  }
1042
1042
  resolvedDeps.stdoutLine(
1043
- "WARNING: If the agenr watcher daemon is running, stop it before proceeding. Reset will not abort if the daemon is running."
1043
+ "WARNING: If the agenr watcher is running, stop it before proceeding. Reset will not abort if the watcher is running."
1044
1044
  );
1045
1045
  let backupPath;
1046
1046
  try {
@@ -1690,7 +1690,7 @@ async function runDbCheckCommand(options, deps) {
1690
1690
  }
1691
1691
  }
1692
1692
 
1693
- // src/commands/daemon.ts
1693
+ // src/commands/watcher.ts
1694
1694
  import { execFile, spawn } from "child_process";
1695
1695
  import fs9 from "fs/promises";
1696
1696
  import os6 from "os";
@@ -2024,7 +2024,7 @@ function getDefaultPlatformDir(platform, homeDir = os5.homedir()) {
2024
2024
  throw new Error(`No default directory for platform: ${platform}`);
2025
2025
  }
2026
2026
 
2027
- // src/commands/daemon.ts
2027
+ // src/commands/watcher.ts
2028
2028
  var LAUNCH_LABEL = "com.agenr.watch";
2029
2029
  var DEFAULT_INTERVAL_SECONDS = 120;
2030
2030
  var DEFAULT_STATUS_LOG_LINES = 20;
@@ -2131,7 +2131,7 @@ function resolveDeps2(deps) {
2131
2131
  }
2132
2132
  function ensureSupportedPlatform(platform) {
2133
2133
  if (platform !== "darwin") {
2134
- throw new Error("Daemon commands are currently supported on macOS only.");
2134
+ throw new Error("Watcher commands are currently supported on macOS only.");
2135
2135
  }
2136
2136
  }
2137
2137
  async function runLaunchctl(deps, args, strict) {
@@ -2266,7 +2266,7 @@ function printLines(lines) {
2266
2266
  `);
2267
2267
  }
2268
2268
  }
2269
- async function runDaemonInstallCommand(options, deps) {
2269
+ async function runWatcherInstallCommand(options, deps) {
2270
2270
  const resolvedDeps = resolveDeps2(deps);
2271
2271
  ensureSupportedPlatform(resolvedDeps.platformFn());
2272
2272
  const intervalSeconds = parsePositiveInt(options.interval, DEFAULT_INTERVAL_SECONDS, "--interval");
@@ -2278,7 +2278,7 @@ async function runDaemonInstallCommand(options, deps) {
2278
2278
  const { launchAgentsDir, plistPath, logDir, logPath } = getPaths(homeDir);
2279
2279
  const plistExists = await fileExists(resolvedDeps.statFn, plistPath);
2280
2280
  if (plistExists && options.force !== true) {
2281
- throw new Error(`Daemon plist already exists: ${plistPath}. Re-run with --force to overwrite.`);
2281
+ throw new Error(`Watcher plist already exists: ${plistPath}. Re-run with --force to overwrite.`);
2282
2282
  }
2283
2283
  await resolvedDeps.mkdirFn(launchAgentsDir, { recursive: true });
2284
2284
  await resolvedDeps.mkdirFn(logDir, { recursive: true });
@@ -2307,7 +2307,7 @@ async function runDaemonInstallCommand(options, deps) {
2307
2307
  await resolvedDeps.writeFileFn(plistPath, plist, "utf8");
2308
2308
  await runLaunchctl(resolvedDeps, ["bootout", `gui/${uid}/${LAUNCH_LABEL}`], false);
2309
2309
  await runLaunchctl(resolvedDeps, ["bootstrap", `gui/${uid}`, plistPath], true);
2310
- clack2.log.success(`Installed daemon plist: ${plistPath}`);
2310
+ clack2.log.success(`Installed watcher plist: ${plistPath}`);
2311
2311
  clack2.log.info(`Log file: ${logPath}`);
2312
2312
  return { exitCode: 0 };
2313
2313
  }
@@ -2327,7 +2327,7 @@ async function requireInstalledPlist(deps, homeDir, message) {
2327
2327
  }
2328
2328
  return plistPath;
2329
2329
  }
2330
- async function runDaemonStartCommand(_options = {}, deps) {
2330
+ async function runWatcherStartCommand(_options = {}, deps) {
2331
2331
  const resolvedDeps = resolveDeps2(deps);
2332
2332
  ensureSupportedPlatform(resolvedDeps.platformFn());
2333
2333
  const homeDir = resolvedDeps.homedirFn();
@@ -2338,21 +2338,21 @@ async function runDaemonStartCommand(_options = {}, deps) {
2338
2338
  const plistPath = await requireInstalledPlist(
2339
2339
  resolvedDeps,
2340
2340
  homeDir,
2341
- "Daemon not installed. Run `agenr daemon install` first."
2341
+ "Watcher not installed. Run `agenr watcher install` first."
2342
2342
  );
2343
2343
  const state = await getLaunchctlState(resolvedDeps, uid);
2344
2344
  if (state.loaded && state.running) {
2345
- clack2.log.info("Daemon is already running.");
2345
+ clack2.log.info("Watcher is already running.");
2346
2346
  return { exitCode: 0 };
2347
2347
  }
2348
2348
  if (state.loaded && !state.running) {
2349
2349
  await runLaunchctl(resolvedDeps, ["bootout", `gui/${uid}/${LAUNCH_LABEL}`], false);
2350
2350
  }
2351
2351
  await runLaunchctl(resolvedDeps, ["bootstrap", `gui/${uid}`, plistPath], true);
2352
- clack2.log.success("Daemon started.");
2352
+ clack2.log.success("Watcher started.");
2353
2353
  return { exitCode: 0 };
2354
2354
  }
2355
- async function runDaemonStopCommand(_options = {}, deps) {
2355
+ async function runWatcherStopCommand(_options = {}, deps) {
2356
2356
  const resolvedDeps = resolveDeps2(deps);
2357
2357
  ensureSupportedPlatform(resolvedDeps.platformFn());
2358
2358
  const homeDir = resolvedDeps.homedirFn();
@@ -2363,18 +2363,18 @@ async function runDaemonStopCommand(_options = {}, deps) {
2363
2363
  await requireInstalledPlist(
2364
2364
  resolvedDeps,
2365
2365
  homeDir,
2366
- "Daemon not installed. Run `agenr daemon install` first."
2366
+ "Watcher not installed. Run `agenr watcher install` first."
2367
2367
  );
2368
2368
  const state = await getLaunchctlState(resolvedDeps, uid);
2369
2369
  if (!state.loaded) {
2370
- clack2.log.info("Daemon is not loaded.");
2370
+ clack2.log.info("Watcher is not loaded.");
2371
2371
  return { exitCode: 0 };
2372
2372
  }
2373
2373
  await runLaunchctl(resolvedDeps, ["bootout", `gui/${uid}/${LAUNCH_LABEL}`], true);
2374
- clack2.log.success("Daemon stopped.");
2374
+ clack2.log.success("Watcher stopped.");
2375
2375
  return { exitCode: 0 };
2376
2376
  }
2377
- async function runDaemonRestartCommand(_options = {}, deps) {
2377
+ async function runWatcherRestartCommand(_options = {}, deps) {
2378
2378
  const resolvedDeps = resolveDeps2(deps);
2379
2379
  ensureSupportedPlatform(resolvedDeps.platformFn());
2380
2380
  const homeDir = resolvedDeps.homedirFn();
@@ -2382,7 +2382,7 @@ async function runDaemonRestartCommand(_options = {}, deps) {
2382
2382
  if (uid < 0) {
2383
2383
  throw new Error("Unable to resolve current user ID for launchctl.");
2384
2384
  }
2385
- const plistPath = await requireInstalledPlist(resolvedDeps, homeDir, "Daemon not installed.");
2385
+ const plistPath = await requireInstalledPlist(resolvedDeps, homeDir, "Watcher not installed.");
2386
2386
  await runLaunchctl(resolvedDeps, ["bootout", `gui/${uid}/${LAUNCH_LABEL}`], false);
2387
2387
  const maxWaitMs = 1e4;
2388
2388
  const pollMs = 500;
@@ -2394,13 +2394,13 @@ async function runDaemonRestartCommand(_options = {}, deps) {
2394
2394
  }
2395
2395
  const finalState = await getLaunchctlState(resolvedDeps, uid);
2396
2396
  if (finalState.loaded) {
2397
- clack2.log.warn("Daemon did not unload within 10s - attempting bootstrap anyway.");
2397
+ clack2.log.warn("Watcher did not unload within 10s - attempting bootstrap anyway.");
2398
2398
  }
2399
2399
  await runLaunchctl(resolvedDeps, ["bootstrap", `gui/${uid}`, plistPath], true);
2400
- clack2.log.success("Daemon restarted.");
2400
+ clack2.log.success("Watcher restarted.");
2401
2401
  return { exitCode: 0 };
2402
2402
  }
2403
- async function runDaemonUninstallCommand(options, deps) {
2403
+ async function runWatcherUninstallCommand(options, deps) {
2404
2404
  const resolvedDeps = resolveDeps2(deps);
2405
2405
  ensureSupportedPlatform(resolvedDeps.platformFn());
2406
2406
  const homeDir = resolvedDeps.homedirFn();
@@ -2411,11 +2411,11 @@ async function runDaemonUninstallCommand(options, deps) {
2411
2411
  const { plistPath } = getPaths(homeDir);
2412
2412
  const exists = await fileExists(resolvedDeps.statFn, plistPath);
2413
2413
  if (!exists) {
2414
- clack2.log.info(`Daemon plist not found: ${plistPath}`);
2414
+ clack2.log.info(`Watcher plist not found: ${plistPath}`);
2415
2415
  return { exitCode: 0 };
2416
2416
  }
2417
2417
  if (options.yes !== true) {
2418
- const confirmed = await resolvedDeps.confirmFn("Remove agenr watch daemon?");
2418
+ const confirmed = await resolvedDeps.confirmFn("Remove agenr watcher?");
2419
2419
  if (!confirmed) {
2420
2420
  clack2.log.warn("Uninstall cancelled.");
2421
2421
  return { exitCode: 1 };
@@ -2423,10 +2423,10 @@ async function runDaemonUninstallCommand(options, deps) {
2423
2423
  }
2424
2424
  await runLaunchctl(resolvedDeps, ["bootout", `gui/${uid}/${LAUNCH_LABEL}`], false);
2425
2425
  await resolvedDeps.rmFn(plistPath, { force: true });
2426
- clack2.log.success("Daemon uninstalled.");
2426
+ clack2.log.success("Watcher uninstalled.");
2427
2427
  return { exitCode: 0 };
2428
2428
  }
2429
- async function runDaemonStatusCommand(options, deps) {
2429
+ async function runWatcherStatusCommand(options, deps) {
2430
2430
  const resolvedDeps = resolveDeps2(deps);
2431
2431
  ensureSupportedPlatform(resolvedDeps.platformFn());
2432
2432
  const homeDir = resolvedDeps.homedirFn();
@@ -2454,8 +2454,8 @@ async function runDaemonStatusCommand(options, deps) {
2454
2454
  const lineCount = parsePositiveInt(options.lines, DEFAULT_STATUS_LOG_LINES, "--lines");
2455
2455
  const logTail = await readLastLines(resolvedDeps.readFileFn, logPath, lineCount);
2456
2456
  const nowMs = resolvedDeps.nowFn();
2457
- const daemonLines = [
2458
- "-- Daemon --",
2457
+ const serviceLines = [
2458
+ "-- Service --",
2459
2459
  `Loaded: ${loaded ? "yes" : "no"}`,
2460
2460
  `Running: ${running ? "yes" : "no"}`,
2461
2461
  `Current file: ${currentFile ?? "(none)"}`,
@@ -2471,7 +2471,7 @@ async function runDaemonStatusCommand(options, deps) {
2471
2471
  `Sessions watched: ${health.sessionsWatched}`,
2472
2472
  `Entries stored: ${health.entriesStored}`
2473
2473
  ] : ["-- Watcher --", "Heartbeat: no data"];
2474
- resolvedDeps.noteFn([...daemonLines, "", ...watcherLines].join("\n"), "Status");
2474
+ resolvedDeps.noteFn([...serviceLines, "", ...watcherLines].join("\n"), "Status");
2475
2475
  if (logTail.length > 0) {
2476
2476
  clack2.log.info(`Last ${logTail.length} log lines:`);
2477
2477
  printLines(logTail);
@@ -2500,14 +2500,14 @@ async function followLog(deps, logPath, lines) {
2500
2500
  });
2501
2501
  });
2502
2502
  }
2503
- async function runDaemonLogsCommand(options, deps) {
2503
+ async function runWatcherLogsCommand(options, deps) {
2504
2504
  const resolvedDeps = resolveDeps2(deps);
2505
2505
  ensureSupportedPlatform(resolvedDeps.platformFn());
2506
2506
  const homeDir = resolvedDeps.homedirFn();
2507
2507
  const { logPath } = getPaths(homeDir);
2508
2508
  const logExists = await fileExists(resolvedDeps.statFn, logPath);
2509
2509
  if (!logExists) {
2510
- throw new Error(`Daemon log file not found: ${logPath}`);
2510
+ throw new Error(`Watcher log file not found: ${logPath}`);
2511
2511
  }
2512
2512
  const lineCount = parsePositiveInt(options.lines, DEFAULT_LOG_LINES, "--lines");
2513
2513
  const follow = options.follow === true || options.lines === void 0;
@@ -6400,7 +6400,8 @@ async function buildClusters(db, options = {}) {
6400
6400
  unionFind.add(entry.id);
6401
6401
  }
6402
6402
  for (const entry of candidates) {
6403
- const neighbors = await findSimilar(db, entry.embedding, neighborLimit);
6403
+ const fetchLimit = typeFilter ? neighborLimit * 3 : neighborLimit;
6404
+ const neighbors = await findSimilar(db, entry.embedding, fetchLimit);
6404
6405
  for (const neighbor of neighbors) {
6405
6406
  const candidate = entryById.get(neighbor.entry.id);
6406
6407
  if (!candidate || candidate.id === entry.id) {
@@ -7006,17 +7007,19 @@ function checkpointToProcessedMaps(checkpoint) {
7006
7007
  }
7007
7008
  return {
7008
7009
  phase1,
7009
- phase2: new Set(checkpoint.processed.phase2 ?? [])
7010
+ phase2: new Set(checkpoint.processed.phase2 ?? []),
7011
+ phase3: new Set(checkpoint.processed.phase3 ?? [])
7010
7012
  };
7011
7013
  }
7012
- function processedMapsToCheckpoint(phase1, phase2) {
7014
+ function processedMapsToCheckpoint(phase1, phase2, phase3) {
7013
7015
  const serializedPhase1 = {};
7014
7016
  for (const [type, fingerprints] of phase1.entries()) {
7015
7017
  serializedPhase1[type] = [...fingerprints];
7016
7018
  }
7017
7019
  return {
7018
7020
  phase1: serializedPhase1,
7019
- phase2: [...phase2]
7021
+ phase2: [...phase2],
7022
+ phase3: [...phase3]
7020
7023
  };
7021
7024
  }
7022
7025
  function createEmptyPlan(types) {
@@ -7044,8 +7047,10 @@ function createDefaultCheckpoint(dbPathSignature, optionsSignature, types) {
7044
7047
  optionsSignature,
7045
7048
  processed: {
7046
7049
  phase1: {},
7047
- phase2: []
7050
+ phase2: [],
7051
+ phase3: []
7048
7052
  },
7053
+ createdEntryIds: [],
7049
7054
  plan: createEmptyPlan(types)
7050
7055
  };
7051
7056
  }
@@ -7119,12 +7124,15 @@ async function processPhaseClusters(params, deps) {
7119
7124
  stats.clustersMerged += 1;
7120
7125
  stats.entriesConsolidatedFrom += item.cluster.entries.length;
7121
7126
  stats.canonicalEntriesCreated += 1;
7127
+ if (outcome.mergedEntryId) {
7128
+ params.context.createdEntryIds.add(outcome.mergedEntryId);
7129
+ }
7122
7130
  }
7123
7131
  params.checkpoint.phase = params.phase;
7124
7132
  params.checkpoint.projectIndex = params.projectIndex;
7125
7133
  params.checkpoint.typeIndex = params.typeIndex;
7126
7134
  params.checkpoint.clusterIndex = item.index + 1;
7127
- params.checkpoint.processed = processedMapsToCheckpoint(params.context.processedPhase1, params.context.processedPhase2);
7135
+ params.checkpoint.processed = processedMapsToCheckpoint(params.context.processedPhase1, params.context.processedPhase2, params.context.processedPhase3);
7128
7136
  await saveCheckpoint(params.checkpoint);
7129
7137
  if (isShutdownRequested()) {
7130
7138
  params.context.batchReached = true;
@@ -7229,6 +7237,8 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
7229
7237
  checkpoint,
7230
7238
  processedPhase1: processedMaps.phase1,
7231
7239
  processedPhase2: processedMaps.phase2,
7240
+ processedPhase3: processedMaps.phase3,
7241
+ createdEntryIds: new Set(checkpoint.createdEntryIds ?? []),
7232
7242
  minCluster,
7233
7243
  phase1Threshold,
7234
7244
  phase2Threshold,
@@ -7493,23 +7503,85 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
7493
7503
  }
7494
7504
  }
7495
7505
  }
7506
+ if (!context.batchReached && context.batchLimit && context.processedClustersInRun >= context.batchLimit) {
7507
+ context.batchReached = true;
7508
+ }
7509
+ if (!context.batchReached && context.createdEntryIds.size >= 2) {
7510
+ onLog(`Phase 3: Post-merge dedup (${context.createdEntryIds.size} new entries)...`);
7511
+ for (let projectIndex = 0; projectIndex < phase1PlanByProject.length; projectIndex += 1) {
7512
+ if (isShutdownRequested()) {
7513
+ context.batchReached = true;
7514
+ break;
7515
+ }
7516
+ if (context.batchReached) {
7517
+ break;
7518
+ }
7519
+ const projectPlan = phase1PlanByProject[projectIndex];
7520
+ const project = projectPlan.project;
7521
+ const projectLabel = project ?? "(untagged)";
7522
+ const dedupClusters = await resolvedDeps.buildClustersFn(db, {
7523
+ simThreshold: phase1Threshold,
7524
+ minCluster: 2,
7525
+ maxClusterSize: phase1MaxClusterSize,
7526
+ platform,
7527
+ project,
7528
+ idempotencyDays: 0,
7529
+ verbose: options.verbose,
7530
+ onLog: options.verbose ? onLog : void 0
7531
+ });
7532
+ const relevantClusters = dedupClusters.filter(
7533
+ (cluster) => cluster.entries.some((entry) => context.createdEntryIds.has(entry.id))
7534
+ );
7535
+ if (relevantClusters.length === 0) {
7536
+ continue;
7537
+ }
7538
+ onLog(`Phase 3: Found ${relevantClusters.length} dedup clusters for project=${projectLabel}`);
7539
+ const phase3Stats = await processPhaseClusters(
7540
+ {
7541
+ db,
7542
+ clusters: relevantClusters,
7543
+ phase: 3,
7544
+ projectIndex,
7545
+ type: "post-merge-dedup",
7546
+ typeIndex: 0,
7547
+ llmClient,
7548
+ embeddingApiKey,
7549
+ options,
7550
+ checkpoint: context.checkpoint,
7551
+ processedSet: context.processedPhase3,
7552
+ context
7553
+ },
7554
+ resolvedDeps
7555
+ );
7556
+ if (context.report.phase3) {
7557
+ updateAggregateStats(context.report.phase3, phase3Stats);
7558
+ } else {
7559
+ context.report.phase3 = phase3Stats;
7560
+ }
7561
+ if (context.batchReached) {
7562
+ break;
7563
+ }
7564
+ }
7565
+ }
7496
7566
  context.report.phase1.types = phase1Types.map((type) => phase1StatsByType.get(type)).filter((item) => Boolean(item));
7497
7567
  }
7498
7568
  await runFinalization(db, dryRun, onWarn, resolvedDeps);
7499
7569
  context.report.entriesAfter = (await Promise.all(projectGroups.map((project) => resolvedDeps.countActiveEntriesFn(db, platform, project)))).reduce((sum, count) => sum + count, 0);
7500
7570
  context.report.progress.partial = context.batchReached;
7501
- context.report.progress.processedClusters = context.report.phase1.totals.clustersProcessed + (context.report.phase2?.clustersProcessed ?? 0);
7571
+ const estimatedPhasesProcessed = context.report.phase1.totals.clustersProcessed + (context.report.phase2?.clustersProcessed ?? 0);
7572
+ context.report.progress.processedClusters = estimatedPhasesProcessed + (context.report.phase3?.clustersProcessed ?? 0);
7502
7573
  context.report.progress.remainingClusters = Math.max(
7503
- context.report.estimate.totalClusters - context.report.phase1.totals.skippedByResume - (context.report.phase2?.skippedByResume ?? 0) - context.report.progress.processedClusters,
7574
+ context.report.estimate.totalClusters - context.report.phase1.totals.skippedByResume - (context.report.phase2?.skippedByResume ?? 0) - estimatedPhasesProcessed,
7504
7575
  0
7505
7576
  );
7506
- context.report.summary.totalLlmCalls = context.report.phase1.totals.llmCalls + (context.report.phase2?.llmCalls ?? 0);
7507
- context.report.summary.totalFlagged = context.report.phase1.totals.mergesFlagged + (context.report.phase2?.mergesFlagged ?? 0);
7508
- context.report.summary.totalCanonicalEntriesCreated = context.report.phase1.totals.canonicalEntriesCreated + (context.report.phase2?.canonicalEntriesCreated ?? 0);
7509
- context.report.summary.totalEntriesConsolidatedFrom = context.report.phase1.totals.entriesConsolidatedFrom + (context.report.phase2?.entriesConsolidatedFrom ?? 0);
7577
+ context.report.summary.totalLlmCalls = context.report.phase1.totals.llmCalls + (context.report.phase2?.llmCalls ?? 0) + (context.report.phase3?.llmCalls ?? 0);
7578
+ context.report.summary.totalFlagged = context.report.phase1.totals.mergesFlagged + (context.report.phase2?.mergesFlagged ?? 0) + (context.report.phase3?.mergesFlagged ?? 0);
7579
+ context.report.summary.totalCanonicalEntriesCreated = context.report.phase1.totals.canonicalEntriesCreated + (context.report.phase2?.canonicalEntriesCreated ?? 0) + (context.report.phase3?.canonicalEntriesCreated ?? 0);
7580
+ context.report.summary.totalEntriesConsolidatedFrom = context.report.phase1.totals.entriesConsolidatedFrom + (context.report.phase2?.entriesConsolidatedFrom ?? 0) + (context.report.phase3?.entriesConsolidatedFrom ?? 0);
7510
7581
  if (context.batchReached) {
7511
- context.checkpoint.phase = context.report.phase2 ? 2 : 1;
7512
- context.checkpoint.processed = processedMapsToCheckpoint(context.processedPhase1, context.processedPhase2);
7582
+ context.checkpoint.phase = context.report.phase3 ? 3 : context.report.phase2 ? 2 : 1;
7583
+ context.checkpoint.processed = processedMapsToCheckpoint(context.processedPhase1, context.processedPhase2, context.processedPhase3);
7584
+ context.checkpoint.createdEntryIds = [...context.createdEntryIds];
7513
7585
  await saveCheckpoint(context.checkpoint);
7514
7586
  } else {
7515
7587
  await clearCheckpoint();
@@ -7777,6 +7849,15 @@ function renderTextReport(stats, dryRun) {
7777
7849
  `| +- LLM calls: ${formatNumber(stats.phase2.llmCalls)}`
7778
7850
  );
7779
7851
  }
7852
+ if (stats.phase3) {
7853
+ lines.push(
7854
+ "|",
7855
+ "| Phase 3: Post-Merge Dedup",
7856
+ `| +- Clusters processed: ${formatNumber(stats.phase3.clustersProcessed)} / ${formatNumber(stats.phase3.clustersFound)}`,
7857
+ `| +- Clusters merged: ${formatNumber(stats.phase3.clustersMerged)}`,
7858
+ `| +- LLM calls: ${formatNumber(stats.phase3.llmCalls)}`
7859
+ );
7860
+ }
7780
7861
  lines.push(
7781
7862
  "|",
7782
7863
  "| Summary",
@@ -17044,8 +17125,8 @@ var initWizardRuntime = {
17044
17125
  scanSessionFiles,
17045
17126
  runIngestCommand,
17046
17127
  runConsolidateCommand,
17047
- runDaemonInstallCommand,
17048
- runDaemonStopCommand,
17128
+ runWatcherInstallCommand,
17129
+ runWatcherStopCommand,
17049
17130
  runDbResetCommand
17050
17131
  };
17051
17132
  async function runInitWizard(options) {
@@ -17496,7 +17577,7 @@ but requires clearing your existing knowledge database first.`
17496
17577
  if (process.platform === "darwin") {
17497
17578
  spinner4.start("Stopping watcher...");
17498
17579
  try {
17499
- await initWizardRuntime.runDaemonStopCommand({});
17580
+ await initWizardRuntime.runWatcherStopCommand({});
17500
17581
  } catch {
17501
17582
  }
17502
17583
  spinner4.stop("Watcher stopped");
@@ -17633,27 +17714,27 @@ Estimated cost with ${model}:
17633
17714
  }
17634
17715
  if (setupWatcher) {
17635
17716
  const spinner4 = clack7.spinner();
17636
- spinner4.start("Installing watcher daemon...");
17717
+ spinner4.start("Installing watcher...");
17637
17718
  try {
17638
- const daemonResult = await initWizardRuntime.runDaemonInstallCommand({
17719
+ const watcherResult = await initWizardRuntime.runWatcherInstallCommand({
17639
17720
  force: true,
17640
17721
  interval: 120,
17641
17722
  dir: selectedPlatform.sessionsDir,
17642
17723
  platform: selectedPlatform.id
17643
17724
  });
17644
- if (daemonResult.exitCode === 0) {
17645
- spinner4.stop("Watcher daemon installed and running (120s interval)");
17725
+ if (watcherResult.exitCode === 0) {
17726
+ spinner4.stop("Watcher installed and running (120s interval)");
17646
17727
  watcherStatus = "Running (120s interval)";
17647
17728
  } else {
17648
17729
  spinner4.stop(
17649
- `Daemon install failed. Run manually:
17730
+ `Watcher install failed. Run manually:
17650
17731
  agenr watch --dir ${selectedPlatform.sessionsDir} --platform ${selectedPlatform.id}`
17651
17732
  );
17652
17733
  watcherStatus = "Failed";
17653
17734
  }
17654
17735
  } catch (error) {
17655
17736
  const message = error instanceof Error ? error.message : String(error);
17656
- spinner4.stop(`Daemon install failed: ${message}`);
17737
+ spinner4.stop(`Watcher install failed: ${message}`);
17657
17738
  watcherStatus = "Failed";
17658
17739
  }
17659
17740
  } else {
@@ -21448,35 +21529,40 @@ function createProgram() {
21448
21529
  program.command("mcp").description("Start MCP server for cross-tool AI memory").option("--db <path>", "Database path override").option("--verbose", "Log requests to stderr", false).action(async (opts) => {
21449
21530
  await runMcpCommand(opts);
21450
21531
  });
21451
- const daemonCommand = program.command("daemon").description("Manage the agenr watch daemon");
21452
- daemonCommand.command("install").description("Install and start the watch daemon (macOS launchd)").option("--force", "Overwrite existing launchd plist", false).option("--interval <seconds>", "Watch interval for daemon mode", parseIntOption, 120).option("--dir <path>", "Sessions directory to watch (overrides auto-detection)").option("--platform <name>", "Platform name (openclaw, claude-code, codex, plaud)").option("--node-path <path>", "Node binary path override for launchd").option("--context <path>", "Regenerate context file after each cycle").action(async (opts) => {
21453
- const result = await runDaemonInstallCommand(opts);
21454
- process.exitCode = result.exitCode;
21455
- });
21456
- daemonCommand.command("start").description("Start the watch daemon if installed").action(async () => {
21457
- const result = await runDaemonStartCommand({});
21458
- process.exitCode = result.exitCode;
21459
- });
21460
- daemonCommand.command("stop").description("Stop the watch daemon without uninstalling").action(async () => {
21461
- const result = await runDaemonStopCommand({});
21462
- process.exitCode = result.exitCode;
21463
- });
21464
- daemonCommand.command("restart").description("Restart the watch daemon").action(async () => {
21465
- const result = await runDaemonRestartCommand({});
21466
- process.exitCode = result.exitCode;
21467
- });
21468
- daemonCommand.command("uninstall").description("Stop and remove the watch daemon").option("--yes", "Skip confirmation prompt", false).action(async (opts) => {
21469
- const result = await runDaemonUninstallCommand(opts);
21470
- process.exitCode = result.exitCode;
21471
- });
21472
- daemonCommand.command("status").description("Show daemon status and recent logs").option("--lines <n>", "Number of log lines to include", parseIntOption, 20).action(async (opts) => {
21473
- const result = await runDaemonStatusCommand(opts);
21474
- process.exitCode = result.exitCode;
21475
- });
21476
- daemonCommand.command("logs").description("Show or follow daemon logs").option("--lines <n>", "Number of log lines", parseIntOption, 100).option("--follow", "Follow logs continuously", false).action(async (opts) => {
21477
- const result = await runDaemonLogsCommand(opts);
21478
- process.exitCode = result.exitCode;
21479
- });
21532
+ const registerWatcherSubcommands = (command) => {
21533
+ command.command("install").description("Install and start the watcher (macOS launchd)").option("--force", "Overwrite existing launchd plist", false).option("--interval <seconds>", "Watch interval for watcher mode", parseIntOption, 120).option("--dir <path>", "Sessions directory to watch (overrides auto-detection)").option("--platform <name>", "Platform name (openclaw, claude-code, codex, plaud)").option("--node-path <path>", "Node binary path override for launchd").option("--context <path>", "Regenerate context file after each cycle").action(async (opts) => {
21534
+ const result = await runWatcherInstallCommand(opts);
21535
+ process.exitCode = result.exitCode;
21536
+ });
21537
+ command.command("start").description("Start the watcher if installed").action(async () => {
21538
+ const result = await runWatcherStartCommand({});
21539
+ process.exitCode = result.exitCode;
21540
+ });
21541
+ command.command("stop").description("Stop the watcher without uninstalling").action(async () => {
21542
+ const result = await runWatcherStopCommand({});
21543
+ process.exitCode = result.exitCode;
21544
+ });
21545
+ command.command("restart").description("Restart the watcher").action(async () => {
21546
+ const result = await runWatcherRestartCommand({});
21547
+ process.exitCode = result.exitCode;
21548
+ });
21549
+ command.command("uninstall").description("Stop and remove the watcher").option("--yes", "Skip confirmation prompt", false).action(async (opts) => {
21550
+ const result = await runWatcherUninstallCommand(opts);
21551
+ process.exitCode = result.exitCode;
21552
+ });
21553
+ command.command("status").description("Show watcher status and recent logs").option("--lines <n>", "Number of log lines to include", parseIntOption, 20).action(async (opts) => {
21554
+ const result = await runWatcherStatusCommand(opts);
21555
+ process.exitCode = result.exitCode;
21556
+ });
21557
+ command.command("logs").description("Show or follow watcher logs").option("--lines <n>", "Number of log lines", parseIntOption, 100).option("--follow", "Follow logs continuously", false).action(async (opts) => {
21558
+ const result = await runWatcherLogsCommand(opts);
21559
+ process.exitCode = result.exitCode;
21560
+ });
21561
+ };
21562
+ const watcherCommand = program.command("watcher").description("Manage the agenr watcher");
21563
+ registerWatcherSubcommands(watcherCommand);
21564
+ const daemonAliasCommand = program.command("daemon", { hidden: true }).description("Alias for watcher (deprecated)");
21565
+ registerWatcherSubcommands(daemonAliasCommand);
21480
21566
  const dbCommand = program.command("db").description("Manage the local knowledge database");
21481
21567
  dbCommand.command("stats").description("Show database statistics").option("--db <path>", "Database path override").option("--platform <name>", "Filter stats by platform").option("--project <name>", "Filter by project (repeatable)", (val, prev) => [...prev, val], []).option("--exclude-project <name>", "Exclude entries from project (repeatable)", (val, prev) => [...prev, val], []).action(async (opts) => {
21482
21568
  await runDbStatsCommand({ db: opts.db, platform: opts.platform, project: opts.project, excludeProject: opts.excludeProject });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"
@@ -11,6 +11,13 @@
11
11
  "bin": {
12
12
  "agenr": "dist/cli.js"
13
13
  },
14
+ "scripts": {
15
+ "build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
16
+ "dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "typecheck": "tsc --noEmit"
20
+ },
14
21
  "dependencies": {
15
22
  "@clack/prompts": "^1.0.1",
16
23
  "@libsql/client": "^0.17.0",
@@ -54,11 +61,9 @@
54
61
  "README.md"
55
62
  ],
56
63
  "author": "agenr-ai",
57
- "scripts": {
58
- "build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
59
- "dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
60
- "test": "vitest run",
61
- "test:watch": "vitest",
62
- "typecheck": "tsc --noEmit"
64
+ "pnpm": {
65
+ "overrides": {
66
+ "fast-xml-parser": "^5.3.6"
67
+ }
63
68
  }
64
- }
69
+ }