reasonix 0.4.19 → 0.4.21
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 +47 -2
- package/dist/cli/chunk-K6MR4SWS.js +601 -0
- package/dist/cli/chunk-K6MR4SWS.js.map +1 -0
- package/dist/cli/index.js +460 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-JNNNJLYF.js → prompt-VDN5U3YE.js} +2 -2
- package/dist/index.d.ts +149 -5
- package/dist/index.js +658 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-HNEWBEWZ.js +0 -152
- package/dist/cli/chunk-HNEWBEWZ.js.map +0 -1
- /package/dist/cli/{prompt-JNNNJLYF.js.map → prompt-VDN5U3YE.js.map} +0 -0
package/README.md
CHANGED
|
@@ -209,8 +209,53 @@ EOF
|
|
|
209
209
|
|
|
210
210
|
Re-launch (or `/new`) to pick it up; the prefix is hashed once per
|
|
211
211
|
session to keep the DeepSeek cache warm. `/memory` prints what's
|
|
212
|
-
currently pinned. `REASONIX_MEMORY=off` disables
|
|
213
|
-
offline repro.
|
|
212
|
+
currently pinned. `REASONIX_MEMORY=off` disables every memory source
|
|
213
|
+
(REASONIX.md + `~/.reasonix/memory/`) for CI / offline repro.
|
|
214
|
+
|
|
215
|
+
### User memory — `~/.reasonix/memory/`
|
|
216
|
+
|
|
217
|
+
A second, **private per-user** memory layer lives under your home
|
|
218
|
+
directory. Unlike `REASONIX.md` it's never committed, and the model
|
|
219
|
+
can write to it itself via the `remember` tool. Two scopes:
|
|
220
|
+
|
|
221
|
+
- `~/.reasonix/memory/global/` — cross-project (your preferences, tooling).
|
|
222
|
+
- `~/.reasonix/memory/<project-hash>/` — scoped to one sandbox root in
|
|
223
|
+
`reasonix code` (decisions, local facts, per-repo shortcuts).
|
|
224
|
+
|
|
225
|
+
Each scope keeps an always-loaded `MEMORY.md` index of one-liners plus
|
|
226
|
+
zero or more `<name>.md` detail files (loaded on demand via
|
|
227
|
+
`recall_memory`). Writes land immediately; pinning into the system
|
|
228
|
+
prompt takes effect on next `/new` or launch so the cache prefix stays
|
|
229
|
+
stable for the current session.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
reasonix code › 我用 bun 而不是 npm,请以后都用 bun 跑构建
|
|
233
|
+
|
|
234
|
+
assistant
|
|
235
|
+
▸ tool<remember> → project/bun_build saved
|
|
236
|
+
"Build command on this machine is `bun run build`"
|
|
237
|
+
|
|
238
|
+
reasonix code › /memory list
|
|
239
|
+
project/project bun_build Build command on this machine is `bun run build`
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Slash commands:**
|
|
243
|
+
|
|
244
|
+
- `/memory` — show everything pinned (REASONIX.md + both MEMORY.md scopes)
|
|
245
|
+
- `/memory list` — every memory file with scope + type + description
|
|
246
|
+
- `/memory show <name>` — dump one file's full body
|
|
247
|
+
- `/memory forget <name>` — delete one memory (no LLM turn)
|
|
248
|
+
- `/memory clear <global|project> confirm` — wipe a scope (typed literal)
|
|
249
|
+
|
|
250
|
+
**Model tools** (available in every session):
|
|
251
|
+
|
|
252
|
+
- `remember(type, scope, name, description, content)` — save a memory
|
|
253
|
+
- `forget(scope, name)` — delete it
|
|
254
|
+
- `recall_memory(scope, name)` — read the full body when the one-liner isn't enough
|
|
255
|
+
|
|
256
|
+
Project scope is only available inside `reasonix code` (there has to be
|
|
257
|
+
a real sandbox root to hash); plain `reasonix` / `reasonix chat` gets
|
|
258
|
+
the global scope only.
|
|
214
259
|
|
|
215
260
|
```bash
|
|
216
261
|
npx reasonix code src/ # narrower sandbox (only src/ is writable)
|
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/code/prompt.ts
|
|
4
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { join as join4 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/user-memory.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import {
|
|
10
|
+
existsSync as existsSync3,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync as readFileSync3,
|
|
13
|
+
readdirSync as readdirSync2,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { homedir as homedir2 } from "os";
|
|
18
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
19
|
+
|
|
20
|
+
// src/project-memory.ts
|
|
21
|
+
import { existsSync, readFileSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
var PROJECT_MEMORY_FILE = "REASONIX.md";
|
|
24
|
+
var PROJECT_MEMORY_MAX_CHARS = 8e3;
|
|
25
|
+
function readProjectMemory(rootDir) {
|
|
26
|
+
const path = join(rootDir, PROJECT_MEMORY_FILE);
|
|
27
|
+
if (!existsSync(path)) return null;
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = readFileSync(path, "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (!trimmed) return null;
|
|
36
|
+
const originalChars = trimmed.length;
|
|
37
|
+
const truncated = originalChars > PROJECT_MEMORY_MAX_CHARS;
|
|
38
|
+
const content = truncated ? `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}
|
|
39
|
+
\u2026 (truncated ${originalChars - PROJECT_MEMORY_MAX_CHARS} chars)` : trimmed;
|
|
40
|
+
return { path, content, originalChars, truncated };
|
|
41
|
+
}
|
|
42
|
+
function memoryEnabled() {
|
|
43
|
+
const env = process.env.REASONIX_MEMORY;
|
|
44
|
+
if (env === "off" || env === "false" || env === "0") return false;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function applyProjectMemory(basePrompt, rootDir) {
|
|
48
|
+
if (!memoryEnabled()) return basePrompt;
|
|
49
|
+
const mem = readProjectMemory(rootDir);
|
|
50
|
+
if (!mem) return basePrompt;
|
|
51
|
+
return `${basePrompt}
|
|
52
|
+
|
|
53
|
+
# Project memory (REASONIX.md)
|
|
54
|
+
|
|
55
|
+
The user pinned these notes about this project \u2014 treat them as authoritative context for every turn:
|
|
56
|
+
|
|
57
|
+
\`\`\`
|
|
58
|
+
${mem.content}
|
|
59
|
+
\`\`\`
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/skills.ts
|
|
64
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
65
|
+
import { homedir } from "os";
|
|
66
|
+
import { join as join2, resolve } from "path";
|
|
67
|
+
var SKILLS_DIRNAME = "skills";
|
|
68
|
+
var SKILL_FILE = "SKILL.md";
|
|
69
|
+
var SKILLS_INDEX_MAX_CHARS = 4e3;
|
|
70
|
+
var VALID_SKILL_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
71
|
+
function parseFrontmatter(raw) {
|
|
72
|
+
const lines = raw.split(/\r?\n/);
|
|
73
|
+
if (lines[0] !== "---") return { data: {}, body: raw };
|
|
74
|
+
const end = lines.indexOf("---", 1);
|
|
75
|
+
if (end < 0) return { data: {}, body: raw };
|
|
76
|
+
const data = {};
|
|
77
|
+
for (let i = 1; i < end; i++) {
|
|
78
|
+
const line = lines[i];
|
|
79
|
+
if (!line) continue;
|
|
80
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
81
|
+
if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
data,
|
|
85
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isValidSkillName(name) {
|
|
89
|
+
return VALID_SKILL_NAME.test(name);
|
|
90
|
+
}
|
|
91
|
+
var SkillStore = class {
|
|
92
|
+
homeDir;
|
|
93
|
+
projectRoot;
|
|
94
|
+
constructor(opts = {}) {
|
|
95
|
+
this.homeDir = opts.homeDir ?? homedir();
|
|
96
|
+
this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
|
|
97
|
+
}
|
|
98
|
+
/** True iff this store was configured with a project root. */
|
|
99
|
+
hasProjectScope() {
|
|
100
|
+
return this.projectRoot !== void 0;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Root directories scanned, in priority order. Project scope first
|
|
104
|
+
* so a per-repo skill overrides a global one with the same name —
|
|
105
|
+
* users expect the local copy to win when both exist.
|
|
106
|
+
*/
|
|
107
|
+
roots() {
|
|
108
|
+
const out = [];
|
|
109
|
+
if (this.projectRoot) {
|
|
110
|
+
out.push({
|
|
111
|
+
dir: join2(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
|
|
112
|
+
scope: "project"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
out.push({ dir: join2(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* List every skill visible to this store. On name collisions the
|
|
120
|
+
* higher-priority root (project over global) wins. Sorted by name
|
|
121
|
+
* for stable prefix hashing.
|
|
122
|
+
*/
|
|
123
|
+
list() {
|
|
124
|
+
const byName = /* @__PURE__ */ new Map();
|
|
125
|
+
for (const { dir, scope } of this.roots()) {
|
|
126
|
+
if (!existsSync2(dir)) continue;
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
130
|
+
} catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const skill = this.readEntry(dir, scope, entry);
|
|
135
|
+
if (!skill) continue;
|
|
136
|
+
if (!byName.has(skill.name)) byName.set(skill.name, skill);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
140
|
+
}
|
|
141
|
+
/** Resolve one skill by name. Returns `null` if not found or malformed. */
|
|
142
|
+
read(name) {
|
|
143
|
+
if (!isValidSkillName(name)) return null;
|
|
144
|
+
for (const { dir, scope } of this.roots()) {
|
|
145
|
+
if (!existsSync2(dir)) continue;
|
|
146
|
+
const dirCandidate = join2(dir, name, SKILL_FILE);
|
|
147
|
+
if (existsSync2(dirCandidate) && statSync(dirCandidate).isFile()) {
|
|
148
|
+
return this.parse(dirCandidate, name, scope);
|
|
149
|
+
}
|
|
150
|
+
const flatCandidate = join2(dir, `${name}.md`);
|
|
151
|
+
if (existsSync2(flatCandidate) && statSync(flatCandidate).isFile()) {
|
|
152
|
+
return this.parse(flatCandidate, name, scope);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
readEntry(dir, scope, entry) {
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
if (!isValidSkillName(entry.name)) return null;
|
|
160
|
+
const file = join2(dir, entry.name, SKILL_FILE);
|
|
161
|
+
if (!existsSync2(file)) return null;
|
|
162
|
+
return this.parse(file, entry.name, scope);
|
|
163
|
+
}
|
|
164
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
165
|
+
const stem = entry.name.slice(0, -3);
|
|
166
|
+
if (!isValidSkillName(stem)) return null;
|
|
167
|
+
return this.parse(join2(dir, entry.name), stem, scope);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
parse(path, stem, scope) {
|
|
172
|
+
let raw;
|
|
173
|
+
try {
|
|
174
|
+
raw = readFileSync2(path, "utf8");
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const { data, body } = parseFrontmatter(raw);
|
|
179
|
+
const name = data.name && isValidSkillName(data.name) ? data.name : stem;
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
description: (data.description ?? "").trim(),
|
|
183
|
+
body: body.trim(),
|
|
184
|
+
scope,
|
|
185
|
+
path,
|
|
186
|
+
allowedTools: data["allowed-tools"]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function skillIndexLine(s) {
|
|
191
|
+
const safeDesc = s.description.replace(/\n/g, " ").trim();
|
|
192
|
+
const max = 130 - s.name.length;
|
|
193
|
+
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
194
|
+
return clipped ? `- ${s.name} \u2014 ${clipped}` : `- ${s.name}`;
|
|
195
|
+
}
|
|
196
|
+
function applySkillsIndex(basePrompt, opts = {}) {
|
|
197
|
+
const store = new SkillStore(opts);
|
|
198
|
+
const skills = store.list().filter((s) => s.description);
|
|
199
|
+
if (skills.length === 0) return basePrompt;
|
|
200
|
+
const lines = skills.map(skillIndexLine);
|
|
201
|
+
const joined = lines.join("\n");
|
|
202
|
+
const truncated = joined.length > SKILLS_INDEX_MAX_CHARS ? `${joined.slice(0, SKILLS_INDEX_MAX_CHARS)}
|
|
203
|
+
\u2026 (truncated ${joined.length - SKILLS_INDEX_MAX_CHARS} chars)` : joined;
|
|
204
|
+
return [
|
|
205
|
+
basePrompt,
|
|
206
|
+
"",
|
|
207
|
+
"# Skills \u2014 user-defined prompt packs",
|
|
208
|
+
"",
|
|
209
|
+
'One-liner index. Each skill is a self-contained instruction block (plus optional tool hints) the user or an earlier session saved. To load the full body, call `run_skill({ name: "<skill-name>" })` \u2014 the body is NOT in this prompt, only the name and description are. The user can also invoke a skill directly as `/skill <name>`.',
|
|
210
|
+
"",
|
|
211
|
+
"```",
|
|
212
|
+
truncated,
|
|
213
|
+
"```"
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/user-memory.ts
|
|
218
|
+
var USER_MEMORY_DIR = "memory";
|
|
219
|
+
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
220
|
+
var MEMORY_INDEX_MAX_CHARS = 4e3;
|
|
221
|
+
var VALID_NAME = /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{1,38}[a-zA-Z0-9]$/;
|
|
222
|
+
function sanitizeMemoryName(raw) {
|
|
223
|
+
const trimmed = String(raw ?? "").trim();
|
|
224
|
+
if (!VALID_NAME.test(trimmed)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`invalid memory name: ${JSON.stringify(raw)} \u2014 must be 3-40 chars, alnum/_/-, no path separators`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return trimmed;
|
|
230
|
+
}
|
|
231
|
+
function projectHash(rootDir) {
|
|
232
|
+
const abs = resolve2(rootDir);
|
|
233
|
+
return createHash("sha1").update(abs).digest("hex").slice(0, 16);
|
|
234
|
+
}
|
|
235
|
+
function scopeDir(opts) {
|
|
236
|
+
if (opts.scope === "global") {
|
|
237
|
+
return join3(opts.homeDir, USER_MEMORY_DIR, "global");
|
|
238
|
+
}
|
|
239
|
+
if (!opts.projectRoot) {
|
|
240
|
+
throw new Error("scope=project requires a projectRoot on MemoryStore");
|
|
241
|
+
}
|
|
242
|
+
return join3(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
|
|
243
|
+
}
|
|
244
|
+
function ensureDir(p) {
|
|
245
|
+
if (!existsSync3(p)) mkdirSync(p, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
function parseFrontmatter2(raw) {
|
|
248
|
+
const lines = raw.split(/\r?\n/);
|
|
249
|
+
if (lines[0] !== "---") return { data: {}, body: raw };
|
|
250
|
+
const end = lines.indexOf("---", 1);
|
|
251
|
+
if (end < 0) return { data: {}, body: raw };
|
|
252
|
+
const data = {};
|
|
253
|
+
for (let i = 1; i < end; i++) {
|
|
254
|
+
const line = lines[i];
|
|
255
|
+
if (!line) continue;
|
|
256
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
|
|
257
|
+
if (m?.[1]) data[m[1]] = (m[2] ?? "").trim();
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
data,
|
|
261
|
+
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "")
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function formatFrontmatter(e) {
|
|
265
|
+
return [
|
|
266
|
+
"---",
|
|
267
|
+
`name: ${e.name}`,
|
|
268
|
+
`description: ${e.description.replace(/\n/g, " ")}`,
|
|
269
|
+
`type: ${e.type}`,
|
|
270
|
+
`scope: ${e.scope}`,
|
|
271
|
+
`created: ${e.createdAt}`,
|
|
272
|
+
"---",
|
|
273
|
+
""
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function todayIso() {
|
|
277
|
+
const d = /* @__PURE__ */ new Date();
|
|
278
|
+
return d.toISOString().slice(0, 10);
|
|
279
|
+
}
|
|
280
|
+
function indexLine(e) {
|
|
281
|
+
const safeDesc = e.description.replace(/\n/g, " ").trim();
|
|
282
|
+
const max = 130 - e.name.length;
|
|
283
|
+
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
284
|
+
return `- [${e.name}](${e.name}.md) \u2014 ${clipped}`;
|
|
285
|
+
}
|
|
286
|
+
var MemoryStore = class {
|
|
287
|
+
homeDir;
|
|
288
|
+
projectRoot;
|
|
289
|
+
constructor(opts = {}) {
|
|
290
|
+
this.homeDir = opts.homeDir ?? join3(homedir2(), ".reasonix");
|
|
291
|
+
this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
|
|
292
|
+
}
|
|
293
|
+
/** Directory this store writes `scope` files into, creating it if needed. */
|
|
294
|
+
dir(scope) {
|
|
295
|
+
const d = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
296
|
+
ensureDir(d);
|
|
297
|
+
return d;
|
|
298
|
+
}
|
|
299
|
+
/** Absolute path to a memory file (no existence check). */
|
|
300
|
+
pathFor(scope, name) {
|
|
301
|
+
return join3(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
|
|
302
|
+
}
|
|
303
|
+
/** True iff this store is configured with a project scope available. */
|
|
304
|
+
hasProjectScope() {
|
|
305
|
+
return this.projectRoot !== void 0;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Read the `MEMORY.md` index for a scope. Returns post-cap content
|
|
309
|
+
* (with a truncation marker if clipped), or `null` when absent / empty.
|
|
310
|
+
*/
|
|
311
|
+
loadIndex(scope) {
|
|
312
|
+
if (scope === "project" && !this.projectRoot) return null;
|
|
313
|
+
const file = join3(
|
|
314
|
+
scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
|
|
315
|
+
MEMORY_INDEX_FILE
|
|
316
|
+
);
|
|
317
|
+
if (!existsSync3(file)) return null;
|
|
318
|
+
let raw;
|
|
319
|
+
try {
|
|
320
|
+
raw = readFileSync3(file, "utf8");
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const trimmed = raw.trim();
|
|
325
|
+
if (!trimmed) return null;
|
|
326
|
+
const originalChars = trimmed.length;
|
|
327
|
+
const truncated = originalChars > MEMORY_INDEX_MAX_CHARS;
|
|
328
|
+
const content = truncated ? `${trimmed.slice(0, MEMORY_INDEX_MAX_CHARS)}
|
|
329
|
+
\u2026 (truncated ${originalChars - MEMORY_INDEX_MAX_CHARS} chars)` : trimmed;
|
|
330
|
+
return { content, originalChars, truncated };
|
|
331
|
+
}
|
|
332
|
+
/** Read one memory file's body (frontmatter stripped). Throws if missing. */
|
|
333
|
+
read(scope, name) {
|
|
334
|
+
const file = this.pathFor(scope, name);
|
|
335
|
+
if (!existsSync3(file)) {
|
|
336
|
+
throw new Error(`memory not found: scope=${scope} name=${name}`);
|
|
337
|
+
}
|
|
338
|
+
const raw = readFileSync3(file, "utf8");
|
|
339
|
+
const { data, body } = parseFrontmatter2(raw);
|
|
340
|
+
return {
|
|
341
|
+
name: data.name ?? name,
|
|
342
|
+
type: data.type ?? "project",
|
|
343
|
+
scope: data.scope ?? scope,
|
|
344
|
+
description: data.description ?? "",
|
|
345
|
+
body: body.trim(),
|
|
346
|
+
createdAt: data.created ?? ""
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* List every memory in this store. Scans both scopes (skips project
|
|
351
|
+
* scope if unconfigured). Silently skips malformed files; the index
|
|
352
|
+
* must stay queryable even if one file is hand-edited into nonsense.
|
|
353
|
+
*/
|
|
354
|
+
list() {
|
|
355
|
+
const out = [];
|
|
356
|
+
const scopes = this.projectRoot ? ["global", "project"] : ["global"];
|
|
357
|
+
for (const scope of scopes) {
|
|
358
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
359
|
+
if (!existsSync3(dir)) continue;
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = readdirSync2(dir);
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (entry === MEMORY_INDEX_FILE) continue;
|
|
368
|
+
if (!entry.endsWith(".md")) continue;
|
|
369
|
+
const name = entry.slice(0, -3);
|
|
370
|
+
try {
|
|
371
|
+
out.push(this.read(scope, name));
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Write a new memory (or overwrite existing). Creates the scope dir,
|
|
380
|
+
* writes the `.md` file, and regenerates `MEMORY.md`. Returns the
|
|
381
|
+
* absolute path written to.
|
|
382
|
+
*/
|
|
383
|
+
write(input) {
|
|
384
|
+
if (input.scope === "project" && !this.projectRoot) {
|
|
385
|
+
throw new Error("cannot write project-scoped memory: no projectRoot configured");
|
|
386
|
+
}
|
|
387
|
+
const name = sanitizeMemoryName(input.name);
|
|
388
|
+
const desc = String(input.description ?? "").trim();
|
|
389
|
+
if (!desc) throw new Error("memory description cannot be empty");
|
|
390
|
+
const body = String(input.body ?? "").trim();
|
|
391
|
+
if (!body) throw new Error("memory body cannot be empty");
|
|
392
|
+
const entry = {
|
|
393
|
+
...input,
|
|
394
|
+
name,
|
|
395
|
+
description: desc,
|
|
396
|
+
body,
|
|
397
|
+
createdAt: todayIso()
|
|
398
|
+
};
|
|
399
|
+
const dir = this.dir(input.scope);
|
|
400
|
+
const file = join3(dir, `${name}.md`);
|
|
401
|
+
const content = `${formatFrontmatter(entry)}${body}
|
|
402
|
+
`;
|
|
403
|
+
writeFileSync(file, content, "utf8");
|
|
404
|
+
this.regenerateIndex(input.scope);
|
|
405
|
+
return file;
|
|
406
|
+
}
|
|
407
|
+
/** Delete one memory + its index line. No-op if the file is already gone. */
|
|
408
|
+
delete(scope, rawName) {
|
|
409
|
+
if (scope === "project" && !this.projectRoot) {
|
|
410
|
+
throw new Error("cannot delete project-scoped memory: no projectRoot configured");
|
|
411
|
+
}
|
|
412
|
+
const file = this.pathFor(scope, rawName);
|
|
413
|
+
if (!existsSync3(file)) return false;
|
|
414
|
+
unlinkSync(file);
|
|
415
|
+
this.regenerateIndex(scope);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.
|
|
420
|
+
* Called after every write/delete. Sorted by name for stable prefix
|
|
421
|
+
* hashing — two stores with the same set of files produce byte-identical
|
|
422
|
+
* MEMORY.md content, keeping the cache prefix reproducible.
|
|
423
|
+
*/
|
|
424
|
+
regenerateIndex(scope) {
|
|
425
|
+
const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });
|
|
426
|
+
if (!existsSync3(dir)) return;
|
|
427
|
+
let files;
|
|
428
|
+
try {
|
|
429
|
+
files = readdirSync2(dir);
|
|
430
|
+
} catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
434
|
+
const indexPath = join3(dir, MEMORY_INDEX_FILE);
|
|
435
|
+
if (mdFiles.length === 0) {
|
|
436
|
+
if (existsSync3(indexPath)) unlinkSync(indexPath);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const lines = [];
|
|
440
|
+
for (const f of mdFiles) {
|
|
441
|
+
const name = f.slice(0, -3);
|
|
442
|
+
try {
|
|
443
|
+
const entry = this.read(scope, name);
|
|
444
|
+
lines.push(indexLine({ name: entry.name || name, description: entry.description }));
|
|
445
|
+
} catch {
|
|
446
|
+
lines.push(`- [${name}](${name}.md) \u2014 (malformed, check frontmatter)`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
writeFileSync(indexPath, `${lines.join("\n")}
|
|
450
|
+
`, "utf8");
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function applyUserMemory(basePrompt, opts = {}) {
|
|
454
|
+
if (!memoryEnabled()) return basePrompt;
|
|
455
|
+
const store = new MemoryStore(opts);
|
|
456
|
+
const global = store.loadIndex("global");
|
|
457
|
+
const project = store.hasProjectScope() ? store.loadIndex("project") : null;
|
|
458
|
+
if (!global && !project) return basePrompt;
|
|
459
|
+
const parts = [basePrompt];
|
|
460
|
+
if (global) {
|
|
461
|
+
parts.push(
|
|
462
|
+
"",
|
|
463
|
+
"# User memory \u2014 global (~/.reasonix/memory/global/MEMORY.md)",
|
|
464
|
+
"",
|
|
465
|
+
"Cross-project facts and preferences the user has told you in prior sessions. TREAT AS AUTHORITATIVE \u2014 don't re-verify via filesystem or web. One-liners index detail files; call `recall_memory` for full bodies only when the one-liner isn't enough.",
|
|
466
|
+
"",
|
|
467
|
+
"```",
|
|
468
|
+
global.content,
|
|
469
|
+
"```"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (project) {
|
|
473
|
+
parts.push(
|
|
474
|
+
"",
|
|
475
|
+
"# User memory \u2014 this project",
|
|
476
|
+
"",
|
|
477
|
+
"Per-project facts the user established in prior sessions (not committed to the repo). TREAT AS AUTHORITATIVE. Same recall pattern as global memory.",
|
|
478
|
+
"",
|
|
479
|
+
"```",
|
|
480
|
+
project.content,
|
|
481
|
+
"```"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return parts.join("\n");
|
|
485
|
+
}
|
|
486
|
+
function applyMemoryStack(basePrompt, rootDir) {
|
|
487
|
+
const withProject = applyProjectMemory(basePrompt, rootDir);
|
|
488
|
+
const withMemory = applyUserMemory(withProject, { projectRoot: rootDir });
|
|
489
|
+
return applySkillsIndex(withMemory, { projectRoot: rootDir });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/code/prompt.ts
|
|
493
|
+
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
|
|
494
|
+
|
|
495
|
+
# When to propose a plan (submit_plan)
|
|
496
|
+
|
|
497
|
+
You have a \`submit_plan\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:
|
|
498
|
+
|
|
499
|
+
- Multi-file refactors or renames.
|
|
500
|
+
- Architecture changes (moving modules, splitting / merging files, new abstractions).
|
|
501
|
+
- Anything where "undo" after the fact would be expensive \u2014 migrations, destructive cleanups, API shape changes.
|
|
502
|
+
- When the user's request is ambiguous and multiple reasonable interpretations exist \u2014 propose your reading as a plan and let them confirm.
|
|
503
|
+
|
|
504
|
+
Skip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.
|
|
505
|
+
|
|
506
|
+
Plan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an "Open questions" section \u2014 the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP \u2014 don't call any more tools, wait for the user's verdict.
|
|
507
|
+
|
|
508
|
+
# Plan mode (/plan)
|
|
509
|
+
|
|
510
|
+
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
511
|
+
- Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
|
|
512
|
+
- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
|
|
513
|
+
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# When to edit vs. when to explore
|
|
517
|
+
|
|
518
|
+
Only propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:
|
|
519
|
+
- analyze, read, explore, describe, or summarize a project
|
|
520
|
+
- explain how something works
|
|
521
|
+
- answer a question about the code
|
|
522
|
+
|
|
523
|
+
In those cases, use tools to gather what you need, then reply in prose. No SEARCH/REPLACE blocks, no file changes. If you're unsure what the user wants, ask.
|
|
524
|
+
|
|
525
|
+
When you do propose edits, the user will review them and decide whether to \`/apply\` or \`/discard\`. Don't assume they'll accept \u2014 write as if each edit will be audited, because it will.
|
|
526
|
+
|
|
527
|
+
# Editing files
|
|
528
|
+
|
|
529
|
+
When you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:
|
|
530
|
+
|
|
531
|
+
path/to/file.ext
|
|
532
|
+
<<<<<<< SEARCH
|
|
533
|
+
exact existing lines from the file, including whitespace
|
|
534
|
+
=======
|
|
535
|
+
the new lines
|
|
536
|
+
>>>>>>> REPLACE
|
|
537
|
+
|
|
538
|
+
Rules:
|
|
539
|
+
- Always read_file first so your SEARCH matches byte-for-byte. If it doesn't match, the edit is rejected and you'll have to retry with the exact current content.
|
|
540
|
+
- One edit per block. Multiple blocks in one response are fine.
|
|
541
|
+
- To create a new file, leave SEARCH empty:
|
|
542
|
+
path/to/new.ts
|
|
543
|
+
<<<<<<< SEARCH
|
|
544
|
+
=======
|
|
545
|
+
(whole file content here)
|
|
546
|
+
>>>>>>> REPLACE
|
|
547
|
+
- Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
|
|
548
|
+
- Paths are relative to the working directory. Don't use absolute paths.
|
|
549
|
+
|
|
550
|
+
# Trust what you already know
|
|
551
|
+
|
|
552
|
+
Before exploring the filesystem to answer a factual question, check whether the answer is already in context: the user's current message, earlier turns in this conversation (including prior tool results from \`remember\`), and the pinned memory blocks at the top of this prompt. When the user has stated a fact or you have remembered one, it outranks what the files say \u2014 don't re-derive from code what the user already told you. Explore when you genuinely don't know.
|
|
553
|
+
|
|
554
|
+
# Exploration
|
|
555
|
+
|
|
556
|
+
- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
|
|
557
|
+
- Prefer search_files / grep over list_directory when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees.
|
|
558
|
+
|
|
559
|
+
# Style
|
|
560
|
+
|
|
561
|
+
- Show edits; don't narrate them in prose. "Here's the fix:" is enough.
|
|
562
|
+
- One short paragraph explaining *why*, then the blocks.
|
|
563
|
+
- If you need to explore first (list / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
564
|
+
`;
|
|
565
|
+
function codeSystemPrompt(rootDir) {
|
|
566
|
+
const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
|
|
567
|
+
const gitignorePath = join4(rootDir, ".gitignore");
|
|
568
|
+
if (!existsSync4(gitignorePath)) return withMemory;
|
|
569
|
+
let content;
|
|
570
|
+
try {
|
|
571
|
+
content = readFileSync4(gitignorePath, "utf8");
|
|
572
|
+
} catch {
|
|
573
|
+
return withMemory;
|
|
574
|
+
}
|
|
575
|
+
const MAX = 2e3;
|
|
576
|
+
const truncated = content.length > MAX ? `${content.slice(0, MAX)}
|
|
577
|
+
\u2026 (truncated ${content.length - MAX} chars)` : content;
|
|
578
|
+
return `${withMemory}
|
|
579
|
+
|
|
580
|
+
# Project .gitignore
|
|
581
|
+
|
|
582
|
+
The user's repo ships this .gitignore \u2014 treat every pattern as "don't traverse or edit inside these paths unless explicitly asked":
|
|
583
|
+
|
|
584
|
+
\`\`\`
|
|
585
|
+
${truncated}
|
|
586
|
+
\`\`\`
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export {
|
|
591
|
+
PROJECT_MEMORY_FILE,
|
|
592
|
+
readProjectMemory,
|
|
593
|
+
memoryEnabled,
|
|
594
|
+
SkillStore,
|
|
595
|
+
sanitizeMemoryName,
|
|
596
|
+
MemoryStore,
|
|
597
|
+
applyMemoryStack,
|
|
598
|
+
CODE_SYSTEM_PROMPT,
|
|
599
|
+
codeSystemPrompt
|
|
600
|
+
};
|
|
601
|
+
//# sourceMappingURL=chunk-K6MR4SWS.js.map
|