opencode-session-recall 0.3.0 → 0.5.0

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/LICENSE CHANGED
@@ -1,7 +1,5 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 maelos
4
-
5
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
4
  of this software and associated documentation files (the "Software"), to deal
7
5
  in the Software without restriction, including without limitation the rights
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # opencode-session-recall
2
2
 
3
- **Give your agent a memory that survives compaction — without building another memory system.**
3
+ A plugin for [opencode](https://github.com/opencode-ai/opencode) that gives your agent a memory that survives compaction — without building another memory system.
4
4
 
5
- Most agent "memory" solutions add a new subsystem: vector databases, embedding pipelines, separate knowledge stores. They duplicate your data into yet another place that needs to be maintained, synced, and debugged.
5
+ opencode is an open-source AI coding agent that runs in your terminal. It manages long conversations through compaction: summarizing older context to keep the active window focused. But compaction means the agent forgets original tool outputs, earlier reasoning, the user's exact words.
6
6
 
7
- `opencode-session-recall` takes a different approach: **your conversation history is already the richest source of context you have.** opencode stores every message, every tool output, every reasoning trace in its database — even after compaction prunes them from the agent's context window. This plugin simply gives the agent tools to search and retrieve what's already there.
7
+ This plugin adds five tools to the agent's toolkit that let it search and retrieve that lost context on demand, within the current session, across all sessions in the project, or across every project on the machine.
8
8
 
9
- No embeddings. No vector store. No data duplication. Just direct access to the context your agent already generated.
9
+ **It doesn't create a separate memory store.** Most agent "memory" solutions add vector databases, embedding pipelines, or knowledge graphs — duplicating your data into yet another system. `opencode-session-recall` does none of that. opencode already stores every message, every tool output, every reasoning trace in its database, even after compaction prunes them from context. This plugin simply gives the agent access to what's already there.
10
+
11
+ No embeddings. No vector store. No data duplication. No setup. Just install the plugin and the agent can remember.
10
12
 
11
13
  ## What this enables
12
14
 
package/dist/context.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
- export declare function context(client: OpencodeClient): ToolDefinition;
3
+ import { type Limits } from "./types.js";
4
+ export declare function context(client: OpencodeClient, limits: Limits): ToolDefinition;
4
5
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,wBAAgB,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAuG9D"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAGpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,GACb,cAAc,CA2GhB"}
package/dist/extract.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Part, Message } from "@opencode-ai/sdk/v2";
2
- import type { PartOutput, MessageItem } from "./types.js";
2
+ import { type PartOutput, type MessageItem } from "./types.js";
3
3
  export declare function matches(text: string, query: string): boolean;
4
4
  export declare function searchable(part: Part): string[];
5
5
  export declare function snippet(text: string, query: string, width?: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAEJ,OAAO,EAGR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW1D,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CA2B/C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,MAAM,CAexE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAI1C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CA0D7C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;CACpB,GAAG,WAAW,CAgBd"}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAEJ,OAAO,EAGR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAS,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAYtE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CA4B/C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,MAAM,CAexE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAI1C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CA0D7C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;CACpB,GAAG,WAAW,CAgBd"}
