open-think 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 OpenThinkAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+
3
+ // src/lib/paths.ts
4
+ import path from "path";
5
+ import fs from "fs";
6
+ function getHome() {
7
+ const home = process.env.HOME;
8
+ if (!home) {
9
+ throw new Error("HOME environment variable is not set");
10
+ }
11
+ return home;
12
+ }
13
+ function sanitizeName(name) {
14
+ if (!name || /[\/\\\.]{2}/.test(name) || /[^a-zA-Z0-9_-]/.test(name)) {
15
+ throw new Error(`Invalid cortex name: "${name}". Use only alphanumeric characters, hyphens, and underscores.`);
16
+ }
17
+ return name;
18
+ }
19
+ function getThinkHome() {
20
+ const thinkHome = process.env.THINK_HOME;
21
+ if (thinkHome === void 0 || thinkHome === "") return null;
22
+ return thinkHome;
23
+ }
24
+ function getThinkDir() {
25
+ return getThinkHome() ?? path.join(getHome(), ".think");
26
+ }
27
+ function getThinkConfigDir() {
28
+ const thinkHome = getThinkHome();
29
+ if (thinkHome) return path.join(thinkHome, "config");
30
+ const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(getHome(), ".config");
31
+ return path.join(xdgConfig, "think");
32
+ }
33
+ function getThinkDataDir() {
34
+ const thinkHome = getThinkHome();
35
+ if (thinkHome) return path.join(thinkHome, "data");
36
+ const xdgData = process.env.XDG_DATA_HOME || path.join(getHome(), ".local", "share");
37
+ return path.join(xdgData, "think");
38
+ }
39
+ function getEngramsDir() {
40
+ return path.join(getThinkDir(), "engrams");
41
+ }
42
+ function getEngramDbPath(cortexName) {
43
+ return path.join(getEngramsDir(), `${sanitizeName(cortexName)}.db`);
44
+ }
45
+ function getRepoPath() {
46
+ return path.join(getThinkDir(), "repo");
47
+ }
48
+ function getLongtermDir() {
49
+ return path.join(getThinkDir(), "longterm");
50
+ }
51
+ function getLongtermPath(cortexName) {
52
+ return path.join(getLongtermDir(), `${sanitizeName(cortexName)}.md`);
53
+ }
54
+ function getCuratorMdPath() {
55
+ return path.join(getThinkDir(), "curator.md");
56
+ }
57
+ function ensureThinkDirs() {
58
+ fs.mkdirSync(getEngramsDir(), { recursive: true });
59
+ fs.mkdirSync(getLongtermDir(), { recursive: true });
60
+ }
61
+
62
+ export {
63
+ getThinkConfigDir,
64
+ getThinkDataDir,
65
+ getEngramsDir,
66
+ getEngramDbPath,
67
+ getRepoPath,
68
+ getLongtermPath,
69
+ getCuratorMdPath,
70
+ ensureThinkDirs
71
+ };
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import {
3
+ ensureThinkDirs,
4
+ getEngramDbPath
5
+ } from "./chunk-DCTG6IK4.js";
6
+
7
+ // src/db/memory-queries.ts
8
+ import { v7 as uuidv7 } from "uuid";
9
+
10
+ // src/db/engrams.ts
11
+ import { DatabaseSync } from "node:sqlite";
12
+
13
+ // src/db/migrate.ts
14
+ function runMigrations(db, migrations2) {
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS _migrations (
17
+ version INTEGER PRIMARY KEY NOT NULL,
18
+ applied_at TEXT NOT NULL
19
+ ) STRICT;
20
+ `);
21
+ const currentVersion = db.prepare(
22
+ "SELECT COALESCE(MAX(version), 0) as version FROM _migrations"
23
+ ).get();
24
+ const pending = migrations2.filter((m) => m.version > currentVersion.version).sort((a, b) => a.version - b.version);
25
+ for (const migration of pending) {
26
+ db.exec("BEGIN");
27
+ try {
28
+ migration.up(db);
29
+ db.prepare("INSERT INTO _migrations (version, applied_at) VALUES (?, ?)").run(
30
+ migration.version,
31
+ (/* @__PURE__ */ new Date()).toISOString()
32
+ );
33
+ db.exec("COMMIT");
34
+ } catch (err) {
35
+ db.exec("ROLLBACK");
36
+ throw new Error(`Migration v${migration.version} failed: ${err instanceof Error ? err.message : String(err)}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ // src/db/engrams.ts
42
+ var dbs = /* @__PURE__ */ new Map();
43
+ var migrations = [
44
+ {
45
+ version: 1,
46
+ up: (db) => {
47
+ db.exec(`
48
+ CREATE TABLE IF NOT EXISTS engrams (
49
+ id TEXT PRIMARY KEY NOT NULL,
50
+ content TEXT NOT NULL,
51
+ created_at TEXT NOT NULL,
52
+ expires_at TEXT NOT NULL,
53
+ evaluated_at TEXT,
54
+ promoted INTEGER,
55
+ deleted_at TEXT
56
+ ) STRICT;
57
+ `);
58
+ db.exec(`
59
+ CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts
60
+ USING fts5(content, content='engrams', content_rowid='rowid');
61
+ `);
62
+ db.exec(`
63
+ CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
64
+ INSERT INTO engrams_fts(rowid, content) VALUES (new.rowid, new.content);
65
+ END;
66
+ `);
67
+ db.exec(`
68
+ CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
69
+ INSERT INTO engrams_fts(engrams_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
70
+ END;
71
+ `);
72
+ }
73
+ },
74
+ {
75
+ version: 2,
76
+ up: (db) => {
77
+ db.exec(`
78
+ CREATE TABLE IF NOT EXISTS memories (
79
+ id TEXT PRIMARY KEY NOT NULL,
80
+ ts TEXT NOT NULL,
81
+ author TEXT NOT NULL,
82
+ content TEXT NOT NULL,
83
+ source_ids TEXT NOT NULL DEFAULT '[]',
84
+ created_at TEXT NOT NULL,
85
+ deleted_at TEXT,
86
+ sync_version INTEGER NOT NULL DEFAULT 0
87
+ ) STRICT;
88
+ `);
89
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_ts ON memories(ts);");
90
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_sync_version ON memories(sync_version);");
91
+ db.exec(`
92
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
93
+ USING fts5(content, content='memories', content_rowid='rowid');
94
+ `);
95
+ db.exec(`
96
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
97
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
98
+ END;
99
+ `);
100
+ db.exec(`
101
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
102
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
103
+ END;
104
+ `);
105
+ db.exec(`
106
+ CREATE TABLE IF NOT EXISTS longterm_summary (
107
+ id INTEGER PRIMARY KEY CHECK (id = 1),
108
+ content TEXT NOT NULL,
109
+ updated_at TEXT NOT NULL,
110
+ sync_version INTEGER NOT NULL DEFAULT 0
111
+ ) STRICT;
112
+ `);
113
+ db.exec(`
114
+ CREATE TABLE IF NOT EXISTS sync_cursors (
115
+ backend TEXT NOT NULL,
116
+ direction TEXT NOT NULL,
117
+ cursor_value TEXT NOT NULL,
118
+ updated_at TEXT NOT NULL,
119
+ PRIMARY KEY (backend, direction)
120
+ ) STRICT;
121
+ `);
122
+ }
123
+ },
124
+ {
125
+ version: 3,
126
+ up: (db) => {
127
+ db.exec("ALTER TABLE engrams ADD COLUMN episode_key TEXT;");
128
+ db.exec("CREATE INDEX IF NOT EXISTS idx_engrams_episode_key ON engrams(episode_key);");
129
+ db.exec("ALTER TABLE memories ADD COLUMN episode_key TEXT;");
130
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_episode_key ON memories(episode_key);");
131
+ }
132
+ },
133
+ {
134
+ version: 4,
135
+ up: (db) => {
136
+ db.exec("ALTER TABLE engrams ADD COLUMN context TEXT;");
137
+ db.exec("ALTER TABLE engrams ADD COLUMN decisions TEXT;");
138
+ }
139
+ }
140
+ ];
141
+ function getCortexDb(cortexName) {
142
+ const cached = dbs.get(cortexName);
143
+ if (cached) return cached;
144
+ ensureThinkDirs();
145
+ const dbPath = getEngramDbPath(cortexName);
146
+ const db = new DatabaseSync(dbPath);
147
+ db.exec("PRAGMA journal_mode = WAL");
148
+ db.exec("PRAGMA synchronous = NORMAL");
149
+ runMigrations(db, migrations);
150
+ dbs.set(cortexName, db);
151
+ return db;
152
+ }
153
+ function closeCortexDb(cortexName) {
154
+ const db = dbs.get(cortexName);
155
+ if (db) {
156
+ db.close();
157
+ dbs.delete(cortexName);
158
+ }
159
+ }
160
+
161
+ // src/db/memory-queries.ts
162
+ function insertMemory(cortexName, params) {
163
+ const db = getCortexDb(cortexName);
164
+ const id = params.id ?? uuidv7();
165
+ const now = (/* @__PURE__ */ new Date()).toISOString();
166
+ const sourceIds = JSON.stringify(params.source_ids ?? []);
167
+ const episodeKey = params.episode_key ?? null;
168
+ db.prepare(
169
+ `INSERT INTO memories (id, ts, author, content, source_ids, created_at, deleted_at, sync_version, episode_key)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories), ?)`
171
+ ).run(id, params.ts, params.author, params.content, sourceIds, now, params.deleted_at ?? null, episodeKey);
172
+ const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
173
+ return row;
174
+ }
175
+ function insertMemoryIfNotExists(cortexName, params) {
176
+ const db = getCortexDb(cortexName);
177
+ const existing = db.prepare("SELECT id FROM memories WHERE id = ?").get(params.id);
178
+ if (existing) return false;
179
+ insertMemory(cortexName, params);
180
+ return true;
181
+ }
182
+ function getMemories(cortexName, params = {}) {
183
+ const db = getCortexDb(cortexName);
184
+ const conditions = ["deleted_at IS NULL"];
185
+ const values = [];
186
+ if (params.since) {
187
+ conditions.push("ts >= ?");
188
+ values.push(params.since);
189
+ }
190
+ if (params.until) {
191
+ conditions.push("ts <= ?");
192
+ values.push(params.until);
193
+ }
194
+ const where = `WHERE ${conditions.join(" AND ")}`;
195
+ if (params.limit) {
196
+ values.push(params.limit);
197
+ return db.prepare(
198
+ `SELECT * FROM memories ${where} ORDER BY ts ASC LIMIT ?`
199
+ ).all(...values);
200
+ }
201
+ return db.prepare(
202
+ `SELECT * FROM memories ${where} ORDER BY ts ASC`
203
+ ).all(...values);
204
+ }
205
+ function getMemoriesBySyncVersion(cortexName, sinceVersion) {
206
+ const db = getCortexDb(cortexName);
207
+ return db.prepare(
208
+ "SELECT * FROM memories WHERE sync_version > ? ORDER BY sync_version ASC"
209
+ ).all(sinceVersion);
210
+ }
211
+ function searchMemories(cortexName, query, limit = 20) {
212
+ const db = getCortexDb(cortexName);
213
+ try {
214
+ return db.prepare(
215
+ `SELECT m.* FROM memories m JOIN memories_fts f ON m.rowid = f.rowid
216
+ WHERE memories_fts MATCH ? AND m.deleted_at IS NULL
217
+ ORDER BY rank LIMIT ?`
218
+ ).all(query, limit);
219
+ } catch {
220
+ const pattern = `%${query.replace(/%/g, "\\%").replace(/_/g, "\\_")}%`;
221
+ return db.prepare(
222
+ `SELECT * FROM memories WHERE content LIKE ? ESCAPE '\\' AND deleted_at IS NULL ORDER BY ts DESC LIMIT ?`
223
+ ).all(pattern, limit);
224
+ }
225
+ }
226
+ function tombstoneMemory(cortexName, id) {
227
+ const db = getCortexDb(cortexName);
228
+ db.prepare(
229
+ `UPDATE memories SET deleted_at = ?, sync_version = (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories)
230
+ WHERE id = ? AND deleted_at IS NULL`
231
+ ).run((/* @__PURE__ */ new Date()).toISOString(), id);
232
+ }
233
+ function getLongtermSummary(cortexName) {
234
+ const db = getCortexDb(cortexName);
235
+ const row = db.prepare("SELECT content FROM longterm_summary WHERE id = 1").get();
236
+ return row?.content ?? null;
237
+ }
238
+ function setLongtermSummary(cortexName, content) {
239
+ const db = getCortexDb(cortexName);
240
+ db.prepare(
241
+ `INSERT INTO longterm_summary (id, content, updated_at, sync_version)
242
+ VALUES (1, ?, ?, (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories))
243
+ ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at, sync_version = excluded.sync_version`
244
+ ).run(content, (/* @__PURE__ */ new Date()).toISOString());
245
+ }
246
+ function getSyncCursor(cortexName, backend, direction) {
247
+ const db = getCortexDb(cortexName);
248
+ const row = db.prepare(
249
+ "SELECT cursor_value FROM sync_cursors WHERE backend = ? AND direction = ?"
250
+ ).get(backend, direction);
251
+ return row?.cursor_value ?? null;
252
+ }
253
+ function setSyncCursor(cortexName, backend, direction, cursorValue) {
254
+ const db = getCortexDb(cortexName);
255
+ db.prepare(
256
+ `INSERT INTO sync_cursors (backend, direction, cursor_value, updated_at)
257
+ VALUES (?, ?, ?, ?)
258
+ ON CONFLICT(backend, direction) DO UPDATE SET cursor_value = excluded.cursor_value, updated_at = excluded.updated_at`
259
+ ).run(backend, direction, cursorValue, (/* @__PURE__ */ new Date()).toISOString());
260
+ }
261
+ function getMemoryCount(cortexName) {
262
+ const db = getCortexDb(cortexName);
263
+ const row = db.prepare("SELECT COUNT(*) as count FROM memories WHERE deleted_at IS NULL").get();
264
+ return row.count;
265
+ }
266
+ function getMemoryByEpisodeKey(cortexName, episodeKey) {
267
+ const db = getCortexDb(cortexName);
268
+ const row = db.prepare(
269
+ "SELECT * FROM memories WHERE episode_key = ? AND deleted_at IS NULL LIMIT 1"
270
+ ).get(episodeKey);
271
+ return row ?? null;
272
+ }
273
+
274
+ export {
275
+ getCortexDb,
276
+ closeCortexDb,
277
+ insertMemory,
278
+ insertMemoryIfNotExists,
279
+ getMemories,
280
+ getMemoriesBySyncVersion,
281
+ searchMemories,
282
+ tombstoneMemory,
283
+ getLongtermSummary,
284
+ setLongtermSummary,
285
+ getSyncCursor,
286
+ setSyncCursor,
287
+ getMemoryCount,
288
+ getMemoryByEpisodeKey
289
+ };
@@ -1,88 +1,33 @@
1
1
  #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import {
3
+ getRepoPath,
4
+ getThinkConfigDir
5
+ } from "./chunk-DCTG6IK4.js";
2
6
 
