pi-posthorse 0.10.3 → 0.10.4
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 +7 -2
- package/index.ts +13 -3
- package/package.json +6 -6
- package/src/contracts.ts +3 -0
- package/src/note-discovery.ts +291 -0
- package/src/note-evidence.ts +179 -0
- package/src/note-guards.ts +203 -0
- package/src/note-inventory.ts +111 -0
- package/src/note-provenance.ts +299 -0
- package/src/note-scopes.ts +293 -0
- package/src/note-snapshots.ts +185 -0
- package/src/note-store.ts +278 -0
- package/src/note-tool-effects.ts +190 -0
- package/src/notes.ts +117 -133
- package/src/recovery-note-touches.ts +59 -106
- package/src/recovery-notes.ts +50 -69
- package/src/recovery.ts +5 -0
- package/src/retention.ts +3 -0
- package/src/rollover.ts +22 -5
- package/src/session-files.ts +7 -2
package/src/notes.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, isAbsolute, join, normalize, sep } from "node:path";
|
|
2
|
+
import { dirname } from "node:path";
|
|
4
3
|
import { setImmediate } from "node:timers/promises";
|
|
5
4
|
import {
|
|
6
5
|
formatSize,
|
|
@@ -12,13 +11,15 @@ import { Type } from "typebox";
|
|
|
12
11
|
import { toolCards } from "../ui.ts";
|
|
13
12
|
import {
|
|
14
13
|
MAX_PAGE_CHARS,
|
|
15
|
-
isRecord,
|
|
16
14
|
requireValue,
|
|
17
15
|
textResult,
|
|
18
16
|
type PolicyContext,
|
|
19
17
|
type TextResult,
|
|
20
18
|
} from "./contracts.ts";
|
|
21
19
|
import { isBinaryFile, notesDir, publishFile, readText } from "./files.ts";
|
|
20
|
+
import { notePath, scopeDirectory } from "./note-scopes.ts";
|
|
21
|
+
import type { NoteCall, NotesStore, SessionNotes } from "./note-store.ts";
|
|
22
|
+
import { NoteDiscovery, noteLabel } from "./note-discovery.ts";
|
|
22
23
|
import { excerpt, excerptAround } from "./message-text.ts";
|
|
23
24
|
import type { PageAccess } from "./paging.ts";
|
|
24
25
|
|
|
@@ -28,122 +29,42 @@ export type NotesParams = {
|
|
|
28
29
|
readonly content?: string;
|
|
29
30
|
readonly query?: string;
|
|
30
31
|
readonly offset?: number;
|
|
32
|
+
readonly scope?: string;
|
|
31
33
|
};
|
|
32
|
-
type NoteFile = { readonly path: string; readonly size: number; readonly mtimeMs: number };
|
|
33
34
|
type NoteHits = { readonly rows: string[]; readonly more: boolean; readonly skipped: number };
|
|
34
35
|
/** Paged operations take a fresh policy snapshot after their filesystem awaits. */
|
|
35
36
|
type NotesAccess = {
|
|
36
37
|
readonly host: () => PolicyContext;
|
|
37
38
|
readonly pages: PageAccess;
|
|
38
39
|
readonly signal: AbortSignal | undefined;
|
|
40
|
+
readonly call: NoteCall;
|
|
41
|
+
readonly discovery: NoteDiscovery;
|
|
39
42
|
};
|
|
40
43
|
/** Larger files are logs or evidence rather than notes; reading them would dominate a search. */
|
|
41
44
|
const SEARCH_FILE_BYTES = 1024 * 1024;
|
|
42
45
|
/** Overlapping reads cut a full search of a 212,000-file notes tree from 39 s to 14 s. */
|
|
43
46
|
const SEARCH_BATCH = 16;
|
|
44
|
-
/**
|
|
45
|
-
* Maximum explicit `stat` calls overlapped per batch. On a 212,751-note tree, 16, 64 and 256 took
|
|
46
|
-
* equal time within host noise. The walk enters one folder at a time, so this bounds our pending
|
|
47
|
-
* `stat` calls at any depth. On filesystems that report unknown entry types, Node 24's
|
|
48
|
-
* `readdir({ withFileTypes: true })` also issues its own `lstat` for every entry at once.
|
|
49
|
-
*/
|
|
50
|
-
const WALK_BATCH = 64;
|
|
51
47
|
/** Formatting all 212,751 rows of one listing at once stalled Pi for 156-163 ms. */
|
|
52
48
|
const FORMAT_BATCH = 10_000;
|
|
53
49
|
/** Search rescans up to the requested offset, so this also caps the matches one call keeps. */
|
|
54
50
|
const SEARCH_OFFSET_LIMIT = 1_000_000;
|
|
55
|
-
function safeJoin(dir: string, path: string): string {
|
|
56
|
-
const relative = normalize(path.replace(/^[/\\]+/, ""));
|
|
57
|
-
if (relative === ".." || relative.startsWith(`..${sep}`) || isAbsolute(relative)) {
|
|
58
|
-
throw new Error(`Invalid path "${path}": must stay inside the notes directory.`);
|
|
59
|
-
}
|
|
60
|
-
return join(dir, relative);
|
|
61
|
-
}
|
|
62
|
-
/** Like `statSync(path, { throwIfNoEntry: false })`: an entry deleted mid-walk is skipped. */
|
|
63
|
-
async function statIfPresent(path: string) {
|
|
64
|
-
try {
|
|
65
|
-
return await stat(path);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (isRecord(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** A folder's present entries in listing order. */
|
|
74
|
-
async function* folderEntries(directory: string, signal: AbortSignal | undefined) {
|
|
75
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
76
|
-
let start = 0;
|
|
77
|
-
while (start < entries.length) {
|
|
78
|
-
signal?.throwIfAborted();
|
|
79
|
-
const batch = entries.slice(start, start + WALK_BATCH);
|
|
80
|
-
const firstFolder = batch.findIndex(
|
|
81
|
-
(entry) => entry.isDirectory() || entry.isSymbolicLink(),
|
|
82
|
-
);
|
|
83
|
-
const paths = batch
|
|
84
|
-
.slice(0, firstFolder === -1 ? batch.length : firstFolder + 1)
|
|
85
|
-
.map((entry) => join(directory, entry.name));
|
|
86
|
-
// Stats overlap only up to the next entry that may be a folder.
|
|
87
|
-
const infos = await Promise.all(paths.map(statIfPresent));
|
|
88
|
-
for (const [index, path] of paths.entries()) {
|
|
89
|
-
start += 1;
|
|
90
|
-
const info = infos[index];
|
|
91
|
-
if (info !== undefined) {
|
|
92
|
-
yield { path, info };
|
|
93
|
-
}
|
|
94
|
-
if (info?.isDirectory() === true) {
|
|
95
|
-
// Stat later entries only after this folder's subtree is walked, discarding any
|
|
96
|
-
// already taken, so entries removed meanwhile are skipped.
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Files depth-first in directory-listing order, yielding between filesystem calls. */
|
|
103
|
-
async function walk(
|
|
104
|
-
directory: string,
|
|
105
|
-
signal: AbortSignal | undefined,
|
|
106
|
-
ancestors: readonly string[] = [],
|
|
107
|
-
): Promise<NoteFile[]> {
|
|
108
|
-
signal?.throwIfAborted();
|
|
109
|
-
const root = await realpath(directory);
|
|
110
|
-
if (ancestors.includes(root)) {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
const next = [...ancestors, root];
|
|
114
|
-
const files: NoteFile[] = [];
|
|
115
|
-
for await (const { path, info } of folderEntries(directory, signal)) {
|
|
116
|
-
if (info.isDirectory()) {
|
|
117
|
-
for (const file of await walk(path, signal, next)) {
|
|
118
|
-
files.push(file);
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
files.push({ path, size: info.size, mtimeMs: info.mtimeMs });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return files;
|
|
125
|
-
}
|
|
126
|
-
async function notesUnder(target: string, signal: AbortSignal | undefined): Promise<NoteFile[]> {
|
|
127
|
-
const info = await statIfPresent(target);
|
|
128
|
-
signal?.throwIfAborted();
|
|
129
|
-
if (info === undefined) {
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
const files = info.isDirectory()
|
|
133
|
-
? await walk(target, signal)
|
|
134
|
-
: [{ path: target, size: info.size, mtimeMs: info.mtimeMs }];
|
|
135
|
-
signal?.throwIfAborted();
|
|
136
|
-
return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
137
|
-
}
|
|
138
51
|
async function listNotes(
|
|
139
|
-
|
|
52
|
+
session: SessionNotes,
|
|
140
53
|
params: NotesParams,
|
|
141
54
|
access: NotesAccess,
|
|
142
55
|
): Promise<TextResult> {
|
|
143
56
|
const { host, pages, signal } = access;
|
|
144
57
|
const folder = params.path;
|
|
145
|
-
const
|
|
146
|
-
|
|
58
|
+
const inventory = await access.discovery.list(session, {
|
|
59
|
+
path: folder,
|
|
60
|
+
scope: params.scope,
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
const { files } = inventory;
|
|
64
|
+
const dir =
|
|
65
|
+
params.scope === undefined
|
|
66
|
+
? session.directory
|
|
67
|
+
: scopeDirectory(await session.selectScope(params.scope));
|
|
147
68
|
const rows: string[] = [];
|
|
148
69
|
for (let start = 0; start < files.length; start += FORMAT_BATCH) {
|
|
149
70
|
// Pi gets an event-loop turn before each batch of rows.
|
|
@@ -151,10 +72,11 @@ async function listNotes(
|
|
|
151
72
|
signal?.throwIfAborted();
|
|
152
73
|
for (const file of files.slice(start, start + FORMAT_BATCH)) {
|
|
153
74
|
rows.push(
|
|
154
|
-
`${file
|
|
75
|
+
`${noteLabel(file, session, inventory.labeled)} ${formatSize(file.size)} ${new Date(file.mtimeMs).toISOString().slice(0, 16)}Z`,
|
|
155
76
|
);
|
|
156
77
|
}
|
|
157
78
|
}
|
|
79
|
+
rows.push(...inventory.notices);
|
|
158
80
|
return pages.notes(host(), {
|
|
159
81
|
rows,
|
|
160
82
|
offset: params.offset ?? 0,
|
|
@@ -164,19 +86,20 @@ async function listNotes(
|
|
|
164
86
|
});
|
|
165
87
|
}
|
|
166
88
|
async function readNote(
|
|
167
|
-
|
|
89
|
+
session: SessionNotes,
|
|
168
90
|
params: NotesParams,
|
|
169
91
|
access: NotesAccess,
|
|
170
92
|
): Promise<TextResult> {
|
|
171
93
|
const { pages } = access;
|
|
172
94
|
const relative = requireValue(params.path, "path", params.op);
|
|
173
|
-
const
|
|
95
|
+
const target = await session.read(relative, { scope: params.scope, signal: access.signal });
|
|
96
|
+
const path = target.path;
|
|
174
97
|
const info = statSync(path, { throwIfNoEntry: false });
|
|
175
98
|
if (info === undefined) {
|
|
176
99
|
throw new Error(`No note at ${relative}. Use op "list" to see available notes.`);
|
|
177
100
|
}
|
|
178
101
|
if (info.isDirectory()) {
|
|
179
|
-
return listNotes(
|
|
102
|
+
return listNotes(session, { ...params, scope: scopeDirectory(target.address) }, access);
|
|
180
103
|
}
|
|
181
104
|
if (await isBinaryFile(path)) {
|
|
182
105
|
throw new Error(
|
|
@@ -188,7 +111,9 @@ async function readNote(
|
|
|
188
111
|
if (offset > 0 && offset >= text.length) {
|
|
189
112
|
throw new Error(`Offset ${offset} is past the end of ${relative} (${text.length} chars).`);
|
|
190
113
|
}
|
|
191
|
-
const header =
|
|
114
|
+
const header = target.writable
|
|
115
|
+
? `File: ${path}\n`
|
|
116
|
+
: `Read-only live reference: notes read ${JSON.stringify({ scope: scopeDirectory(target.address), path: target.address.path })}\n`;
|
|
192
117
|
const host = access.host();
|
|
193
118
|
const chars = pages.size(host, { offset }) - header.length;
|
|
194
119
|
if (chars <= 0) {
|
|
@@ -202,6 +127,7 @@ async function readNote(
|
|
|
202
127
|
end < text.length
|
|
203
128
|
? `\n[chars ${offset}-${end} of ${text.length}; continue with offset ${end}]`
|
|
204
129
|
: "";
|
|
130
|
+
session.record([{ ...target.address, kind: "read" }], access.call);
|
|
205
131
|
return pages.result(`${header}${text.slice(offset, end)}${more}`, [], {
|
|
206
132
|
kind: "note-read",
|
|
207
133
|
headerLength: header.length,
|
|
@@ -211,36 +137,64 @@ async function readNote(
|
|
|
211
137
|
});
|
|
212
138
|
}
|
|
213
139
|
async function writeNote(
|
|
214
|
-
|
|
140
|
+
session: SessionNotes,
|
|
215
141
|
params: NotesParams,
|
|
216
|
-
|
|
142
|
+
access: NotesAccess,
|
|
217
143
|
): Promise<TextResult> {
|
|
218
144
|
const relative = requireValue(params.path, "path", params.op);
|
|
219
145
|
const content = params.content;
|
|
220
146
|
if (content === undefined) {
|
|
221
147
|
throw new Error('"content" is required for op "write" (use "" to clear a note).');
|
|
222
148
|
}
|
|
223
|
-
const path =
|
|
224
|
-
|
|
225
|
-
|
|
149
|
+
const path = notePath(session.binding, relative);
|
|
150
|
+
await withFileMutationQueue(path, async () => {
|
|
151
|
+
await session.prepareWrite();
|
|
152
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
153
|
+
await publishFile(path, content, access.signal);
|
|
154
|
+
session.record(
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
root: session.binding.root,
|
|
158
|
+
scope: session.binding.scope,
|
|
159
|
+
path: relative,
|
|
160
|
+
kind: content === "" ? "clear" : "write",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
access.call,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
226
166
|
return textResult(`Wrote ${path}`, [], { kind: "note-write" });
|
|
227
167
|
}
|
|
228
168
|
async function appendNote(
|
|
229
|
-
|
|
169
|
+
session: SessionNotes,
|
|
230
170
|
params: NotesParams,
|
|
231
|
-
|
|
171
|
+
access: NotesAccess,
|
|
232
172
|
): Promise<TextResult> {
|
|
233
173
|
const relative = requireValue(params.path, "path", params.op);
|
|
234
174
|
const content = requireValue(params.content, "content", params.op);
|
|
235
|
-
const path =
|
|
236
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
175
|
+
const path = notePath(session.binding, relative);
|
|
237
176
|
await withFileMutationQueue(path, async () => {
|
|
238
|
-
signal?.throwIfAborted();
|
|
177
|
+
access.signal?.throwIfAborted();
|
|
178
|
+
await session.adopt(relative, access.call);
|
|
179
|
+
access.signal?.throwIfAborted();
|
|
180
|
+
await session.prepareWrite();
|
|
181
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
239
182
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
240
183
|
const separator = existing !== "" && !existing.endsWith("\n") ? "\n" : "";
|
|
241
184
|
// One O_APPEND write keeps records separate across processes. A racing
|
|
242
185
|
// unterminated write can at worst introduce an extra blank separator.
|
|
243
186
|
appendFileSync(path, `${separator}${content.replace(/\n?$/, "\n")}`);
|
|
187
|
+
session.record(
|
|
188
|
+
[
|
|
189
|
+
{
|
|
190
|
+
root: session.binding.root,
|
|
191
|
+
scope: session.binding.scope,
|
|
192
|
+
path: relative,
|
|
193
|
+
kind: "write",
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
access.call,
|
|
197
|
+
);
|
|
244
198
|
});
|
|
245
199
|
return textResult(`Appended to ${path}`, [], { kind: "note-append" });
|
|
246
200
|
}
|
|
@@ -259,12 +213,14 @@ function matchingLines(name: string, text: string, query: string): string[] {
|
|
|
259
213
|
}
|
|
260
214
|
/** Matches in newest-first note order, stopping once they pass `limit` characters. */
|
|
261
215
|
async function noteHits(
|
|
262
|
-
|
|
216
|
+
session: SessionNotes,
|
|
263
217
|
query: string,
|
|
264
|
-
limit: number,
|
|
265
|
-
|
|
218
|
+
options: { readonly limit: number; readonly scope: string | undefined },
|
|
219
|
+
access: NotesAccess,
|
|
266
220
|
): Promise<NoteHits> {
|
|
267
|
-
const
|
|
221
|
+
const { signal } = access;
|
|
222
|
+
const inventory = await access.discovery.list(session, { scope: options.scope, signal });
|
|
223
|
+
const notes = inventory.files;
|
|
268
224
|
const rows: string[] = [];
|
|
269
225
|
let length = -1;
|
|
270
226
|
let skipped = 0;
|
|
@@ -275,12 +231,12 @@ async function noteHits(
|
|
|
275
231
|
const batch = await Promise.all(
|
|
276
232
|
notes
|
|
277
233
|
.slice(start, start + SEARCH_BATCH)
|
|
278
|
-
.map(async (
|
|
279
|
-
size > SEARCH_FILE_BYTES
|
|
234
|
+
.map(async (note) =>
|
|
235
|
+
note.size > SEARCH_FILE_BYTES
|
|
280
236
|
? undefined
|
|
281
237
|
: matchingLines(
|
|
282
|
-
|
|
283
|
-
(await readText(path, signal)) ?? "",
|
|
238
|
+
noteLabel(note, session, inventory.labeled),
|
|
239
|
+
(await readText(note.path, signal)) ?? "",
|
|
284
240
|
query,
|
|
285
241
|
),
|
|
286
242
|
),
|
|
@@ -289,15 +245,15 @@ async function noteHits(
|
|
|
289
245
|
for (const row of batch.flatMap((found) => found ?? [])) {
|
|
290
246
|
rows.push(row);
|
|
291
247
|
length += row.length + 1;
|
|
292
|
-
if (length > limit) {
|
|
248
|
+
if (length > options.limit) {
|
|
293
249
|
return { rows, more: true, skipped };
|
|
294
250
|
}
|
|
295
251
|
}
|
|
296
252
|
}
|
|
297
|
-
return { rows, more: false, skipped };
|
|
253
|
+
return { rows: [...rows, ...inventory.notices], more: false, skipped };
|
|
298
254
|
}
|
|
299
255
|
async function searchNotes(
|
|
300
|
-
|
|
256
|
+
session: SessionNotes,
|
|
301
257
|
params: NotesParams,
|
|
302
258
|
access: NotesAccess,
|
|
303
259
|
): Promise<TextResult> {
|
|
@@ -310,7 +266,12 @@ async function searchNotes(
|
|
|
310
266
|
`Notes search pages end before offset ${SEARCH_OFFSET_LIMIT}; search for something more specific.`,
|
|
311
267
|
);
|
|
312
268
|
}
|
|
313
|
-
const hits = await noteHits(
|
|
269
|
+
const hits = await noteHits(
|
|
270
|
+
session,
|
|
271
|
+
query.toLowerCase(),
|
|
272
|
+
{ limit: offset + MAX_PAGE_CHARS, scope: params.scope },
|
|
273
|
+
access,
|
|
274
|
+
);
|
|
314
275
|
const skipped = hits.skipped === 1 ? "1 note" : `${hits.skipped} notes`;
|
|
315
276
|
return pages.notes(host(), {
|
|
316
277
|
rows: hits.rows,
|
|
@@ -325,14 +286,16 @@ export function registerNotes(
|
|
|
325
286
|
pi: ExtensionAPI,
|
|
326
287
|
policy: (ctx: ExtensionContext) => PolicyContext,
|
|
327
288
|
pages: PageAccess,
|
|
289
|
+
store: NotesStore,
|
|
328
290
|
): void {
|
|
291
|
+
const discovery = new NoteDiscovery();
|
|
329
292
|
pi.registerTool({
|
|
330
293
|
name: "notes",
|
|
331
294
|
label: "Notes",
|
|
332
295
|
...toolCards("notes"),
|
|
333
296
|
description:
|
|
334
|
-
"Durable notes
|
|
335
|
-
promptSnippet: "save and recall durable state
|
|
297
|
+
"Durable notes across Git worktrees and context resets. Forks own private copies of selected-branch notes. Ops: list (newest first, size/date; optional folder), read, search (case-insensitive text), write (replace; empty clears), append (one newline-terminated record). list/search discover project-wide notes with scope selectors; use scope for read-only comparisons or a single-scope view. Omit scope for current notes and live references. list/read/search are paged; continue with the returned offset. list shows the current writable directory; current-file reads show an editable path, foreign reads a read-only reference. Edit the returned current path for small changes. A blocked foreign edit may have created a private copy: read that copy before retrying. Append adopts unowned references without losing their text. Keep one concise current-state note and link fuller evidence.",
|
|
298
|
+
promptSnippet: "save and recall durable state with private notes for forks",
|
|
336
299
|
parameters: Type.Object({
|
|
337
300
|
op: Type.Union(
|
|
338
301
|
[
|
|
@@ -347,6 +310,12 @@ export function registerNotes(
|
|
|
347
310
|
path: Type.Optional(
|
|
348
311
|
Type.String({ description: "Note or folder path relative to the notes directory" }),
|
|
349
312
|
),
|
|
313
|
+
scope: Type.Optional(
|
|
314
|
+
Type.String({
|
|
315
|
+
description:
|
|
316
|
+
"Absolute scope selector from list/search; only for read/list/search. Omit for current notes and project-wide discovery.",
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
350
319
|
content: Type.Optional(
|
|
351
320
|
Type.String({ description: "Full file content (write) or text to add (append)" }),
|
|
352
321
|
),
|
|
@@ -363,19 +332,34 @@ export function registerNotes(
|
|
|
363
332
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
364
333
|
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
365
334
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
366
|
-
|
|
367
|
-
|
|
335
|
+
// Migrate newly returned legacy package notes without changing this session’s binding.
|
|
336
|
+
notesDir(ctx.cwd);
|
|
337
|
+
const session = await store.get(ctx);
|
|
338
|
+
if (params.scope !== undefined && (params.op === "write" || params.op === "append")) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
"scope is only valid for read/list/search; writes use this session’s notes scope.",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const access: NotesAccess = {
|
|
344
|
+
host: () => policy(ctx),
|
|
345
|
+
pages,
|
|
346
|
+
signal,
|
|
347
|
+
call: session.call(_id),
|
|
348
|
+
discovery,
|
|
349
|
+
};
|
|
368
350
|
switch (params.op) {
|
|
369
351
|
case "list":
|
|
370
|
-
return listNotes(
|
|
352
|
+
return listNotes(session, params, access);
|
|
371
353
|
case "read":
|
|
372
|
-
return
|
|
354
|
+
return params.path === "."
|
|
355
|
+
? listNotes(session, params, access)
|
|
356
|
+
: readNote(session, params, access);
|
|
373
357
|
case "write":
|
|
374
|
-
return writeNote(
|
|
358
|
+
return writeNote(session, params, access);
|
|
375
359
|
case "append":
|
|
376
|
-
return appendNote(
|
|
360
|
+
return appendNote(session, params, access);
|
|
377
361
|
case "search":
|
|
378
|
-
return searchNotes(
|
|
362
|
+
return searchNotes(session, params, access);
|
|
379
363
|
}
|
|
380
364
|
},
|
|
381
365
|
});
|