package/dist/get.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../src/get.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,wBAAgB,GAAG,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA0D1D"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../src/get.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,wBAAgB,GAAG,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAkE1D"}
@@ -1,4 +1,5 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
- export declare function messages(client: OpencodeClient): ToolDefinition;
3
+ import { type Limits } from "./types.js";
4
+ export declare function messages(client: OpencodeClient, limits: Limits): ToolDefinition;
4
5
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAahE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA0G/D"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAYpB,wBAAgB,QAAQ,CACtB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,GACb,cAAc,CAgHhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-session-recall.d.ts","sourceRoot":"","sources":["../src/opencode-session-recall.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;;;;;AAkFlD,wBAGE"}
1
+ {"version":3,"file":"opencode-session-recall.d.ts","sourceRoot":"","sources":["../src/opencode-session-recall.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;;;;;AAmFlD,wBAGE"}
@@ -7,6 +7,22 @@ import {
7
7
  } from "@opencode-ai/plugin";
8
8
 
9
9
  // src/types.ts
10
+ var TOOLS = [
11
+ "recall",
12
+ "recall_get",
13
+ "recall_sessions",
14
+ "recall_context",
15
+ "recall_messages"
16
+ ];
17
+ var DEFAULTS = {
18
+ concurrency: 3,
19
+ maxSessions: 50,
20
+ maxResults: 50,
21
+ maxSessionList: 100,
22
+ maxMessages: 50,
23
+ maxWindow: 10,
24
+ defaultWidth: 200
25
+ };
10
26
  function errmsg(e) {
11
27
  if (e instanceof Error) return e.message;
12
28
  if (typeof e === "string") return e;
@@ -22,13 +38,17 @@ function errmsg(e) {
22
38
  }
23
39
 
24
40
  // src/sessions.ts
25
- function sessions(client, unscoped, global) {
41
+ function sessions(client, unscoped, global, limits) {
26
42
  return tool({
27
- description: `List sessions from the opencode database. Use this FIRST to discover which sessions exist, then search their content with recall. Returns session titles, directories, and timestamps. For cross-project discovery, use scope "global" (requires plugin option global: true).`,
43
+ description: `List sessions from the opencode database. Use this FIRST to discover which sessions exist, then search their content with recall. Returns session titles, directories, and timestamps. For cross-project discovery, use scope "global" (requires plugin option global: true).
44
+
45
+ Search filters by session title only (case-insensitive substring match \u2014 use recall for content search). Sessions are returned newest-updated first. This is a cheap metadata-only call.
46
+
47
+ Returns { ok, sessions: [{ id, title, directory, time, archived }], returned, scope }. All tools return JSON with ok: true on success or ok: false with error on failure.`,
28
48
  args: {
29
49
  scope: tool.schema.enum(["project", "global"]).default("project").describe("project = current project, global = all projects"),
30
- search: tool.schema.string().optional().describe("Filter by session title"),
31
- limit: tool.schema.number().min(1).max(100).default(20).describe("Max sessions to return")
50
+ search: tool.schema.string().optional().describe("Case-insensitive substring match on session title"),
51
+ limit: tool.schema.number().min(1).max(limits.maxSessionList).default(Math.min(20, limits.maxSessionList)).describe("Max sessions to return (newest first)")
32
52
  },
33
53
  async execute(args, ctx) {
34
54
  ctx.metadata({
@@ -116,6 +136,7 @@ import {
116
136
 
117
137
  // src/extract.ts
118
138
  var INPUT_SEARCH_LIMIT = 1e4;
139
+ var SELF = new Set(TOOLS);
119
140
  function input(val) {
120
141
  const raw = JSON.stringify(val);
121
142
  return raw.length > INPUT_SEARCH_LIMIT ? raw.slice(0, INPUT_SEARCH_LIMIT) : raw;
@@ -124,6 +145,7 @@ function matches(text, query) {
124
145
  return text.toLowerCase().includes(query.toLowerCase());
125
146
  }
126
147
  function searchable(part) {
148
+ if (part.type === "tool" && SELF.has(part.tool)) return [];
127
149
  switch (part.type) {
128
150
  case "text":
129
151
  case "reasoning":
@@ -158,7 +180,7 @@ function snippet(text, query, width = 200) {
158
180
  return text.slice(0, width) + (text.length > width ? "..." : "");
159
181
  const half = Math.floor(width / 2);
160
182
  let start = Math.max(0, idx - half);
161
- let end = Math.min(text.length, start + width);
183
+ const end = Math.min(text.length, start + width);
162
184
  if (end - start < width && start > 0) start = Math.max(0, end - width);
163
185
  let result = text.slice(start, end);
164
186
  if (start > 0) result = "..." + result;
@@ -246,7 +268,6 @@ function formatMsg(msg) {
246
268
  }
247
269
 
248
270
  // src/search.ts
249
- var CONCURRENCY = 3;
250
271
  function meta(s) {
251
272
  return { id: s.id, title: s.title, directory: s.directory };
252
273
  }
@@ -289,27 +310,41 @@ function scan(messages2, session, query, type, role, limit, before, after, width
289
310
  }
290
311
  return { results, total };
291
312
  }
292
- function search(client, unscoped, global) {
313
+ function search(client, unscoped, global, limits) {
293
314
  return tool2({
294
315
  description: `Search your conversation history in the opencode database. Use this to recover context lost to compaction \u2014 original tool outputs, earlier messages, reasoning, and user instructions that were pruned from your context window.
295
316
 
296
- Searches text content, tool inputs/outputs, and reasoning. Returns matching snippets with session/message IDs you can pass to recall_get for full content.
317
+ Searches text content, tool inputs/outputs, and reasoning via case-insensitive substring matching. Returns matching snippets with session/message IDs you can pass to recall_get for full content, or recall_context if you need surrounding messages.
318
+
319
+ Start with scope "session" (fastest). Widen to "project" if not found. Use sessionID param to target a specific session found via recall_sessions. Use role "user" to find original requirements.
320
+
321
+ Scope costs: "session" scans 1 session. "project" scans up to \`sessions\` sessions (default 10). "global" scans across all projects.
322
+
323
+ Returns { ok, results: [{ sessionID, messageID, role, time, partID, partType, pruned, snippet, toolName? }], scanned, total, truncated }. Each result includes a pruned flag \u2014 if true, the content was compacted from your context window and recall_get will return the original full output. Check truncated to know if more matches exist beyond your results limit.
297
324
 
298
- Start with scope "session" (fastest). Widen to "project" if not found. Use sessionID param to target a specific session found via recall_sessions. Use role "user" to find original requirements.`,
325
+ This tool's own outputs are excluded from search results to prevent recursive noise, but remain visible via recall_get, recall_context, and recall_messages.`,
299
326
  args: {
300
- query: tool2.schema.string().min(1).describe("Text to search for (case-insensitive)"),
327
+ query: tool2.schema.string().min(1).describe("Text to search for (case-insensitive substring match)"),
301
328
  scope: tool2.schema.enum(["session", "project", "global"]).default("session").describe(
302
329
  "session = current, project = all project sessions, global = all"
303
330
  ),
304
331
  sessionID: tool2.schema.string().optional().describe("Search a specific session (overrides scope)"),
305
332
  type: tool2.schema.enum(["text", "tool", "reasoning", "all"]).default("all").describe("Filter by part type"),
306
333
  role: tool2.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
307
- sessions: tool2.schema.number().min(1).max(50).default(10).describe("Max sessions to scan"),
308
- results: tool2.schema.number().min(1).max(50).default(10).describe("Max results to return"),
309
- title: tool2.schema.string().optional().describe("Filter sessions by title"),
334
+ sessions: tool2.schema.number().min(1).max(limits.maxSessions).default(Math.min(10, limits.maxSessions)).describe(
335
+ "Max sessions to scan (controls cost for project/global scope)"
336
+ ),
337
+ results: tool2.schema.number().min(1).max(limits.maxResults).default(Math.min(10, limits.maxResults)).describe(
338
+ "Max results to return. Check truncated in response for more."
339
+ ),
340
+ title: tool2.schema.string().optional().describe(
341
+ "Filter sessions by title before scanning (same as recall_sessions search)"
342
+ ),
310
343
  before: tool2.schema.number().optional().describe("Only match messages before this timestamp (ms epoch)"),
311
344
  after: tool2.schema.number().optional().describe("Only match messages after this timestamp (ms epoch)"),
312
- width: tool2.schema.number().min(50).max(1e3).default(200).describe("Snippet width in characters")
345
+ width: tool2.schema.number().min(50).max(Math.max(limits.defaultWidth, 1e3)).default(limits.defaultWidth).describe(
346
+ "Characters of context around each match in the returned snippet. Only a snippet is returned \u2014 use recall_get for full content."
347
+ )
313
348
  },
314
349
  async execute(args, ctx) {
315
350
  ctx.metadata({ title: `Searching ${args.scope} for "${args.query}"` });
@@ -379,7 +414,7 @@ Start with scope "session" (fastest). Widen to "project" if not found. Use sessi
379
414
  let scanned = 0;
380
415
  let total = 0;
381
416
  let early = false;
382
- for (let i = 0; i < targets.length; i += CONCURRENCY) {
417
+ for (let i = 0; i < targets.length; i += limits.concurrency) {
383
418
  if (ctx.abort.aborted) {
384
419
  early = true;
385
420
  break;
@@ -389,7 +424,7 @@ Start with scope "session" (fastest). Widen to "project" if not found. Use sessi
389
424
  break;
390
425
  }
391
426
  const remaining = args.results - collected.length;
392
- const batch = targets.slice(i, i + CONCURRENCY);
427
+ const batch = targets.slice(i, i + limits.concurrency);
393
428
  const loaded = await Promise.all(
394
429
  batch.map(async (t) => {
395
430
  try {
@@ -447,10 +482,16 @@ import {
447
482
  } from "@opencode-ai/plugin";
448
483
  function get(client) {
449
484
  return tool3({
450
- description: `Retrieve the full content of a specific message from any session, including all parts (text, tool outputs, reasoning, etc). Use after recall to get the complete content of a search result. For tool parts, returns the original output even if it was pruned from your context window. Large outputs may be truncated by the opencode runtime.`,
485
+ description: `Retrieve the full content of a specific message from any session, including all parts (text, tool outputs, reasoning, etc). Use after recall to get the complete content of a search result. For tool parts, returns the original output even if it was pruned from your context window. Large outputs may be truncated by the opencode runtime.
486
+
487
+ Returns { message: { id, role, time, model }, parts: [{ type, content, toolName, input, output, pruned, ... }], context: { sessionTitle, directory } }. Each part has a pruned flag indicating whether it was compacted.
488
+
489
+ Use recall_context instead if you need surrounding messages for context, not just a single message. Use sessionID and messageID from recall search results.`,
451
490
  args: {
452
- sessionID: tool3.schema.string().describe("Session containing the message"),
453
- messageID: tool3.schema.string().describe("Message to retrieve")
491
+ sessionID: tool3.schema.string().describe(
492
+ "Session containing the message (from recall search results)"
493
+ ),
494
+ messageID: tool3.schema.string().describe("Message to retrieve (from recall search results)")
454
495
  },
455
496
  async execute(args, ctx) {
456
497
  ctx.metadata({
@@ -499,20 +540,24 @@ function get(client) {
499
540
  import {
500
541
  tool as tool4
501
542
  } from "@opencode-ai/plugin";
502
- function context(client) {
543
+ function context(client, limits) {
503
544
  return tool4({
504
- description: `Get messages surrounding a specific message in a session. Use after recall finds a match and you need conversation context \u2014 what was asked before, what came after. Returns a window of messages centered on the target.`,
545
+ description: `Get messages surrounding a specific message in a session. Use after recall finds a match and you need conversation context \u2014 what was asked before, what came after. Returns a chronological window of full messages (with all parts) centered on the target.
546
+
547
+ Returns { ok, messages: [{ message, parts, center? }], context, hasMoreBefore, hasMoreAfter }. The center message is marked with center: true. Use hasMoreBefore/hasMoreAfter with asymmetric before/after params to expand in either direction. window=3 returns up to 3 before + target + 3 after = 7 messages. window=0 returns only the target.
548
+
549
+ Use recall_get for a single message without neighbors. Use recall_messages for paginated browsing of an entire session.`,
505
550
  args: {
506
551
  sessionID: tool4.schema.string().describe("Session containing the message"),
507
552
  messageID: tool4.schema.string().describe("Center message to get context around"),
508
- window: tool4.schema.number().min(0).max(10).default(3).describe(
509
- "Number of messages to include before AND after the target (symmetric). Overridden by before/after if set."
553
+ window: tool4.schema.number().min(0).max(limits.maxWindow).default(Math.min(3, limits.maxWindow)).describe(
554
+ "Messages on each side of target (window=3 \u2192 up to 7 total). Overridden by before/after if set."
510
555
  ),
511
- before: tool4.schema.number().min(0).max(10).optional().describe(
512
- "Messages to include before the target (overrides window for the before side)"
556
+ before: tool4.schema.number().min(0).max(limits.maxWindow).optional().describe(
557
+ "Messages before the target (overrides window for before side). Set before=0, after=5 to see only what followed."
513
558
  ),
514
- after: tool4.schema.number().min(0).max(10).optional().describe(
515
- "Messages to include after the target (overrides window for the after side)"
559
+ after: tool4.schema.number().min(0).max(limits.maxWindow).optional().describe(
560
+ "Messages after the target (overrides window for after side)"
516
561
  )
517
562
  },
518
563
  async execute(args, ctx) {
@@ -588,9 +633,13 @@ function msgMatches(msg, query) {
588
633
  }
589
634
  return false;
590
635
  }
591
- function messages(client) {
636
+ function messages(client, limits) {
592
637
  return tool5({
593
- description: `Browse messages in a session chronologically with pagination. Use to play back conversation history, see what happened in order, or find the user's original requirements. Use reverse=true to start from the most recent messages (offset 0 = newest). Use offset to paginate through results.`,
638
+ description: `Browse messages in a session chronologically with pagination. Unlike recall (which searches for specific text and returns snippets), this returns full messages with all parts in chronological order. Use it to replay a session when you don't have a specific search term, or when you need complete message content rather than search hits.
639
+
640
+ Use reverse=true to start from the most recent messages (offset 0 = newest). Use offset to paginate through results. Good for finding the user's original requirements at the start of a session.
641
+
642
+ Returns { messages: [{ message: { id, role, time }, parts: [...] }], pagination: { offset, returned, total, hasMore } }. The query and role params filter client-side after fetching, so they may return fewer results than limit. pagination.total reflects the count after filtering.`,
594
643
  args: {
595
644
  sessionID: tool5.schema.string().optional().describe(
596
645
  "Session to browse. Defaults to current session if not provided."
@@ -598,11 +647,13 @@ function messages(client) {
598
647
  offset: tool5.schema.number().min(0).default(0).describe(
599
648
  "Skip this many messages from the start (or end if reversed)"
600
649
  ),
601
- limit: tool5.schema.number().min(1).max(50).default(10).describe("Max messages to return"),
602
- role: tool5.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
650
+ limit: tool5.schema.number().min(1).max(limits.maxMessages).default(Math.min(10, limits.maxMessages)).describe("Max messages to return"),
651
+ role: tool5.schema.enum(["user", "assistant", "all"]).default("all").describe(
652
+ "Filter by message role (client-side, may return fewer than limit)"
653
+ ),
603
654
  reverse: tool5.schema.boolean().default(false).describe("If true, start from most recent messages"),
604
655
  query: tool5.schema.string().min(1).optional().describe(
605
- "Only include messages containing this text (searches all parts)"
656
+ "Case-insensitive substring filter on message content (client-side, may return fewer than limit)"
606
657
  )
607
658
  },
608
659
  async execute(args, ctx) {
@@ -667,17 +718,20 @@ function messages(client) {
667
718
  }
668
719
 
669
720
  // src/opencode-session-recall.ts
670
- var TOOLS = [
671
- "recall",
672
- "recall_get",
673
- "recall_sessions",
674
- "recall_context",
675
- "recall_messages"
676
- ];
677
721
  var server = async (ctx, options) => {
678
722
  const opts = options ?? {};
679
723
  const primary = opts.primary !== false;
680
724
  const global = opts.global === true;
725
+ const clamp = (val, fallback, min = 1) => Math.max(min, Math.floor(val ?? fallback));
726
+ const limits = {
727
+ concurrency: clamp(opts.concurrency, DEFAULTS.concurrency),
728
+ maxSessions: clamp(opts.maxSessions, DEFAULTS.maxSessions),
729
+ maxResults: clamp(opts.maxResults, DEFAULTS.maxResults),
730
+ maxSessionList: clamp(opts.maxSessionList, DEFAULTS.maxSessionList),
731
+ maxMessages: clamp(opts.maxMessages, DEFAULTS.maxMessages),
732
+ maxWindow: clamp(opts.maxWindow, DEFAULTS.maxWindow),
733
+ defaultWidth: clamp(opts.defaultWidth, DEFAULTS.defaultWidth, 50)
734
+ };
681
735
  const inner = ctx.client._client;
682
736
  if (!inner?.getConfig)
683
737
  throw new Error(
@@ -700,13 +754,14 @@ var server = async (ctx, options) => {
700
754
  });
701
755
  return {
702
756
  tool: {
703
- recall_sessions: sessions(client, unscoped, global),
704
- recall: search(client, unscoped, global),
757
+ recall_sessions: sessions(client, unscoped, global, limits),
758
+ recall: search(client, unscoped, global, limits),
705
759
  recall_get: get(client),
706
- recall_context: context(client),
707
- recall_messages: messages(client)
760
+ recall_context: context(client, limits),
761
+ recall_messages: messages(client, limits)
708
762
  },
709
763
  ...primary && {
764
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- opencode config type not exported
710
765
  config: async (c) => {
711
766
  c.experimental ??= {};
712
767
  const existing = c.experimental.primary_tools ?? [];
package/dist/search.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
- export declare function search(client: OpencodeClient, unscoped: OpencodeClient, global: boolean): ToolDefinition;
3
+ import { type Limits } from "./types.js";
4
+ export declare function search(client: OpencodeClient, unscoped: OpencodeClient, global: boolean, limits: Limits): ToolDefinition;
4
5
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,qBAAqB,CAAC;AA6E7B,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,GACd,cAAc,CA2MhB"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAqEpB,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GACb,cAAc,CAyNhB"}
@@ -1,4 +1,5 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
- export declare function sessions(client: OpencodeClient, unscoped: OpencodeClient, global: boolean): ToolDefinition;
3
+ import { type Limits } from "./types.js";
4
+ export declare function sessions(client: OpencodeClient, unscoped: OpencodeClient, global: boolean, limits: Limits): ToolDefinition;
4
5
  //# sourceMappingURL=sessions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,wBAAgB,QAAQ,CACtB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,GACd,cAAc,CA0GhB"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAEpB,wBAAgB,QAAQ,CACtB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GACb,cAAc,CA8GhB"}
package/dist/types.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ export declare const TOOLS: readonly ["recall", "recall_get", "recall_sessions", "recall_context", "recall_messages"];
2
+ export type Limits = {
3
+ concurrency: number;
4
+ maxSessions: number;
5
+ maxResults: number;
6
+ maxSessionList: number;
7
+ maxMessages: number;
8
+ maxWindow: number;
9
+ defaultWidth: number;
10
+ };
11
+ export declare const DEFAULTS: Limits;
1
12
  export type SearchResult = {
2
13
  sessionID: string;
3
14
  sessionTitle: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-session-recall",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "description": "Agent memory without a memory system — search and retrieve opencode conversation history that was lost to compaction, across sessions and projects",
7
7
  "main": "./dist/opencode-session-recall.js",
@@ -57,8 +57,11 @@
57
57
  "zod": "^4.3.6"
58
58
  },
59
59
  "devDependencies": {
60
+ "@eslint/js": "^10.0.1",
60
61
  "@opencode-ai/plugin": "^1.4.3",
62
+ "eslint": "^10.2.0",
61
63
  "tsup": "^8.5.1",
62
- "typescript": "^6.0.2"
64
+ "typescript": "^6.0.2",
65
+ "typescript-eslint": "^8.58.1"
63
66
  }
64
67
  }