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 +6 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/get.d.ts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/opencode-session-recall.d.ts.map +1 -1
- package/dist/opencode-session-recall.js +56 -21
- package/dist/search.d.ts.map +1 -1
- package/dist/sessions.d.ts.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# opencode-session-recall
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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"}
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -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,
|
|
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;;;;;
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
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(
|
|
470
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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
|
-
"
|
|
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 ?? [];
|
package/dist/search.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/sessions.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
+
"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
|
}
|