agenr 0.8.14 → 0.8.17

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.17] - 2026-02-23
4
+
5
+ ### Changed
6
+ - chore: rebuild dist to include browse mode CLI flag inadvertently omitted from 0.8.16 publish
7
+
8
+ ## [0.8.16] - 2026-02-23
9
+
10
+ ### Added
11
+ - feat(recall): new temporal browse mode for recall via `agenr recall --browse` and MCP `agenr_recall` with `context="browse"` (issue #190)
12
+ - docs(recall): added `docs/usage/recall.md` with browse-mode CLI and MCP usage examples
13
+
14
+ ### Changed
15
+ - recall browse mode now uses a SQL-only path that requires no query and performs zero embedding/OpenAI API calls
16
+ - browse mode does not increment recall metadata (`recall_count`, `last_recalled_at`, `recall_intervals`)
17
+ - OpenClaw plugin tool wiring now maps `context="browse"` to the CLI `--browse` flag (and omits query/context positional args appropriately)
18
+
19
+ ### Tests
20
+ - test(recall): added browse-mode coverage in DB recall, CLI command recall, MCP server recall, and OpenClaw plugin recall tool argument wiring
21
+
22
+ ## [0.8.15] - 2026-02-23
23
+
24
+ ### Fixed
25
+ - fix(consolidate): switch GROUP_CONCAT separator from comma to pipe in buildClusters to prevent silent tag corruption when tag values contain commas (issue #155)
26
+ - fix(consolidate): Tier 1 near-exact duplicate merge now preserves the highest importance across merged entries by raising the keeper's `importance` floor to the group max (issue #156)
27
+ - fix(consolidate): Tier 1 near-exact duplicate merge now preserves oldest provenance by inheriting the oldest `created_at` across the merge group into the keeper (issue #156)
28
+ - test(consolidate): new cluster.test.ts with pipe-separator roundtrip and comma-in-tag regression coverage (issue #155)
29
+ - test(consolidate): added merge coverage for tag union transfer, keeper importance floor, and keeper `created_at` inheritance in rules consolidation tests (issue #156)
30
+
3
31
  ## [0.8.13] - 2026-02-23
4
32
 
5
33
  ### Fixed
package/dist/cli-main.js CHANGED
@@ -3873,6 +3873,13 @@ function scoreEntryWithBreakdown(entry, vectorSim, ftsMatch, now, freshnessNow =
3873
3873
  function scoreEntry(entry, vectorSim, ftsMatch, now) {
3874
3874
  return scoreEntryWithBreakdown(entry, vectorSim, ftsMatch, now).score;
3875
3875
  }
3876
+ function scoreBrowseEntry(entry, now) {
3877
+ const daysOld = parseDaysBetween(now, entry.created_at);
3878
+ const HALF_LIFE_DAYS = 30;
3879
+ const recencyFactor = Math.exp(-(daysOld / HALF_LIFE_DAYS) * Math.LN2);
3880
+ const impScore = importanceScore(entry.importance);
3881
+ return clamp01(impScore * recencyFactor);
3882
+ }
3876
3883
  function shapeRecallText(text2, context) {
3877
3884
  const trimmedText = text2.trim();
3878
3885
  if (!trimmedText) {
@@ -4020,6 +4027,97 @@ async function fetchSessionCandidates(db, limit, context, platform, project, exc
4020
4027
  };
4021
4028
  });
4022
4029
  }
4030
+ async function fetchBrowseCandidates(db, query, limit, now) {
4031
+ const normalizedProject = parseProjectList(query.project);
4032
+ const normalizedExclude = parseProjectList(query.excludeProject);
4033
+ const projectSql = buildProjectFilter({
4034
+ column: "project",
4035
+ strict: Boolean(query.projectStrict && normalizedProject.length > 0),
4036
+ project: normalizedProject.length > 0 ? normalizedProject : void 0,
4037
+ excludeProject: normalizedExclude.length > 0 ? normalizedExclude : void 0
4038
+ });
4039
+ const whereClauses = [
4040
+ "superseded_by IS NULL",
4041
+ "retired = 0"
4042
+ ];
4043
+ if (projectSql.clause.trim()) {
4044
+ whereClauses.push(projectSql.clause.trim().replace(/^AND\s+/i, ""));
4045
+ }
4046
+ const args = [...projectSql.args];
4047
+ const since = parseSince(query.since, now);
4048
+ if (since) {
4049
+ whereClauses.push("created_at >= ?");
4050
+ args.push(since.toISOString());
4051
+ }
4052
+ const until = parseSince(query.until, now);
4053
+ if (until) {
4054
+ whereClauses.push("created_at <= ?");
4055
+ args.push(until.toISOString());
4056
+ }
4057
+ if (query.minImportance !== void 0) {
4058
+ whereClauses.push("importance >= ?");
4059
+ args.push(Math.max(1, Math.min(10, Math.round(query.minImportance))));
4060
+ }
4061
+ if (query.types && query.types.length > 0) {
4062
+ const placeholders = query.types.map(() => "?").join(", ");
4063
+ whereClauses.push(`type IN (${placeholders})`);
4064
+ args.push(...query.types);
4065
+ }
4066
+ if (query.platform) {
4067
+ whereClauses.push("platform = ?");
4068
+ args.push(query.platform);
4069
+ }
4070
+ args.push(limit);
4071
+ const whereClause = whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : "";
4072
+ const result = await db.execute({
4073
+ sql: `
4074
+ SELECT
4075
+ id,
4076
+ type,
4077
+ subject,
4078
+ canonical_key,
4079
+ content,
4080
+ importance,
4081
+ expiry,
4082
+ scope,
4083
+ platform,
4084
+ project,
4085
+ source_file,
4086
+ source_context,
4087
+ embedding,
4088
+ created_at,
4089
+ updated_at,
4090
+ last_recalled_at,
4091
+ recall_count,
4092
+ recall_intervals,
4093
+ confirmations,
4094
+ contradictions,
4095
+ superseded_by,
4096
+ retired,
4097
+ retired_at,
4098
+ retired_reason,
4099
+ suppressed_contexts
4100
+ FROM entries
4101
+ ${whereClause}
4102
+ -- SQL pre-sort is a best-effort approximation only.
4103
+ -- Final order is determined by scoreBrowseEntry() (importance * recency decay)
4104
+ -- which re-sorts post-fetch. The over-fetch buffer (limit*3, min 50)
4105
+ -- ensures the final top-N are present in the candidate pool.
4106
+ ORDER BY importance DESC, created_at DESC
4107
+ LIMIT ?
4108
+ `,
4109
+ args
4110
+ });
4111
+ const ids = result.rows.map((row) => toStringValue2(row.id)).filter((id) => id.length > 0);
4112
+ const tagsByEntryId = await getTagsForEntryIds(db, ids);
4113
+ return result.rows.map((row) => {
4114
+ const entryId = toStringValue2(row.id);
4115
+ return {
4116
+ entry: mapStoredEntry(row, tagsByEntryId.get(entryId) ?? []),
4117
+ vectorSim: 0
4118
+ };
4119
+ });
4120
+ }
4023
4121
  async function runFts(db, text2, platform, project, excludeProject, projectStrict) {
4024
4122
  if (!text2.trim()) {
4025
4123
  return /* @__PURE__ */ new Set();
@@ -4085,10 +4183,10 @@ async function recall(db, query, apiKey, options = {}) {
4085
4183
  const excludeProject = query.excludeProject;
4086
4184
  const projectStrict = query.projectStrict === true;
4087
4185
  const isSessionStart = context === "session-start";
4088
- if (!text2 && context !== "session-start") {
4186
+ if (!text2 && context !== "session-start" && query.browse !== true) {
4089
4187
  throw new Error("Query text is required unless --context session-start is used.");
4090
4188
  }
4091
- if (!text2 && query.noBoost) {
4189
+ if (!text2 && query.noBoost && query.browse !== true) {
4092
4190
  throw new Error("--no-boost requires query text.");
4093
4191
  }
4094
4192
  const normalizedTags = normalizeTags(query.tags);
@@ -4113,6 +4211,41 @@ async function recall(db, query, apiKey, options = {}) {
4113
4211
  );
4114
4212
  }
4115
4213
  const allowedScopes = resolveScopeSet(query.scope);
4214
+ if (query.browse === true) {
4215
+ if (text2) {
4216
+ process.stderr.write(
4217
+ "[agenr] Warning: --browse mode ignores query text. Remove the query or use agenr recall <query> for semantic search.\n"
4218
+ );
4219
+ }
4220
+ if (query.noBoost) {
4221
+ throw new Error("--no-boost is not applicable in browse mode.");
4222
+ }
4223
+ const requestedLimit = Number.isFinite(query.limit) && (query.limit ?? 0) > 0 ? Math.floor(query.limit) : DEFAULT_LIMIT;
4224
+ const browseLimit = Math.max(requestedLimit * 3, 50);
4225
+ const browseCandidates = await fetchBrowseCandidates(db, query, browseLimit, now);
4226
+ const filtered2 = browseCandidates.filter(
4227
+ (candidate) => passesFilters(candidate.entry, query, cutoff, ceiling, allowedScopes, normalizedTags, false)
4228
+ );
4229
+ const scored2 = filtered2.map((candidate) => {
4230
+ const score = scoreBrowseEntry(candidate.entry, now);
4231
+ return {
4232
+ entry: candidate.entry,
4233
+ score,
4234
+ scores: {
4235
+ vector: 0,
4236
+ recency: recency(parseDaysBetween(now, candidate.entry.created_at), candidate.entry.expiry),
4237
+ importance: importanceScore(candidate.entry.importance),
4238
+ recall: 0,
4239
+ freshness: 1,
4240
+ todoPenalty: 1,
4241
+ fts: 0,
4242
+ spacing: 1
4243
+ }
4244
+ };
4245
+ });
4246
+ scored2.sort((a, b) => b.score - a.score);
4247
+ return scored2.slice(0, requestedLimit);
4248
+ }
4116
4249
  const hasDateBounds = cutoff !== void 0 || ceiling !== void 0;
4117
4250
  let candidates;
4118
4251
  let effectiveText = text2;
@@ -6540,7 +6673,7 @@ async function mergeNearExactDuplicates(db, options) {
6540
6673
  args.push(...projectSql.args);
6541
6674
  const result = await db.execute({
6542
6675
  sql: `
6543
- SELECT id, type, subject, content, project, embedding, confirmations, recall_count, created_at
6676
+ SELECT id, type, subject, content, project, importance, embedding, confirmations, recall_count, created_at
6544
6677
  FROM entries
6545
6678
  WHERE superseded_by IS NULL
6546
6679
  AND retired = 0
@@ -6556,6 +6689,7 @@ async function mergeNearExactDuplicates(db, options) {
6556
6689
  subject: toStringValue2(row.subject),
6557
6690
  content: toStringValue2(row.content),
6558
6691
  project: toProjectValue(row.project),
6692
+ importance: Number.isFinite(toNumber3(row.importance)) ? toNumber3(row.importance) : 5,
6559
6693
  embedding: mapBufferToVector3(row.embedding),
6560
6694
  confirmations: Number.isFinite(toNumber3(row.confirmations)) ? toNumber3(row.confirmations) : 0,
6561
6695
  recallCount: Number.isFinite(toNumber3(row.recall_count)) ? toNumber3(row.recall_count) : 0,
@@ -6628,6 +6762,12 @@ async function mergeNearExactDuplicates(db, options) {
6628
6762
  continue;
6629
6763
  }
6630
6764
  const totalConfirmations = sorted.reduce((sum, entry) => sum + entry.confirmations, 0);
6765
+ const maxImportance = sorted.reduce((max, entry) => Math.max(max, entry.importance ?? 5), 0);
6766
+ const oldestCreatedAt = sorted.reduce((oldest, entry) => {
6767
+ const t = Date.parse(entry.createdAt);
6768
+ const o = Date.parse(oldest);
6769
+ return Number.isFinite(t) && t < (Number.isFinite(o) ? o : Infinity) ? entry.createdAt : oldest;
6770
+ }, keeper.createdAt);
6631
6771
  for (const source of sources) {
6632
6772
  await db.execute({
6633
6773
  sql: `
@@ -6662,10 +6802,12 @@ async function mergeNearExactDuplicates(db, options) {
6662
6802
  UPDATE entries
6663
6803
  SET merged_from = ?,
6664
6804
  consolidated_at = datetime('now'),
6665
- confirmations = ?
6805
+ confirmations = ?,
6806
+ importance = CASE WHEN importance < ? THEN ? ELSE importance END,
6807
+ created_at = CASE WHEN created_at > ? THEN ? ELSE created_at END
6666
6808
  WHERE id = ?
6667
6809
  `,
6668
- args: [sources.length, totalConfirmations, keeper.id]
6810
+ args: [sources.length, totalConfirmations, maxImportance, maxImportance, oldestCreatedAt, oldestCreatedAt, keeper.id]
6669
6811
  });
6670
6812
  }
6671
6813
  return mergedCount;
@@ -6882,7 +7024,7 @@ function mapActiveEmbeddedEntry(row) {
6882
7024
  const projectRaw = toStringValue3(row.project);
6883
7025
  const project = projectRaw.trim().length > 0 ? projectRaw.trim().toLowerCase() : null;
6884
7026
  const tagsRaw = toStringValue3(row.tags_csv);
6885
- const tags = tagsRaw.length > 0 ? tagsRaw.split(",").map((tag) => tag.trim()).filter(Boolean) : [];
7027
+ const tags = tagsRaw.length > 0 ? tagsRaw.split("|").map((tag) => tag.trim()).filter(Boolean) : [];
6886
7028
  return {
6887
7029
  id,
6888
7030
  type: toStringValue3(row.type),
@@ -6943,7 +7085,7 @@ async function buildClusters(db, options = {}) {
6943
7085
  e.merged_from,
6944
7086
  e.consolidated_at,
6945
7087
  (
6946
- SELECT GROUP_CONCAT(t.tag, ',')
7088
+ SELECT GROUP_CONCAT(t.tag, '|')
6947
7089
  FROM tags t
6948
7090
  WHERE t.entry_id = e.id
6949
7091
  ) AS tags_csv
@@ -15768,8 +15910,8 @@ var TOOL_DEFINITIONS = [
15768
15910
  },
15769
15911
  context: {
15770
15912
  type: "string",
15771
- description: "Recall context. Use 'session-start' for fast bootstrap without embedding (no query needed).",
15772
- enum: ["default", "session-start"]
15913
+ description: "Recall context. Use 'session-start' for fast bootstrap (no query needed). Use 'browse' for temporal browsing sorted by date+importance without semantic search (no query needed, zero API calls).",
15914
+ enum: ["default", "session-start", "browse"]
15773
15915
  },
15774
15916
  limit: {
15775
15917
  type: "integer",
@@ -16152,6 +16294,27 @@ function formatRecallText(query, results) {
16152
16294
  }
16153
16295
  return lines.join("\n");
16154
16296
  }
16297
+ function formatBrowseText(results) {
16298
+ if (results.length === 0) {
16299
+ return "No entries found in the specified time window.";
16300
+ }
16301
+ const lines = [`Found ${results.length} entries (browse mode):`, ""];
16302
+ for (let i = 0; i < results.length; i += 1) {
16303
+ const result = results[i];
16304
+ if (!result) {
16305
+ continue;
16306
+ }
16307
+ const dateStr = new Date(result.entry.created_at).toISOString().slice(0, 10);
16308
+ lines.push(
16309
+ `[${i + 1}] [id=${result.entry.id}] [${dateStr}] importance=${result.entry.importance} type=${result.entry.type}`
16310
+ );
16311
+ lines.push(result.entry.content);
16312
+ if (i < results.length - 1) {
16313
+ lines.push("");
16314
+ }
16315
+ }
16316
+ return lines.join("\n");
16317
+ }
16155
16318
  function formatStoreSummary(result) {
16156
16319
  const total = result.added + result.updated + result.skipped + result.superseded;
16157
16320
  const parts = [`${result.added} new`, `${result.updated} updated`, `${result.skipped} duplicates skipped`];
@@ -16303,12 +16466,12 @@ function createMcpServer(options = {}, deps = {}) {
16303
16466
  async function callRecallTool(args) {
16304
16467
  const contextRaw = typeof args.context === "string" ? args.context.trim().toLowerCase() : "default";
16305
16468
  const context = contextRaw || "default";
16306
- if (context !== "default" && context !== "session-start") {
16307
- throw new RpcError(JSON_RPC_INVALID_PARAMS, "context must be one of: default, session-start");
16469
+ if (context !== "default" && context !== "session-start" && context !== "browse") {
16470
+ throw new RpcError(JSON_RPC_INVALID_PARAMS, "context must be one of: default, session-start, browse");
16308
16471
  }
16309
16472
  const query = typeof args.query === "string" ? args.query.trim() : "";
16310
- if (!query && context !== "session-start") {
16311
- throw new RpcError(JSON_RPC_INVALID_PARAMS, "query is required unless context is session-start");
16473
+ if (!query && context === "default") {
16474
+ throw new RpcError(JSON_RPC_INVALID_PARAMS, "query is required unless context is session-start or browse");
16312
16475
  }
16313
16476
  const limit = parsePositiveInt5(args.limit, 10, "limit");
16314
16477
  const threshold = parseThreshold(args.threshold);
@@ -16359,7 +16522,26 @@ function createMcpServer(options = {}, deps = {}) {
16359
16522
  }
16360
16523
  const db = await ensureDb();
16361
16524
  let results;
16362
- if (context === "session-start") {
16525
+ if (context === "browse") {
16526
+ results = await resolvedDeps.recallFn(
16527
+ db,
16528
+ {
16529
+ text: void 0,
16530
+ context: "browse",
16531
+ browse: true,
16532
+ limit,
16533
+ types,
16534
+ since,
16535
+ until,
16536
+ platform: platform ?? void 0,
16537
+ project,
16538
+ projectStrict: projectStrict ? true : void 0,
16539
+ scope: "private",
16540
+ noUpdate: true
16541
+ },
16542
+ ""
16543
+ );
16544
+ } else if (context === "session-start") {
16363
16545
  const grouped = await sessionStartRecall(db, {
16364
16546
  query: {
16365
16547
  context,
@@ -16406,6 +16588,9 @@ function createMcpServer(options = {}, deps = {}) {
16406
16588
  result.entry.last_recalled_at = nowIso;
16407
16589
  }
16408
16590
  }
16591
+ if (context === "browse") {
16592
+ return formatBrowseText(filtered);
16593
+ }
16409
16594
  return formatRecallText(query || context, filtered);
16410
16595
  }
16411
16596
  async function callStoreTool(args) {
@@ -16874,6 +17059,24 @@ ${ui.bold(section.title)}`, clackOutput);
16874
17059
  index += 1;
16875
17060
  }
16876
17061
  }
17062
+ function printBrowseResults(results, elapsedMs) {
17063
+ const clackOutput = { output: process.stderr };
17064
+ clack6.log.info(`${ui.bold(String(results.length))} entries (browse mode, ${elapsedMs}ms)`, clackOutput);
17065
+ if (results.length === 0) {
17066
+ clack6.log.info("No entries found in the specified time window.", clackOutput);
17067
+ return;
17068
+ }
17069
+ let index = 1;
17070
+ for (const result of results) {
17071
+ const dateStr = new Date(result.entry.created_at).toISOString().slice(0, 10);
17072
+ clack6.log.info(
17073
+ `${index}. [${dateStr}] [importance=${result.entry.importance}] (${result.entry.type}) ${result.entry.subject}: ${result.entry.content}`,
17074
+ clackOutput
17075
+ );
17076
+ clack6.log.info(` tags: ${result.entry.tags.length > 0 ? result.entry.tags.join(", ") : "none"}`, clackOutput);
17077
+ index += 1;
17078
+ }
17079
+ }
16877
17080
  async function runRecallCommand(queryInput, options, deps) {
16878
17081
  const resolvedDeps = {
16879
17082
  readConfigFn: deps?.readConfigFn ?? readConfig,
@@ -16891,12 +17094,24 @@ async function runRecallCommand(queryInput, options, deps) {
16891
17094
  const queryText = queryInput?.trim() ?? "";
16892
17095
  const context = options.context?.trim() || "default";
16893
17096
  const isSessionStart = context === "session-start";
16894
- if (!queryText && !isSessionStart) {
16895
- throw new Error("Provide a query or use --context session-start.");
17097
+ const isBrowse = options.browse === true;
17098
+ if (isBrowse && isSessionStart) {
17099
+ throw new Error("--browse and --context session-start cannot be combined.");
17100
+ }
17101
+ if (!queryText && !isSessionStart && !isBrowse) {
17102
+ throw new Error("Provide a query, use --context session-start, or use --browse.");
17103
+ }
17104
+ if (options.noBoost && isBrowse) {
17105
+ throw new Error("--no-boost is not applicable in browse mode.");
16896
17106
  }
16897
17107
  if (options.noBoost && !queryText) {
16898
17108
  throw new Error("--no-boost requires a query.");
16899
17109
  }
17110
+ if (isBrowse && queryText) {
17111
+ process.stderr.write(
17112
+ "[agenr] Warning: --browse mode ignores query text. Remove the query or use agenr recall <query> for semantic search.\n"
17113
+ );
17114
+ }
16900
17115
  const limit = parsePositiveInt6(options.limit, DEFAULT_LIMIT4, "--limit");
16901
17116
  const budget = options.budget !== void 0 ? parsePositiveInt6(options.budget, DEFAULT_LIMIT4, "--budget") : void 0;
16902
17117
  const parsedTypes = parseCsv(options.type);
@@ -16951,7 +17166,7 @@ async function runRecallCommand(queryInput, options, deps) {
16951
17166
  const sinceIso = parseSinceToIso(options.since, now, "Invalid --since value. Use 1h, 7d, 1m, 1y, or an ISO date.");
16952
17167
  const untilIso = parseSinceToIso(options.until, now, "Invalid --until value. Use 1h, 7d, 1m, 1y, or an ISO date.");
16953
17168
  const queryForRecall = {
16954
- text: queryText ? shapeRecallText(queryText, context) : void 0,
17169
+ text: queryText && !isBrowse ? shapeRecallText(queryText, context) : void 0,
16955
17170
  limit,
16956
17171
  types: types.length > 0 ? types : void 0,
16957
17172
  tags: tags.length > 0 ? tags : void 0,
@@ -16967,18 +17182,23 @@ async function runRecallCommand(queryInput, options, deps) {
16967
17182
  noBoost: options.noBoost === true,
16968
17183
  noUpdate: true,
16969
17184
  context,
16970
- budget
17185
+ budget,
17186
+ browse: isBrowse
16971
17187
  };
16972
17188
  const config = resolvedDeps.readConfigFn(process.env);
16973
17189
  const dbPath = options.db?.trim() || config?.db?.path;
16974
17190
  const db = resolvedDeps.getDbFn(dbPath);
16975
17191
  try {
16976
17192
  await resolvedDeps.initDbFn(db);
16977
- const apiKey = queryText ? resolvedDeps.resolveEmbeddingApiKeyFn(config, process.env) : "";
17193
+ const apiKey = queryText && !isBrowse ? resolvedDeps.resolveEmbeddingApiKeyFn(config, process.env) : "";
16978
17194
  const startedAt = Date.now();
16979
17195
  let finalResults = [];
16980
17196
  let budgetUsed = 0;
16981
- if (isSessionStart) {
17197
+ if (isBrowse) {
17198
+ const baseResults = await resolvedDeps.recallFn(db, queryForRecall, apiKey);
17199
+ finalResults = baseResults.map((result) => ({ ...result }));
17200
+ budgetUsed = finalResults.reduce((sum, item) => sum + estimateEntryTokens(item), 0);
17201
+ } else if (isSessionStart) {
16982
17202
  const grouped = await sessionStartRecall(db, {
16983
17203
  query: queryForRecall,
16984
17204
  apiKey,
@@ -16999,7 +17219,7 @@ async function runRecallCommand(queryInput, options, deps) {
16999
17219
  finalResults = consumed.selected.map((result) => ({ ...result }));
17000
17220
  }
17001
17221
  }
17002
- if (!options.noUpdate && finalResults.length > 0) {
17222
+ if (!isBrowse && !options.noUpdate && finalResults.length > 0) {
17003
17223
  const ids = finalResults.map((result) => result.entry.id);
17004
17224
  await resolvedDeps.updateRecallMetadataFn(db, ids, now);
17005
17225
  const nowIso = now.toISOString();
@@ -17010,7 +17230,7 @@ async function runRecallCommand(queryInput, options, deps) {
17010
17230
  }
17011
17231
  const stripped = finalResults.map((result) => stripEmbedding(result));
17012
17232
  const payload = {
17013
- query: queryText,
17233
+ query: isBrowse ? "[browse]" : queryText,
17014
17234
  results: stripped,
17015
17235
  total: stripped.length,
17016
17236
  budget_used: budgetUsed,
@@ -17021,7 +17241,11 @@ async function runRecallCommand(queryInput, options, deps) {
17021
17241
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
17022
17242
  `);
17023
17243
  } else {
17024
- printHumanResults(stripped, elapsedMs, now, isSessionStart);
17244
+ if (isBrowse) {
17245
+ printBrowseResults(stripped, elapsedMs);
17246
+ } else {
17247
+ printHumanResults(stripped, elapsedMs, now, isSessionStart);
17248
+ }
17025
17249
  }
17026
17250
  clack6.outro(void 0, clackOutput);
17027
17251
  return {
@@ -19438,7 +19662,7 @@ function createProgram() {
19438
19662
  process.exitCode = result.exitCode;
19439
19663
  }
19440
19664
  );
19441
- program.command("recall").description("Recall knowledge from the local database").argument("[query]", "Natural language query").option("--limit <n>", "Maximum number of results", parseIntOption, 10).option("--type <types>", "Filter by comma-separated entry types").option("--tags <tags>", "Filter by comma-separated tags").option("--min-importance <n>", "Minimum importance: 1-10").option("--since <duration>", "Filter by recency (1h, 7d, 30d, 1y) or ISO timestamp").option("--until <date>", "Only entries at or before this time (ISO date or relative, e.g. 7d, 1m)").option("--expiry <level>", "Filter by expiry: core|permanent|temporary").option("--platform <name>", "Filter by platform: openclaw, claude-code, codex, plaud").option("--project <name>", "Filter by project (repeatable)", (val, prev) => [...prev, val], []).option("--exclude-project <name>", "Exclude entries from project (repeatable)", (val, prev) => [...prev, val], []).option("--strict", "Exclude NULL-project entries from results", false).option("--json", "Output JSON", false).option("--db <path>", "Database path override").option("--budget <tokens>", "Approximate token budget", parseIntOption).option("--context <mode>", "Context mode: default|session-start|topic:<query>", "default").option("--scope <level>", "Visibility scope: private|personal|public", "private").option("--no-boost", "Disable scoring boosts and use raw vector similarity", false).option("--no-update", "Do not increment recall metadata", false).action(async (query, opts) => {
19665
+ program.command("recall").description("Recall knowledge from the local database").argument("[query]", "Natural language query").option("--limit <n>", "Maximum number of results", parseIntOption, 10).option("--type <types>", "Filter by comma-separated entry types").option("--tags <tags>", "Filter by comma-separated tags").option("--min-importance <n>", "Minimum importance: 1-10").option("--since <duration>", "Filter by recency (1h, 7d, 30d, 1y) or ISO timestamp").option("--until <date>", "Only entries at or before this time (ISO date or relative, e.g. 7d, 1m)").option("--expiry <level>", "Filter by expiry: core|permanent|temporary").option("--platform <name>", "Filter by platform: openclaw, claude-code, codex, plaud").option("--project <name>", "Filter by project (repeatable)", (val, prev) => [...prev, val], []).option("--exclude-project <name>", "Exclude entries from project (repeatable)", (val, prev) => [...prev, val], []).option("--strict", "Exclude NULL-project entries from results", false).option("--json", "Output JSON", false).option("--db <path>", "Database path override").option("--budget <tokens>", "Approximate token budget", parseIntOption).option("--context <mode>", "Context mode: default|session-start|topic:<query>", "default").option("--scope <level>", "Visibility scope: private|personal|public", "private").option("--browse", "Skip semantic search and return entries sorted by importance and date", false).option("--no-boost", "Disable scoring boosts and use raw vector similarity", false).option("--no-update", "Do not increment recall metadata", false).action(async (query, opts) => {
19442
19666
  const result = await runRecallCommand(query, {
19443
19667
  limit: opts.limit,
19444
19668
  type: opts.type,
@@ -19456,6 +19680,7 @@ function createProgram() {
19456
19680
  budget: opts.budget,
19457
19681
  context: opts.context,
19458
19682
  scope: opts.scope,
19683
+ browse: opts.browse === true,
19459
19684
  noBoost: opts.noBoost === true,
19460
19685
  noUpdate: opts.noUpdate === true
19461
19686
  });
@@ -573,16 +573,19 @@ async function runRecallTool(agenrPath, params, defaultProject) {
573
573
  const args = ["recall", "--json"];
574
574
  const query = asString(params.query);
575
575
  const context = asString(params.context);
576
+ const isBrowse = context === "browse";
576
577
  const limit = asNumber(params.limit);
577
578
  const types = asString(params.types);
578
579
  const since = asString(params.since);
579
580
  const until = asString(params.until);
580
581
  const platform = asString(params.platform);
581
582
  const project = asString(params.project) || defaultProject;
582
- if (query) {
583
+ if (isBrowse) {
584
+ args.push("--browse");
585
+ } else if (query) {
583
586
  args.push(query);
584
587
  }
585
- if (context) {
588
+ if (context && !isBrowse) {
586
589
  args.push("--context", context);
587
590
  }
588
591
  if (limit !== void 0) {
@@ -1045,8 +1048,10 @@ var plugin = {
1045
1048
  query: Type.Optional(Type.String({ description: "What to search for." })),
1046
1049
  context: Type.Optional(
1047
1050
  Type.Union(
1048
- [Type.Literal("default"), Type.Literal("session-start")],
1049
- { description: "Use session-start for fast bootstrap without embedding." }
1051
+ [Type.Literal("default"), Type.Literal("session-start"), Type.Literal("browse")],
1052
+ {
1053
+ description: "Use session-start for fast bootstrap without embedding. Use browse for temporal browsing (date+importance, no query needed, no semantic search)."
1054
+ }
1050
1055
  )
1051
1056
  ),
1052
1057
  limit: Type.Optional(Type.Number({ description: "Max results (default: 10)." })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.8.14",
3
+ "version": "0.8.17",
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
+ }