open-think 0.1.14 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,33 +25,76 @@ think summary
25
25
  think summary --last-week --raw # raw entries, no AI
26
26
  ```
27
27
 
28
+ ## Local-first architecture
29
+
30
+ All reads and writes go to local SQLite. Sync is optional and eventual — your agents work fully offline.
31
+
32
+ ```
33
+ your machine remote (optional)
34
+ ───────────── ────────────────
35
+ entries → engrams → curator → memories ⇄ git | pg*
36
+ (local AI)
37
+ ```
38
+
39
+ Engrams (raw events) never leave your machine. Only curated memories sync to the backend you choose.
40
+
41
+ *Postgres adapter coming soon.*
42
+
28
43
  ## Cortex — shared team memory
29
44
 
30
- Cortexes connect agents across a team via a shared git repo. Engrams (raw events) stay local. A curator agent evaluates them and appends curated memories to the repo.
45
+ Cortexes are memory workspaces. Each has its own engrams, memories, and sync state.
31
46
 
32
47
  ```bash
33
48
  # Set up (once)
34
49
  think cortex setup git@github.com:org/hivedb.git
35
50
  think cortex create engineering
36
51
 
37
- # Work normally — think sync logs engrams locally
52
+ # Work normally — syncs log engrams locally
38
53
  think sync "deployed auth service to staging"
39
54
 
40
- # Curate — evaluate engrams, append memories to the branch
55
+ # Curate — evaluate engrams, promote memories
41
56
  think curate # full run
42
- think curate --dry-run # preview without pushing
57
+ think curate --dry-run # preview without saving
43
58
 
44
59
  # Read team memories
45
60
  think recall "auth" # search memories + local engrams
46
- think memory # show all memories from branch
61
+ think memory # show all memories
62
+
63
+ # Sync with remote
64
+ think cortex push # push local memories to remote
65
+ think cortex pull # pull remote memories to local
66
+ think cortex sync # push + pull
67
+ think cortex status # show sync state
47
68
 
48
69
  # Monitor curation quality
49
70
  think monitor # what got promoted vs dropped
50
71
 
51
- # Pull another team's memories
72
+ # Read another team's memories
52
73
  think pull product
53
74
  ```
54
75
 
76
+ Cortexes work without a remote — `think cortex setup` with no repo URL creates an offline-only workspace.
77
+
78
+ ## Episodes — narrative memory for task agents
79
+
80
+ Episodes let task-oriented agents (review bots, bug fixers, deploy agents) accumulate work across multiple rounds and synthesize it into a single narrative memory.
81
+
82
+ ```bash
83
+ # Tag engrams with an episode key
84
+ think sync -e "org/repo#42" "found SQL injection in auth middleware"
85
+ think sync -e "org/repo#42" "author fixed queries but missed token rotation"
86
+ think sync -e "org/repo#42" "all paths encrypted, approved"
87
+
88
+ # Synthesize into a narrative memory
89
+ think curate --episode "org/repo#42"
90
+ ```
91
+
92
+ Episode curation produces stories, not logs:
93
+
94
+ > *"A code review was opened against the auth middleware rewrite. The initial review identified plaintext session token storage — a direct violation of the encryption-at-rest requirement from the engineering standards doc. The author addressed this but missed the token rotation endpoint. After a third round, all session paths were encrypted and rotation was confirmed working."*
95
+
96
+ Re-curating after new rounds updates the existing narrative rather than creating a duplicate.
97
+
55
98
  ### Privacy
56
99
 
57
100
  ```bash
@@ -70,39 +113,52 @@ think curator show # print current guidance
70
113
 
71
114
  ## Data
72
115
 
73
- - **Engrams:** `~/.local/share/think/think.db` (no cortex) or `~/.think/engrams/<cortex>.db`
116
+ - **Cortex DB:** `~/.think/engrams/<cortex>.db` (engrams, memories, sync state — all in one SQLite file)
74
117
  - **Config:** `~/.config/think/config.json`
75
118
  - **Curator guidance:** `~/.think/curator.md`
76
- - **Memories:** `memories.jsonl` on cortex git branches (append-only JSONL)
119
+ - **Entries (no cortex):** `~/.local/share/think/think.db`
120
+
121
+ Override the data directory with `$THINK_HOME`.
77
122
 
78
123
  ## All commands
79
124
 
