🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP β†’
Sign In

@josxa/opencode-recall

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@josxa/opencode-recall - npm Package Compare versions

Comparing version
0.3.0
to
1.0.0
+3
dist/src/node-worker-client.d.ts
import type { HistoryWorkerRequest } from './worker-protocol.js';
export declare function executeNodeWorker(packageDir: string, request: HistoryWorkerRequest, signal: AbortSignal): Promise<string>;
export declare function executeNodeWorkerSync(packageDir: string, request: HistoryWorkerRequest): string;
import { Buffer } from 'node:buffer';
import { spawn, spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
const MAX_WORKER_OUTPUT_BYTES = 10 * 1024 * 1024;
export function executeNodeWorker(packageDir, request, signal) {
const worker = resolveWorkerCommand(packageDir);
return new Promise((resolve, reject) => {
const child = spawn(worker.command, worker.args, {
env: process.env,
stdio: ['pipe', 'pipe', 'pipe'],
});
const stdout = [];
const stderr = [];
let stdoutBytes = 0;
let stderrBytes = 0;
let settled = false;
const settle = (result) => {
if (settled) {
return;
}
settled = true;
signal.removeEventListener('abort', abort);
if ('error' in result) {
reject(result.error);
return;
}
resolve(result.value);
};
const abort = () => {
child.kill();
settle({ error: new Error('opencode-recall Node worker was aborted') });
};
signal.addEventListener('abort', abort, { once: true });
child.on('error', (error) => settle({ error }));
child.stdout.on('data', (chunk) => {
stdoutBytes += chunk.length;
if (stdoutBytes > MAX_WORKER_OUTPUT_BYTES) {
child.kill();
settle({ error: new Error('opencode-recall Node worker exceeded stdout limit') });
return;
}
stdout.push(chunk);
});
child.stderr.on('data', (chunk) => {
stderrBytes += chunk.length;
if (stderrBytes <= MAX_WORKER_OUTPUT_BYTES) {
stderr.push(chunk);
}
});
child.on('close', (code) => {
settle(parseWorkerResult(code, stdout, stderr));
});
child.stdin.end(JSON.stringify(request));
});
}
export function executeNodeWorkerSync(packageDir, request) {
const worker = resolveWorkerCommand(packageDir);
const result = spawnSync(worker.command, worker.args, {
env: process.env,
input: JSON.stringify(request),
maxBuffer: MAX_WORKER_OUTPUT_BYTES,
});
if (result.error !== undefined) {
throw result.error;
}
const parsed = parseWorkerResult(result.status, [result.stdout], [result.stderr]);
if ('error' in parsed) {
throw parsed.error;
}
return parsed.value;
}
function resolveWorkerCommand(packageDir) {
const builtWorkerPath = join(packageDir, 'src/node-worker.js');
if (existsSync(builtWorkerPath)) {
return { command: 'node', args: [builtWorkerPath] };
}
return { command: 'node', args: ['--import', 'tsx', join(packageDir, 'src/node-worker.ts')] };
}
function parseWorkerResult(code, stdout, stderr) {
const stdoutText = Buffer.concat(stdout).toString('utf-8');
const stderrText = Buffer.concat(stderr).toString('utf-8');
if (code !== 0) {
return {
error: new Error(nonEmpty(stderrText) ?? nonEmpty(stdoutText) ?? `Node worker exited ${code}`),
};
}
const response = parseWorkerResponse(stdoutText);
if (response === undefined) {
return { error: new Error(nonEmpty(stderrText) ?? 'Node worker returned invalid JSON') };
}
if (!response.ok) {
return { error: new Error(response.error.message) };
}
return { value: response.data };
}
function parseWorkerResponse(value) {
try {
const parsed = JSON.parse(value);
return isHistoryWorkerResponse(parsed) ? parsed : undefined;
}
catch {
return undefined;
}
}
function isHistoryWorkerResponse(value) {
if (!isHistoryWorkerResponseShape(value) || typeof value.ok !== 'boolean') {
return false;
}
if (value.ok) {
return typeof value.data === 'string';
}
const error = value.error;
return isHistoryWorkerErrorShape(error) && typeof error.message === 'string';
}
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isHistoryWorkerResponseShape(value) {
return isRecord(value);
}
function isHistoryWorkerErrorShape(value) {
return isRecord(value);
}
function nonEmpty(value) {
const trimmed = value.trim();
return trimmed.length === 0 ? undefined : trimmed;
}
import { executeWorkerRequest } from './worker-actions.js';
const response = await executeMain();
process.stdout.write(JSON.stringify(response));
async function executeMain() {
try {
return { ok: true, data: await executeWorkerRequest(parseRequest(await readStdin())) };
}
catch (error) {
return { ok: false, error: { message: errorMessage(error) } };
}
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString('utf-8');
}
function parseRequest(input) {
const parsed = JSON.parse(input);
if (isHistoryWorkerRequest(parsed)) {
return parsed;
}
throw new Error('Invalid opencode-recall worker request');
}
function isHistoryWorkerRequest(value) {
if (!isHistoryWorkerRequestShape(value)) {
return false;
}
const kind = value.kind;
const args = value.args;
const context = value.context;
if (kind === 'read' || kind === 'read-window') {
return isRecord(args);
}
return kind === 'search' && isRecord(args) && isRecord(context);
}
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isHistoryWorkerRequestShape(value) {
return isRecord(value);
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
import { type ReadMode, type SearchRow } from './db.js';
import { type EmbeddingProvider } from './embedding.js';
import { type SyncOptions, type SyncResult } from './sidecar.js';
import type { TranscriptWindow } from './transcript.js';
export type { EmbeddingProvider, OllamaEmbeddingProviderOptions } from './embedding.js';
export { OllamaEmbeddingProvider } from './embedding.js';
export type { SyncOptions, SyncResult } from './sidecar.js';
export type { TranscriptWindow } from './transcript.js';
export interface OpenCodeRecallOptions {
readonly historyDbPath?: string;
readonly sidecarDbPath?: string;
readonly embeddingProvider?: EmbeddingProvider;
}
export interface RecallSearchOptions {
readonly limit?: number;
readonly after?: Date | number | string;
readonly before?: Date | number | string;
readonly directory?: string;
readonly includeCurrentSession?: boolean;
readonly currentSessionId?: string;
readonly excludeSessionId?: string;
readonly semantic?: boolean;
readonly lexical?: boolean;
readonly sync?: boolean;
readonly syncOptions?: SyncOptions;
}
export interface RecallSearchHit {
readonly cursor: string;
readonly sessionId: string;
readonly sessionTitle: string;
readonly directory: string;
readonly messageId: string;
readonly partId: string;
readonly role: string;
readonly score?: number;
readonly timeCreated: number;
readonly time: string;
readonly text: string;
readonly source?: SearchRow['source'];
}
export interface RecallReadOptions {
readonly mode?: ReadMode;
readonly limit?: number;
}
export interface RecallSearchResult {
readonly hits: readonly RecallSearchHit[];
readonly sync?: SyncResult;
}
export declare class DirectOpenCodeRecall {
#private;
constructor(options?: OpenCodeRecallOptions);
close(): void;
sync(options?: SyncOptions): Promise<SyncResult>;
syncLexical(): SyncResult;
search(query: string, options?: RecallSearchOptions): Promise<RecallSearchResult>;
read(cursorValue: string, options?: RecallReadOptions): TranscriptWindow;
render(cursorValue: string, options?: RecallReadOptions): string;
}
export declare function directSearchHistory(query: string, options?: RecallSearchOptions & OpenCodeRecallOptions): Promise<RecallSearchResult>;
export declare function directReadHistoryWindow(cursor: string, options?: RecallReadOptions & OpenCodeRecallOptions): TranscriptWindow;
export declare function directRenderHistoryWindow(cursor: string, options?: RecallReadOptions & OpenCodeRecallOptions): string;
import { ChatmlRenderer } from './chatml-renderer.js';
import { decodeCursor } from './cursor.js';
import { HistoryDatabase } from './db.js';
import { OllamaEmbeddingProvider } from './embedding.js';
import { normalizeWindow } from './normalizer.js';
import { parseReadMode } from './read-mode.js';
import { rankSearchRows } from './search.js';
import { RecallSidecarIndex } from './sidecar.js';
export { OllamaEmbeddingProvider } from './embedding.js';
const DEFAULT_SEARCH_LIMIT = 50;
const DEFAULT_READ_LIMIT = 12;
const DEFAULT_FRESHNESS_EXCLUSION_MS = 30_000;
export class DirectOpenCodeRecall {
#history;
#sidecar;
#provider;
#ownsProvider;
constructor(options = {}) {
this.#history = new HistoryDatabase(options.historyDbPath);
this.#sidecar = new RecallSidecarIndex(options.sidecarDbPath);
this.#provider = options.embeddingProvider ?? new OllamaEmbeddingProvider();
this.#ownsProvider = options.embeddingProvider === undefined;
}
close() {
this.#sidecar.close();
this.#history.close();
if (this.#ownsProvider && 'close' in this.#provider) {
const close = this.#provider.close;
if (typeof close === 'function') {
close.call(this.#provider);
}
}
}
async sync(options = {}) {
return this.#sidecar.sync((since) => this.#history.readTextPartsForIndex(since), this.#provider, () => this.#history.readTextPartIds(), options);
}
// Build/refresh just the FTS5 lexical index, skipping embeddings.
// Used when callers opt out of semantic but still want lexical recall.
syncLexical() {
const start = performance.now();
const result = this.#sidecar.syncLexicalOnly((since) => this.#history.readTextPartsForIndex(since), () => this.#history.readTextPartIds());
return {
elapsedMs: performance.now() - start,
indexedRows: result.indexedRows,
deletedRows: result.deletedRows,
lockAcquired: result.lockAcquired,
};
}
async search(query, options = {}) {
const searchOptions = normalizeSearchOptions(options);
if (isBlankQuery(query)) {
return { hits: this.#history.recent(searchOptions).map(toSearchHit) };
}
const lexicalEnabled = options.lexical !== false;
const semanticEnabled = options.semantic !== false;
const shouldSync = (semanticEnabled || lexicalEnabled) && options.sync !== false;
const syncResult = shouldSync
? semanticEnabled
? await this.sync(options.syncOptions)
: this.syncLexical()
: undefined;
const lexicalRows = lexicalEnabled ? this.#sidecar.lexicalSearch(query, searchOptions) : [];
const semanticRows = semanticEnabled
? await this.#sidecar.search(query, searchOptions, this.#provider)
: [];
const rows = rankSearchRows(query, [...lexicalRows, ...semanticRows], searchOptions.limit);
return {
hits: rows.map(toSearchHit),
...(syncResult === undefined ? {} : { sync: syncResult }),
};
}
read(cursorValue, options = {}) {
const cursor = decodeCursor(cursorValue);
const readOptions = {
mode: parseReadMode(options.mode),
limit: options.limit ?? DEFAULT_READ_LIMIT,
};
return normalizeWindow(cursor.messageId === undefined
? this.#history.readWindowForSession(requiredSessionId(cursor.sessionId), readOptions)
: this.#history.readWindow(cursor.messageId, readOptions));
}
render(cursorValue, options = {}) {
return new ChatmlRenderer().render(this.read(cursorValue, options));
}
}
export async function directSearchHistory(query, options = {}) {
const recall = new DirectOpenCodeRecall(options);
try {
return await recall.search(query, options);
}
finally {
recall.close();
}
}
export function directReadHistoryWindow(cursor, options = {}) {
const recall = new DirectOpenCodeRecall(options);
try {
return recall.read(cursor, options);
}
finally {
recall.close();
}
}
export function directRenderHistoryWindow(cursor, options = {}) {
const recall = new DirectOpenCodeRecall(options);
try {
return recall.render(cursor, options);
}
finally {
recall.close();
}
}
function normalizeSearchOptions(options) {
const excluded = excludedSessionId(options);
return {
limit: options.limit ?? DEFAULT_SEARCH_LIMIT,
...optionalTimestampFilter('after', options.after),
...optionalTimestampFilter('before', options.before ?? defaultBefore(options)),
...(options.directory === undefined ? {} : { directory: options.directory }),
...(excluded === undefined ? {} : { excludeSessionId: excluded }),
};
}
function isBlankQuery(query) {
return query.trim().length === 0;
}
function excludedSessionId(options) {
if (options.includeCurrentSession === true) {
return options.excludeSessionId;
}
return options.excludeSessionId ?? options.currentSessionId;
}
function defaultBefore(options) {
if (options.includeCurrentSession === true || options.currentSessionId === undefined) {
return undefined;
}
return Date.now() - DEFAULT_FRESHNESS_EXCLUSION_MS;
}
function optionalTimestampFilter(name, value) {
const timestamp = optionalTimestamp(value);
if (timestamp === undefined) {
return {};
}
return { [name]: timestamp };
}
function optionalTimestamp(value) {
if (value === undefined) {
return undefined;
}
if (value instanceof Date) {
return value.getTime();
}
if (typeof value === 'number') {
return Number.isFinite(value) ? value : undefined;
}
if (value.length === 0) {
return undefined;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function toSearchHit(row) {
const cursor = row.messageId;
return {
cursor,
sessionId: row.sessionId,
sessionTitle: row.sessionTitle,
directory: row.directory,
messageId: row.messageId,
partId: row.partId,
role: row.role,
...(row.score === undefined ? {} : { score: Number(row.score.toFixed(4)) }),
timeCreated: row.timeCreated,
time: new Date(row.timeCreated).toISOString(),
text: row.text,
...(row.source === undefined ? {} : { source: row.source }),
};
}
function requiredSessionId(value) {
if (value !== undefined) {
return value;
}
throw new Error('History cursor does not contain a message or session id');
}
import { type StatementSync } from 'node:sqlite';
export type SqliteBindValue = string | number | bigint | null | Buffer | Uint8Array;
export type SqliteBindParams = readonly SqliteBindValue[];
export interface SqliteRunResult {
readonly changes: number | bigint;
readonly lastInsertRowid: number | bigint;
}
export declare class Database {
#private;
constructor(path: string, options?: {
readonly?: boolean;
});
exec(sql: string): void;
close(): void;
query<TResult, TParams extends SqliteBindParams = SqliteBindParams>(sql: string): Statement<TResult, TParams>;
prepare<TResult, TParams extends SqliteBindParams = SqliteBindParams>(sql: string): Statement<TResult, TParams>;
transaction(callback: () => void): () => void;
}
declare class Statement<TResult, TParams extends SqliteBindParams> {
#private;
constructor(statement: StatementSync);
all(...params: TParams): TResult[];
get(...params: TParams): TResult | null;
run(...params: TParams): SqliteRunResult;
}
export {};
import { DatabaseSync } from 'node:sqlite';
export class Database {
#db;
constructor(path, options = {}) {
this.#db = new DatabaseSync(path, {
readOnly: options.readonly === true,
});
}
exec(sql) {
this.#db.exec(sql);
}
close() {
this.#db.close();
}
query(sql) {
return new Statement(this.#db.prepare(sql));
}
prepare(sql) {
return this.query(sql);
}
transaction(callback) {
return () => {
this.#db.exec('begin');
try {
callback();
this.#db.exec('commit');
}
catch (error) {
this.#db.exec('rollback');
throw error;
}
};
}
}
class Statement {
#statement;
constructor(statement) {
this.#statement = statement;
}
all(...params) {
return this.#statement.all(...params);
}
get(...params) {
return this.#statement.get(...params) ?? null;
}
run(...params) {
return this.#statement.run(...params);
}
}
export declare const DEFAULT_SEARCH_LIMIT = 8;
export declare const MAX_SEARCH_LIMIT = 25;
export declare const DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS = 30000;
export declare const DEFAULT_READ_LIMIT = 12;
export declare const MAX_READ_LIMIT = 50;
export const DEFAULT_SEARCH_LIMIT = 8;
export const MAX_SEARCH_LIMIT = 25;
export const DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS = 30_000;
export const DEFAULT_READ_LIMIT = 12;
export const MAX_READ_LIMIT = 50;
import type { HistoryWorkerRequest } from './worker-protocol.js';
export declare function executeWorkerRequest(request: HistoryWorkerRequest): Promise<string>;
import { ChatmlRenderer } from './chatml-renderer.js';
import { decodeCursor } from './cursor.js';
import { HistoryDatabase } from './db.js';
import { OllamaEmbeddingProvider } from './embedding.js';
import { normalizeWindow } from './normalizer.js';
import { parseReadMode } from './read-mode.js';
import { formatSearchResults, rankSearchRows } from './search.js';
import { RecallSidecarIndex } from './sidecar.js';
import { DEFAULT_READ_LIMIT, DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS, DEFAULT_SEARCH_LIMIT, MAX_READ_LIMIT, MAX_SEARCH_LIMIT, } from './tool-defaults.js';
export async function executeWorkerRequest(request) {
if (request.kind === 'search') {
return executeHistorySearch(request);
}
const window = executeHistoryReadWindow(request);
if (request.kind === 'read-window') {
return JSON.stringify(window);
}
return new ChatmlRenderer().render(window);
}
async function executeHistorySearch(request) {
const query = request.args.q ?? '';
const includeCurrentSession = request.args.includeCurrentSession === true;
const semanticEnabled = request.args.semantic !== false;
const lexicalEnabled = request.args.lexical !== false;
const shouldSync = (semanticEnabled || lexicalEnabled) && request.args.sync !== false;
const before = optionalDateFilterValue('before', request.args.before);
const options = {
limit: clampNumber(request.args.n, DEFAULT_SEARCH_LIMIT, 1, MAX_SEARCH_LIMIT),
...optionalStringFilter('directory', request.args.directory),
...currentSessionExclusion(includeCurrentSession, request.context.sessionID, request.args.excludeSessionId),
...optionalDateFilter('after', request.args.after),
...searchBeforeFilter(before, includeCurrentSession),
};
const db = new HistoryDatabase(request.args.historyDbPath);
const sidecar = new RecallSidecarIndex(request.args.sidecarDbPath);
try {
if (query.trim().length === 0) {
return formatSearchResults(db.recent(options));
}
const syncStart = performance.now();
const provider = semanticEnabled ? new OllamaEmbeddingProvider() : undefined;
const syncResult = shouldSync
? semanticEnabled && provider !== undefined
? await sidecar.sync((since) => db.readTextPartsForIndex(since), provider, () => db.readTextPartIds())
: {
...sidecar.syncLexicalOnly((since) => db.readTextPartsForIndex(since), () => db.readTextPartIds()),
elapsedMs: performance.now() - syncStart,
}
: undefined;
const [semanticRows, lexicalRows] = await Promise.all([
semanticEnabled && provider !== undefined
? sidecar.search(query, options, provider)
: Promise.resolve([]),
Promise.resolve(lexicalEnabled ? sidecar.lexicalSearch(query, options) : []),
]);
const rows = rankSearchRows(query, [...lexicalRows, ...semanticRows], options.limit);
return formatSearchResults(rows, syncResult);
}
finally {
sidecar.close();
db.close();
}
}
function executeHistoryReadWindow(request) {
const cursorValue = request.args.cursor;
if (cursorValue === undefined || cursorValue.length === 0) {
throw new Error('history_read requires cursor');
}
const cursor = decodeCursor(cursorValue);
const mode = parseReadMode(request.args.mode);
const limit = clampNumber(request.args.n, DEFAULT_READ_LIMIT, 1, MAX_READ_LIMIT);
const db = new HistoryDatabase(request.args.historyDbPath);
try {
const readOptions = { mode, limit };
return normalizeWindow(cursor.messageId === undefined
? db.readWindowForSession(requiredSessionId(cursor.sessionId), readOptions)
: db.readWindow(cursor.messageId, readOptions));
}
finally {
db.close();
}
}
function optionalDateFilter(name, value) {
const timestamp = optionalDateFilterValue(name, value);
if (timestamp === undefined) {
return {};
}
return { [name]: timestamp };
}
function optionalDateFilterValue(name, value) {
if (value === undefined || value.length === 0) {
return undefined;
}
const timestamp = optionalTimestamp(value);
if (timestamp === undefined) {
throw new Error(`Invalid ${name} date filter: ${value}`);
}
return timestamp;
}
function optionalTimestamp(value) {
if (value === undefined || value.length === 0) {
return undefined;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function optionalStringFilter(name, value) {
if (value === undefined || value.length === 0) {
return {};
}
return { [name]: value };
}
function currentSessionExclusion(includeCurrentSession, sessionID, excludeSessionId) {
if (includeCurrentSession === true) {
return excludeSessionId === undefined ? {} : { excludeSessionId };
}
return { excludeSessionId: excludeSessionId ?? sessionID };
}
function searchBeforeFilter(before, includeCurrentSession) {
if (before !== undefined) {
return { before };
}
if (includeCurrentSession) {
return {};
}
return { before: Date.now() - DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS };
}
function clampNumber(value, fallback, min, max) {
if (value === undefined || !Number.isFinite(value)) {
return fallback;
}
return Math.min(max, Math.max(min, Math.trunc(value)));
}
function requiredSessionId(value) {
if (value !== undefined) {
return value;
}
throw new Error('History cursor does not contain a message or session id');
}
export interface HistorySearchWorkerArgs {
readonly q?: string | undefined;
readonly n?: number | undefined;
readonly directory?: string | undefined;
readonly includeCurrentSession?: boolean | undefined;
readonly excludeSessionId?: string | undefined;
readonly after?: string | undefined;
readonly before?: string | undefined;
readonly historyDbPath?: string | undefined;
readonly sidecarDbPath?: string | undefined;
readonly semantic?: boolean | undefined;
readonly lexical?: boolean | undefined;
readonly sync?: boolean | undefined;
}
export interface HistoryReadWorkerArgs {
readonly cursor?: string | undefined;
readonly mode?: string | undefined;
readonly n?: number | undefined;
readonly historyDbPath?: string | undefined;
}
export type HistoryWorkerRequest = {
readonly kind: 'search';
readonly args: HistorySearchWorkerArgs;
readonly context: {
readonly sessionID: string;
};
} | {
readonly kind: 'read';
readonly args: HistoryReadWorkerArgs;
} | {
readonly kind: 'read-window';
readonly args: HistoryReadWorkerArgs;
};
export type HistoryWorkerResponse = {
readonly ok: true;
readonly data: string;
} | {
readonly ok: false;
readonly error: {
readonly message: string;
};
};
+22
-123

@@ -0,19 +1,12 @@

import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { tool } from '@opencode-ai/plugin';
import { ChatmlRenderer } from './src/chatml-renderer';
import { HISTORY_READ_COMMAND, HISTORY_SEARCH_COMMAND, RECALL_AGENT_DESCRIPTION, RECALL_AGENT_NAME, } from './src/commands';
import { decodeCursor } from './src/cursor';
import { HistoryDatabase } from './src/db';
import { OllamaEmbeddingProvider } from './src/embedding';
import { normalizeWindow } from './src/normalizer';
import { parseReadMode } from './src/read-mode';
import { formatSearchResults, rankSearchRows } from './src/search';
import { RecallSidecarIndex } from './src/sidecar';
const DEFAULT_SEARCH_LIMIT = 8;
const MAX_SEARCH_LIMIT = 25;
const DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS = 30_000;
const DEFAULT_READ_LIMIT = 12;
const MAX_READ_LIMIT = 50;
import { HISTORY_READ_COMMAND, HISTORY_SEARCH_COMMAND, RECALL_AGENT_DESCRIPTION, RECALL_AGENT_NAME, } from './src/commands.js';
import { executeNodeWorker } from './src/node-worker-client.js';
import { DEFAULT_READ_LIMIT, DEFAULT_SEARCH_LIMIT } from './src/tool-defaults.js';
const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url));
const RECALL_AGENT_PROMPT_PATHS = [
`${import.meta.dir}/prompts/recall-agent-prompt.txt`,
`${import.meta.dir}/../prompts/recall-agent-prompt.txt`,
join(PACKAGE_DIR, 'prompts/recall-agent-prompt.txt'),
join(PACKAGE_DIR, '../prompts/recall-agent-prompt.txt'),
];

@@ -58,31 +51,3 @@ export const RecallPlugin = async () => {

assertRecallAgent(context.agent);
const query = args.q ?? '';
const includeCurrentSession = args.includeCurrentSession === true;
const before = optionalDateFilterValue('before', args.before);
const options = {
limit: clampNumber(args.n, DEFAULT_SEARCH_LIMIT, 1, MAX_SEARCH_LIMIT),
...optionalStringFilter('directory', args.directory),
...currentSessionExclusion(includeCurrentSession, context.sessionID),
...optionalDateFilter('after', args.after),
...searchBeforeFilter(before, includeCurrentSession),
};
const db = new HistoryDatabase();
const sidecar = new RecallSidecarIndex();
try {
if (query.trim().length === 0) {
return formatSearchResults(db.recent(options));
}
const provider = new OllamaEmbeddingProvider();
const syncResult = await sidecar.sync((since) => db.readTextPartsForIndex(since), provider, () => db.readTextPartIds());
const [semanticRows, lexicalRows] = await Promise.all([
sidecar.search(query, options, provider),
Promise.resolve(sidecar.lexicalSearch(query, options)),
]);
const rows = rankSearchRows(query, [...lexicalRows, ...semanticRows], options.limit);
return formatSearchResults(rows, syncResult);
}
finally {
sidecar.close();
db.close();
}
return executeNodeWorker(PACKAGE_DIR, { kind: 'search', args, context: { sessionID: context.sessionID } }, context.abort);
},

@@ -108,20 +73,3 @@ }),

assertRecallAgent(context.agent);
const cursorValue = args.cursor;
if (cursorValue === undefined || cursorValue.length === 0) {
throw new Error('history_read requires cursor');
}
const cursor = decodeCursor(cursorValue);
const mode = parseReadMode(args.mode);
const limit = clampNumber(args.n, DEFAULT_READ_LIMIT, 1, MAX_READ_LIMIT);
const db = new HistoryDatabase();
try {
const readOptions = { mode, limit };
const window = normalizeWindow(cursor.messageId === undefined
? db.readWindowForSession(requiredSessionId(cursor.sessionId), readOptions)
: db.readWindow(cursor.messageId, readOptions));
return new ChatmlRenderer().render(window);
}
finally {
db.close();
}
return executeNodeWorker(PACKAGE_DIR, { kind: 'read', args }, context.abort);
},

@@ -134,9 +82,17 @@ }),

for (const path of RECALL_AGENT_PROMPT_PATHS) {
const file = Bun.file(path);
if (await file.exists()) {
return file.text();
try {
return await readFile(path, 'utf-8');
}
catch (error) {
if (isMissingFileError(error)) {
continue;
}
throw error;
}
}
throw new Error('Cannot load recall-agent-prompt.txt from package prompts directory');
}
function isMissingFileError(error) {
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
}
function recallAgentPermission() {

@@ -155,59 +111,2 @@ return {

}
function requiredSessionId(value) {
if (value !== undefined) {
return value;
}
throw new Error('History cursor does not contain a message or session id');
}
export default RecallPlugin;
function optionalDateFilter(name, value) {
const timestamp = optionalDateFilterValue(name, value);
if (timestamp === undefined) {
return {};
}
return { [name]: timestamp };
}
function optionalDateFilterValue(name, value) {
if (value === undefined || value.length === 0) {
return undefined;
}
const timestamp = optionalTimestamp(value);
if (timestamp === undefined) {
throw new Error(`Invalid ${name} date filter: ${value}`);
}
return timestamp;
}
function optionalTimestamp(value) {
if (value === undefined || value.length === 0) {
return undefined;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function optionalStringFilter(name, value) {
if (value === undefined || value.length === 0) {
return {};
}
return { [name]: value };
}
function currentSessionExclusion(includeCurrentSession, sessionID) {
if (includeCurrentSession === true) {
return {};
}
return { excludeSessionId: sessionID };
}
function searchBeforeFilter(before, includeCurrentSession) {
if (before !== undefined) {
return { before };
}
if (includeCurrentSession) {
return {};
}
return { before: Date.now() - DEFAULT_SEARCH_FRESHNESS_EXCLUSION_MS };
}
function clampNumber(value, fallback, min, max) {
if (value === undefined || !Number.isFinite(value)) {
return fallback;
}
return Math.min(max, Math.max(min, Math.trunc(value)));
}

@@ -1,2 +0,2 @@

import type { HistoryRenderer, TranscriptWindow } from './transcript';
import type { HistoryRenderer, TranscriptWindow } from './transcript.js';
export declare class ChatmlRenderer implements HistoryRenderer<string> {

@@ -3,0 +3,0 @@ readonly format = "chatml";

@@ -185,3 +185,3 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';

// Embedding model. Try "mxbai-embed-large" for higher quality at the cost
// of speed and memory. Run \`bun run eval:embeddings\` to compare models
// of speed and memory. Run \`pnpm run eval:embeddings\` to compare models
// against the regression cases in docs/real-history-regressions.md.

@@ -188,0 +188,0 @@ // Default: all-minilm

@@ -1,3 +0,3 @@

import { Database } from 'bun:sqlite';
import { loadConfig } from './config';
import { loadConfig } from './config.js';
import { Database } from './sqlite.js';
const WHITESPACE_REGEX = /\s+/u;

@@ -4,0 +4,0 @@ export class HistoryDatabase {

@@ -1,2 +0,3 @@

import { loadConfig } from './config';
import { spawn } from 'node:child_process';
import { loadConfig } from './config.js';
const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434';

@@ -67,7 +68,5 @@ const DEFAULT_EMBED_MODEL = 'all-minilm';

try {
this.#serverProcess = Bun.spawn({
cmd: ['ollama', 'serve'],
this.#serverProcess = spawn('ollama', ['serve'], {
env: { ...process.env, OLLAMA_HOST: ollamaHost(this.#baseUrl) },
stdout: 'ignore',
stderr: 'ignore',
stdio: 'ignore',
});

@@ -83,3 +82,3 @@ this.#serverProcess.unref();

while (Date.now() < deadline) {
await Bun.sleep(OLLAMA_STARTUP_POLL_MS);
await sleep(OLLAMA_STARTUP_POLL_MS);
if (await this.#isServerReady()) {

@@ -156,2 +155,7 @@ return true;

}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function parseEmbeddingResponse(response) {

@@ -158,0 +162,0 @@ const body = (await response.json());

@@ -1,3 +0,3 @@

import type { Database } from 'bun:sqlite';
import type { IndexSourceRow, SearchOptions, SearchRow } from './db';
import type { IndexSourceRow, SearchOptions, SearchRow } from './db.js';
import type { Database } from './sqlite.js';
export declare class LexicalIndex {

@@ -4,0 +4,0 @@ #private;

@@ -1,3 +0,3 @@

import type { WindowRows } from './db';
import type { TranscriptWindow } from './transcript';
import type { WindowRows } from './db.js';
import type { TranscriptWindow } from './transcript.js';
export declare function normalizeWindow(rows: WindowRows): TranscriptWindow;

@@ -1,2 +0,2 @@

import { encodeCursor } from './cursor';
import { encodeCursor } from './cursor.js';
const MAX_TOOL_INPUT_CHARS = 2_000;

@@ -3,0 +3,0 @@ const MAX_TOOL_OUTPUT_CHARS = 6_000;

@@ -1,3 +0,3 @@

import type { ReadMode } from './db';
import type { ReadMode } from './db.js';
export declare const FULL_MODE_RECOMMENDATION = "history_read mode=full is not supported for LLM-safe reading. Use mode=tail to read from the end, mode=head to read from the start, and page with the returned <nav prev=\"...\"> or <nav next=\"...\"> cursors.";
export declare function parseReadMode(value: string | undefined): ReadMode;

@@ -1,49 +0,9 @@

import { type ReadMode, type SearchRow } from './db';
import { type EmbeddingProvider } from './embedding';
import { type SyncOptions, type SyncResult } from './sidecar';
import type { TranscriptWindow } from './transcript';
export type { EmbeddingProvider, OllamaEmbeddingProviderOptions } from './embedding';
export { OllamaEmbeddingProvider } from './embedding';
export type { SyncOptions, SyncResult } from './sidecar';
export type { TranscriptWindow } from './transcript';
export interface OpenCodeRecallOptions {
readonly historyDbPath?: string;
readonly sidecarDbPath?: string;
readonly embeddingProvider?: EmbeddingProvider;
}
export interface RecallSearchOptions {
readonly limit?: number;
readonly after?: Date | number | string;
readonly before?: Date | number | string;
readonly directory?: string;
readonly includeCurrentSession?: boolean;
readonly currentSessionId?: string;
readonly excludeSessionId?: string;
readonly semantic?: boolean;
readonly lexical?: boolean;
readonly sync?: boolean;
readonly syncOptions?: SyncOptions;
}
export interface RecallSearchHit {
readonly cursor: string;
readonly sessionId: string;
readonly sessionTitle: string;
readonly directory: string;
readonly messageId: string;
readonly partId: string;
readonly role: string;
readonly score?: number;
readonly timeCreated: number;
readonly time: string;
readonly text: string;
readonly source?: SearchRow['source'];
}
export interface RecallReadOptions {
readonly mode?: ReadMode;
readonly limit?: number;
}
export interface RecallSearchResult {
readonly hits: readonly RecallSearchHit[];
readonly sync?: SyncResult;
}
import type { OpenCodeRecallOptions, RecallReadOptions, RecallSearchOptions, RecallSearchResult } from './sdk-direct.js';
import type { SyncOptions, SyncResult } from './sidecar.js';
import type { TranscriptWindow } from './transcript.js';
export type { EmbeddingProvider, OllamaEmbeddingProviderOptions } from './embedding.js';
export { OllamaEmbeddingProvider } from './embedding.js';
export type { OpenCodeRecallOptions, RecallReadOptions, RecallSearchHit, RecallSearchOptions, RecallSearchResult, } from './sdk-direct.js';
export type { SyncOptions, SyncResult } from './sidecar.js';
export type { TranscriptWindow } from './transcript.js';
export declare class OpenCodeRecall {

@@ -54,3 +14,3 @@ #private;

sync(options?: SyncOptions): Promise<SyncResult>;
syncLexical(): SyncResult;
syncLexical(): Promise<SyncResult>;
search(query: string, options?: RecallSearchOptions): Promise<RecallSearchResult>;

@@ -57,0 +17,0 @@ read(cursorValue: string, options?: RecallReadOptions): TranscriptWindow;

@@ -1,131 +0,203 @@

import { ChatmlRenderer } from './chatml-renderer';
import { decodeCursor } from './cursor';
import { HistoryDatabase } from './db';
import { OllamaEmbeddingProvider } from './embedding';
import { normalizeWindow } from './normalizer';
import { parseReadMode } from './read-mode';
import { rankSearchRows } from './search';
import { RecallSidecarIndex } from './sidecar';
export { OllamaEmbeddingProvider } from './embedding';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { decodeCursor } from './cursor.js';
import { executeNodeWorker, executeNodeWorkerSync } from './node-worker-client.js';
export { OllamaEmbeddingProvider } from './embedding.js';
const DEFAULT_SEARCH_LIMIT = 50;
const DEFAULT_READ_LIMIT = 12;
const DEFAULT_FRESHNESS_EXCLUSION_MS = 30_000;
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
export class OpenCodeRecall {
#history;
#sidecar;
#provider;
#ownsProvider;
#options;
constructor(options = {}) {
this.#history = new HistoryDatabase(options.historyDbPath);
this.#sidecar = new RecallSidecarIndex(options.sidecarDbPath);
this.#provider = options.embeddingProvider ?? new OllamaEmbeddingProvider();
this.#ownsProvider = options.embeddingProvider === undefined;
this.#options = options;
}
close() {
this.#sidecar.close();
this.#history.close();
if (this.#ownsProvider && 'close' in this.#provider) {
const close = this.#provider.close;
if (typeof close === 'function') {
close.call(this.#provider);
}
}
// Worker-backed SDK calls do not keep resources open in the caller process.
}
async sync(options = {}) {
return this.#sidecar.sync((since) => this.#history.readTextPartsForIndex(since), this.#provider, () => this.#history.readTextPartIds(), options);
return this.#direct().then((recall) => recall.sync(options));
}
// Build/refresh just the FTS5 lexical index, skipping embeddings.
// Used when callers opt out of semantic but still want lexical recall.
syncLexical() {
const start = performance.now();
const result = this.#sidecar.syncLexicalOnly((since) => this.#history.readTextPartsForIndex(since), () => this.#history.readTextPartIds());
return {
elapsedMs: performance.now() - start,
indexedRows: result.indexedRows,
deletedRows: result.deletedRows,
lockAcquired: result.lockAcquired,
};
async syncLexical() {
return this.#direct().then((recall) => recall.syncLexical());
}
async search(query, options = {}) {
const searchOptions = normalizeSearchOptions(options);
if (isBlankQuery(query)) {
return { hits: this.#history.recent(searchOptions).map(toSearchHit) };
if (this.#options.embeddingProvider !== undefined) {
return this.#direct().then((recall) => recall.search(query, options));
}
const lexicalEnabled = options.lexical !== false;
const semanticEnabled = options.semantic !== false;
const shouldSync = (semanticEnabled || lexicalEnabled) && options.sync !== false;
const syncResult = shouldSync
? semanticEnabled
? await this.sync(options.syncOptions)
: this.syncLexical()
: undefined;
const lexicalRows = lexicalEnabled ? this.#sidecar.lexicalSearch(query, searchOptions) : [];
const semanticRows = semanticEnabled
? await this.#sidecar.search(query, searchOptions, this.#provider)
: [];
const rows = rankSearchRows(query, [...lexicalRows, ...semanticRows], searchOptions.limit);
return {
hits: rows.map(toSearchHit),
...(syncResult === undefined ? {} : { sync: syncResult }),
};
return searchHistory(query, { ...this.#options, ...options });
}
read(cursorValue, options = {}) {
const cursor = decodeCursor(cursorValue);
const readOptions = {
mode: parseReadMode(options.mode),
limit: options.limit ?? DEFAULT_READ_LIMIT,
};
return normalizeWindow(cursor.messageId === undefined
? this.#history.readWindowForSession(requiredSessionId(cursor.sessionId), readOptions)
: this.#history.readWindow(cursor.messageId, readOptions));
return readHistoryWindow(cursorValue, { ...this.#options, ...options });
}
render(cursorValue, options = {}) {
return new ChatmlRenderer().render(this.read(cursorValue, options));
return renderHistoryWindow(cursorValue, { ...this.#options, ...options });
}
async #direct() {
const { DirectOpenCodeRecall } = await import('./sdk-direct.js');
const recall = new DirectOpenCodeRecall(this.#options);
return {
sync: async (options) => {
try {
return await recall.sync(options);
}
finally {
recall.close();
}
},
syncLexical: () => {
try {
return recall.syncLexical();
}
finally {
recall.close();
}
},
search: async (query, options) => {
try {
return await recall.search(query, options);
}
finally {
recall.close();
}
},
read: (cursor, options) => {
try {
return recall.read(cursor, options);
}
finally {
recall.close();
}
},
render: (cursor, options) => {
try {
return recall.render(cursor, options);
}
finally {
recall.close();
}
},
};
}
}
export async function searchHistory(query, options = {}) {
const recall = new OpenCodeRecall(options);
try {
return await recall.search(query, options);
if (options.embeddingProvider !== undefined) {
const { directSearchHistory } = await import('./sdk-direct.js');
return directSearchHistory(query, options);
}
finally {
recall.close();
}
const raw = await executeNodeWorker(PACKAGE_DIR, {
kind: 'search',
args: searchWorkerArgs(query, options),
context: { sessionID: options.currentSessionId ?? '' },
}, AbortSignal.timeout(120_000));
return { hits: parseWorkerSearchHits(raw) };
}
export function readHistoryWindow(cursor, options = {}) {
const recall = new OpenCodeRecall(options);
try {
return recall.read(cursor, options);
const parsed = JSON.parse(executeNodeWorkerSync(PACKAGE_DIR, {
kind: 'read-window',
args: readWorkerArgs(cursor, options),
}));
if (isTranscriptWindow(parsed)) {
return parsed;
}
finally {
recall.close();
}
throw new Error('Node worker returned invalid transcript window JSON');
}
export function renderHistoryWindow(cursor, options = {}) {
const recall = new OpenCodeRecall(options);
try {
return recall.render(cursor, options);
return executeNodeWorkerSync(PACKAGE_DIR, {
kind: 'read',
args: readWorkerArgs(cursor, options),
});
}
function readWorkerArgs(cursor, options) {
return {
cursor,
mode: options.mode,
n: options.limit,
historyDbPath: options.historyDbPath,
};
}
function searchWorkerArgs(query, options) {
return {
q: query,
n: options.limit ?? DEFAULT_SEARCH_LIMIT,
directory: options.directory,
includeCurrentSession: options.includeCurrentSession,
excludeSessionId: options.excludeSessionId ?? defaultExcludedSessionId(options),
after: optionalDateArg(options.after),
before: optionalDateArg(options.before ?? defaultBefore(options)),
historyDbPath: options.historyDbPath,
sidecarDbPath: options.sidecarDbPath,
semantic: options.semantic,
lexical: options.lexical,
sync: options.sync,
};
}
function parseWorkerSearchHits(value) {
const json = searchJsonPayload(value);
if (json === undefined) {
return [];
}
finally {
recall.close();
const parsed = JSON.parse(json);
if (!Array.isArray(parsed)) {
return [];
}
return parsed.flatMap((item) => (isHistorySearchResult(item) ? [toSearchHit(item)] : []));
}
function normalizeSearchOptions(options) {
const excluded = excludedSessionId(options);
function searchJsonPayload(value) {
const start = value.indexOf('[');
if (start === -1) {
return undefined;
}
return value.slice(start);
}
function isHistorySearchResult(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const result = value;
return (typeof result.cursor === 'string' &&
typeof result.sid === 'string' &&
typeof result.title === 'string' &&
typeof result.directory === 'string' &&
typeof result.time === 'string' &&
typeof result.role === 'string' &&
typeof result.text === 'string');
}
function toSearchHit(result) {
const cursor = decodeCursor(result.cursor);
const timeCreated = Date.parse(result.time);
return {
limit: options.limit ?? DEFAULT_SEARCH_LIMIT,
...optionalTimestampFilter('after', options.after),
...optionalTimestampFilter('before', options.before ?? defaultBefore(options)),
...(options.directory === undefined ? {} : { directory: options.directory }),
...(excluded === undefined ? {} : { excludeSessionId: excluded }),
cursor: result.cursor,
sessionId: result.sid,
sessionTitle: result.title,
directory: result.directory,
messageId: cursor.messageId ?? '',
partId: cursor.partId ?? '',
role: result.role,
...(result.score === undefined ? {} : { score: result.score }),
timeCreated: Number.isFinite(timeCreated) ? timeCreated : 0,
time: result.time,
text: result.text,
...(result.source === undefined ? {} : { source: result.source }),
};
}
function isBlankQuery(query) {
return query.trim().length === 0;
function isTranscriptWindow(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const window = value;
return (typeof window.sessionId === 'string' &&
typeof window.directory === 'string' &&
typeof window.mode === 'string' &&
typeof window.startIndex === 'number' &&
typeof window.endIndex === 'number' &&
typeof window.anchorIndex === 'number' &&
typeof window.anchorCursor === 'string' &&
typeof window.totalMessages === 'number' &&
Array.isArray(window.messages));
}
function excludedSessionId(options) {
function defaultExcludedSessionId(options) {
if (options.includeCurrentSession === true) {
return options.excludeSessionId;
return undefined;
}
return options.excludeSessionId ?? options.currentSessionId;
return options.currentSessionId;
}

@@ -138,10 +210,3 @@ function defaultBefore(options) {

}
function optionalTimestampFilter(name, value) {
const timestamp = optionalTimestamp(value);
if (timestamp === undefined) {
return {};
}
return { [name]: timestamp };
}
function optionalTimestamp(value) {
function optionalDateArg(value) {
if (value === undefined) {

@@ -151,35 +216,8 @@ return undefined;

if (value instanceof Date) {
return value.getTime();
return value.toISOString();
}
if (typeof value === 'number') {
return Number.isFinite(value) ? value : undefined;
return Number.isFinite(value) ? new Date(value).toISOString() : undefined;
}
if (value.length === 0) {
return undefined;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : undefined;
return value.length === 0 ? undefined : value;
}
function toSearchHit(row) {
const cursor = row.messageId;
return {
cursor,
sessionId: row.sessionId,
sessionTitle: row.sessionTitle,
directory: row.directory,
messageId: row.messageId,
partId: row.partId,
role: row.role,
...(row.score === undefined ? {} : { score: Number(row.score.toFixed(4)) }),
timeCreated: row.timeCreated,
time: new Date(row.timeCreated).toISOString(),
text: row.text,
...(row.source === undefined ? {} : { source: row.source }),
};
}
function requiredSessionId(value) {
if (value !== undefined) {
return value;
}
throw new Error('History cursor does not contain a message or session id');
}

@@ -1,3 +0,3 @@

import type { SearchRow } from './db';
import type { SyncResult } from './sidecar';
import type { SearchRow } from './db.js';
import type { SyncResult } from './sidecar.js';
export interface HistorySearchResult {

@@ -11,2 +11,3 @@ readonly cursor: string;

readonly score?: number;
readonly source?: SearchRow['source'];
readonly text: string;

@@ -13,0 +14,0 @@ }

@@ -1,2 +0,2 @@

import { encodeCursor } from './cursor';
import { encodeCursor } from './cursor.js';
const SYNC_NOTICE_THRESHOLD_MS = 5_000;

@@ -68,2 +68,3 @@ const MAX_RESULTS_PER_SESSION = 2;

...(row.score === undefined ? {} : { score: Number(row.score.toFixed(4)) }),
...(row.source === undefined ? {} : { source: row.source }),
time: new Date(row.timeCreated).toISOString(),

@@ -70,0 +71,0 @@ text: makeSnippet(row.text),

@@ -1,3 +0,3 @@

import type { IndexSourceRow, SearchOptions, SearchRow } from './db';
import type { EmbeddingProvider } from './embedding';
import type { IndexSourceRow, SearchOptions, SearchRow } from './db.js';
import type { EmbeddingProvider } from './embedding.js';
export interface SyncResult {

@@ -4,0 +4,0 @@ readonly elapsedMs: number;

@@ -1,7 +0,7 @@

import { Database } from 'bun:sqlite';
import { createHash, randomUUID } from 'node:crypto';
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { loadConfig } from './config';
import { LexicalIndex } from './lexical-index';
import { loadConfig } from './config.js';
import { LexicalIndex } from './lexical-index.js';
import { Database } from './sqlite.js';
const INDEX_SCHEMA_VERSION = '2';

@@ -109,4 +109,4 @@ const SYNC_OVERLAP_MS = 30 * 60 * 1000;

}
// Bun's `Database.transaction(fn)` returns a callable that re-enters via
// begin/commit. We use it inside LexicalIndex so an external sync is enough.
// LexicalIndex owns nested transactions, so the semantic path keeps its sync
// steps explicit instead of wrapping the whole method in another transaction.
syncLexicalOnly(sourceRows, sourcePartIds) {

@@ -113,0 +113,0 @@ if (!this.#acquireLock()) {

{
"name": "@josxa/opencode-recall",
"version": "0.3.0",
"version": "1.0.0",
"description": "OpenCode plugin that adds semantic + lexical recall over your local session history, with cursor-paginated ChatML transcript reads.",
"type": "module",
"packageManager": "pnpm@11.1.2",
"sideEffects": false,

@@ -24,3 +25,3 @@ "main": "./dist/index.js",

"engines": {
"bun": ">=1.0.0"
"node": ">=22.12.0"
},

@@ -49,7 +50,7 @@ "keywords": [

"scripts": {
"clean": "rm -rf dist",
"build": "bun run clean && tsc -p tsconfig.build.json",
"test": "bun test",
"eval:real-history": "bun scripts/evaluate-real-history.ts",
"eval:embeddings": "bun scripts/evaluate-embedding-models.ts",
"clean": "node -e \"import('node:fs').then(({ rmSync }) => rmSync('dist', { recursive: true, force: true }))\"",
"build": "pnpm run clean && tsc -p tsconfig.build.json",
"test": "vitest run",
"eval:real-history": "tsx scripts/evaluate-real-history.ts",
"eval:embeddings": "tsx scripts/evaluate-embedding-models.ts",
"typecheck": "tsgo --noEmit",

@@ -60,3 +61,3 @@ "lint": "biome check --error-on-warnings .",

"prepare": "husky",
"prepublishOnly": "bun run build"
"prepublishOnly": "pnpm run build"
},

@@ -72,10 +73,11 @@ "files": [

"devDependencies": {
"@biomejs/biome": "latest",
"@biomejs/biome": "2.4.15",
"@opencode-ai/plugin": "latest",
"@types/bun": "latest",
"@types/node": "latest",
"@typescript/native-preview": "latest",
"husky": "latest",
"typescript": "latest"
"tsx": "latest",
"typescript": "latest",
"vitest": "latest"
}
}

@@ -74,3 +74,3 @@ # @josxa/opencode-recall

```sh
bun add -d @josxa/opencode-recall
pnpm add -D @josxa/opencode-recall
```

@@ -165,3 +165,3 @@

Run `bun run eval:embeddings` to compare installed embedding models against the local regression cases in [`docs/real-history-regressions.md`](./docs/real-history-regressions.md).
Run `pnpm run eval:embeddings` to compare installed embedding models against the local regression cases in [`docs/real-history-regressions.md`](./docs/real-history-regressions.md).

@@ -256,7 +256,7 @@ ## Tool reference

```sh
bun install
bun run ai:check # biome + tsgo type-check
bun test # deterministic ranking + cursor tests
bun run eval:real-history # regression suite against your local opencode.db
bun run build # emits dist/
pnpm install
pnpm run ai:check # biome + tsgo type-check
pnpm test # deterministic ranking + cursor tests
pnpm run eval:real-history # regression suite against your local opencode.db
pnpm run build # emits dist/
```

@@ -263,0 +263,0 @@