@aflueza/dsh-notes 0.0.0-stage → 0.2.1-rc.2

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,259 @@
1
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { NotesDatabase } from './database/notes-database.js';
4
+ import { LABEL_COLOR_KEYS, RPC_CHANNEL, } from './shared/notes.js';
5
+ export const name = 'dsh-notes';
6
+ export const inject = ['connection', 'webServer'];
7
+ export const Config = z.object({}).default({});
8
+ export function apply(ctx, config) {
9
+ ctx.inject(['connection', 'webServer'], (rpcCtx) => {
10
+ const webServer = rpcCtx.webServer;
11
+ const database = new NotesDatabase(dshHomePath('plugins', 'dsh-notes', 'dsh-notes.sqlite'));
12
+ rpcCtx.effect(() => {
13
+ const unregister = webServer.register({
14
+ kind: 'prefix',
15
+ path: RPC_CHANNEL,
16
+ handler: (request, response) => handleRpcRequest(rpcCtx.connection, request, response, database),
17
+ });
18
+ return () => {
19
+ unregister();
20
+ database.close();
21
+ };
22
+ }, 'dsh-notes: RPC route');
23
+ });
24
+ }
25
+ async function handleRpcRequest(connection, request, response, database) {
26
+ const rejection = connection.requestRejection(request);
27
+ if (rejection !== undefined) {
28
+ response.writeHead(rejection);
29
+ response.end(rejection === 401 ? 'unauthorized' : 'forbidden');
30
+ return;
31
+ }
32
+ const endpoint = endpointFromRequest(request);
33
+ if (request.method !== 'POST' || endpoint === null) {
34
+ response.writeHead(404);
35
+ response.end('not found');
36
+ return;
37
+ }
38
+ const mediaType = request.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase();
39
+ if (mediaType !== 'application/json') {
40
+ response.writeHead(415);
41
+ response.end('content type must be application/json');
42
+ return;
43
+ }
44
+ let message;
45
+ try {
46
+ message = parseClientRequest(await readJsonBody(request));
47
+ }
48
+ catch {
49
+ response.writeHead(400);
50
+ response.end('invalid rpc request');
51
+ return;
52
+ }
53
+ if (message.method !== endpoint) {
54
+ writeRpcResult(response, message.rpcId, fail('invalid-payload', `method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(endpoint)}`));
55
+ return;
56
+ }
57
+ writeRpcResult(response, message.rpcId, await dispatch(database, endpoint, message.payload));
58
+ }
59
+ function parseClientRequest(value) {
60
+ if (!isRecord(value))
61
+ throw new TypeError('request must be an object');
62
+ if (value.type !== 'client-request' ||
63
+ typeof value.rpcId !== 'string' ||
64
+ typeof value.method !== 'string' ||
65
+ !('payload' in value)) {
66
+ throw new TypeError('invalid client request envelope');
67
+ }
68
+ return {
69
+ type: 'client-request',
70
+ rpcId: value.rpcId,
71
+ method: value.method,
72
+ payload: value.payload,
73
+ };
74
+ }
75
+ function endpointFromRequest(request) {
76
+ const pathname = new URL(request.url ?? '/', 'http://x').pathname;
77
+ const prefix = `${RPC_CHANNEL}/`;
78
+ if (!pathname.startsWith(prefix))
79
+ return null;
80
+ const endpoint = pathname.slice(prefix.length);
81
+ return endpoint || null;
82
+ }
83
+ async function readJsonBody(request) {
84
+ const chunks = [];
85
+ let size = 0;
86
+ for await (const chunk of request) {
87
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
88
+ size += buffer.byteLength;
89
+ if (size > 1024 * 1024)
90
+ throw new RangeError('request body exceeds 1 MiB');
91
+ chunks.push(buffer);
92
+ }
93
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
94
+ }
95
+ function writeRpcResult(response, rpcId, result) {
96
+ response.writeHead(200, { 'content-type': 'application/json' });
97
+ response.end(JSON.stringify({ type: 'server-response', rpcId, result }));
98
+ }
99
+ async function dispatch(database, endpoint, payload) {
100
+ try {
101
+ switch (endpoint) {
102
+ case 'notes/index':
103
+ return ok(database.index());
104
+ case 'notes/list':
105
+ return ok(database.list(parseFilter(payload)));
106
+ case 'labels/map':
107
+ return ok(database.labelMap(parseLabelMapView(payload)));
108
+ case 'notes/get':
109
+ return ok(database.get(requiredString(payload, 'id')));
110
+ case 'notes/create':
111
+ return ok(await database.create(parseCreateNote(payload)));
112
+ case 'notes/update':
113
+ return ok(await database.update(parseUpdateNote(payload)));
114
+ case 'notes/trash':
115
+ return ok(await database.trash(requiredString(payload, 'id')));
116
+ case 'notes/restore':
117
+ return ok(await database.restore(requiredString(payload, 'id')));
118
+ case 'notes/trash/empty':
119
+ return ok(await database.emptyTrash());
120
+ case 'notebooks/create':
121
+ return ok(await database.createNotebook(parseCreateNotebook(payload)));
122
+ case 'notebooks/rename':
123
+ return ok(await database.renameNotebook(parseRenameNotebook(payload)));
124
+ case 'notebooks/delete':
125
+ return ok(await database.deleteNotebook(requiredString(payload, 'id')));
126
+ case 'labels/create':
127
+ return ok(await database.createLabel(parseLabel(payload)));
128
+ case 'labels/update':
129
+ return ok(await database.updateLabel({ id: requiredString(payload, 'id'), ...parseLabel(payload) }));
130
+ case 'labels/delete':
131
+ return ok(await database.deleteLabel(requiredString(payload, 'id')));
132
+ case 'notes/import-markdown':
133
+ return ok(await database.importMarkdown(parseMarkdownImport(payload)));
134
+ default:
135
+ return fail('unknown-endpoint', `unknown endpoint "${endpoint}"`);
136
+ }
137
+ }
138
+ catch (error) {
139
+ const known = isRecord(error) ? error : {};
140
+ const code = typeof known.code === 'string' ? known.code : 'internal';
141
+ const message = typeof known.message === 'string' ? known.message : String(error);
142
+ const details = isRecord(known.details) ? known.details : {};
143
+ return fail(code, message, details);
144
+ }
145
+ }
146
+ function ok(value) {
147
+ return { ok: true, value };
148
+ }
149
+ function fail(code, message, details) {
150
+ return { ok: false, error: { code, message, details: details ?? {} } };
151
+ }
152
+ function requiredString(payload, key) {
153
+ if (!isRecord(payload) || typeof payload[key] !== 'string' || !payload[key].trim()) {
154
+ throw new NotesInputError('invalid-payload', `${key} must be a non-empty string`);
155
+ }
156
+ return payload[key];
157
+ }
158
+ function parseFilter(payload) {
159
+ if (!isRecord(payload))
160
+ throw new NotesInputError('invalid-payload', 'filter must be an object');
161
+ const view = payload.view;
162
+ if (view !== 'all' && view !== 'pinned' && view !== 'archived' && view !== 'trash') {
163
+ throw new NotesInputError('invalid-payload', 'view is invalid');
164
+ }
165
+ return {
166
+ view: view,
167
+ query: typeof payload.query === 'string' ? payload.query : '',
168
+ notebookId: typeof payload.notebookId === 'string' ? payload.notebookId : undefined,
169
+ labelIds: Array.isArray(payload.labelIds) && payload.labelIds.every((id) => typeof id === 'string')
170
+ ? payload.labelIds
171
+ : [],
172
+ };
173
+ }
174
+ function parseLabelMapView(payload) {
175
+ if (!isRecord(payload))
176
+ throw new NotesInputError('invalid-payload', 'view must be an object');
177
+ const view = payload.view;
178
+ if (view !== 'all' && view !== 'pinned' && view !== 'archived' && view !== 'trash') {
179
+ throw new NotesInputError('invalid-payload', 'view is invalid');
180
+ }
181
+ return view;
182
+ }
183
+ function parseCreateNote(payload) {
184
+ if (payload === null || payload === undefined)
185
+ return {};
186
+ if (!isRecord(payload))
187
+ throw new NotesInputError('invalid-payload', 'note must be an object');
188
+ return {
189
+ notebookId: typeof payload.notebookId === 'string' ? payload.notebookId : undefined,
190
+ title: typeof payload.title === 'string' ? payload.title : '',
191
+ body: typeof payload.body === 'string' ? payload.body : '',
192
+ };
193
+ }
194
+ function parseUpdateNote(payload) {
195
+ if (!isRecord(payload))
196
+ throw new NotesInputError('invalid-payload', 'note must be an object');
197
+ const labelIds = payload.labelIds;
198
+ if (!Array.isArray(labelIds) || !labelIds.every((id) => typeof id === 'string')) {
199
+ throw new NotesInputError('invalid-payload', 'labelIds must be an array of strings');
200
+ }
201
+ if (!Number.isInteger(payload.revision) || payload.revision < 1) {
202
+ throw new NotesInputError('invalid-payload', 'revision must be a positive integer');
203
+ }
204
+ if (typeof payload.title !== 'string' || typeof payload.body !== 'string') {
205
+ throw new NotesInputError('invalid-payload', 'title and body must be strings');
206
+ }
207
+ if (typeof payload.isPinned !== 'boolean' || typeof payload.isArchived !== 'boolean') {
208
+ throw new NotesInputError('invalid-payload', 'pin and archive flags must be booleans');
209
+ }
210
+ return {
211
+ id: requiredString(payload, 'id'),
212
+ revision: payload.revision,
213
+ notebookId: requiredString(payload, 'notebookId'),
214
+ title: payload.title,
215
+ body: payload.body,
216
+ labelIds,
217
+ isPinned: payload.isPinned,
218
+ isArchived: payload.isArchived,
219
+ };
220
+ }
221
+ function parseLabel(payload) {
222
+ if (!isRecord(payload) || typeof payload.name !== 'string' || !payload.name.trim()) {
223
+ throw new NotesInputError('invalid-payload', 'label name is required');
224
+ }
225
+ if (!LABEL_COLOR_KEYS.includes(payload.colorKey)) {
226
+ throw new NotesInputError('invalid-payload', 'label colorKey is invalid');
227
+ }
228
+ return { name: payload.name, colorKey: payload.colorKey };
229
+ }
230
+ function parseCreateNotebook(payload) {
231
+ return { name: requiredString(payload, 'name') };
232
+ }
233
+ function parseRenameNotebook(payload) {
234
+ return { id: requiredString(payload, 'id'), name: requiredString(payload, 'name') };
235
+ }
236
+ function parseMarkdownImport(payload) {
237
+ if (!isRecord(payload) ||
238
+ typeof payload.filename !== 'string' ||
239
+ typeof payload.body !== 'string') {
240
+ throw new NotesInputError('invalid-payload', 'filename and body are required');
241
+ }
242
+ return {
243
+ filename: payload.filename,
244
+ body: payload.body,
245
+ notebookId: typeof payload.notebookId === 'string' ? payload.notebookId : undefined,
246
+ };
247
+ }
248
+ function isRecord(value) {
249
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
250
+ }
251
+ class NotesInputError extends Error {
252
+ code;
253
+ details;
254
+ constructor(code, message, details) {
255
+ super(message);
256
+ this.code = code;
257
+ this.details = details;
258
+ }
259
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@aflueza/dsh-notes",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1-rc.2",
4
+ "description": "A local Markdown notes plugin for DeepSeek Harness.",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.inline.js",
10
+ "./package.json": "./package.json",
11
+ "./cordis.patch.yml": "./cordis.patch.yml"
12
+ },
13
+ "files": [
14
+ "lib/index.js",
15
+ "lib/client.inline.js",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "package.json"
19
+ ],
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "platform": "web",
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-connection",
28
+ "@deepseek-ai/dsh-client-ui-slots",
29
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
30
+ "@deepseek-ai/dsh-client-ui-primitives",
31
+ "@deepseek-ai/dsh-client-ui-theme"
32
+ ]
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json && pnpm run build:client",
37
+ "build:client": "node scripts/clean-client-output.mjs && tsdown && node scripts/inline-css.mjs",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
39
+ "check": "pnpm run typecheck && pnpm run build && node --check lib/index.js && node --check lib/client.inline.js && node --test tests/*.spec.mjs"
40
+ },
41
+ "devDependencies": {
42
+ "@deepseek-ai/cordis": "4.0.5",
43
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.2",
44
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
45
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
46
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.1-rc.2",
47
+ "@deepseek-ai/dsh-client-ui-theme": "0.2.1-rc.2",
48
+ "@tsdown/css": "^0.23.5",
49
+ "@types/node": "^22.10.0",
50
+ "@types/react": "^18.2.0",
51
+ "react": "^18.2.0",
52
+ "tsdown": "^0.23.5",
53
+ "typescript": "^7.0.2"
54
+ },
55
+ "dependencies": {
56
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
57
+ "@deepseek-ai/schemastery": "3.18.5",
58
+ "monaco-editor": "^0.55.1"
59
+ },
60
+ "peerDependencies": {
61
+ "@deepseek-ai/cordis": "^4.0.5",
62
+ "@deepseek-ai/dsh-client-connection": ">=0.2.1-rc.2 <0.2.3-0",
63
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.1-rc.2 <0.2.3-0",
64
+ "@deepseek-ai/dsh-client-ui-sidebar-right": ">=0.2.1-rc.2 <0.2.3-0",
65
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.2 <0.2.3-0",
66
+ "@deepseek-ai/dsh-client-ui-theme": ">=0.2.1-rc.2 <0.2.3-0"
67
+ }
68
+ }