3
7
  // src/lib/git.ts
4
8
  import { execFileSync } from "child_process";
5
- import fs3 from "fs";
6
- import path3 from "path";
9
+ import fs2 from "fs";
10
+ import path2 from "path";
7
11
 
8
- // src/lib/paths.ts
12
+ // src/lib/config.ts
9
13
  import path from "path";
10
14
  import fs from "fs";
11
- function getHome() {
12
- const home = process.env.HOME;
13
- if (!home) {
14
- throw new Error("HOME environment variable is not set");
15
- }
16
- return home;
17
- }
18
- function sanitizeName(name) {
19
- if (!name || /[\/\\\.]{2}/.test(name) || /[^a-zA-Z0-9_-]/.test(name)) {
20
- throw new Error(`Invalid cortex name: "${name}". Use only alphanumeric characters, hyphens, and underscores.`);
21
- }
22
- return name;
23
- }
24
- function getThinkHome() {
25
- const thinkHome = process.env.THINK_HOME;
26
- if (thinkHome === void 0 || thinkHome === "") return null;
27
- return thinkHome;
28
- }
29
- function getThinkDir() {
30
- return getThinkHome() ?? path.join(getHome(), ".think");
31
- }
32
- function getThinkConfigDir() {
33
- const thinkHome = getThinkHome();
34
- if (thinkHome) return path.join(thinkHome, "config");
35
- const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(getHome(), ".config");
36
- return path.join(xdgConfig, "think");
37
- }
38
- function getThinkDataDir() {
39
- const thinkHome = getThinkHome();
40
- if (thinkHome) return path.join(thinkHome, "data");
41
- const xdgData = process.env.XDG_DATA_HOME || path.join(getHome(), ".local", "share");
42
- return path.join(xdgData, "think");
43
- }
44
- function getEngramsDir() {
45
- return path.join(getThinkDir(), "engrams");
46
- }
47
- function getEngramDbPath(cortexName) {
48
- return path.join(getEngramsDir(), `${sanitizeName(cortexName)}.db`);
49
- }
50
- function getRepoPath() {
51
- return path.join(getThinkDir(), "repo");
52
- }
53
- function getLongtermDir() {
54
- return path.join(getThinkDir(), "longterm");
55
- }
56
- function getLongtermPath(cortexName) {
57
- return path.join(getLongtermDir(), `${sanitizeName(cortexName)}.md`);
58
- }
59
- function getCuratorMdPath() {
60
- return path.join(getThinkDir(), "curator.md");
61
- }
62
- function ensureThinkDirs() {
63
- fs.mkdirSync(getEngramsDir(), { recursive: true });
64
- fs.mkdirSync(getLongtermDir(), { recursive: true });
65
- }
66
-
67
- // src/lib/config.ts
68
- import path2 from "path";
69
- import fs2 from "fs";
70
15
  import { v4 as uuidv4 } from "uuid";
