dsh-quicknote 0.0.0-stage → 0.1.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/AGENTS.md +28 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +11 -0
- package/README.md +68 -2
- package/README.zh.md +67 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +4 -0
- package/lib/client.js +1407 -0
- package/lib/index.js +351 -0
- package/llms.txt +32 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5
|
+
// src/shared/constants.ts
|
|
6
|
+
/** Package name; must equal package.json `name` (the client bundle id). */
|
|
7
|
+
const NAME = "dsh-quicknote";
|
|
8
|
+
/** Host JSON API used by the page. */
|
|
9
|
+
const API = "/api/quicknote";
|
|
10
|
+
// src/host/paths.ts
|
|
11
|
+
/** Keep the example's data separate from other plugins. */
|
|
12
|
+
function resolveNotesFile(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const dshHomePath = typeof ctx.get === "function" ? ctx.get("dshHomePath") : void 0;
|
|
15
|
+
if (typeof dshHomePath === "function") {
|
|
16
|
+
const dir = dshHomePath("quicknote");
|
|
17
|
+
if (typeof dir === "string" && dir !== "") return join(dir, "notes.json");
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
const home = (process.env.DSH_HOME ?? "").trim() || join(homedir(), ".dsh");
|
|
21
|
+
return join(home, "quicknote", "notes.json");
|
|
22
|
+
}
|
|
23
|
+
// src/host/routes.ts
|
|
24
|
+
var HttpError = class extends Error {
|
|
25
|
+
status;
|
|
26
|
+
constructor(status, message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function json(status, body) {
|
|
32
|
+
return new Response(`${JSON.stringify(body)}\n`, {
|
|
33
|
+
status,
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json; charset=utf-8",
|
|
36
|
+
"cache-control": "no-store"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function body(request) {
|
|
41
|
+
try {
|
|
42
|
+
const value = await request.json();
|
|
43
|
+
return value && typeof value === "object" ? value : {};
|
|
44
|
+
} catch {
|
|
45
|
+
throw new HttpError(400, "invalid-json");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function registerRoutes(ctx, store) {
|
|
49
|
+
ctx.inject(["connection"], (scope) => {
|
|
50
|
+
const registry = scope.get("connection")?.fetch;
|
|
51
|
+
if (typeof registry?.register !== "function") {
|
|
52
|
+
ctx.logger?.warn?.("dsh-quicknote: no fetch registry; the page will not load notes");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const wrap = (handler) => async (request) => {
|
|
56
|
+
try {
|
|
57
|
+
return await handler(request);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return json(error instanceof HttpError ? error.status : 500, {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: error instanceof HttpError ? error.message : String(error?.message ?? error)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const routes = [
|
|
66
|
+
{
|
|
67
|
+
path: `${API}/notes`,
|
|
68
|
+
methods: ["GET"],
|
|
69
|
+
fetch: wrap(async () => json(200, {
|
|
70
|
+
ok: true,
|
|
71
|
+
notes: await store.list()
|
|
72
|
+
}))
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
path: `${API}/notes/create`,
|
|
76
|
+
methods: ["POST"],
|
|
77
|
+
fetch: wrap(async (request) => {
|
|
78
|
+
const input = await body(request);
|
|
79
|
+
return json(200, {
|
|
80
|
+
ok: true,
|
|
81
|
+
note: await store.add(input.text, input.origin)
|
|
82
|
+
});
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: `${API}/notes/update`,
|
|
87
|
+
methods: ["POST"],
|
|
88
|
+
fetch: wrap(async (request) => {
|
|
89
|
+
const input = await body(request);
|
|
90
|
+
const note = await store.update(input.id, input.done === true);
|
|
91
|
+
return note ? json(200, {
|
|
92
|
+
ok: true,
|
|
93
|
+
note
|
|
94
|
+
}) : json(404, {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: "unknown-note"
|
|
97
|
+
});
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
path: `${API}/notes/delete`,
|
|
102
|
+
methods: ["POST"],
|
|
103
|
+
fetch: wrap(async (request) => json(200, {
|
|
104
|
+
ok: true,
|
|
105
|
+
note: await store.remove((await body(request)).id)
|
|
106
|
+
}))
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: `${API}/notes/restore`,
|
|
110
|
+
methods: ["POST"],
|
|
111
|
+
fetch: wrap(async (request) => {
|
|
112
|
+
try {
|
|
113
|
+
return json(200, {
|
|
114
|
+
ok: true,
|
|
115
|
+
note: await store.restore((await body(request)).note)
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (String(error?.message) === "invalid-note") throw new HttpError(400, "invalid-note");
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
for (const route of routes) scope.effect(() => registry.register({
|
|
125
|
+
path: route.path,
|
|
126
|
+
methods: route.methods,
|
|
127
|
+
requestBody: "buffered",
|
|
128
|
+
fetch: route.fetch
|
|
129
|
+
}), `dsh-quicknote: ${route.path}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// src/host/store.ts
|
|
133
|
+
const MAX_TEXT = 2e4;
|
|
134
|
+
function parseNote(value) {
|
|
135
|
+
if (value === null || typeof value !== "object") return null;
|
|
136
|
+
const raw = value;
|
|
137
|
+
if (typeof raw.id !== "string" || raw.id === "" || raw.id.length > 128) return null;
|
|
138
|
+
if (typeof raw.text !== "string") return null;
|
|
139
|
+
const text = raw.text.trim();
|
|
140
|
+
if (text === "" || text.length > MAX_TEXT) return null;
|
|
141
|
+
const createdAt = typeof raw.createdAt === "number" && Number.isFinite(raw.createdAt) ? raw.createdAt : Date.now();
|
|
142
|
+
const note = {
|
|
143
|
+
id: raw.id,
|
|
144
|
+
text,
|
|
145
|
+
done: raw.done === true,
|
|
146
|
+
createdAt
|
|
147
|
+
};
|
|
148
|
+
const origin = parseOrigin(raw.origin);
|
|
149
|
+
if (origin) note.origin = origin;
|
|
150
|
+
return note;
|
|
151
|
+
}
|
|
152
|
+
function parseOrigin(value) {
|
|
153
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
154
|
+
const sessionId = String(value.sessionId ?? "");
|
|
155
|
+
const title = String(value.title ?? "");
|
|
156
|
+
if (sessionId === "" && title === "") return void 0;
|
|
157
|
+
return {
|
|
158
|
+
sessionId,
|
|
159
|
+
title
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** A small durable store: one JSON file, atomic whole-file writes, newest first. */
|
|
163
|
+
function createStore(file) {
|
|
164
|
+
let cache = null;
|
|
165
|
+
let tail = Promise.resolve();
|
|
166
|
+
function serialized(run) {
|
|
167
|
+
const result = tail.then(run);
|
|
168
|
+
tail = result.then(() => void 0, () => void 0);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
async function load() {
|
|
172
|
+
if (cache) return cache;
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
175
|
+
cache = Array.isArray(parsed.notes) ? parsed.notes.filter((note) => typeof note === "object" && note !== null && typeof note.id === "string" && typeof note.text === "string" && note.text.trim() !== "") : [];
|
|
176
|
+
} catch {
|
|
177
|
+
cache = [];
|
|
178
|
+
}
|
|
179
|
+
return cache;
|
|
180
|
+
}
|
|
181
|
+
async function save(notes) {
|
|
182
|
+
await mkdir(dirname(file), { recursive: true });
|
|
183
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
184
|
+
await writeFile(temporary, `${JSON.stringify({ notes }, null, 2)}\n`, { mode: 384 });
|
|
185
|
+
await rename(temporary, file);
|
|
186
|
+
cache = notes;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
file,
|
|
190
|
+
list() {
|
|
191
|
+
return serialized(async () => [...await load()]);
|
|
192
|
+
},
|
|
193
|
+
add(text, origin) {
|
|
194
|
+
return serialized(async () => {
|
|
195
|
+
const value = String(text ?? "").trim();
|
|
196
|
+
if (value === "") throw new Error("empty-note");
|
|
197
|
+
if (value.length > MAX_TEXT) throw new Error("note-too-long");
|
|
198
|
+
const notes = await load();
|
|
199
|
+
const createdAt = Math.max(Date.now(), (notes[0]?.createdAt ?? 0) + 1);
|
|
200
|
+
const note = {
|
|
201
|
+
id: randomUUID(),
|
|
202
|
+
text: value,
|
|
203
|
+
done: false,
|
|
204
|
+
createdAt
|
|
205
|
+
};
|
|
206
|
+
const cleanOrigin = parseOrigin(origin);
|
|
207
|
+
if (cleanOrigin) note.origin = cleanOrigin;
|
|
208
|
+
await save([note, ...notes]);
|
|
209
|
+
return note;
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
update(id, done) {
|
|
213
|
+
return serialized(async () => {
|
|
214
|
+
const notes = await load();
|
|
215
|
+
const current = notes.find((note) => note.id === id);
|
|
216
|
+
if (!current) return null;
|
|
217
|
+
const updated = {
|
|
218
|
+
...current,
|
|
219
|
+
done
|
|
220
|
+
};
|
|
221
|
+
await save(notes.map((note) => note.id === id ? updated : note));
|
|
222
|
+
return updated;
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
remove(id) {
|
|
226
|
+
return serialized(async () => {
|
|
227
|
+
const notes = await load();
|
|
228
|
+
const removed = notes.find((note) => note.id === id) ?? null;
|
|
229
|
+
if (!removed) return null;
|
|
230
|
+
await save(notes.filter((note) => note.id !== id));
|
|
231
|
+
return removed;
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
restore(value) {
|
|
235
|
+
return serialized(async () => {
|
|
236
|
+
const note = parseNote(value);
|
|
237
|
+
if (!note) throw new Error("invalid-note");
|
|
238
|
+
const notes = await load();
|
|
239
|
+
const existing = notes.find((item) => item.id === note.id);
|
|
240
|
+
if (existing) return existing;
|
|
241
|
+
const index = notes.findIndex((item) => item.createdAt <= note.createdAt);
|
|
242
|
+
const next = [...notes];
|
|
243
|
+
next.splice(index === -1 ? notes.length : index, 0, note);
|
|
244
|
+
await save(next);
|
|
245
|
+
return note;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// src/host/tools.ts
|
|
251
|
+
function textOutput() {
|
|
252
|
+
return {
|
|
253
|
+
schema: { type: "string" },
|
|
254
|
+
render: (_args, value) => [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: String(value)
|
|
257
|
+
}]
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function describe(note) {
|
|
261
|
+
const text = note.text.length > 200 ? `${note.text.slice(0, 200)}…` : note.text;
|
|
262
|
+
return `- ${note.id} (${note.done ? "done" : "open"}): ${text}`;
|
|
263
|
+
}
|
|
264
|
+
function createTools(store) {
|
|
265
|
+
return [
|
|
266
|
+
{
|
|
267
|
+
name: "quicknote_add",
|
|
268
|
+
description: "Save one note in the user's Quick Note list. Use it when the user asks to remember or park something for later.",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
additionalProperties: false,
|
|
272
|
+
required: ["text"],
|
|
273
|
+
properties: { text: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "The note body."
|
|
276
|
+
} }
|
|
277
|
+
},
|
|
278
|
+
output: textOutput(),
|
|
279
|
+
async execute(args) {
|
|
280
|
+
const note = await store.add(args?.text);
|
|
281
|
+
return `Saved as ${note.id}.\n${describe(note)}`;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "quicknote_list",
|
|
286
|
+
description: "List notes in the user's Quick Note list, newest first.",
|
|
287
|
+
parameters: {
|
|
288
|
+
type: "object",
|
|
289
|
+
additionalProperties: false,
|
|
290
|
+
properties: {}
|
|
291
|
+
},
|
|
292
|
+
output: textOutput(),
|
|
293
|
+
async execute() {
|
|
294
|
+
const notes = await store.list();
|
|
295
|
+
return notes.length === 0 ? "The Quick Note list is empty." : `${notes.length} note${notes.length === 1 ? "" : "s"}:\n${notes.map(describe).join("\n")}`;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "quicknote_done",
|
|
300
|
+
description: "Mark one Quick Note as done, or reopen it with done=false.",
|
|
301
|
+
parameters: {
|
|
302
|
+
type: "object",
|
|
303
|
+
additionalProperties: false,
|
|
304
|
+
required: ["id"],
|
|
305
|
+
properties: {
|
|
306
|
+
id: { type: "string" },
|
|
307
|
+
done: { type: "boolean" }
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
output: textOutput(),
|
|
311
|
+
async execute(args) {
|
|
312
|
+
const note = await store.update(args?.id, args?.done === false ? false : true);
|
|
313
|
+
return note ? `Note ${note.id} is ${note.done ? "done" : "open"}.` : `No note with id "${args?.id}".`;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "quicknote_delete",
|
|
318
|
+
description: "Delete one Quick Note permanently. Only when the user explicitly asks.",
|
|
319
|
+
parameters: {
|
|
320
|
+
type: "object",
|
|
321
|
+
additionalProperties: false,
|
|
322
|
+
required: ["id"],
|
|
323
|
+
properties: { id: { type: "string" } }
|
|
324
|
+
},
|
|
325
|
+
output: textOutput(),
|
|
326
|
+
async execute(args) {
|
|
327
|
+
return await store.remove(args?.id) ? `Deleted ${args?.id}.` : `No note with id "${args?.id}".`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
// src/host/index.ts
|
|
333
|
+
/**
|
|
334
|
+
* Host half of the Quick Note example: one JSON file, four routes, four tools.
|
|
335
|
+
*
|
|
336
|
+
* @module dsh-quicknote/host
|
|
337
|
+
*/
|
|
338
|
+
const name = NAME;
|
|
339
|
+
function apply(ctx) {
|
|
340
|
+
const store = createStore(resolveNotesFile(ctx));
|
|
341
|
+
ctx.effect(() => {
|
|
342
|
+
store.list().catch((error) => {
|
|
343
|
+
ctx.logger?.warn?.(`${NAME}: could not read notes: ${String(error)}`);
|
|
344
|
+
});
|
|
345
|
+
}, `${NAME}: warm the note store`);
|
|
346
|
+
registerRoutes(ctx, store);
|
|
347
|
+
ctx.inject(["tools"], (scope) => {
|
|
348
|
+
for (const definition of createTools(store)) scope.effect(() => scope.tools.register(definition), `${NAME}: tool ${definition.name}`);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
export { apply, createStore, name, resolveNotesFile };
|
package/llms.txt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dsh-quicknote
|
|
2
|
+
|
|
3
|
+
> Capture a note from the DSH composer, keep it out of the way, and manage the list on one page.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
dsh plugin --profile <profile> add dsh-quicknote
|
|
8
|
+
|
|
9
|
+
## Verify
|
|
10
|
+
|
|
11
|
+
dsh --profile <profile> --dump-config | grep -A2 "id: dsh-quicknote"
|
|
12
|
+
|
|
13
|
+
## Develop
|
|
14
|
+
|
|
15
|
+
pnpm install
|
|
16
|
+
pnpm verify
|
|
17
|
+
|
|
18
|
+
## Read next
|
|
19
|
+
|
|
20
|
+
- README.md — user guide
|
|
21
|
+
- AGENTS.md — repository rules and invariants
|
|
22
|
+
- src/host/ — Host half
|
|
23
|
+
- src/client/ — browser half
|
|
24
|
+
- tests/ — both suites
|
|
25
|
+
|
|
26
|
+
## Contracts
|
|
27
|
+
|
|
28
|
+
- Package: dsh-quicknote
|
|
29
|
+
- Patch row: `dsh-quicknote`
|
|
30
|
+
- Client module id: `dsh-quicknote`
|
|
31
|
+
- Storage: `~/.dsh/quicknote/notes.json`
|
|
32
|
+
- Routes: `/api/quicknote/notes`, `/create`, `/update`, `/delete`, `/restore`
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quicknote",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Capture a note from the DSH composer, keep it out of the way, and manage the list on one page.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "dsh-kit contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"icon": "./icon.svg",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/index.js",
|
|
11
|
+
"./client": "./lib/client.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"icon.svg",
|
|
18
|
+
"locale/*.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.zh.md",
|
|
21
|
+
"AGENTS.md",
|
|
22
|
+
"llms.txt",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"PUBLISHING.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "oxlint src tests scripts",
|
|
31
|
+
"format": "oxfmt --check src tests scripts",
|
|
32
|
+
"format:write": "oxfmt src tests scripts",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"verify": "oxfmt --check src tests scripts && oxlint src tests scripts && tsc --noEmit && vitest run && tsdown && node scripts/check.mjs && npm pack --dry-run",
|
|
35
|
+
"prepack": "tsdown"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=24"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"manifestVersion": 1,
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"dsh": ">=0.2.1-rc.5"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.6.6",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
55
|
+
"jsdom": "^30.1.3",
|
|
56
|
+
"oxfmt": "^0.75.0",
|
|
57
|
+
"oxlint": "^1.90.0",
|
|
58
|
+
"react": "^19.3.1",
|
|
59
|
+
"tsdown": "^0.23.4",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^5.0.5"
|
|
62
|
+
},
|
|
63
|
+
"keywords": [
|
|
64
|
+
"dsh",
|
|
65
|
+
"dsh-plugin",
|
|
66
|
+
"deepseek-harness",
|
|
67
|
+
"quicknote",
|
|
68
|
+
"notes"
|
|
69
|
+
],
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/dsh-kit/dsh-quicknote.git"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://dsh-kit.github.io/dsh-quicknote/",
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/dsh-kit/dsh-quicknote/issues"
|
|
77
|
+
}
|
|
78
|
+
}
|