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/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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Quick Note (dsh-quicknote)",
4
+ "description": "Capture a note, mark it done, keep the list on one page."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "随手记 (dsh-quicknote)",
4
+ "description": "记下一条笔记,标记完成,在一页里管理。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "dsh-quicknote",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }