clawvault 2.3.0 → 2.4.0

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.
Files changed (55) hide show
  1. package/bin/clawvault.js +6 -0
  2. package/bin/register-core-commands.js +25 -2
  3. package/bin/register-tailscale-commands.js +106 -0
  4. package/bin/register-task-commands.js +18 -2
  5. package/bin/register-task-commands.test.js +24 -0
  6. package/dist/{chunk-SOTWYGH7.js → chunk-33GW63WK.js} +4 -35
  7. package/dist/chunk-4GBPTBFJ.js +628 -0
  8. package/dist/{chunk-GQVYQCY5.js → chunk-AHGUJG76.js} +3 -3
  9. package/dist/{chunk-GJEGPO7U.js → chunk-BI6SGGZP.js} +1 -1
  10. package/dist/chunk-CLE2HHNT.js +513 -0
  11. package/dist/{chunk-NGVAEFT2.js → chunk-DEFBIVQ3.js} +21 -0
  12. package/dist/{chunk-DPS7NYIU.js → chunk-DHJPXGC7.js} +2 -2
  13. package/dist/{chunk-2HM7ZI4X.js → chunk-FEFPBHH4.js} +287 -12
  14. package/dist/{chunk-K6XHCUFL.js → chunk-FHFUXL6G.js} +8 -1
  15. package/dist/{chunk-6BBTI7NV.js → chunk-GBIDDDSL.js} +2 -2
  16. package/dist/{chunk-VR5NE7PZ.js → chunk-HVTTYDCJ.js} +1 -1
  17. package/dist/chunk-IFTEGE4D.js +361 -0
  18. package/dist/{chunk-5WR6RRPX.js → chunk-JXY6T5R7.js} +2 -2
  19. package/dist/chunk-L3DJ36BZ.js +40 -0
  20. package/dist/{chunk-Z2XBWN7A.js → chunk-NAMFB7ZA.js} +2 -0
  21. package/dist/chunk-NZ4ZZNSR.js +373 -0
  22. package/dist/{chunk-OTQW3OMC.js → chunk-Q3WBH4P4.js} +97 -0
  23. package/dist/{chunk-MQUJNOHK.js → chunk-QALB2V3E.js} +1 -1
  24. package/dist/{chunk-PTSEIWXZ.js → chunk-SNEMCQP7.js} +13 -6
  25. package/dist/commands/archive.js +3 -3
  26. package/dist/commands/backlog.js +9 -2
  27. package/dist/commands/blocked.js +9 -2
  28. package/dist/commands/canvas.d.ts +11 -3
  29. package/dist/commands/canvas.js +1333 -25
  30. package/dist/commands/context.js +5 -4
  31. package/dist/commands/doctor.js +1 -1
  32. package/dist/commands/migrate-observations.js +3 -3
  33. package/dist/commands/observe.d.ts +1 -0
  34. package/dist/commands/observe.js +5 -4
  35. package/dist/commands/rebuild.js +5 -4
  36. package/dist/commands/reflect.js +5 -5
  37. package/dist/commands/replay.js +7 -6
  38. package/dist/commands/setup.d.ts +10 -2
  39. package/dist/commands/setup.js +1 -1
  40. package/dist/commands/sleep.js +7 -6
  41. package/dist/commands/status.js +1 -1
  42. package/dist/commands/tailscale.d.ts +52 -0
  43. package/dist/commands/tailscale.js +25 -0
  44. package/dist/commands/task.js +1 -1
  45. package/dist/commands/wake.js +4 -4
  46. package/dist/index.d.ts +9 -0
  47. package/dist/index.js +79 -15
  48. package/dist/lib/tailscale.d.ts +225 -0
  49. package/dist/lib/tailscale.js +49 -0
  50. package/dist/lib/task-utils.d.ts +13 -1
  51. package/dist/lib/task-utils.js +3 -1
  52. package/dist/lib/webdav.d.ts +109 -0
  53. package/dist/lib/webdav.js +34 -0
  54. package/package.json +2 -2
  55. package/dist/chunk-W463YRED.js +0 -97