71
16
  function getConfigDir() {
72
17
  return getThinkConfigDir();
73
18
  }
74
19
  function configPath() {
75
- return path2.join(getConfigDir(), "config.json");
20
+ return path.join(getConfigDir(), "config.json");
76
21
  }
77
22
  function saveConfig(config) {
78
23
  const dir = getConfigDir();
79
- fs2.mkdirSync(dir, { recursive: true });
80
- fs2.writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
24
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
25
+ fs.writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
81
26
  }
82
27
  function getConfig() {
83
28
  const fp = configPath();
84
- if (fs2.existsSync(fp)) {
85
- const raw = fs2.readFileSync(fp, "utf-8");
29
+ if (fs.existsSync(fp)) {
30
+ const raw = fs.readFileSync(fp, "utf-8");
86
31
  return JSON.parse(raw);
87
32
  }
88
33
  const config = {
@@ -94,12 +39,34 @@ function getConfig() {
94
39
  }
95
40
 
96
41
  // src/lib/git.ts
42
+ function safeGitEnv() {
43
+ const env = { ...process.env };
44
+ delete env.GIT_SSH_COMMAND;
45
+ delete env.GIT_PROXY_COMMAND;
46
+ delete env.GIT_ASKPASS;
47
+ delete env.GIT_CONFIG_GLOBAL;
48
+ delete env.GIT_CONFIG_SYSTEM;
49
+ delete env.GIT_WORK_TREE;
50
+ delete env.GIT_DIR;
51
+ delete env.GIT_EXEC_PATH;
52
+ env.GIT_CONFIG_NOSYSTEM = "1";
53
+ env.GIT_TEMPLATE_DIR = "";
54
+ return env;
55
+ }
97
56
  function runGit(args, cwd) {
98
57
  const repoPath = cwd ?? getRepoPath();
99
- return execFileSync("git", args, {
58
+ const safeArgs = [
59
+ "-c",
60
+ "core.hooksPath=/dev/null",
61
+ "-c",
62
+ "core.fsmonitor=",
63
+ ...args
64
+ ];
65
+ return execFileSync("git", safeArgs, {
100
66
  cwd: repoPath,
101
67
  encoding: "utf-8",
102
- stdio: ["pipe", "pipe", "pipe"]
68
+ stdio: ["pipe", "pipe", "pipe"],
69
+ env: safeGitEnv()
103
70
  }).trim();
104
71
  }
105
72
  function ensureRepoCloned() {
@@ -108,17 +75,18 @@ function ensureRepoCloned() {
108
75
  throw new Error("No cortex repo configured. Run: think cortex setup");
109
76
  }
110
77
  const repoPath = getRepoPath();
111
- if (fs3.existsSync(path3.join(repoPath, ".git"))) {
78
+ if (fs2.existsSync(path2.join(repoPath, ".git"))) {
112
79
  const remote = runGit(["remote", "get-url", "origin"], repoPath);
113
80
  if (remote !== config.cortex.repo) {
114
81
  throw new Error(`Repo at ${repoPath} points to ${remote}, expected ${config.cortex.repo}`);
115
82
  }
116
83
  return;
117
84
  }
118
- fs3.mkdirSync(repoPath, { recursive: true });
119
- execFileSync("git", ["clone", "--no-checkout", config.cortex.repo, repoPath], {
85
+ fs2.mkdirSync(repoPath, { recursive: true });
86
+ execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=", "clone", "--no-checkout", config.cortex.repo, repoPath], {
120
87
  encoding: "utf-8",
121
- stdio: ["pipe", "pipe", "pipe"]
88
+ stdio: ["pipe", "pipe", "pipe"],
89
+ env: safeGitEnv()
122
90
  });
123
91
  }
124
92
  function branchExists(branchName) {
@@ -136,7 +104,7 @@ function createOrphanBranch(branchName) {
136
104
  } catch {
137
105
  }
138
106
  const repoPath = getRepoPath();
139
- fs3.writeFileSync(path3.join(repoPath, "000001.jsonl"), "", "utf-8");
107
+ fs2.writeFileSync(path2.join(repoPath, "000001.jsonl"), "", "utf-8");
140
108
  runGit(["add", "000001.jsonl"]);
141
109
  runGit(["commit", "-m", `init: create cortex ${branchName}`]);
142
110
  runGit(["push", "--set-upstream", "origin", branchName]);
@@ -153,7 +121,7 @@ function readFileFromBranch(branchName, filePath) {
153
121
  }
154
122
  function appendAndCommit(branchName, newLines, commitMessage, maxRetries = 3, targetFile = "memories.jsonl") {
155
123
  const repoPath = getRepoPath();
156
- const filePath = path3.join(repoPath, targetFile);
124
+ const filePath = path2.join(repoPath, targetFile);
157
125
  try {
158
126
  runGit(["switch", branchName]);
159
127
  } catch {
@@ -172,7 +140,7 @@ function appendAndCommit(branchName, newLines, commitMessage, maxRetries = 3, ta
172
140
  }
173
141
  }
174
142
  const content = newLines.join("\n") + "\n";
175
- fs3.appendFileSync(filePath, content, "utf-8");
143
+ fs2.appendFileSync(filePath, content, "utf-8");
176
144
  runGit(["add", targetFile]);
177
145
  runGit(["commit", "-m", commitMessage]);
178
146
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -222,11 +190,11 @@ function migrateToBuckets(branchName) {
222
190
  runGit(["pull", "--rebase", "origin", branchName]);
223
191
  } catch {
224
192
  }
225
- const legacyPath = path3.join(repoPath, "memories.jsonl");
226
- const bucketPath = path3.join(repoPath, "000001.jsonl");
227
- if (fs3.existsSync(legacyPath) && !fs3.existsSync(bucketPath)) {
193
+ const legacyPath = path2.join(repoPath, "memories.jsonl");
194
+ const bucketPath = path2.join(repoPath, "000001.jsonl");
195
+ if (fs2.existsSync(legacyPath) && !fs2.existsSync(bucketPath)) {
228
196
  const preMigrationRef = runGit(["rev-parse", "HEAD"]);
229
- fs3.renameSync(legacyPath, bucketPath);
197
+ fs2.renameSync(legacyPath, bucketPath);
230
198
  runGit(["add", "-A"]);
231
199
  runGit(["commit", "-m", "migrate: memories.jsonl -> 000001.jsonl"]);
232
200
  for (let attempt = 1; attempt <= 3; attempt++) {
@@ -248,12 +216,6 @@ function migrateToBuckets(branchName) {
248
216
  }
249
217
 
250
218
  export {
251
- getThinkDataDir,
252
- getEngramsDir,
253
- getEngramDbPath,
254
- getLongtermPath,
255
- getCuratorMdPath,
256
- ensureThinkDirs,
257
219
  getConfigDir,
258
220
  saveConfig,
259
221
  getConfig,
@@ -11,7 +11,8 @@ import {
11
11
  listRemoteBranches,
12
12
  migrateToBuckets,
13
13
  readFileFromBranch
14
- } from "./chunk-N4VAGRBF.js";
14
+ } from "./chunk-ZKUJ5M2W.js";
15
+ import "./chunk-DCTG6IK4.js";
15
16
  export {
16
17
  appendAndCommit,
17
18
  branchExists,
package/dist/index.js CHANGED
@@ -1,24 +1,41 @@
1
1
  #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import {
3
+ closeCortexDb,
4
+ getCortexDb,
5
+ getLongtermSummary,
6
+ getMemories,
7
+ getMemoriesBySyncVersion,
8
+ getMemoryByEpisodeKey,
9
+ getMemoryCount,
10
+ getSyncCursor,
11
+ insertMemory,
12
+ insertMemoryIfNotExists,
13
+ searchMemories,
14
+ setLongtermSummary,
15
+ setSyncCursor,
16
+ tombstoneMemory
17
+ } from "./chunk-MSOBQE64.js";
2
18
  import {
3
19
  appendAndCommit,
4
20
  countBranchFileLines,
5
21
  createOrphanBranch,
6
22
  ensureRepoCloned,
7
- ensureThinkDirs,
8
23
  fetchBranch,
9
24
  getConfig,
10
25
  getConfigDir,
11
- getCuratorMdPath,
12
- getEngramDbPath,
13
- getEngramsDir,
14
- getLongtermPath,
15
- getThinkDataDir,
16
26
  listBranchFiles,
17
27
  listRemoteBranches,
18
28
  migrateToBuckets,
19
29
  readFileFromBranch,
20
30
  saveConfig
21
- } from "./chunk-N4VAGRBF.js";
31
+ } from "./chunk-ZKUJ5M2W.js";
32
+ import {
33
+ ensureThinkDirs,
34
+ getCuratorMdPath,
35
+ getEngramsDir,
36
+ getLongtermPath,
37
+ getThinkDataDir
38
+ } from "./chunk-DCTG6IK4.js";
22
39
 
23
40
  // src/index.ts
24
41
  import fs11 from "fs";
@@ -153,159 +170,6 @@ function deleteEntriesByContent(pattern) {
153
170
 
154
171
  // src/db/engram-queries.ts
155
172
  import { v7 as uuidv72 } from "uuid";
156
-
157
- // src/db/engrams.ts
158
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
159
-
160
- // src/db/migrate.ts
161
- function runMigrations(db2, migrations2) {
162
- db2.exec(`
163
- CREATE TABLE IF NOT EXISTS _migrations (
164
- version INTEGER PRIMARY KEY NOT NULL,
165
- applied_at TEXT NOT NULL
166
- ) STRICT;
167
- `);
168
- const currentVersion = db2.prepare(
169
- "SELECT COALESCE(MAX(version), 0) as version FROM _migrations"
170
- ).get();
171
- const pending = migrations2.filter((m) => m.version > currentVersion.version).sort((a, b) => a.version - b.version);
172
- for (const migration of pending) {
173
- db2.exec("BEGIN");
174
- try {
175
- migration.up(db2);
176
- db2.prepare("INSERT INTO _migrations (version, applied_at) VALUES (?, ?)").run(
177
- migration.version,
178
- (/* @__PURE__ */ new Date()).toISOString()
179
- );
180
- db2.exec("COMMIT");
181
- } catch (err) {
182
- db2.exec("ROLLBACK");
183
- throw new Error(`Migration v${migration.version} failed: ${err instanceof Error ? err.message : String(err)}`);
184
- }
185
- }
186
- }
187
-
188
- // src/db/engrams.ts
189
- var dbs = /* @__PURE__ */ new Map();
190
- var migrations = [
191
- {
192
- version: 1,
193
- up: (db2) => {
194
- db2.exec(`
195
- CREATE TABLE IF NOT EXISTS engrams (
196
- id TEXT PRIMARY KEY NOT NULL,
197
- content TEXT NOT NULL,
198
- created_at TEXT NOT NULL,
199
- expires_at TEXT NOT NULL,
200
- evaluated_at TEXT,
201
- promoted INTEGER,
202
- deleted_at TEXT
203
- ) STRICT;
204
- `);
205
- db2.exec(`
206
- CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts
207
- USING fts5(content, content='engrams', content_rowid='rowid');
208
- `);
209
- db2.exec(`
210
- CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
211
- INSERT INTO engrams_fts(rowid, content) VALUES (new.rowid, new.content);
212
- END;
213
- `);
214
- db2.exec(`
215
- CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
216
- INSERT INTO engrams_fts(engrams_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
217
- END;
218
- `);
219
- }
220
- },
221
- {
222
- version: 2,
223
- up: (db2) => {
224
- db2.exec(`
225
- CREATE TABLE IF NOT EXISTS memories (
226
- id TEXT PRIMARY KEY NOT NULL,
227
- ts TEXT NOT NULL,
228
- author TEXT NOT NULL,
229
- content TEXT NOT NULL,
230
- source_ids TEXT NOT NULL DEFAULT '[]',
231
- created_at TEXT NOT NULL,
232
- deleted_at TEXT,
233
- sync_version INTEGER NOT NULL DEFAULT 0
234
- ) STRICT;
235
- `);
236
- db2.exec("CREATE INDEX IF NOT EXISTS idx_memories_ts ON memories(ts);");
237
- db2.exec("CREATE INDEX IF NOT EXISTS idx_memories_sync_version ON memories(sync_version);");
238
- db2.exec(`
239
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
240
- USING fts5(content, content='memories', content_rowid='rowid');
241
- `);
242
- db2.exec(`
243
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
244
- INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
245
- END;
246
- `);
247
- db2.exec(`
248
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
249
- INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
250
- END;
251
- `);
252
- db2.exec(`
253
- CREATE TABLE IF NOT EXISTS longterm_summary (
254
- id INTEGER PRIMARY KEY CHECK (id = 1),
255
- content TEXT NOT NULL,
256
- updated_at TEXT NOT NULL,
257
- sync_version INTEGER NOT NULL DEFAULT 0
258
- ) STRICT;
259
- `);
260
- db2.exec(`
261
- CREATE TABLE IF NOT EXISTS sync_cursors (
262
- backend TEXT NOT NULL,
263
- direction TEXT NOT NULL,
264
- cursor_value TEXT NOT NULL,
265
- updated_at TEXT NOT NULL,
266
- PRIMARY KEY (backend, direction)
267
- ) STRICT;
268
- `);
269
- }
270
- },
271
- {
272
- version: 3,
273
- up: (db2) => {
274
- db2.exec("ALTER TABLE engrams ADD COLUMN episode_key TEXT;");
275
- db2.exec("CREATE INDEX IF NOT EXISTS idx_engrams_episode_key ON engrams(episode_key);");
276
- db2.exec("ALTER TABLE memories ADD COLUMN episode_key TEXT;");
277
- db2.exec("CREATE INDEX IF NOT EXISTS idx_memories_episode_key ON memories(episode_key);");
278
- }
279
- },
280
- {
281
- version: 4,
282
- up: (db2) => {
283
- db2.exec("ALTER TABLE engrams ADD COLUMN context TEXT;");
284
- db2.exec("ALTER TABLE engrams ADD COLUMN decisions TEXT;");
285
- }
286
- }
287
- ];
288
- function getCortexDb(cortexName) {
289
- const cached = dbs.get(cortexName);
290
- if (cached) return cached;
291
- ensureThinkDirs();
292
- const dbPath = getEngramDbPath(cortexName);
293
- const db2 = new DatabaseSync2(dbPath);
294
- db2.exec("PRAGMA journal_mode = WAL");
295
- db2.exec("PRAGMA synchronous = NORMAL");
296
- runMigrations(db2, migrations);
297
- dbs.set(cortexName, db2);
298
- return db2;
299
- }
300
- function closeCortexDb(cortexName) {
301
- const db2 = dbs.get(cortexName);
302
- if (db2) {
303
- db2.close();
304
- dbs.delete(cortexName);
305
- }
306
- }
307
-
308
- // src/db/engram-queries.ts
309
173
  function insertEngram(cortexName, params) {
310
174
  const db2 = getCortexDb(cortexName);
311
175
  const id = uuidv72();
@@ -321,11 +185,11 @@ function insertEngram(cortexName, params) {
321
185
  ).run(id, params.content, created_at, expires_at, episodeKey, context, decisions);
322
186
  return { id, content: params.content, created_at, expires_at, evaluated_at: null, promoted: null, deleted_at: null, episode_key: episodeKey, context, decisions };
323
187
  }
324
- function getPendingEngrams(cortexName) {
188
+ function getPendingEngrams(cortexName, limit = 200) {
325
189
  const db2 = getCortexDb(cortexName);
326
190
  return db2.prepare(
327
- `SELECT * FROM engrams WHERE evaluated_at IS NULL AND deleted_at IS NULL AND episode_key IS NULL AND expires_at > ? ORDER BY created_at ASC`
328
- ).all((/* @__PURE__ */ new Date()).toISOString());
191
+ `SELECT * FROM engrams WHERE evaluated_at IS NULL AND deleted_at IS NULL AND episode_key IS NULL AND expires_at > ? ORDER BY created_at ASC LIMIT ?`
192
+ ).all((/* @__PURE__ */ new Date()).toISOString(), limit);
329
193
  }
330
194
  function getPendingEpisodeEngrams(cortexName, episodeKey) {
331
195
  const db2 = getCortexDb(cortexName);
@@ -477,7 +341,7 @@ function validateEngramContent(content) {
477
341
  return { content, warnings };
478
342
  }
479
343
  function wrapData(label, content) {
480
- const escaped = content.replace(/<\/data/gi, "&lt;/data");
344
+ const escaped = content.replace(/<\/?data/gi, (match) => `&lt;${match.slice(1)}`);
481
345
  return `<data source="${label}">
482
346
  ${escaped}
483
347
  </data>`;
@@ -922,12 +786,20 @@ function readAuditLog() {
922
786
  }
923
787
 
924
788
  // src/commands/import.ts
789
+ var MAX_IMPORT_FILE_SIZE = 50 * 1024 * 1024;
790
+ var MAX_IMPORT_ENTRIES = 5e4;
925
791
  var importCommand = new Command6("import").description("Import a sync bundle from another device").argument("<file>", "Path to the sync bundle JSON file").action((file) => {
926
792
  if (!fs5.existsSync(file)) {
927
793
  console.error(chalk6.red(`File not found: ${file}`));
928
794
  closeDb();
929
795
  process.exit(1);
930
796
  }
797
+ const stat = fs5.statSync(file);
798
+ if (stat.size > MAX_IMPORT_FILE_SIZE) {
799
+ console.error(chalk6.red(`File too large (${Math.round(stat.size / 1024 / 1024)}MB). Maximum import size is ${MAX_IMPORT_FILE_SIZE / 1024 / 1024}MB.`));
800
+ closeDb();
801
+ process.exit(1);
802
+ }
931
803
  let bundle;
932
804
  try {
933
805
  const raw = fs5.readFileSync(file, "utf-8");
@@ -937,7 +809,7 @@ var importCommand = new Command6("import").description("Import a sync bundle fro
937
809
  closeDb();
938
810
  process.exit(1);
939
811
  }
940
- if (bundle.format !== "think-sync-bundle" || !bundle.entries) {
812
+ if (bundle.format !== "think-sync-bundle" || !Array.isArray(bundle.entries)) {
941
813
  console.error(chalk6.red("Not a valid think sync bundle."));
942
814
  closeDb();
943
815
  process.exit(1);
@@ -947,6 +819,11 @@ var importCommand = new Command6("import").description("Import a sync bundle fro
947
819
  closeDb();
948
820
  return;
949
821
  }
822
+ if (bundle.entries.length > MAX_IMPORT_ENTRIES) {
823
+ console.error(chalk6.red(`Bundle contains ${bundle.entries.length} entries. Maximum is ${MAX_IMPORT_ENTRIES}.`));
824
+ closeDb();
825
+ process.exit(1);
826
+ }
950
827
  const db2 = getDb();
951
828
  const insert = db2.prepare(
952
829
  `INSERT OR IGNORE INTO entries (id, timestamp, source, category, content, tags, deleted_at)
@@ -954,16 +831,23 @@ var importCommand = new Command6("import").description("Import a sync bundle fro
954
831
  );
955
832
  let imported = 0;
956
833
  let skipped = 0;
834
+ let warnings = 0;
957
835
  try {
958
836
  db2.exec("BEGIN");
959
837
  for (const entry of bundle.entries) {
838
+ if (typeof entry.id !== "string" || typeof entry.content !== "string") {
839
+ skipped++;
840
+ continue;
841
+ }
842
+ const validated = validateEngramContent(entry.content);
843
+ if (validated.warnings.length > 0) warnings++;
960
844
  const result = insert.run(
961
845
  entry.id,
962
- entry.timestamp,
963
- entry.source,
964
- entry.category,
965
- entry.content,
966
- entry.tags,
846
+ typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
847
+ typeof entry.source === "string" ? entry.source : "import",
848
+ typeof entry.category === "string" ? entry.category : "",
849
+ validated.content,
850
+ typeof entry.tags === "string" ? entry.tags : "",
967
851
  entry.deleted_at ?? null
968
852
  );
969
853
  if (result.changes > 0) {
@@ -989,10 +873,13 @@ var importCommand = new Command6("import").description("Import a sync bundle fro
989
873
  count: bundle.entries.length
990
874
  });
991
875
  if (imported > 0) {
992
- console.log(chalk6.green("\u2713") + ` Imported ${imported} entries` + (skipped > 0 ? ` (${skipped} already existed)` : ""));
876
+ console.log(chalk6.green("\u2713") + ` Imported ${imported} entries` + (skipped > 0 ? ` (${skipped} skipped)` : ""));
993
877
  } else {
994
878
  console.log(chalk6.green("\u2713") + ` All ${skipped} entries already present \u2014 nothing new.`);
995
879
  }
880
+ if (warnings > 0) {
881
+ console.log(chalk6.yellow("\u26A0") + ` ${warnings} entries contained suspicious content patterns`);
882
+ }
996
883
  if (bundle.peerId) {
997
884
  console.log(chalk6.dim(` from peer: ${bundle.peerId.slice(0, 8)}`));
998
885
  }
@@ -1118,105 +1005,6 @@ import { Command as Command9 } from "commander";
1118
1005
  import chalk9 from "chalk";
1119
1006
  import readline2 from "readline";
1120
1007
 
1121
- // src/db/memory-queries.ts
1122
- import { v7 as uuidv73 } from "uuid";
1123
- function insertMemory(cortexName, params) {
1124
- const db2 = getCortexDb(cortexName);
1125
- const id = params.id ?? uuidv73();
1126
- const now = (/* @__PURE__ */ new Date()).toISOString();
1127
- const sourceIds = JSON.stringify(params.source_ids ?? []);
1128
- const episodeKey = params.episode_key ?? null;
1129
- db2.prepare(
1130
- `INSERT INTO memories (id, ts, author, content, source_ids, created_at, deleted_at, sync_version, episode_key)
1131
- VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories), ?)`
1132
- ).run(id, params.ts, params.author, params.content, sourceIds, now, params.deleted_at ?? null, episodeKey);
1133
- const row = db2.prepare("SELECT * FROM memories WHERE id = ?").get(id);
1134
- return row;
1135
- }
1136
- function insertMemoryIfNotExists(cortexName, params) {
1137
- const db2 = getCortexDb(cortexName);
1138
- const existing = db2.prepare("SELECT id FROM memories WHERE id = ?").get(params.id);
1139
- if (existing) return false;
1140
- insertMemory(cortexName, params);
1141
- return true;
1142
- }
1143
- function getMemories(cortexName, params = {}) {
1144
- const db2 = getCortexDb(cortexName);
1145
- const conditions = ["deleted_at IS NULL"];
1146
- const values = [];
1147
- if (params.since) {
1148
- conditions.push("ts >= ?");
1149
- values.push(params.since);
1150
- }
1151
- if (params.until) {
1152
- conditions.push("ts <= ?");
1153
- values.push(params.until);
1154
- }
1155
- const where = `WHERE ${conditions.join(" AND ")}`;
1156
- if (params.limit) {
1157
- values.push(params.limit);
1158
- return db2.prepare(
1159
- `SELECT * FROM memories ${where} ORDER BY ts ASC LIMIT ?`
1160
- ).all(...values);
1161
- }
1162
- return db2.prepare(
1163
- `SELECT * FROM memories ${where} ORDER BY ts ASC`
1164
- ).all(...values);
1165
- }
1166
- function getMemoriesBySyncVersion(cortexName, sinceVersion) {
1167
- const db2 = getCortexDb(cortexName);
1168
- return db2.prepare(
1169
- "SELECT * FROM memories WHERE sync_version > ? ORDER BY sync_version ASC"
1170
- ).all(sinceVersion);
1171
- }
1172
- function tombstoneMemory(cortexName, id) {
1173
- const db2 = getCortexDb(cortexName);
1174
- db2.prepare(
1175
- `UPDATE memories SET deleted_at = ?, sync_version = (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories)
1176
- WHERE id = ? AND deleted_at IS NULL`
1177
- ).run((/* @__PURE__ */ new Date()).toISOString(), id);
1178
- }
1179
- function getLongtermSummary(cortexName) {
1180
- const db2 = getCortexDb(cortexName);
1181
- const row = db2.prepare("SELECT content FROM longterm_summary WHERE id = 1").get();
1182
- return row?.content ?? null;
1183
- }
1184
- function setLongtermSummary(cortexName, content) {
1185
- const db2 = getCortexDb(cortexName);
1186
- db2.prepare(
1187
- `INSERT INTO longterm_summary (id, content, updated_at, sync_version)
1188
- VALUES (1, ?, ?, (SELECT COALESCE(MAX(sync_version), 0) + 1 FROM memories))
1189
- ON CONFLICT(id) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at, sync_version = excluded.sync_version`
1190
- ).run(content, (/* @__PURE__ */ new Date()).toISOString());
1191
- }
1192
- function getSyncCursor(cortexName, backend, direction) {
1193
- const db2 = getCortexDb(cortexName);
1194
- const row = db2.prepare(
1195
- "SELECT cursor_value FROM sync_cursors WHERE backend = ? AND direction = ?"
1196
- ).get(backend, direction);
1197
- return row?.cursor_value ?? null;
1198
- }
1199
- function setSyncCursor(cortexName, backend, direction, cursorValue) {
1200
- const db2 = getCortexDb(cortexName);
1201
- db2.prepare(
1202
- `INSERT INTO sync_cursors (backend, direction, cursor_value, updated_at)
1203
- VALUES (?, ?, ?, ?)
1204
- ON CONFLICT(backend, direction) DO UPDATE SET cursor_value = excluded.cursor_value, updated_at = excluded.updated_at`
1205
- ).run(backend, direction, cursorValue, (/* @__PURE__ */ new Date()).toISOString());
1206
- }
1207
- function getMemoryCount(cortexName) {
1208
- const db2 = getCortexDb(cortexName);
1209
- const row = db2.prepare("SELECT COUNT(*) as count FROM memories WHERE deleted_at IS NULL").get();
1210
- return row.count;
1211
- }
1212
- function getMemoryByEpisodeKey(cortexName, episodeKey) {
1213
- const db2 = getCortexDb(cortexName);
1214
- const row = db2.prepare(
1215
- "SELECT * FROM memories WHERE episode_key = ? AND deleted_at IS NULL LIMIT 1"
1216
- ).get(episodeKey);
1217
- return row ?? null;
1218
- }
1219
-
1220
1008
  // src/lib/curator.ts
1221
1009
  import fs7 from "fs";
1222
1010
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
@@ -1623,11 +1411,15 @@ var GitSyncAdapter = class {
1623
1411
  tombstoneMemory(cortex, id);
1624
1412
  continue;
1625
1413
  }
1414
+ const { content: sanitizedContent, warnings } = validateEngramContent(m.content);
1415
+ if (warnings.length > 0) {
1416
+ result.errors.push(`Pulled memory from ${m.author} flagged: ${warnings.join(", ")}`);
1417
+ }
1626
1418
  const wasInserted = insertMemoryIfNotExists(cortex, {
1627
1419
  id,
1628
1420
  ts: m.ts,
1629
1421
  author: m.author,
1630
- content: m.content,
1422
+ content: sanitizedContent,
1631
1423
  source_ids: m.source_ids,
1632
1424
  episode_key: m.episode_key
1633
1425
  });
@@ -1752,7 +1544,7 @@ cortexCommand.addCommand(new Command9("setup").description("Configure a sync bac
1752
1544
  const adapter = getSyncAdapter();
1753
1545
  if (adapter) {
1754
1546
  try {
1755
- const { ensureRepoCloned: ensureRepoCloned2 } = await import("./git-R4CVMKV7.js");
1547
+ const { ensureRepoCloned: ensureRepoCloned2 } = await import("./git-TG6OJFBT.js");
1756
1548
  ensureRepoCloned2();
1757
1549
  console.log(chalk9.green("\u2713") + " Repo cloned");
1758
1550
  } catch (err) {
@@ -2256,44 +2048,77 @@ var monitorCommand = new Command11("monitor").description("Show what got promote
2256
2048
  // src/commands/recall.ts
2257
2049
  import { Command as Command12 } from "commander";
2258
2050
  import chalk12 from "chalk";
2259
- var recallCommand = new Command12("recall").argument("<query>", "What to recall").description("Search memories and local engrams").option("--days <n>", "Days of memories to include", "14").action(async (query3, opts) => {
2051
+ var recallCommand = new Command12("recall").argument("<query>", "What to recall").description("Search memories and local engrams").option("--engrams", "Also search local engrams (not just memories)").option("--all", "Dump all recent memories + long-term summary (ignores query for memories)").option("--days <n>", "Days of memories to include (only with --all)", "14").option("--limit <n>", "Max results to return", "20").action(async (query3, opts) => {
2260
2052
  const config = getConfig();
2261
2053
  const cortex = config.cortex?.active;
2262
2054
  if (!cortex) {
2263
2055
  console.error(chalk12.red("No active cortex. Run: think cortex switch <name>"));
2264
2056
  process.exit(1);
2265
2057
  }
2266
- const days = parseInt(opts.days, 10);
2267
- const cutoff = new Date(Date.now() - days * 864e5).toISOString();
2268
- const recentMemories = getMemories(cortex, { since: cutoff });
2269
- const matchingEngrams = searchEngrams(cortex, query3);
2270
- const longterm = getLongtermSummary(cortex);
2271
- if (recentMemories.length > 0) {
2272
- console.log(chalk12.cyan(`Team memories (last ${days} days):`));
2273
- for (const m of recentMemories) {
2058
+ const limit = parseInt(opts.limit, 10);
2059
+ if (opts.all) {
2060
+ const { getMemories: getMemories2 } = await import("./memory-queries-IPGGUAQW.js");
2061
+ const days = parseInt(opts.days, 10);
2062
+ const cutoff = new Date(Date.now() - days * 864e5).toISOString();
2063
+ const recentMemories = getMemories2(cortex, { since: cutoff });
2064
+ const longterm = getLongtermSummary(cortex);
2065
+ const matchingEngrams = searchEngrams(cortex, query3);
2066
+ if (recentMemories.length > 0) {
2067
+ console.log(chalk12.cyan(`Team memories (last ${days} days):`));
2068
+ for (const m of recentMemories) {
2069
+ const ts = m.ts.slice(0, 16).replace("T", " ");
2070
+ console.log(` ${chalk12.gray(ts)} ${chalk12.dim(m.author + ":")} ${m.content}`);
2071
+ }
2072
+ console.log();
2073
+ }
2074
+ if (longterm) {
2075
+ console.log(chalk12.cyan("Long-term context:"));
2076
+ console.log(` ${longterm}`);
2077
+ console.log();
2078
+ }
2079
+ if (matchingEngrams.length > 0) {
2080
+ console.log(chalk12.cyan(`Matching engrams (local):`));
2081
+ for (const e of matchingEngrams) {
2082
+ const ts = e.created_at.slice(0, 16).replace("T", " ");
2083
+ console.log(` ${chalk12.gray(ts)} ${e.content}`);
2084
+ }
2085
+ console.log();
2086
+ }
2087
+ if (recentMemories.length === 0 && matchingEngrams.length === 0 && !longterm) {
2088
+ console.log(chalk12.dim("No results found."));
2089
+ }
2090
+ closeCortexDb(cortex);
2091
+ return;
2092
+ }
2093
+ const matchingMemories = searchMemories(cortex, query3, limit);
2094
+ if (matchingMemories.length > 0) {
2095
+ console.log(chalk12.cyan(`Matching memories (${matchingMemories.length}):`));
2096
+ for (const m of matchingMemories) {
2274
2097
  const ts = m.ts.slice(0, 16).replace("T", " ");
2275
2098
  console.log(` ${chalk12.gray(ts)} ${chalk12.dim(m.author + ":")} ${m.content}`);
2276
2099
  }
2277
2100
  console.log();
2278
2101
  } else {
2279
- console.log(chalk12.dim("No recent memories."));
2280
- console.log();
2281
- }
2282
- if (longterm) {
2283
- console.log(chalk12.cyan("Long-term context:"));
2284
- console.log(` ${longterm}`);
2285
- console.log();
2286
- }
2287
- if (matchingEngrams.length > 0) {
2288
- console.log(chalk12.cyan(`Matching engrams (local):`));
2289
- for (const e of matchingEngrams) {
2290
- const ts = e.created_at.slice(0, 16).replace("T", " ");
2291
- console.log(` ${chalk12.gray(ts)} ${e.content}`);
2102
+ const longterm = getLongtermSummary(cortex);
2103
+ if (longterm) {
2104
+ console.log(chalk12.dim("No matching memories. Showing long-term context:"));
2105
+ console.log(` ${longterm}`);
2106
+ console.log();
2107
+ } else {
2108
+ console.log(chalk12.dim("No matching memories."));
2109
+ console.log();
2292
2110
  }
2293
- console.log();
2294
2111
  }
2295
- if (recentMemories.length === 0 && matchingEngrams.length === 0 && !longterm) {
2296
- console.log(chalk12.dim("No results found."));
2112
+ if (opts.engrams) {
2113
+ const matchingEngrams = searchEngrams(cortex, query3, limit);
2114
+ if (matchingEngrams.length > 0) {
2115
+ console.log(chalk12.cyan(`Matching engrams (${matchingEngrams.length}):`));
2116
+ for (const e of matchingEngrams) {
2117
+ const ts = e.created_at.slice(0, 16).replace("T", " ");
2118
+ console.log(` ${chalk12.gray(ts)} ${e.content}`);
2119
+ }
2120
+ console.log();
2121
+ }
2297
2122
  }
2298
2123
  closeCortexDb(cortex);
2299
2124
  });
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import {
3
+ getLongtermSummary,
4
+ getMemories,
5
+ getMemoriesBySyncVersion,
6
+ getMemoryByEpisodeKey,
7
+ getMemoryCount,
8
+ getSyncCursor,
9
+ insertMemory,
10
+ insertMemoryIfNotExists,
11
+ searchMemories,
12
+ setLongtermSummary,
13
+ setSyncCursor,
14
+ tombstoneMemory
15
+ } from "./chunk-MSOBQE64.js";
16
+ import "./chunk-DCTG6IK4.js";
17
+ export {
18
+ getLongtermSummary,
19
+ getMemories,
20
+ getMemoriesBySyncVersion,
21
+ getMemoryByEpisodeKey,
22
+ getMemoryCount,
23
+ getSyncCursor,
24
+ insertMemory,
25
+ insertMemoryIfNotExists,
26
+ searchMemories,
27
+ setLongtermSummary,
28
+ setSyncCursor,
29
+ tombstoneMemory
30
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-think",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Local-first CLI that gives AI agents persistent, curated memory",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "homepage": "https://openthink.dev",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/MicroMediaSites/think-cli.git"
20
+ "url": "git+https://github.com/OpenThinkAi/think-cli.git"
21
21
  },
22
22
  "keywords": [
23
23
  "cli",