@askjo/pi-mem 1.0.0 → 1.2.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/.github/FUNDING.yml +1 -0
- package/.github/workflows/publish.yml +46 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/index.ts +112 -53
- package/lib.ts +377 -23
- package/package.json +4 -2
- package/release.sh +39 -0
- package/tests/config.test.ts +107 -4
- package/tests/date-helpers.test.ts +13 -1
- package/tests/file-helpers.test.ts +39 -1
- package/tests/helpers.ts +2 -0
- package/tests/memory-context.test.ts +201 -26
- package/tests/memory-read.test.ts +84 -0
- package/tests/search.test.ts +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: skyfallsin
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: npm-publish
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 24
|
|
20
|
+
cache: npm
|
|
21
|
+
- run: npm ci
|
|
22
|
+
- run: npm test
|
|
23
|
+
|
|
24
|
+
publish:
|
|
25
|
+
needs: test
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
id-token: write
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: 24
|
|
35
|
+
registry-url: "https://registry.npmjs.org"
|
|
36
|
+
- name: Verify tag matches package.json version
|
|
37
|
+
run: |
|
|
38
|
+
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
|
|
39
|
+
PKG_VERSION=$(node -p "require('./package.json').version")
|
|
40
|
+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
|
|
41
|
+
echo "::error::Tag version ($TAG_VERSION) != package.json version ($PKG_VERSION)"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
- run: npm ci --ignore-scripts
|
|
45
|
+
- name: Publish with provenance
|
|
46
|
+
run: npm publish --provenance --access public
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jo-inc
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
3
|
+
<a href="https://www.npmjs.com/package/@askjo/pi-mem"><img src="https://img.shields.io/npm/v/@askjo/pi-mem" alt="npm version" /></a>
|
|
4
|
+
<a href="https://github.com/jo-inc/pi-mem/stargazers"><img src="https://img.shields.io/github/stars/jo-inc/pi-mem" alt="GitHub stars" /></a>
|
|
5
|
+
</p>
|
|
1
6
|
<p align="center">
|
|
2
7
|
<picture>
|
|
3
8
|
<source media="(prefers-color-scheme: dark)" srcset="logo-dark.png" />
|
|
@@ -48,12 +53,32 @@ Files in `notes/` and older daily logs are **not** injected — they're accessib
|
|
|
48
53
|
|
|
49
54
|
## Configuration
|
|
50
55
|
|
|
56
|
+
Settings can be configured via environment variables or a `.pi-mem.json` file in the memory directory. Environment variables take precedence over file values.
|
|
57
|
+
|
|
58
|
+
### .pi-mem.json
|
|
59
|
+
|
|
60
|
+
Place a `.pi-mem.json` in your memory directory (default `~/.pi/agent/memory/.pi-mem.json`):
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"searchDirs": ["catchup", "projects"],
|
|
65
|
+
"contextFiles": ["SOUL.md", "AGENTS.md"],
|
|
66
|
+
"autocommit": true
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Environment variables
|
|
71
|
+
|
|
72
|
+
Environment variables override `.pi-mem.json` values when set.
|
|
73
|
+
|
|
51
74
|
| Env Var | Default | Description |
|
|
52
75
|
|---------|---------|-------------|
|
|
53
76
|
| `PI_MEMORY_DIR` | `~/.pi/agent/memory/` | Root directory for all memory files |
|
|
54
77
|
| `PI_DAILY_DIR` | `$PI_MEMORY_DIR/daily/` | Directory for daily logs |
|
|
55
78
|
| `PI_CONTEXT_FILES` | *(empty)* | Comma-separated list of extra files to inject into context (e.g. `SOUL.md,AGENTS.md,HEARTBEAT.md`) |
|
|
79
|
+
| `PI_SEARCH_DIRS` | *(empty)* | Comma-separated list of subdirectories (relative to `PI_MEMORY_DIR`) to include in `memory_search`. Searched recursively one level deep. (e.g. `catchup,projects`) |
|
|
56
80
|
| `PI_AUTOCOMMIT` | `false` | When `1` or `true`, auto-commit to git after every write |
|
|
81
|
+
| `PI_TIMEZONE` | `TZ`, then `UTC` | IANA timezone used for daily log filenames and today/yesterday context windows. Invalid values fall back to `UTC`. |
|
|
57
82
|
|
|
58
83
|
## Dashboard Widget
|
|
59
84
|
|
|
@@ -76,3 +101,13 @@ pi install git:github.com/jo-inc/pi-mem
|
|
|
76
101
|
## License
|
|
77
102
|
|
|
78
103
|
MIT
|
|
104
|
+
|
|
105
|
+
## Pi Ecosystem
|
|
106
|
+
|
|
107
|
+
| Package | Description |
|
|
108
|
+
|---------|-------------|
|
|
109
|
+
| [pi-reflect](https://github.com/jo-inc/pi-reflect) | Self-improving behavioral files |
|
|
110
|
+
| [pi-boss](https://github.com/skyfallsin/pi-boss) | Multi-agent orchestration via tmux |
|
|
111
|
+
| [pi-room](https://github.com/skyfallsin/pi-room) | Multi-agent awareness and coordination |
|
|
112
|
+
| [pi-vertex-anthropic](https://github.com/skyfallsin/pi-vertex-anthropic) | Claude via Google Cloud Vertex AI |
|
|
113
|
+
| [pi-skill-posthog](https://github.com/skyfallsin/pi-skill-posthog) | PostHog analytics skill for pi agents |
|
package/index.ts
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
27
|
-
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
28
|
-
import { Markdown } from "@mariozechner/pi-tui";
|
|
27
|
+
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
|
|
28
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
29
29
|
import { Type } from "@sinclair/typebox";
|
|
30
30
|
import { StringEnum, completeSimple, getModel } from "@mariozechner/pi-ai";
|
|
31
31
|
import * as fs from "node:fs";
|
|
@@ -47,9 +47,11 @@ import {
|
|
|
47
47
|
parseScratchpad,
|
|
48
48
|
serializeScratchpad,
|
|
49
49
|
buildMemoryContext,
|
|
50
|
+
readMemoryFile,
|
|
50
51
|
scanSession,
|
|
51
52
|
isHousekeeping,
|
|
52
53
|
searchMemory,
|
|
54
|
+
resolveSessionsDir,
|
|
53
55
|
} from "./lib.ts";
|
|
54
56
|
|
|
55
57
|
const config = buildConfig();
|
|
@@ -66,7 +68,7 @@ function gitCommit(message: string) {
|
|
|
66
68
|
|
|
67
69
|
// --- Session scanner for "Last 24h" dashboard ---
|
|
68
70
|
|
|
69
|
-
const SESSIONS_DIR =
|
|
71
|
+
const SESSIONS_DIR = resolveSessionsDir();
|
|
70
72
|
const SUMMARY_CACHE = path.join(config.dailyDir, "cache.json");
|
|
71
73
|
const REBUILD_INTERVAL_MS = 15 * 60 * 1000;
|
|
72
74
|
const LOOKBACK_MS = 24 * 60 * 60 * 1000;
|
|
@@ -127,13 +129,13 @@ async function summarizeWithLLM(sessions: SessionInfo[], childCountMap: Map<stri
|
|
|
127
129
|
];
|
|
128
130
|
|
|
129
131
|
let model: any = null;
|
|
130
|
-
let
|
|
132
|
+
let auth: { ok: true; apiKey?: string; headers?: Record<string, string> } | undefined;
|
|
131
133
|
for (const candidate of candidates) {
|
|
132
134
|
if (!candidate) continue;
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
+
const result = await modelRegistryRef.getApiKeyAndHeaders(candidate);
|
|
136
|
+
if (result.ok) { model = candidate; auth = result; break; }
|
|
135
137
|
}
|
|
136
|
-
if (!model || !
|
|
138
|
+
if (!model || !auth) return "";
|
|
137
139
|
|
|
138
140
|
const listing = sessions.map((s, _i) => {
|
|
139
141
|
const childCount = childCountMap.get(s.file) || 0;
|
|
@@ -165,7 +167,7 @@ async function summarizeWithLLM(sessions: SessionInfo[], childCountMap: Map<stri
|
|
|
165
167
|
content: [{ type: "text" as const, text: `${sessions.length} sessions, $${totalCost.toFixed(2)} total cost:\n\n${listing}` }],
|
|
166
168
|
timestamp: Date.now(),
|
|
167
169
|
}],
|
|
168
|
-
}, { apiKey });
|
|
170
|
+
}, { apiKey: auth.apiKey, headers: auth.headers });
|
|
169
171
|
|
|
170
172
|
return response.content
|
|
171
173
|
.filter((c: any) => c.type === "text")
|
|
@@ -240,32 +242,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
242
|
const summary = await getOrRebuildSummary();
|
|
241
243
|
const scratchContent = readFileSafe(config.scratchpadFile);
|
|
242
244
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (summary) {
|
|
246
|
-
sections.push(summary);
|
|
247
|
-
}
|
|
248
|
-
|
|
245
|
+
// Parse scratchpad items
|
|
246
|
+
const openItems: string[] = [];
|
|
249
247
|
if (scratchContent?.trim()) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.map((l: string) => l.replace(/^- /, ""));
|
|
256
|
-
if (items.length > 0) {
|
|
257
|
-
sections.push(`## Scratchpad\n\n${items.join("\n")}`);
|
|
248
|
+
const lines = scratchContent.trim().split("\n");
|
|
249
|
+
for (const l of lines) {
|
|
250
|
+
if (l.match(/^- \[ \]/) && !l.match(/^<!--.*-->$/)) {
|
|
251
|
+
openItems.push(l.replace(/^- /, ""));
|
|
252
|
+
}
|
|
258
253
|
}
|
|
259
254
|
}
|
|
260
255
|
|
|
261
|
-
if (
|
|
256
|
+
if (!summary && openItems.length === 0) return;
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ctx.ui.setWidget("memory-dashboard", (_tui: any, _theme: any) => {
|
|
258
|
+
ctx.ui.setWidget("memory-dashboard", (_tui: any, theme: any) => {
|
|
266
259
|
const mdTheme = getMarkdownTheme();
|
|
267
|
-
const
|
|
268
|
-
|
|
260
|
+
const container = new Container();
|
|
261
|
+
let cachedWidth: number | undefined;
|
|
262
|
+
let cachedLines: string[] | undefined;
|
|
263
|
+
let lastExpanded: boolean | undefined;
|
|
264
|
+
|
|
265
|
+
const origRender = container.render.bind(container);
|
|
266
|
+
container.render = (width: number) => {
|
|
267
|
+
const expanded = ctx.ui.getToolsExpanded();
|
|
268
|
+
|
|
269
|
+
if (cachedLines && cachedWidth === width && lastExpanded === expanded) {
|
|
270
|
+
return cachedLines;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
container.clear();
|
|
274
|
+
|
|
275
|
+
if (expanded) {
|
|
276
|
+
if (summary) {
|
|
277
|
+
container.addChild(new Markdown(summary, 1, 0, mdTheme));
|
|
278
|
+
}
|
|
279
|
+
if (openItems.length > 0) {
|
|
280
|
+
if (summary) container.addChild(new Spacer(1));
|
|
281
|
+
const scratchMd = `## Scratchpad\n\n${openItems.join("\n")}`;
|
|
282
|
+
container.addChild(new Markdown(scratchMd, 1, 0, mdTheme));
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const parts: string[] = [];
|
|
286
|
+
if (summary) {
|
|
287
|
+
const costMatch = summary.match(/\$[\d.]+/);
|
|
288
|
+
const cost = costMatch ? costMatch[0] : "";
|
|
289
|
+
const sessMatch = summary.match(/(\d+) sessions/);
|
|
290
|
+
const sessions = sessMatch ? sessMatch[1] : "0";
|
|
291
|
+
parts.push(`Last 24h: ${cost}, ${sessions} log entries`);
|
|
292
|
+
}
|
|
293
|
+
if (openItems.length > 0) {
|
|
294
|
+
parts.push(`${openItems.length} scratchpad item${openItems.length > 1 ? "s" : ""}`);
|
|
295
|
+
}
|
|
296
|
+
const hint = keyHint("expandTools", "to expand");
|
|
297
|
+
const line = theme.fg("muted", parts.join(", ")) + " " + theme.fg("dim", `(${hint})`);
|
|
298
|
+
container.addChild(new Text(line, 1, 0));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
cachedLines = origRender(width);
|
|
302
|
+
cachedWidth = width;
|
|
303
|
+
lastExpanded = expanded;
|
|
304
|
+
return cachedLines;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const origInvalidate = container.invalidate.bind(container);
|
|
308
|
+
container.invalidate = () => {
|
|
309
|
+
cachedWidth = undefined;
|
|
310
|
+
cachedLines = undefined;
|
|
311
|
+
lastExpanded = undefined;
|
|
312
|
+
origInvalidate();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return container;
|
|
269
316
|
});
|
|
270
317
|
}
|
|
271
318
|
|
|
@@ -298,23 +345,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
298
345
|
if (rebuildTimer) { clearInterval(rebuildTimer); rebuildTimer = null; }
|
|
299
346
|
});
|
|
300
347
|
|
|
301
|
-
|
|
348
|
+
// Inject memory as a trailing user message, leaving the stable system prompt
|
|
349
|
+
// untouched for prompt caching. Pi 0.81 drops unsupported system-role entries.
|
|
350
|
+
pi.on("context", async (event) => {
|
|
302
351
|
const memoryContext = buildMemoryContext(config);
|
|
303
|
-
if (!memoryContext) return;
|
|
304
352
|
|
|
305
353
|
const memoryInstructions = [
|
|
306
|
-
"
|
|
354
|
+
"## Memory",
|
|
307
355
|
"The following memory files have been loaded. Use the memory_write tool to persist important information.",
|
|
308
356
|
"- Decisions, preferences, and durable facts \u2192 MEMORY.md",
|
|
309
357
|
"- Day-to-day notes and running context \u2192 daily/<YYYY-MM-DD>.md",
|
|
310
358
|
"- Things to fix later or keep in mind \u2192 scratchpad tool",
|
|
359
|
+
"- Scratchpad is NOT auto-loaded. Use memory_read(target='scratchpad') to fetch it when needed.",
|
|
311
360
|
'- If someone says "remember this," write it immediately.',
|
|
312
361
|
"",
|
|
362
|
+
"### Daily Log Rule",
|
|
363
|
+
"After meaningful interactions, call memory_write(target='daily') with a brief 1-2 sentence summary.",
|
|
364
|
+
"**Log when:** task completed, decision made, bug fixed, new info discovered, config changed.",
|
|
365
|
+
"**Skip when:** greetings, goodbyes, chitchat, simple acks, trivial factual questions.",
|
|
366
|
+
"Log the outcome, not the question (e.g. \"Debugged import error \u2014 missing __init__.py\" not \"User asked about imports\").",
|
|
367
|
+
"",
|
|
313
368
|
memoryContext,
|
|
314
369
|
].join("\n");
|
|
315
370
|
|
|
316
371
|
return {
|
|
317
|
-
|
|
372
|
+
messages: [
|
|
373
|
+
...event.messages,
|
|
374
|
+
{
|
|
375
|
+
role: "user" as const,
|
|
376
|
+
content: `<pi-mem-injected>\n${memoryInstructions}\n</pi-mem-injected>`,
|
|
377
|
+
timestamp: Date.now(),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
318
380
|
};
|
|
319
381
|
});
|
|
320
382
|
|
|
@@ -387,35 +449,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
387
449
|
}
|
|
388
450
|
|
|
389
451
|
if (target === "daily") {
|
|
390
|
-
const
|
|
452
|
+
const date = todayStr(config.timezone);
|
|
453
|
+
const filePath = dailyPath(config.dailyDir, date);
|
|
391
454
|
const existing = readFileSafe(filePath) ?? "";
|
|
392
455
|
|
|
393
|
-
const existingSnippet = existing.trim()
|
|
394
|
-
? `\n\nExisting daily log content:\n${existing.trim()}`
|
|
395
|
-
: "\n\nDaily log was empty.";
|
|
396
|
-
|
|
397
456
|
const separator = existing.trim() ? "\n\n" : "";
|
|
398
457
|
const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
|
|
399
458
|
fs.writeFileSync(filePath, existing + separator + stamped, "utf-8");
|
|
400
|
-
gitCommit(`daily: ${
|
|
459
|
+
gitCommit(`daily: ${date}`);
|
|
401
460
|
return {
|
|
402
|
-
content: [{ type: "text", text: `Appended to daily
|
|
461
|
+
content: [{ type: "text", text: `Appended to daily/${date}.md` }],
|
|
403
462
|
details: { path: filePath, target, mode: "append", sessionId: sid, timestamp: ts },
|
|
404
463
|
};
|
|
405
464
|
}
|
|
406
465
|
|
|
407
466
|
// long_term
|
|
408
467
|
const existing = readFileSafe(config.memoryFile) ?? "";
|
|
409
|
-
const existingSnippet = existing.trim()
|
|
410
|
-
? `\n\nExisting MEMORY.md content:\n${existing.trim()}`
|
|
411
|
-
: "\n\nMEMORY.md was empty.";
|
|
412
468
|
|
|
413
469
|
if (mode === "overwrite") {
|
|
414
470
|
const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
|
|
415
471
|
fs.writeFileSync(config.memoryFile, stamped, "utf-8");
|
|
416
472
|
gitCommit("memory: overwrite");
|
|
417
473
|
return {
|
|
418
|
-
content: [{ type: "text", text: `Overwrote MEMORY.md
|
|
474
|
+
content: [{ type: "text", text: `Overwrote MEMORY.md` }],
|
|
419
475
|
details: { path: config.memoryFile, target, mode: "overwrite", sessionId: sid, timestamp: ts },
|
|
420
476
|
};
|
|
421
477
|
}
|
|
@@ -425,7 +481,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
425
481
|
fs.writeFileSync(config.memoryFile, existing + separator + stamped, "utf-8");
|
|
426
482
|
gitCommit("memory: append");
|
|
427
483
|
return {
|
|
428
|
-
content: [{ type: "text", text: `Appended to MEMORY.md
|
|
484
|
+
content: [{ type: "text", text: `Appended to MEMORY.md` }],
|
|
429
485
|
details: { path: config.memoryFile, target, mode: "append", sessionId: sid, timestamp: ts },
|
|
430
486
|
};
|
|
431
487
|
},
|
|
@@ -536,7 +592,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
536
592
|
"- 'long_term': Read MEMORY.md",
|
|
537
593
|
"- 'scratchpad': Read SCRATCHPAD.md",
|
|
538
594
|
"- 'daily': Read a specific day's log (default: today). Pass date as YYYY-MM-DD.",
|
|
539
|
-
"- 'file': Read any file by
|
|
595
|
+
"- 'file': Read any file by exact path (e.g. 'SOUL.md', 'catchup/2026-04-26/file.md'). Pass filename. If the exact file is missing and the directory has an INDEX.md with <!-- file:... --> entries, I resolve by title/query within that index.",
|
|
540
596
|
"- 'note': Read a file from notes/ (e.g. 'lessons.md'). Pass filename.",
|
|
541
597
|
"- 'list': List all files in the memory directory.",
|
|
542
598
|
].join("\n"),
|
|
@@ -550,6 +606,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
550
606
|
filename: Type.Optional(
|
|
551
607
|
Type.String({ description: "Filename for 'file' target (e.g. 'lessons.md', 'SOUL.md')" }),
|
|
552
608
|
),
|
|
609
|
+
|
|
553
610
|
}),
|
|
554
611
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
555
612
|
ensureDirs(config);
|
|
@@ -569,6 +626,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
569
626
|
const dailyFiles = fs.readdirSync(config.dailyDir).filter(f => f.endsWith(".md")).sort().reverse();
|
|
570
627
|
if (dailyFiles.length > 0) sections.push(`Daily logs (${dailyFiles.length}):\n${dailyFiles.slice(0, 10).map(f => `- daily/${f}`).join("\n")}${dailyFiles.length > 10 ? `\n ... and ${dailyFiles.length - 10} more` : ""}`);
|
|
571
628
|
} catch {}
|
|
629
|
+
try {
|
|
630
|
+
const catchupDir = path.join(config.memoryDir, "catchup");
|
|
631
|
+
const catchupDates = fs.readdirSync(catchupDir).filter(f => {
|
|
632
|
+
try { return fs.statSync(path.join(catchupDir, f)).isDirectory(); } catch { return false; }
|
|
633
|
+
}).sort().reverse();
|
|
634
|
+
if (catchupDates.length > 0) sections.push(`Catchup (${catchupDates.length} dates):\n${catchupDates.slice(0, 10).map(d => `- catchup/${d}/`).join("\n")}${catchupDates.length > 10 ? `\n ... and ${catchupDates.length - 10} more` : ""}`);
|
|
635
|
+
} catch {}
|
|
572
636
|
if (sections.length === 0) {
|
|
573
637
|
return { content: [{ type: "text", text: "Memory directory is empty." }], details: {} };
|
|
574
638
|
}
|
|
@@ -579,13 +643,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
643
|
if (!filename) {
|
|
580
644
|
return { content: [{ type: "text", text: "Error: 'filename' is required for target 'file'." }], details: {} };
|
|
581
645
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
const content = readFileSafe(filePath);
|
|
585
|
-
if (!content) {
|
|
586
|
-
return { content: [{ type: "text", text: `File not found: ${safe}` }], details: {} };
|
|
587
|
-
}
|
|
588
|
-
return { content: [{ type: "text", text: content }], details: { path: filePath, filename: safe } };
|
|
646
|
+
const result = readMemoryFile(config, filename);
|
|
647
|
+
return { content: [{ type: "text", text: result.text }], details: result.details };
|
|
589
648
|
}
|
|
590
649
|
|
|
591
650
|
if (target === "note") {
|
|
@@ -602,7 +661,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
602
661
|
}
|
|
603
662
|
|
|
604
663
|
if (target === "daily") {
|
|
605
|
-
const d = date ?? todayStr();
|
|
664
|
+
const d = date ?? todayStr(config.timezone);
|
|
606
665
|
const filePath = dailyPath(config.dailyDir, d);
|
|
607
666
|
const content = readFileSafe(filePath);
|
|
608
667
|
if (!content) {
|