@@ -0,0 +1,513 @@
1
+ // src/lib/webdav.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var WEBDAV_PREFIX = "/webdav";
5
+ var BLOCKED_PATHS = [
6
+ ".clawvault",
7
+ ".git",
8
+ ".obsidian",
9
+ "node_modules"
10
+ ];
11
+ var SUPPORTED_METHODS = ["GET", "PUT", "DELETE", "MKCOL", "PROPFIND", "OPTIONS", "HEAD", "MOVE", "COPY"];
12
+ function isPathSafe(requestPath, rootPath) {
13
+ if (requestPath.includes("..")) {
14
+ return false;
15
+ }
16
+ const normalizedPath = path.normalize(requestPath).replace(/^\/+/, "");
17
+ const fullPath = path.resolve(rootPath, normalizedPath);
18
+ const resolvedRoot = path.resolve(rootPath);
19
+ if (!fullPath.startsWith(resolvedRoot + path.sep) && fullPath !== resolvedRoot) {
20
+ return false;
21
+ }
22
+ const pathParts = normalizedPath.split(path.sep);
23
+ for (const part of pathParts) {
24
+ if (BLOCKED_PATHS.includes(part)) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ }
30
+ function resolveWebDAVPath(requestPath, rootPath) {
31
+ if (requestPath.includes("..")) {
32
+ return null;
33
+ }
34
+ const normalizedPath = path.normalize(requestPath).replace(/^\/+/, "");
35
+ const fullPath = path.resolve(rootPath, normalizedPath);
36
+ const resolvedRoot = path.resolve(rootPath);
37
+ if (!fullPath.startsWith(resolvedRoot + path.sep) && fullPath !== resolvedRoot) {
38
+ return null;
39
+ }
40
+ return fullPath;
41
+ }
42
+ function checkAuth(req, auth) {
43
+ if (!auth) {
44
+ return true;
45
+ }
46
+ const authHeader = req.headers.authorization;
47
+ if (!authHeader || !authHeader.startsWith("Basic ")) {
48
+ return false;
49
+ }
50
+ const base64Credentials = authHeader.slice(6);
51
+ const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
52
+ const [username, password] = credentials.split(":");
53
+ return username === auth.username && password === auth.password;
54
+ }
55
+ function escapeXml(str) {
56
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
57
+ }
58
+ function formatWebDAVDate(date) {
59
+ return date.toUTCString();
60
+ }
61
+ function generatePropfindEntry(href, stats, isCollection) {
62
+ const resourceType = isCollection ? "<D:resourcetype><D:collection/></D:resourcetype>" : "<D:resourcetype/>";
63
+ const contentLength = stats && !isCollection ? `<D:getcontentlength>${stats.size}</D:getcontentlength>` : "";
64
+ const lastModified = stats ? `<D:getlastmodified>${formatWebDAVDate(stats.mtime)}</D:getlastmodified>` : "";
65
+ const etag = stats ? `<D:getetag>"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"</D:getetag>` : "";
66
+ const contentType = !isCollection ? "<D:getcontenttype>application/octet-stream</D:getcontenttype>" : "";
67
+ return ` <D:response>
68
+ <D:href>${escapeXml(href)}</D:href>
69
+ <D:propstat>
70
+ <D:prop>
71
+ ${resourceType}
72
+ ${contentLength}
73
+ ${lastModified}
74
+ ${etag}
75
+ ${contentType}
76
+ </D:prop>
77
+ <D:status>HTTP/1.1 200 OK</D:status>
78
+ </D:propstat>
79
+ </D:response>`;
80
+ }
81
+ function generatePropfindResponse(entries) {
82
+ const responseEntries = entries.map(
83
+ (e) => generatePropfindEntry(e.href, e.stats, e.isCollection)
84
+ ).join("\n");
85
+ return `<?xml version="1.0" encoding="utf-8"?>
86
+ <D:multistatus xmlns:D="DAV:">
87
+ ${responseEntries}
88
+ </D:multistatus>`;
89
+ }
90
+ function handleOptions(res, prefix) {
91
+ res.writeHead(200, {
92
+ "Allow": SUPPORTED_METHODS.join(", "),
93
+ "DAV": "1, 2",
94
+ "Content-Length": "0",
95
+ "Access-Control-Allow-Origin": "*",
96
+ "Access-Control-Allow-Methods": SUPPORTED_METHODS.join(", "),
97
+ "Access-Control-Allow-Headers": "Content-Type, Depth, Destination, Overwrite, Authorization",
98
+ "MS-Author-Via": "DAV"
99
+ });
100
+ res.end();
101
+ }
102
+ function handleHead(res, filePath) {
103
+ try {
104
+ const stats = fs.statSync(filePath);
105
+ if (stats.isDirectory()) {
106
+ res.writeHead(200, {
107
+ "Content-Type": "httpd/unix-directory",
108
+ "Last-Modified": formatWebDAVDate(stats.mtime),
109
+ "ETag": `"${stats.mtime.getTime().toString(16)}"`,
110
+ "Access-Control-Allow-Origin": "*"
111
+ });
112
+ } else {
113
+ res.writeHead(200, {
114
+ "Content-Type": "application/octet-stream",
115
+ "Content-Length": stats.size.toString(),
116
+ "Last-Modified": formatWebDAVDate(stats.mtime),
117
+ "ETag": `"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"`,
118
+ "Access-Control-Allow-Origin": "*"
119
+ });
120
+ }
121
+ res.end();
122
+ } catch (err) {
123
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
124
+ res.end("Not Found");
125
+ }
126
+ }
127
+ function handleGet(res, filePath) {
128
+ try {
129
+ const stats = fs.statSync(filePath);
130
+ if (stats.isDirectory()) {
131
+ const entries = fs.readdirSync(filePath);
132
+ const listing = entries.join("\n");
133
+ res.writeHead(200, {
134
+ "Content-Type": "text/plain",
135
+ "Content-Length": Buffer.byteLength(listing).toString(),
136
+ "Access-Control-Allow-Origin": "*"
137
+ });
138
+ res.end(listing);
139
+ } else {
140
+ const content = fs.readFileSync(filePath);
141
+ res.writeHead(200, {
142
+ "Content-Type": "application/octet-stream",
143
+ "Content-Length": content.length.toString(),
144
+ "Last-Modified": formatWebDAVDate(stats.mtime),
145
+ "ETag": `"${stats.mtime.getTime().toString(16)}-${stats.size.toString(16)}"`,
146
+ "Access-Control-Allow-Origin": "*"
147
+ });
148
+ res.end(content);
149
+ }
150
+ } catch (err) {
151
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
152
+ res.end("Not Found");
153
+ }
154
+ }
155
+ function handlePut(res, filePath, body) {
156
+ try {
157
+ const exists = fs.existsSync(filePath);
158
+ const dir = path.dirname(filePath);
159
+ if (!fs.existsSync(dir)) {
160
+ fs.mkdirSync(dir, { recursive: true });
161
+ }
162
+ fs.writeFileSync(filePath, body);
163
+ const status = exists ? 204 : 201;
164
+ res.writeHead(status, {
165
+ "Content-Length": "0",
166
+ "Access-Control-Allow-Origin": "*"
167
+ });
168
+ res.end();
169
+ } catch (err) {
170
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
171
+ res.end(`Error: ${err}`);
172
+ }
173
+ }
174
+ function handleDelete(res, filePath) {
175
+ try {
176
+ if (!fs.existsSync(filePath)) {
177
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
178
+ res.end("Not Found");
179
+ return;
180
+ }
181
+ const stats = fs.statSync(filePath);
182
+ if (stats.isDirectory()) {
183
+ fs.rmSync(filePath, { recursive: true });
184
+ } else {
185
+ fs.unlinkSync(filePath);
186
+ }
187
+ res.writeHead(204, {
188
+ "Content-Length": "0",
189
+ "Access-Control-Allow-Origin": "*"
190
+ });
191
+ res.end();
192
+ } catch (err) {
193
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
194
+ res.end(`Error: ${err}`);
195
+ }
196
+ }
197
+ function handleMkcol(res, filePath) {
198
+ try {
199
+ if (fs.existsSync(filePath)) {
200
+ res.writeHead(405, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
201
+ res.end("Resource already exists");
202
+ return;
203
+ }
204
+ const parent = path.dirname(filePath);
205
+ if (!fs.existsSync(parent)) {
206
+ res.writeHead(409, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
207
+ res.end("Parent directory does not exist");
208
+ return;
209
+ }
210
+ fs.mkdirSync(filePath);
211
+ res.writeHead(201, {
212
+ "Content-Length": "0",
213
+ "Access-Control-Allow-Origin": "*"
214
+ });
215
+ res.end();
216
+ } catch (err) {
217
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
218
+ res.end(`Error: ${err}`);
219
+ }
220
+ }
221
+ function handlePropfind(res, filePath, webdavPath, prefix, depth) {
222
+ try {
223
+ if (!fs.existsSync(filePath)) {
224
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
225
+ res.end("Not Found");
226
+ return;
227
+ }
228
+ const stats = fs.statSync(filePath);
229
+ const entries = [];
230
+ const normalizedWebdavPath = webdavPath.startsWith("/") ? webdavPath : "/" + webdavPath;
231
+ const href = prefix + normalizedWebdavPath;
232
+ entries.push({
233
+ href: href.endsWith("/") || stats.isDirectory() ? href : href,
234
+ stats,
235
+ isCollection: stats.isDirectory()
236
+ });
237
+ if (stats.isDirectory() && depth !== "0") {
238
+ try {
239
+ const children = fs.readdirSync(filePath);
240
+ for (const child of children) {
241
+ if (BLOCKED_PATHS.includes(child)) {
242
+ continue;
243
+ }
244
+ const childPath = path.join(filePath, child);
245
+ const childWebdavPath = normalizedWebdavPath.endsWith("/") ? normalizedWebdavPath + child : normalizedWebdavPath + "/" + child;
246
+ try {
247
+ const childStats = fs.statSync(childPath);
248
+ entries.push({
249
+ href: prefix + childWebdavPath,
250
+ stats: childStats,
251
+ isCollection: childStats.isDirectory()
252
+ });
253
+ } catch {
254
+ }
255
+ }
256
+ } catch {
257
+ }
258
+ }
259
+ const xml = generatePropfindResponse(entries);
260
+ res.writeHead(207, {
261
+ "Content-Type": "application/xml; charset=utf-8",
262
+ "Content-Length": Buffer.byteLength(xml).toString(),
263
+ "Access-Control-Allow-Origin": "*"
264
+ });
265
+ res.end(xml);
266
+ } catch (err) {
267
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
268
+ res.end(`Error: ${err}`);
269
+ }
270
+ }
271
+ function handleMove(res, sourcePath, destinationPath, overwrite) {
272
+ try {
273
+ if (!fs.existsSync(sourcePath)) {
274
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
275
+ res.end("Source not found");
276
+ return;
277
+ }
278
+ if (!destinationPath) {
279
+ res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
280
+ res.end("Destination header required");
281
+ return;
282
+ }
283
+ const destExists = fs.existsSync(destinationPath);
284
+ if (destExists && !overwrite) {
285
+ res.writeHead(412, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
286
+ res.end("Destination exists and Overwrite is F");
287
+ return;
288
+ }
289
+ const destDir = path.dirname(destinationPath);
290
+ if (!fs.existsSync(destDir)) {
291
+ fs.mkdirSync(destDir, { recursive: true });
292
+ }
293
+ if (destExists) {
294
+ const destStats = fs.statSync(destinationPath);
295
+ if (destStats.isDirectory()) {
296
+ fs.rmSync(destinationPath, { recursive: true });
297
+ } else {
298
+ fs.unlinkSync(destinationPath);
299
+ }
300
+ }
301
+ fs.renameSync(sourcePath, destinationPath);
302
+ const status = destExists ? 204 : 201;
303
+ res.writeHead(status, {
304
+ "Content-Length": "0",
305
+ "Access-Control-Allow-Origin": "*"
306
+ });
307
+ res.end();
308
+ } catch (err) {
309
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
310
+ res.end(`Error: ${err}`);
311
+ }
312
+ }
313
+ function handleCopy(res, sourcePath, destinationPath, overwrite) {
314
+ try {
315
+ if (!fs.existsSync(sourcePath)) {
316
+ res.writeHead(404, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
317
+ res.end("Source not found");
318
+ return;
319
+ }
320
+ if (!destinationPath) {
321
+ res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
322
+ res.end("Destination header required");
323
+ return;
324
+ }
325
+ const destExists = fs.existsSync(destinationPath);
326
+ if (destExists && !overwrite) {
327
+ res.writeHead(412, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
328
+ res.end("Destination exists and Overwrite is F");
329
+ return;
330
+ }
331
+ const destDir = path.dirname(destinationPath);
332
+ if (!fs.existsSync(destDir)) {
333
+ fs.mkdirSync(destDir, { recursive: true });
334
+ }
335
+ const sourceStats = fs.statSync(sourcePath);
336
+ if (sourceStats.isDirectory()) {
337
+ copyDirRecursive(sourcePath, destinationPath);
338
+ } else {
339
+ fs.copyFileSync(sourcePath, destinationPath);
340
+ }
341
+ const status = destExists ? 204 : 201;
342
+ res.writeHead(status, {
343
+ "Content-Length": "0",
344
+ "Access-Control-Allow-Origin": "*"
345
+ });
346
+ res.end();
347
+ } catch (err) {
348
+ res.writeHead(500, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
349
+ res.end(`Error: ${err}`);
350
+ }
351
+ }
352
+ function copyDirRecursive(src, dest) {
353
+ if (!fs.existsSync(dest)) {
354
+ fs.mkdirSync(dest, { recursive: true });
355
+ }
356
+ const entries = fs.readdirSync(src, { withFileTypes: true });
357
+ for (const entry of entries) {
358
+ const srcPath = path.join(src, entry.name);
359
+ const destPath = path.join(dest, entry.name);
360
+ if (entry.isDirectory()) {
361
+ copyDirRecursive(srcPath, destPath);
362
+ } else {
363
+ fs.copyFileSync(srcPath, destPath);
364
+ }
365
+ }
366
+ }
367
+ function parseDestinationHeader(destinationHeader, prefix, rootPath) {
368
+ if (!destinationHeader) {
369
+ return null;
370
+ }
371
+ try {
372
+ let destPath;
373
+ if (destinationHeader.startsWith("http://") || destinationHeader.startsWith("https://")) {
374
+ const url = new URL(destinationHeader);
375
+ destPath = decodeURIComponent(url.pathname);
376
+ } else {
377
+ destPath = decodeURIComponent(destinationHeader);
378
+ }
379
+ if (destPath.startsWith(prefix)) {
380
+ destPath = destPath.slice(prefix.length);
381
+ }
382
+ return resolveWebDAVPath(destPath, rootPath);
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ function createWebDAVHandler(config) {
388
+ const { rootPath, prefix = WEBDAV_PREFIX, auth } = config;
389
+ return async (req, res) => {
390
+ const rawUrl = req.url || "/";
391
+ if (rawUrl.includes("..")) {
392
+ if (rawUrl.startsWith(prefix)) {
393
+ res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
394
+ res.end("Forbidden");
395
+ return true;
396
+ }
397
+ }
398
+ const url = new URL(rawUrl, `http://${req.headers.host || "localhost"}`);
399
+ const pathname = decodeURIComponent(url.pathname);
400
+ if (!pathname.startsWith(prefix)) {
401
+ return false;
402
+ }
403
+ let webdavPath = pathname.slice(prefix.length);
404
+ if (!webdavPath.startsWith("/")) {
405
+ webdavPath = "/" + webdavPath;
406
+ }
407
+ if (req.method === "OPTIONS") {
408
+ handleOptions(res, prefix);
409
+ return true;
410
+ }
411
+ if (!checkAuth(req, auth)) {
412
+ res.writeHead(401, {
413
+ "WWW-Authenticate": 'Basic realm="ClawVault WebDAV"',
414
+ "Content-Type": "text/plain",
415
+ "Access-Control-Allow-Origin": "*"
416
+ });
417
+ res.end("Unauthorized");
418
+ return true;
419
+ }
420
+ if (!isPathSafe(webdavPath, rootPath)) {
421
+ res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
422
+ res.end("Forbidden");
423
+ return true;
424
+ }
425
+ const filePath = resolveWebDAVPath(webdavPath, rootPath);
426
+ if (!filePath) {
427
+ res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
428
+ res.end("Forbidden");
429
+ return true;
430
+ }
431
+ const depth = req.headers.depth || "infinity";
432
+ const overwrite = req.headers.overwrite?.toUpperCase() !== "F";
433
+ const destinationHeader = req.headers.destination;
434
+ switch (req.method) {
435
+ case "HEAD":
436
+ handleHead(res, filePath);
437
+ return true;
438
+ case "GET":
439
+ handleGet(res, filePath);
440
+ return true;
441
+ case "PUT": {
442
+ const chunks = [];
443
+ for await (const chunk of req) {
444
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
445
+ }
446
+ const body = Buffer.concat(chunks);
447
+ handlePut(res, filePath, body);
448
+ return true;
449
+ }
450
+ case "DELETE":
451
+ handleDelete(res, filePath);
452
+ return true;
453
+ case "MKCOL":
454
+ handleMkcol(res, filePath);
455
+ return true;
456
+ case "PROPFIND":
457
+ handlePropfind(res, filePath, webdavPath, prefix, depth);
458
+ return true;
459
+ case "MOVE": {
460
+ const destPath = parseDestinationHeader(destinationHeader, prefix, rootPath);
461
+ if (destPath && destinationHeader) {
462
+ const destWebdavPath = destinationHeader.includes(prefix) ? destinationHeader.slice(destinationHeader.indexOf(prefix) + prefix.length) : destinationHeader;
463
+ if (!isPathSafe(destWebdavPath, rootPath)) {
464
+ res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
465
+ res.end("Forbidden");
466
+ return true;
467
+ }
468
+ }
469
+ handleMove(res, filePath, destPath, overwrite);
470
+ return true;
471
+ }
472
+ case "COPY": {
473
+ const destPath = parseDestinationHeader(destinationHeader, prefix, rootPath);
474
+ if (destPath && destinationHeader) {
475
+ const destWebdavPath = destinationHeader.includes(prefix) ? destinationHeader.slice(destinationHeader.indexOf(prefix) + prefix.length) : destinationHeader;
476
+ if (!isPathSafe(destWebdavPath, rootPath)) {
477
+ res.writeHead(403, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
478
+ res.end("Forbidden");
479
+ return true;
480
+ }
481
+ }
482
+ handleCopy(res, filePath, destPath, overwrite);
483
+ return true;
484
+ }
485
+ default:
486
+ res.writeHead(405, {
487
+ "Allow": SUPPORTED_METHODS.join(", "),
488
+ "Content-Type": "text/plain",
489
+ "Access-Control-Allow-Origin": "*"
490
+ });
491
+ res.end("Method Not Allowed");
492
+ return true;
493
+ }
494
+ };
495
+ }
496
+
497
+ export {
498
+ WEBDAV_PREFIX,
499
+ isPathSafe,
500
+ resolveWebDAVPath,
501
+ checkAuth,
502
+ generatePropfindResponse,
503
+ handleOptions,
504
+ handleHead,
505
+ handleGet,
506
+ handlePut,
507
+ handleDelete,
508
+ handleMkcol,
509
+ handlePropfind,
510
+ handleMove,
511
+ handleCopy,
512
+ createWebDAVHandler
513
+ };
@@ -147,6 +147,7 @@ function createTask(vaultPath, title, options = {}) {
147
147
  created: now,
148
148
  updated: now
149
149
  };
150
+ if (options.source) frontmatter.source = options.source;
150
151
  if (options.owner) frontmatter.owner = options.owner;
151
152
  if (options.project) frontmatter.project = options.project;
152
153
  if (options.priority) frontmatter.priority = options.priority;
@@ -264,6 +265,25 @@ ${options.content}
264
265
  path: backlogPath
265
266
  };
266
267
  }
268
+ function updateBacklogItem(vaultPath, slug, updates) {
269
+ const backlogItem = readBacklogItem(vaultPath, slug);
270
+ if (!backlogItem) {
271
+ throw new Error(`Backlog item not found: ${slug}`);
272
+ }
273
+ const newFrontmatter = {
274
+ ...backlogItem.frontmatter
275
+ };
276
+ if (updates.source !== void 0) newFrontmatter.source = updates.source;
277
+ if (updates.project !== void 0) newFrontmatter.project = updates.project;
278
+ if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
279
+ if (updates.lastSeen !== void 0) newFrontmatter.lastSeen = updates.lastSeen;
280
+ const fileContent = matter.stringify(backlogItem.content, newFrontmatter);
281
+ fs.writeFileSync(backlogItem.path, fileContent);
282
+ return {
283
+ ...backlogItem,
284
+ frontmatter: newFrontmatter
285
+ };
286
+ }
267
287
  function promoteBacklogItem(vaultPath, slug, options = {}) {
268
288
  const backlogItem = readBacklogItem(vaultPath, slug);
269
289
  if (!backlogItem) {
@@ -343,6 +363,7 @@ export {
343
363
  updateTask,
344
364
  completeTask,
345
365
  createBacklogItem,
366
+ updateBacklogItem,
346
367
  promoteBacklogItem,
347
368
  getBlockedTasks,
348
369
  getActiveTasks,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Observer
3
- } from "./chunk-2HM7ZI4X.js";
3
+ } from "./chunk-FEFPBHH4.js";
4
4
  import {
5
5
  resolveVaultPath
6
6
  } from "./chunk-MXSSG3QU.js";
@@ -8,7 +8,7 @@ import {
8
8
  getLegacyObservationPath,
9
9
  getObservationPath,
10
10
  listRawTranscriptFiles
11
- } from "./chunk-Z2XBWN7A.js";
11
+ } from "./chunk-NAMFB7ZA.js";
12
12
 
13
13
  // src/commands/rebuild.ts
14
14
  import * as fs from "fs";