80
125
  ```
81
126
  think sync <message> Log a work event
127
+ think sync -e <key> <message> Log an episode-tagged event
82
128
  think log <message> Log a note (with --category, --tags)
83
129
  think list List entries (--week, --since, --category)
84
130
  think summary AI summary (--raw for plain text)
85
131
  think delete Soft-delete entries
86
132
 
87
- think cortex setup <repo> Configure git repo for shared memory
88
- think cortex create <name> Create a cortex branch
89
- think cortex list Show cortex branches
133
+ think cortex setup [repo] Configure sync backend (or offline-only)
134
+ think cortex create <name> Create a cortex
135
+ think cortex list Show all cortexes (local + remote)
90
136
  think cortex switch <name> Set active cortex
91
137
  think cortex current Show active cortex
138
+ think cortex push Push local memories to remote
139
+ think cortex pull Pull remote memories to local
140
+ think cortex sync Push + pull
141
+ think cortex status Show sync state
92
142
 
93
143
  think curate Run curation (--dry-run to preview)
144
+ think curate --episode <key> Curate an episode into a narrative memory
145
+ think curate --consolidate Compress older memories into long-term summary
94
146
  think monitor Show promoted vs dropped engrams
95
147
  think recall <query> Search memories + engrams
96
- think memory Show memories (--history for git log)
97
- think pull <cortex> Pull another cortex's memories
148
+ think memory Show memories (--history for timeline)
149
+ think pull <cortex> Read another cortex's memories
98
150
 
99
151
  think curator edit Edit personal curator guidance
100
152
  think curator show Show current guidance
101
153
  think pause Suppress engram creation
102
154
  think resume Re-enable engram creation
103
155
 
156
+ think migrate-data Import existing git memories into local SQLite
104
157
  think init Set up CLAUDE.md for auto-logging
105
158
  think export Export entries as sync bundle
106
159
  think import <file> Import sync bundle
107
160
  think audit Show sync audit log
161
+ think config show Print configuration
162
+ think config set <key> <val> Update a config value
163
+ think update Update to latest version
108
164
  ```
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+
3
+ // src/lib/git.ts
4
+ import { execFileSync } from "child_process";
5
+ import fs3 from "fs";
6
+ import path3 from "path";
7
+
8
+ // src/lib/paths.ts
9
+ import path from "path";
10
+ 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
+ import { v4 as uuidv4 } from "uuid";
71
+ function getConfigDir() {
72
+ return getThinkConfigDir();
73
+ }
74
+ function configPath() {
75
+ return path2.join(getConfigDir(), "config.json");
76
+ }
77
+ function saveConfig(config) {
78
+ const dir = getConfigDir();
79
+ fs2.mkdirSync(dir, { recursive: true });
80
+ fs2.writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
81
+ }
82
+ function getConfig() {
83
+ const fp = configPath();
84
+ if (fs2.existsSync(fp)) {
85
+ const raw = fs2.readFileSync(fp, "utf-8");
86
+ return JSON.parse(raw);
87
+ }
88
+ const config = {
89
+ peerId: uuidv4(),
90
+ syncPort: 47821
91
+ };
92
+ saveConfig(config);
93
+ return config;
94
+ }
95
+
96
+ // src/lib/git.ts
97
+ function runGit(args, cwd) {
98
+ const repoPath = cwd ?? getRepoPath();
99
+ return execFileSync("git", args, {
100
+ cwd: repoPath,
101
+ encoding: "utf-8",
102
+ stdio: ["pipe", "pipe", "pipe"]
103
+ }).trim();
104
+ }
105
+ function ensureRepoCloned() {
106
+ const config = getConfig();
107
+ if (!config.cortex?.repo) {
108
+ throw new Error("No cortex repo configured. Run: think cortex setup");
109
+ }
110
+ const repoPath = getRepoPath();
111
+ if (fs3.existsSync(path3.join(repoPath, ".git"))) {
112
+ const remote = runGit(["remote", "get-url", "origin"], repoPath);
113
+ if (remote !== config.cortex.repo) {
114
+ throw new Error(`Repo at ${repoPath} points to ${remote}, expected ${config.cortex.repo}`);
115
+ }
116
+ return;
117
+ }
118
+ fs3.mkdirSync(repoPath, { recursive: true });
119
+ execFileSync("git", ["clone", "--no-checkout", config.cortex.repo, repoPath], {
120
+ encoding: "utf-8",
121
+ stdio: ["pipe", "pipe", "pipe"]
122
+ });
123
+ }
124
+ function branchExists(branchName) {
125
+ try {
126
+ runGit(["ls-remote", "--exit-code", "--heads", "origin", branchName]);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function createOrphanBranch(branchName) {
133
+ runGit(["checkout", "--orphan", branchName]);
134
+ try {
135
+ runGit(["rm", "-rf", "."]);
136
+ } catch {
137
+ }
138
+ const repoPath = getRepoPath();
139
+ fs3.writeFileSync(path3.join(repoPath, "memories.jsonl"), "", "utf-8");
140
+ runGit(["add", "memories.jsonl"]);
141
+ runGit(["commit", "-m", `init: create cortex ${branchName}`]);
142
+ runGit(["push", "--set-upstream", "origin", branchName]);
143
+ }
144
+ function fetchBranch(branchName) {
145
+ runGit(["fetch", "origin", branchName]);
146
+ }
147
+ function readFileFromBranch(branchName, filePath) {
148
+ try {
149
+ return runGit(["show", `origin/${branchName}:${filePath}`]);
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ function appendAndCommit(branchName, newLines, commitMessage, maxRetries = 3) {
155
+ const repoPath = getRepoPath();
156
+ const memoriesPath = path3.join(repoPath, "memories.jsonl");
157
+ try {
158
+ runGit(["switch", branchName]);
159
+ } catch {
160
+ runGit(["switch", "-c", branchName, `origin/${branchName}`]);
161
+ }
162
+ try {
163
+ runGit(["pull", "--rebase", "origin", branchName]);
164
+ } catch (err) {
165
+ const message = err instanceof Error ? err.message : String(err);
166
+ if (message.includes("CONFLICT") || message.includes("could not apply")) {
167
+ try {
168
+ runGit(["rebase", "--abort"]);
169
+ } catch {
170
+ }
171
+ throw new Error(`Rebase conflict on ${branchName}. This should not happen with append-only files \u2014 check for manual edits to memories.jsonl.`);
172
+ }
173
+ }
174
+ const content = newLines.join("\n") + "\n";
175
+ fs3.appendFileSync(memoriesPath, content, "utf-8");
176
+ runGit(["add", "memories.jsonl"]);
177
+ runGit(["commit", "-m", commitMessage]);
178
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
179
+ try {
180
+ runGit(["push", "origin", branchName]);
181
+ return;
182
+ } catch {
183
+ if (attempt === maxRetries) {
184
+ throw new Error(`Push failed after ${maxRetries} attempts. Run 'think curate' again.`);
185
+ }
186
+ runGit(["pull", "--rebase", "origin", branchName]);
187
+ }
188
+ }
189
+ }
190
+ function getFileLog(branchName, filePath) {
191
+ return runGit(["log", "--oneline", `origin/${branchName}`, "--", filePath]);
192
+ }
193
+ function listRemoteBranches() {
194
+ const output = runGit(["ls-remote", "--heads", "origin"]);
195
+ return output.trim().split("\n").filter(Boolean).map((line) => line.split(" ")[1]?.replace("refs/heads/", "")).filter(Boolean);
196
+ }
197
+
198
+ export {
199
+ getThinkDataDir,
200
+ getEngramsDir,
201
+ getEngramDbPath,
202
+ getLongtermPath,
203
+ getCuratorMdPath,
204
+ ensureThinkDirs,
205
+ getConfigDir,
206
+ saveConfig,
207
+ getConfig,
208
+ ensureRepoCloned,
209
+ branchExists,
210
+ createOrphanBranch,
211
+ fetchBranch,
212
+ readFileFromBranch,
213
+ appendAndCommit,
214
+ getFileLog,
215
+ listRemoteBranches
216
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node --no-warnings=ExperimentalWarning
2
+ import {
3
+ appendAndCommit,
4
+ branchExists,
5
+ createOrphanBranch,
6
+ ensureRepoCloned,
7
+ fetchBranch,
8
+ getFileLog,
9
+ listRemoteBranches,
10
+ readFileFromBranch
11
+ } from "./chunk-K2FT7ZHJ.js";
12
+ export {
13
+ appendAndCommit,
14
+ branchExists,
15
+ createOrphanBranch,
16
+ ensureRepoCloned,
17
+ fetchBranch,
18
+ getFileLog,
19
+ listRemoteBranches,
20
+ readFileFromBranch
21
+ };