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 +69 -13
- package/dist/chunk-K2FT7ZHJ.js +216 -0
- package/dist/git-Y3N244VA.js +21 -0
- package/dist/index.js +930 -465
- package/package.json +2 -1
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
|
|
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 —
|
|
52
|
+
# Work normally — syncs log engrams locally
|
|
38
53
|
think sync "deployed auth service to staging"
|
|
39
54
|
|
|
40
|
-
# Curate — evaluate engrams,
|
|
55
|
+
# Curate — evaluate engrams, promote memories
|
|
41
56
|
think curate # full run
|
|
42
|
-
think curate --dry-run # preview without
|
|
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
|
|
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
|
-
#
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
|
88
|
-
think cortex create <name> Create a cortex
|
|
89
|
-
think cortex list Show
|
|
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
|
|
97
|
-
think pull <cortex>
|
|
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
|
+
};
|