opencode-codex-memory 0.1.3 → 0.1.5
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/dist/src/capture.d.ts +19 -0
- package/dist/src/capture.js +120 -0
- package/dist/src/citation.d.ts +14 -0
- package/dist/src/citation.js +81 -0
- package/dist/src/db.d.ts +3 -0
- package/dist/src/db.js +78 -0
- package/dist/src/git-baseline.d.ts +24 -0
- package/dist/src/git-baseline.js +150 -0
- package/dist/src/index.d.ts +163 -0
- package/dist/src/index.js +365 -0
- package/dist/src/llm.d.ts +19 -0
- package/dist/src/llm.js +251 -0
- package/dist/src/path-guard.d.ts +10 -0
- package/dist/src/path-guard.js +44 -0
- package/dist/src/paths.d.ts +4 -0
- package/dist/src/paths.js +23 -0
- package/dist/src/phase1.d.ts +11 -0
- package/dist/src/phase1.js +104 -0
- package/dist/src/phase2.d.ts +11 -0
- package/dist/src/phase2.js +83 -0
- package/dist/src/ratelimit.d.ts +5 -0
- package/dist/src/ratelimit.js +20 -0
- package/dist/src/redact.d.ts +8 -0
- package/dist/src/redact.js +37 -0
- package/dist/src/source.d.ts +3 -0
- package/dist/src/source.js +46 -0
- package/dist/src/store.d.ts +96 -0
- package/dist/src/store.js +346 -0
- package/dist/src/token.d.ts +8 -0
- package/dist/src/token.js +19 -0
- package/dist/src/workspace.d.ts +8 -0
- package/dist/src/workspace.js +194 -0
- package/dist/tools/control.d.ts +29 -0
- package/dist/tools/control.js +153 -0
- package/dist/tools/memory.d.ts +52 -0
- package/dist/tools/memory.js +322 -0
- package/package.json +23 -6
- package/src/capture.ts +0 -135
- package/src/citation.ts +0 -94
- package/src/db.ts +0 -80
- package/src/git-baseline.ts +0 -162
- package/src/index.ts +0 -366
- package/src/llm.ts +0 -267
- package/src/path-guard.ts +0 -44
- package/src/paths.ts +0 -29
- package/src/phase1.ts +0 -116
- package/src/phase2.ts +0 -99
- package/src/ratelimit.ts +0 -26
- package/src/redact.ts +0 -44
- package/src/source.ts +0 -59
- package/src/store.ts +0 -430
- package/src/templates/consolidation.md +0 -448
- package/src/templates/read_path.md +0 -104
- package/src/templates/stage_one_input.md +0 -11
- package/src/templates/stage_one_system.md +0 -333
- package/src/token.ts +0 -21
- package/src/workspace.ts +0 -181
- package/tools/control.ts +0 -145
- package/tools/memory.ts +0 -318
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MemoryStore } from "./store.js";
|
|
2
|
+
export interface SessionRow {
|
|
3
|
+
id: string;
|
|
4
|
+
updated_at: number;
|
|
5
|
+
directory: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function listRecentSessions(limit?: number): SessionRow[];
|
|
8
|
+
export interface TranscriptMessage {
|
|
9
|
+
type: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadTranscript(sessionId: string): TranscriptMessage[];
|
|
14
|
+
export interface EligibilityOptions {
|
|
15
|
+
maxAgeDays: number;
|
|
16
|
+
minIdleHours: number;
|
|
17
|
+
excludeSession?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function selectEligibleSessions(store: MemoryStore, opts: EligibilityOptions): SessionRow[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { opencodeDbPath } from "./paths.js";
|
|
3
|
+
import { SCAN_LIMIT } from "./store.js";
|
|
4
|
+
let opencodeDb = null;
|
|
5
|
+
function openOpencodeDb() {
|
|
6
|
+
if (opencodeDb)
|
|
7
|
+
return opencodeDb;
|
|
8
|
+
const p = opencodeDbPath();
|
|
9
|
+
try {
|
|
10
|
+
opencodeDb = new Database(p, { readonly: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
opencodeDb = null;
|
|
14
|
+
}
|
|
15
|
+
return opencodeDb;
|
|
16
|
+
}
|
|
17
|
+
export function listRecentSessions(limit = SCAN_LIMIT) {
|
|
18
|
+
const db = openOpencodeDb();
|
|
19
|
+
if (!db)
|
|
20
|
+
return [];
|
|
21
|
+
try {
|
|
22
|
+
// Top-level sessions only: task-tool children are summarized into their
|
|
23
|
+
// parent, and the plugin's own sub-sessions must never be memorized.
|
|
24
|
+
return db
|
|
25
|
+
.prepare(`SELECT id, time_updated AS updated_at, directory FROM session
|
|
26
|
+
WHERE parent_id IS NULL AND title NOT LIKE 'codex-memory-%'
|
|
27
|
+
ORDER BY time_updated DESC LIMIT ?`)
|
|
28
|
+
.all(limit);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function loadTranscript(sessionId) {
|
|
35
|
+
const db = openOpencodeDb();
|
|
36
|
+
if (!db)
|
|
37
|
+
return [];
|
|
38
|
+
try {
|
|
39
|
+
const rows = db
|
|
40
|
+
.prepare(`SELECT p.data, m.data AS msg_data
|
|
41
|
+
FROM part p
|
|
42
|
+
JOIN message m ON p.message_id = m.id
|
|
43
|
+
WHERE p.session_id = ?
|
|
44
|
+
ORDER BY p.time_created ASC`)
|
|
45
|
+
.all(sessionId);
|
|
46
|
+
return rows.map((r) => {
|
|
47
|
+
let parsed = {};
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(r.data);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
}
|
|
53
|
+
let role;
|
|
54
|
+
try {
|
|
55
|
+
const msg = JSON.parse(r.msg_data);
|
|
56
|
+
role = msg.role;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
type: parsed.type ?? "unknown",
|
|
62
|
+
role,
|
|
63
|
+
text: extractText(parsed),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function extractText(msg) {
|
|
72
|
+
if (!msg)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof msg.text === "string")
|
|
75
|
+
return msg.text;
|
|
76
|
+
if (msg.type === "tool") {
|
|
77
|
+
// Full tool payloads: codex serializes complete FunctionCall/Output items
|
|
78
|
+
// and relies solely on the global transcript truncation. Tool outputs are
|
|
79
|
+
// the extractor's strongest evidence — do not slice them per call.
|
|
80
|
+
const tool = msg.tool ?? "unknown";
|
|
81
|
+
const input = msg.state?.input ? JSON.stringify(msg.state.input) : "";
|
|
82
|
+
const output = typeof msg.state?.output === "string" ? msg.state.output : "";
|
|
83
|
+
return `[tool: ${tool}] ${input}${output ? "\n" + output : ""}`;
|
|
84
|
+
}
|
|
85
|
+
if (msg.type === "step-start" || msg.type === "step-finish")
|
|
86
|
+
return undefined;
|
|
87
|
+
if (Array.isArray(msg.parts)) {
|
|
88
|
+
return msg.parts
|
|
89
|
+
.filter((p) => p?.type === "text" && typeof p.text === "string")
|
|
90
|
+
.map((p) => p.text)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(msg.content)) {
|
|
94
|
+
return msg.content
|
|
95
|
+
.filter((c) => typeof c === "string" || typeof c?.text === "string")
|
|
96
|
+
.map((c) => (typeof c === "string" ? c : c.text))
|
|
97
|
+
.join("\n");
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
export function selectEligibleSessions(store, opts) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const minUpdated = now - opts.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
104
|
+
const maxUpdated = now - opts.minIdleHours * 60 * 60 * 1000;
|
|
105
|
+
const sessions = listRecentSessions();
|
|
106
|
+
return sessions.filter((s) => {
|
|
107
|
+
if (opts.excludeSession && s.id === opts.excludeSession)
|
|
108
|
+
return false;
|
|
109
|
+
if (s.updated_at < minUpdated)
|
|
110
|
+
return false;
|
|
111
|
+
if (s.updated_at > maxUpdated)
|
|
112
|
+
return false;
|
|
113
|
+
const mode = store.getMemoryMode(s.id);
|
|
114
|
+
if (mode === "disabled")
|
|
115
|
+
return false;
|
|
116
|
+
if (store.isPolluted(s.id))
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface MemoryCitationEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
lineStart: number;
|
|
4
|
+
lineEnd: number;
|
|
5
|
+
note: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedCitation {
|
|
8
|
+
sessionIds: string[];
|
|
9
|
+
entries: MemoryCitationEntry[];
|
|
10
|
+
raw: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseCitations(text: string): ParsedCitation[];
|
|
13
|
+
export declare function extractCitedSessionIds(text: string): string[];
|
|
14
|
+
export declare function stripCitations(text: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const CITATION_BLOCK_RE = /<memory-citation>[\s\S]*?<\/memory-citation>/gi;
|
|
2
|
+
function extractSection(block, name) {
|
|
3
|
+
const m = block.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "i"));
|
|
4
|
+
return m ? m[1] : null;
|
|
5
|
+
}
|
|
6
|
+
function parseEntry(line) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (!trimmed)
|
|
9
|
+
return null;
|
|
10
|
+
const noteSplit = trimmed.lastIndexOf("|note=[");
|
|
11
|
+
if (noteSplit === -1 || !trimmed.endsWith("]"))
|
|
12
|
+
return null;
|
|
13
|
+
const location = trimmed.slice(0, noteSplit);
|
|
14
|
+
const note = trimmed.slice(noteSplit + "|note=[".length, -1).trim();
|
|
15
|
+
const colon = location.lastIndexOf(":");
|
|
16
|
+
if (colon === -1)
|
|
17
|
+
return null;
|
|
18
|
+
const path = location.slice(0, colon).trim();
|
|
19
|
+
const range = location.slice(colon + 1);
|
|
20
|
+
const dash = range.indexOf("-");
|
|
21
|
+
if (dash === -1)
|
|
22
|
+
return null;
|
|
23
|
+
const lineStart = Number.parseInt(range.slice(0, dash).trim(), 10);
|
|
24
|
+
const lineEnd = Number.parseInt(range.slice(dash + 1).trim(), 10);
|
|
25
|
+
if (!path || Number.isNaN(lineStart) || Number.isNaN(lineEnd))
|
|
26
|
+
return null;
|
|
27
|
+
return { path, lineStart, lineEnd, note };
|
|
28
|
+
}
|
|
29
|
+
export function parseCitations(text) {
|
|
30
|
+
const results = [];
|
|
31
|
+
const re = new RegExp(CITATION_BLOCK_RE);
|
|
32
|
+
let m;
|
|
33
|
+
while ((m = re.exec(text)) !== null) {
|
|
34
|
+
const raw = m[0];
|
|
35
|
+
const entries = [];
|
|
36
|
+
const sessionIds = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const entriesBlock = extractSection(raw, "citation_entries");
|
|
39
|
+
if (entriesBlock) {
|
|
40
|
+
for (const line of entriesBlock.split(/\r?\n/)) {
|
|
41
|
+
const entry = parseEntry(line);
|
|
42
|
+
if (entry)
|
|
43
|
+
entries.push(entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const idsBlock = extractSection(raw, "session_ids");
|
|
47
|
+
if (idsBlock) {
|
|
48
|
+
for (const line of idsBlock.split(/\r?\n/)) {
|
|
49
|
+
const id = line.trim();
|
|
50
|
+
if (id && !seen.has(id)) {
|
|
51
|
+
seen.add(id);
|
|
52
|
+
sessionIds.push(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (entriesBlock && entries.length === 0) {
|
|
57
|
+
// Legacy format: <citation_entries> held a comma-separated session-id list.
|
|
58
|
+
for (const id of entriesBlock.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
59
|
+
if (!seen.has(id)) {
|
|
60
|
+
seen.add(id);
|
|
61
|
+
sessionIds.push(id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (entries.length > 0 || sessionIds.length > 0) {
|
|
66
|
+
results.push({ sessionIds, entries, raw });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
export function extractCitedSessionIds(text) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const c of parseCitations(text)) {
|
|
74
|
+
for (const id of c.sessionIds)
|
|
75
|
+
seen.add(id);
|
|
76
|
+
}
|
|
77
|
+
return Array.from(seen);
|
|
78
|
+
}
|
|
79
|
+
export function stripCitations(text) {
|
|
80
|
+
return text.replace(CITATION_BLOCK_RE, "").replace(/[ \t]*\n{3,}/g, "\n\n").trimEnd();
|
|
81
|
+
}
|
package/dist/src/db.d.ts
ADDED
package/dist/src/db.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { memoryDbPath } from "./paths.js";
|
|
3
|
+
const SCHEMA_V1 = [
|
|
4
|
+
`CREATE TABLE IF NOT EXISTS memory_stage1_outputs (
|
|
5
|
+
session_id TEXT PRIMARY KEY,
|
|
6
|
+
source_updated_at INTEGER NOT NULL,
|
|
7
|
+
raw_memory TEXT NOT NULL,
|
|
8
|
+
rollout_summary TEXT NOT NULL,
|
|
9
|
+
rollout_slug TEXT,
|
|
10
|
+
cwd TEXT,
|
|
11
|
+
generated_at INTEGER NOT NULL,
|
|
12
|
+
usage_count INTEGER DEFAULT 0,
|
|
13
|
+
last_usage INTEGER,
|
|
14
|
+
selected_for_phase2 INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
selected_for_phase2_source_updated_at INTEGER
|
|
16
|
+
)`,
|
|
17
|
+
`CREATE INDEX IF NOT EXISTS idx_memory_stage1_source_updated_at
|
|
18
|
+
ON memory_stage1_outputs(source_updated_at DESC, session_id DESC)`,
|
|
19
|
+
`CREATE TABLE IF NOT EXISTS memory_jobs (
|
|
20
|
+
kind TEXT NOT NULL,
|
|
21
|
+
job_key TEXT NOT NULL,
|
|
22
|
+
status TEXT NOT NULL,
|
|
23
|
+
worker_id TEXT,
|
|
24
|
+
ownership_token TEXT,
|
|
25
|
+
started_at INTEGER,
|
|
26
|
+
finished_at INTEGER,
|
|
27
|
+
lease_until INTEGER,
|
|
28
|
+
retry_at INTEGER,
|
|
29
|
+
retry_remaining INTEGER NOT NULL,
|
|
30
|
+
last_error TEXT,
|
|
31
|
+
input_watermark INTEGER,
|
|
32
|
+
last_success_watermark INTEGER,
|
|
33
|
+
PRIMARY KEY (kind, job_key)
|
|
34
|
+
)`,
|
|
35
|
+
`CREATE INDEX IF NOT EXISTS idx_memory_jobs_kind_status_retry_lease
|
|
36
|
+
ON memory_jobs(kind, status, retry_at, lease_until)`,
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS memory_session_meta (
|
|
38
|
+
session_id TEXT PRIMARY KEY,
|
|
39
|
+
memory_mode TEXT NOT NULL DEFAULT 'enabled',
|
|
40
|
+
polluted INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
updated_at INTEGER NOT NULL
|
|
42
|
+
)`,
|
|
43
|
+
];
|
|
44
|
+
let dbInstance = null;
|
|
45
|
+
export function openDb() {
|
|
46
|
+
if (dbInstance)
|
|
47
|
+
return dbInstance;
|
|
48
|
+
const dbPath = memoryDbPath();
|
|
49
|
+
const db = new Database(dbPath, { create: true, readwrite: true, strict: false });
|
|
50
|
+
// Match codex's memories-DB open options (runtime.rs): WAL, NORMAL sync,
|
|
51
|
+
// 5s busy timeout for cross-process access, incremental auto-vacuum.
|
|
52
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
53
|
+
db.exec("PRAGMA synchronous=NORMAL");
|
|
54
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
55
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
56
|
+
runMigrations(db);
|
|
57
|
+
dbInstance = db;
|
|
58
|
+
return db;
|
|
59
|
+
}
|
|
60
|
+
function runMigrations(db) {
|
|
61
|
+
db.exec(`CREATE TABLE IF NOT EXISTS schema_version (
|
|
62
|
+
version INTEGER NOT NULL,
|
|
63
|
+
applied_at INTEGER NOT NULL
|
|
64
|
+
)`);
|
|
65
|
+
const current = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
|
|
66
|
+
const currentVersion = current?.version ?? 0;
|
|
67
|
+
if (currentVersion >= 1)
|
|
68
|
+
return;
|
|
69
|
+
for (const stmt of SCHEMA_V1)
|
|
70
|
+
db.exec(stmt);
|
|
71
|
+
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(1, Date.now());
|
|
72
|
+
}
|
|
73
|
+
export function closeDb() {
|
|
74
|
+
if (dbInstance) {
|
|
75
|
+
dbInstance.close();
|
|
76
|
+
dbInstance = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const DIFF_ARTIFACT = "phase2_workspace_diff.md";
|
|
2
|
+
export interface WorkspaceChange {
|
|
3
|
+
status: "A" | "M" | "D";
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WorkspaceDiff {
|
|
7
|
+
changes: WorkspaceChange[];
|
|
8
|
+
unifiedDiff: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Mirrors codex prepare_memory_workspace: an existing baseline is preserved
|
|
12
|
+
* untouched so the phase-2 diff spans last-successful-run -> now — including
|
|
13
|
+
* manual user edits and newly added ad-hoc notes. Committing here would
|
|
14
|
+
* swallow those changes and consolidation would never see them. Only a root
|
|
15
|
+
* without any commit gets a fresh baseline.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensureBaseline(): Promise<boolean>;
|
|
18
|
+
export declare function captureWorkspaceDiff(): Promise<WorkspaceDiff>;
|
|
19
|
+
/**
|
|
20
|
+
* Mirrors codex reset_git_repository: delete .git and re-create a fresh
|
|
21
|
+
* single-commit baseline so deleted/redacted memory content is not retained
|
|
22
|
+
* in unreachable git objects (history is intentionally dropped).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetBaseline(): Promise<boolean>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { memoryRoot } from "./paths.js";
|
|
4
|
+
import * as isogit from "isomorphic-git";
|
|
5
|
+
import { createPatch } from "diff";
|
|
6
|
+
const AUTHOR = { name: "opencode-codex-memory", email: "memory@opencode.local" };
|
|
7
|
+
// Generated prompt artifact; removed before diffing and before baseline
|
|
8
|
+
// commits (mirrors codex's remove_workspace_diff) so it never enters the
|
|
9
|
+
// baseline history or shows up as memory content.
|
|
10
|
+
export const DIFF_ARTIFACT = "phase2_workspace_diff.md";
|
|
11
|
+
function removeDiffArtifact(dir) {
|
|
12
|
+
try {
|
|
13
|
+
fs.unlinkSync(path.join(dir, DIFF_ARTIFACT));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function ensureInit(dir) {
|
|
19
|
+
const gitDir = path.join(dir, ".git");
|
|
20
|
+
if (!fs.existsSync(gitDir)) {
|
|
21
|
+
await isogit.init({ fs, dir });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// statusMatrix rows are [filepath, head, workdir, stage]; head !== workdir
|
|
25
|
+
// means the working tree differs from HEAD (added, modified, or deleted).
|
|
26
|
+
async function stageAll(dir) {
|
|
27
|
+
const matrix = await isogit.statusMatrix({ fs, dir });
|
|
28
|
+
let changes = 0;
|
|
29
|
+
for (const [filepath, head, workdir, stage] of matrix) {
|
|
30
|
+
if (head === 1 && workdir === 1 && stage === 1)
|
|
31
|
+
continue;
|
|
32
|
+
if (workdir === 0) {
|
|
33
|
+
// isogit.add throws on deleted files; they must be staged via remove
|
|
34
|
+
await isogit.remove({ fs, dir, filepath });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
await isogit.add({ fs, dir, filepath });
|
|
38
|
+
}
|
|
39
|
+
if (head !== workdir)
|
|
40
|
+
changes++;
|
|
41
|
+
}
|
|
42
|
+
return changes;
|
|
43
|
+
}
|
|
44
|
+
async function hasHeadCommit(dir) {
|
|
45
|
+
try {
|
|
46
|
+
await isogit.resolveRef({ fs, dir, ref: "HEAD" });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function commitBaseline(dir) {
|
|
54
|
+
await stageAll(dir);
|
|
55
|
+
return isogit.commit({ fs, dir, message: "memory baseline", author: AUTHOR });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mirrors codex prepare_memory_workspace: an existing baseline is preserved
|
|
59
|
+
* untouched so the phase-2 diff spans last-successful-run -> now — including
|
|
60
|
+
* manual user edits and newly added ad-hoc notes. Committing here would
|
|
61
|
+
* swallow those changes and consolidation would never see them. Only a root
|
|
62
|
+
* without any commit gets a fresh baseline.
|
|
63
|
+
*/
|
|
64
|
+
export async function ensureBaseline() {
|
|
65
|
+
try {
|
|
66
|
+
const dir = memoryRoot();
|
|
67
|
+
removeDiffArtifact(dir);
|
|
68
|
+
await ensureInit(dir);
|
|
69
|
+
if (!(await hasHeadCommit(dir))) {
|
|
70
|
+
await commitBaseline(dir);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error("[opencode-codex-memory] ensureBaseline error:", err);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readBaselineText(dir, headOid, filepath) {
|
|
80
|
+
try {
|
|
81
|
+
const { blob } = await isogit.readBlob({ fs, dir, oid: headOid, filepath });
|
|
82
|
+
return new TextDecoder().decode(blob);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function readWorkdirText(dir, filepath) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readFileSync(path.join(dir, filepath), "utf8");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function captureWorkspaceDiff() {
|
|
97
|
+
try {
|
|
98
|
+
const dir = memoryRoot();
|
|
99
|
+
await ensureInit(dir);
|
|
100
|
+
removeDiffArtifact(dir);
|
|
101
|
+
const matrix = await isogit.statusMatrix({ fs, dir });
|
|
102
|
+
const changedRows = matrix.filter(([filepath, head, workdir]) => head !== workdir && filepath !== DIFF_ARTIFACT);
|
|
103
|
+
const changes = changedRows.map(([filepath, head, workdir]) => {
|
|
104
|
+
if (head === 0)
|
|
105
|
+
return { status: "A", path: filepath };
|
|
106
|
+
if (workdir === 0)
|
|
107
|
+
return { status: "D", path: filepath };
|
|
108
|
+
return { status: "M", path: filepath };
|
|
109
|
+
});
|
|
110
|
+
let headOid = null;
|
|
111
|
+
try {
|
|
112
|
+
headOid = await isogit.resolveRef({ fs, dir, ref: "HEAD" });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// no commits yet — every file diffs against empty
|
|
116
|
+
}
|
|
117
|
+
const patches = [];
|
|
118
|
+
for (const [filepath, head, workdir] of changedRows) {
|
|
119
|
+
const oldText = head === 1 && headOid ? await readBaselineText(dir, headOid, filepath) : "";
|
|
120
|
+
const newText = workdir === 0 ? "" : readWorkdirText(dir, filepath);
|
|
121
|
+
// No per-file cap: codex renders every file's patch in full and relies
|
|
122
|
+
// on the global 4 MiB truncation in writeWorkspaceDiff.
|
|
123
|
+
patches.push(createPatch(filepath, oldText, newText));
|
|
124
|
+
}
|
|
125
|
+
return { changes, unifiedDiff: patches.join("\n") };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error("[opencode-codex-memory] captureWorkspaceDiff error:", err);
|
|
129
|
+
return { changes: [], unifiedDiff: "" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Mirrors codex reset_git_repository: delete .git and re-create a fresh
|
|
134
|
+
* single-commit baseline so deleted/redacted memory content is not retained
|
|
135
|
+
* in unreachable git objects (history is intentionally dropped).
|
|
136
|
+
*/
|
|
137
|
+
export async function resetBaseline() {
|
|
138
|
+
try {
|
|
139
|
+
const dir = memoryRoot();
|
|
140
|
+
removeDiffArtifact(dir);
|
|
141
|
+
fs.rmSync(path.join(dir, ".git"), { recursive: true, force: true });
|
|
142
|
+
await isogit.init({ fs, dir });
|
|
143
|
+
await commitBaseline(dir);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error("[opencode-codex-memory] resetBaseline error:", err);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { PluginInput, PluginOptions } from "@opencode-ai/plugin";
|
|
2
|
+
export declare function takeNewCitations(partKey: string, ids: string[]): string[];
|
|
3
|
+
declare const _default: {
|
|
4
|
+
id: string;
|
|
5
|
+
server(input: PluginInput, opts?: PluginOptions): Promise<{
|
|
6
|
+
tool: {
|
|
7
|
+
memory_read: {
|
|
8
|
+
description: string;
|
|
9
|
+
args: {
|
|
10
|
+
path: import("zod").ZodString;
|
|
11
|
+
line_offset: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
12
|
+
max_lines: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
13
|
+
};
|
|
14
|
+
execute(args: {
|
|
15
|
+
path: string;
|
|
16
|
+
line_offset?: number | undefined;
|
|
17
|
+
max_lines?: number | undefined;
|
|
18
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
19
|
+
};
|
|
20
|
+
memory_search: {
|
|
21
|
+
description: string;
|
|
22
|
+
args: {
|
|
23
|
+
query: import("zod").ZodOptional<import("zod").ZodString>;
|
|
24
|
+
case_sensitive: import("zod").ZodDefault<import("zod").ZodBoolean>;
|
|
25
|
+
since: import("zod").ZodOptional<import("zod").ZodString>;
|
|
26
|
+
until: import("zod").ZodOptional<import("zod").ZodString>;
|
|
27
|
+
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
28
|
+
};
|
|
29
|
+
execute(args: {
|
|
30
|
+
case_sensitive: boolean;
|
|
31
|
+
limit: number;
|
|
32
|
+
query?: string | undefined;
|
|
33
|
+
since?: string | undefined;
|
|
34
|
+
until?: string | undefined;
|
|
35
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
36
|
+
};
|
|
37
|
+
memory_list: {
|
|
38
|
+
description: string;
|
|
39
|
+
args: {
|
|
40
|
+
path: import("zod").ZodDefault<import("zod").ZodString>;
|
|
41
|
+
max_results: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
42
|
+
};
|
|
43
|
+
execute(args: {
|
|
44
|
+
path: string;
|
|
45
|
+
max_results: number;
|
|
46
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
47
|
+
};
|
|
48
|
+
memory_add_note: {
|
|
49
|
+
description: string;
|
|
50
|
+
args: {
|
|
51
|
+
note: import("zod").ZodString;
|
|
52
|
+
title: import("zod").ZodOptional<import("zod").ZodString>;
|
|
53
|
+
};
|
|
54
|
+
execute(args: {
|
|
55
|
+
note: string;
|
|
56
|
+
title?: string | undefined;
|
|
57
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
58
|
+
};
|
|
59
|
+
memory_reset: {
|
|
60
|
+
description: string;
|
|
61
|
+
args: {
|
|
62
|
+
confirm: import("zod").ZodBoolean;
|
|
63
|
+
};
|
|
64
|
+
execute(args: {
|
|
65
|
+
confirm: boolean;
|
|
66
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
67
|
+
};
|
|
68
|
+
memory_inspect: {
|
|
69
|
+
description: string;
|
|
70
|
+
args: {};
|
|
71
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
72
|
+
};
|
|
73
|
+
memory_mode: {
|
|
74
|
+
description: string;
|
|
75
|
+
args: {
|
|
76
|
+
mode: import("zod").ZodEnum<{
|
|
77
|
+
enabled: "enabled";
|
|
78
|
+
disabled: "disabled";
|
|
79
|
+
polluted: "polluted";
|
|
80
|
+
}>;
|
|
81
|
+
sessionId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
82
|
+
};
|
|
83
|
+
execute(args: {
|
|
84
|
+
mode: "enabled" | "disabled" | "polluted";
|
|
85
|
+
sessionId?: string | undefined;
|
|
86
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
87
|
+
};
|
|
88
|
+
} | {
|
|
89
|
+
memory_reset: {
|
|
90
|
+
description: string;
|
|
91
|
+
args: {
|
|
92
|
+
confirm: import("zod").ZodBoolean;
|
|
93
|
+
};
|
|
94
|
+
execute(args: {
|
|
95
|
+
confirm: boolean;
|
|
96
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
97
|
+
};
|
|
98
|
+
memory_inspect: {
|
|
99
|
+
description: string;
|
|
100
|
+
args: {};
|
|
101
|
+
execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
102
|
+
};
|
|
103
|
+
memory_mode: {
|
|
104
|
+
description: string;
|
|
105
|
+
args: {
|
|
106
|
+
mode: import("zod").ZodEnum<{
|
|
107
|
+
enabled: "enabled";
|
|
108
|
+
disabled: "disabled";
|
|
109
|
+
polluted: "polluted";
|
|
110
|
+
}>;
|
|
111
|
+
sessionId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
112
|
+
};
|
|
113
|
+
execute(args: {
|
|
114
|
+
mode: "enabled" | "disabled" | "polluted";
|
|
115
|
+
sessionId?: string | undefined;
|
|
116
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
117
|
+
};
|
|
118
|
+
memory_read?: undefined;
|
|
119
|
+
memory_search?: undefined;
|
|
120
|
+
memory_list?: undefined;
|
|
121
|
+
memory_add_note?: undefined;
|
|
122
|
+
};
|
|
123
|
+
config(input: {
|
|
124
|
+
agent?: Record<string, unknown>;
|
|
125
|
+
}): Promise<void>;
|
|
126
|
+
"experimental.chat.system.transform"(input: {
|
|
127
|
+
sessionID?: string;
|
|
128
|
+
model: unknown;
|
|
129
|
+
}, output: {
|
|
130
|
+
system: string[];
|
|
131
|
+
}): Promise<void>;
|
|
132
|
+
"experimental.chat.messages.transform"(_input: unknown, output: {
|
|
133
|
+
messages: {
|
|
134
|
+
info: {
|
|
135
|
+
role?: string;
|
|
136
|
+
};
|
|
137
|
+
parts: {
|
|
138
|
+
type: string;
|
|
139
|
+
text?: string;
|
|
140
|
+
}[];
|
|
141
|
+
}[];
|
|
142
|
+
}): Promise<void>;
|
|
143
|
+
event(input: {
|
|
144
|
+
event: {
|
|
145
|
+
type: string;
|
|
146
|
+
properties: unknown;
|
|
147
|
+
};
|
|
148
|
+
}): Promise<void>;
|
|
149
|
+
dispose(): Promise<void>;
|
|
150
|
+
}>;
|
|
151
|
+
};
|
|
152
|
+
export default _default;
|
|
153
|
+
/**
|
|
154
|
+
* Registers the memorize / memorize-extract sub-agents through the config
|
|
155
|
+
* hook so installing the plugin requires no manual agent setup. Definitions
|
|
156
|
+
* are read from the plugin's bundled opencode.json (single source of truth
|
|
157
|
+
* with the dev checkout). A user-defined agent of the same name always wins —
|
|
158
|
+
* only missing entries are filled. opencode-specific packaging: codex ships
|
|
159
|
+
* its memory agents inside the binary.
|
|
160
|
+
*/
|
|
161
|
+
export declare function injectAgentDefinitions(config: {
|
|
162
|
+
agent?: Record<string, unknown>;
|
|
163
|
+
}): void;
|