opencode-session-recall 0.4.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/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
 
@@ -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;AAC1D,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAGpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,GACb,cAAc,CAuGhB"}
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/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 +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;AAChE,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAYpB,wBAAgB,QAAQ,CACtB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,GACb,cAAc,CA0GhB"}
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;;;;;AAgFlD,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"}
@@ -40,11 +40,15 @@ function errmsg(e) {
40
40
  // src/sessions.ts
41
41
  function sessions(client, unscoped, global, limits) {
42
42
  return tool({
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).`,
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.`,
44
48
  args: {
45
49
  scope: tool.schema.enum(["project", "global"]).default("project").describe("project = current project, global = all projects"),
46
- search: tool.schema.string().optional().describe("Filter by session title"),
47
- limit: tool.schema.number().min(1).max(limits.maxSessionList).default(Math.min(20, limits.maxSessionList)).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)")
48
52
  },
49
53
  async execute(args, ctx) {
50
54
  ctx.metadata({
@@ -176,7 +180,7 @@ function snippet(text, query, width = 200) {
176
180
  return text.slice(0, width) + (text.length > width ? "..." : "");
177
181
  const half = Math.floor(width / 2);
178
182
  let start = Math.max(0, idx - half);
179
- let end = Math.min(text.length, start + width);
183
+ const end = Math.min(text.length, start + width);
180
184
  if (end - start < width && start > 0) start = Math.max(0, end - width);
181
185
  let result = text.slice(start, end);
182
186
  if (start > 0) result = "..." + result;
@@ -310,23 +314,37 @@ function search(client, unscoped, global, limits) {
310
314
  return tool2({
311
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.
312
316
 
313
- 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.
314
324
 
315
- 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.`,
316
326
  args: {
317
- 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)"),
318
328
  scope: tool2.schema.enum(["session", "project", "global"]).default("session").describe(
319
329
  "session = current, project = all project sessions, global = all"
320
330
  ),
321
331
  sessionID: tool2.schema.string().optional().describe("Search a specific session (overrides scope)"),
322
332
  type: tool2.schema.enum(["text", "tool", "reasoning", "all"]).default("all").describe("Filter by part type"),
323
333
  role: tool2.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
324
- sessions: tool2.schema.number().min(1).max(limits.maxSessions).default(Math.min(10, limits.maxSessions)).describe("Max sessions to scan"),
325
- results: tool2.schema.number().min(1).max(limits.maxResults).default(Math.min(10, limits.maxResults)).describe("Max results to return"),
326
- 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
+ ),
327
343
  before: tool2.schema.number().optional().describe("Only match messages before this timestamp (ms epoch)"),
328
344
  after: tool2.schema.number().optional().describe("Only match messages after this timestamp (ms epoch)"),
329
- width: tool2.schema.number().min(50).max(Math.max(limits.defaultWidth, 1e3)).default(limits.defaultWidth).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
+ )
330
348
  },
331
349
  async execute(args, ctx) {
332
350
  ctx.metadata({ title: `Searching ${args.scope} for "${args.query}"` });
@@ -464,10 +482,16 @@ import {
464
482
  } from "@opencode-ai/plugin";
465
483
  function get(client) {
466
484
  return tool3({
467
- 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.`,
468
490
  args: {
469
- sessionID: tool3.schema.string().describe("Session containing the message"),
470
- 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)")
471
495
  },
472
496
  async execute(args, ctx) {
473
497
  ctx.metadata({
@@ -518,18 +542,22 @@ import {
518
542
  } from "@opencode-ai/plugin";
519
543
  function context(client, limits) {
520
544
  return tool4({
521
- 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.`,
522
550
  args: {
523
551
  sessionID: tool4.schema.string().describe("Session containing the message"),
524
552
  messageID: tool4.schema.string().describe("Center message to get context around"),
525
553
  window: tool4.schema.number().min(0).max(limits.maxWindow).default(Math.min(3, limits.maxWindow)).describe(
526
- "Number of messages to include before AND after the target (symmetric). Overridden by before/after if set."
554
+ "Messages on each side of target (window=3 \u2192 up to 7 total). Overridden by before/after if set."
527
555
  ),
528
556
  before: tool4.schema.number().min(0).max(limits.maxWindow).optional().describe(
529
- "Messages to include before the target (overrides window for the before side)"
557
+ "Messages before the target (overrides window for before side). Set before=0, after=5 to see only what followed."
530
558
  ),
531
559
  after: tool4.schema.number().min(0).max(limits.maxWindow).optional().describe(
532
- "Messages to include after the target (overrides window for the after side)"
560
+ "Messages after the target (overrides window for after side)"
533
561
  )
534
562
  },
535
563
  async execute(args, ctx) {
@@ -607,7 +635,11 @@ function msgMatches(msg, query) {
607
635
  }
608
636
  function messages(client, limits) {
609
637
  return tool5({
610
- 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.`,
611
643
  args: {
612
644
  sessionID: tool5.schema.string().optional().describe(
613
645
  "Session to browse. Defaults to current session if not provided."
@@ -616,10 +648,12 @@ function messages(client, limits) {
616
648
  "Skip this many messages from the start (or end if reversed)"
617
649
  ),
618
650
  limit: tool5.schema.number().min(1).max(limits.maxMessages).default(Math.min(10, limits.maxMessages)).describe("Max messages to return"),
619
- role: tool5.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
651
+ role: tool5.schema.enum(["user", "assistant", "all"]).default("all").describe(
652
+ "Filter by message role (client-side, may return fewer than limit)"
653
+ ),
620
654
  reverse: tool5.schema.boolean().default(false).describe("If true, start from most recent messages"),
621
655
  query: tool5.schema.string().min(1).optional().describe(
622
- "Only include messages containing this text (searches all parts)"
656
+ "Case-insensitive substring filter on message content (client-side, may return fewer than limit)"
623
657
  )
624
658
  },
625
659
  async execute(args, ctx) {
@@ -727,6 +761,7 @@ var server = async (ctx, options) => {
727
761
  recall_messages: messages(client, limits)
728
762
  },
729
763
  ...primary && {
764
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- opencode config type not exported
730
765
  config: async (c) => {
731
766
  c.experimental ??= {};
732
767
  const existing = c.experimental.primary_tools ?? [];
@@ -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;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,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 +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;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,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/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.4.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
  }