mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
examples: scratchpad notes persist to .zot/scratchpad-notes.jsonl
Notes are now project-local and survive zot restarts. The
extension reads its cwd from the hello_ack handshake, then:
- on /note appends one line of {"at":..,"text":..} JSONL
under <cwd>/.zot/scratchpad-notes.jsonl
- on /notes reads the in-memory cache (loaded on hello_ack)
- on /clear-notes truncates the file and clears the cache
- on read_notes (tool) returns the cached set
Single-writer assumption (one zot session per cwd at a time);
two concurrent zot processes writing to the same file would
interleave but JSONL line boundaries stay intact under POSIX
PIPE_BUF semantics. Good enough for an example.
Verified end-to-end: round 1 writes two notes via the slash
command, round 2 (fresh extension process, same cwd) loads
them and surfaces them via both /notes and the read_notes
tool.
This commit is contained in:
parent
619ed587cd
commit
83b64e2562
2 changed files with 86 additions and 6 deletions
|
|
@ -44,9 +44,17 @@ The model also has a `read_notes` tool. Ask it:
|
|||
|
||||
> "What did I tell you to remember?"
|
||||
|
||||
…and it will call the tool and tell you. The scratchpad is
|
||||
process-local: it lives only as long as the extension subprocess
|
||||
(i.e. the duration of one `zot` session).
|
||||
…and it will call the tool and tell you.
|
||||
|
||||
## Storage
|
||||
|
||||
Notes persist as JSONL at `<cwd>/.zot/scratchpad-notes.jsonl`. The
|
||||
file is created on first `/note` and survives zot restarts. Each line
|
||||
is one note: `{"at":"2026-04-19T13:00:00.000Z","text":"..."}`.
|
||||
|
||||
Scope is per-project: switching to a different cwd gives you a
|
||||
different scratchpad. Cross-project sharing isn't supported in this
|
||||
example (would just be a matter of changing the path constant).
|
||||
|
||||
## Why TypeScript here
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@
|
|||
|
||||
import { createInterface } from "node:readline";
|
||||
import { stderr, stdin, stdout } from "node:process";
|
||||
import {
|
||||
appendFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// ---- protocol types (a tiny subset of internal/extproto) ----
|
||||
|
||||
|
|
@ -87,14 +95,77 @@ function log(msg: string): void {
|
|||
}
|
||||
|
||||
// ---- the scratchpad state itself ----
|
||||
//
|
||||
// Notes persist as JSONL under <cwd>/.zot/scratchpad-notes.jsonl so
|
||||
// they survive zot restarts and stay scoped to the project. The path
|
||||
// is resolved once HelloAck arrives (which carries cwd); until then
|
||||
// notesPath is empty and reads/writes no-op safely.
|
||||
//
|
||||
// One note per line, format: {"at":"<iso>","text":"<body>"}
|
||||
// Append-only on /note; full rewrite on /clear-notes.
|
||||
//
|
||||
// Single-writer assumption: only one zot session per cwd at a time.
|
||||
// Concurrent writes from two zot instances would interleave but not
|
||||
// corrupt JSONL line boundaries on POSIX (writes ≤ PIPE_BUF are
|
||||
// atomic). Good enough for a demo.
|
||||
|
||||
const notes: Array<{ at: string; text: string }> = [];
|
||||
type Note = { at: string; text: string };
|
||||
|
||||
let notes: Note[] = [];
|
||||
let notesPath = "";
|
||||
|
||||
function setNotesPath(cwd: string): void {
|
||||
notesPath = join(cwd, ".zot", "scratchpad-notes.jsonl");
|
||||
loadNotes();
|
||||
}
|
||||
|
||||
function loadNotes(): void {
|
||||
notes = [];
|
||||
if (!notesPath || !existsSync(notesPath)) return;
|
||||
try {
|
||||
const raw = readFileSync(notesPath, "utf8");
|
||||
for (const line of raw.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Note;
|
||||
if (typeof parsed?.text === "string") notes.push(parsed);
|
||||
} catch {
|
||||
// skip malformed lines silently; the next /note will
|
||||
// append correctly anyway.
|
||||
}
|
||||
}
|
||||
log(`loaded ${notes.length} note(s) from ${notesPath}`);
|
||||
} catch (err) {
|
||||
log(`failed to read ${notesPath}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function appendNote(text: string): number {
|
||||
notes.push({ at: new Date().toISOString(), text });
|
||||
const note: Note = { at: new Date().toISOString(), text };
|
||||
notes.push(note);
|
||||
if (notesPath) {
|
||||
try {
|
||||
mkdirSync(dirname(notesPath), { recursive: true });
|
||||
appendFileSync(notesPath, JSON.stringify(note) + "\n", "utf8");
|
||||
} catch (err) {
|
||||
log(`failed to persist note to ${notesPath}: ${err}`);
|
||||
}
|
||||
}
|
||||
return notes.length;
|
||||
}
|
||||
|
||||
function clearNotes(): void {
|
||||
notes = [];
|
||||
if (notesPath) {
|
||||
try {
|
||||
writeFileSync(notesPath, "", "utf8");
|
||||
} catch (err) {
|
||||
log(`failed to clear ${notesPath}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderNotes(): string {
|
||||
if (notes.length === 0) return "(scratchpad is empty)";
|
||||
return notes
|
||||
|
|
@ -184,6 +255,7 @@ function handleHelloAck(ack: HelloAck): void {
|
|||
`connected to zot ${ack.zot_version} ` +
|
||||
`(${ack.provider}/${ack.model}, cwd=${ack.cwd})`,
|
||||
);
|
||||
if (ack.cwd) setNotesPath(ack.cwd);
|
||||
}
|
||||
|
||||
function handleCommand(frame: CommandInvoked): void {
|
||||
|
|
@ -221,7 +293,7 @@ function handleCommand(frame: CommandInvoked): void {
|
|||
}
|
||||
|
||||
case "clear-notes": {
|
||||
notes.length = 0;
|
||||
clearNotes();
|
||||
respond(frame.id, {
|
||||
type: "command_response",
|
||||
id: frame.id,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue