toolcraft 0.0.2 → 0.0.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.
Files changed (85) hide show
  1. package/README.md +461 -58
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +768 -40
  5. package/dist/human-in-loop/approval-tasks.d.ts +31 -0
  6. package/dist/human-in-loop/approval-tasks.js +201 -0
  7. package/dist/human-in-loop/approvals-commands.d.ts +11 -0
  8. package/dist/human-in-loop/approvals-commands.js +191 -0
  9. package/dist/human-in-loop/config.d.ts +11 -0
  10. package/dist/human-in-loop/config.js +21 -0
  11. package/dist/human-in-loop/default-provider.d.ts +2 -0
  12. package/dist/human-in-loop/default-provider.js +26 -0
  13. package/dist/human-in-loop/gate.d.ts +4 -0
  14. package/dist/human-in-loop/gate.js +57 -0
  15. package/dist/human-in-loop/index.d.ts +7 -0
  16. package/dist/human-in-loop/index.js +4 -0
  17. package/dist/human-in-loop/runner.d.ts +3 -0
  18. package/dist/human-in-loop/runner.js +196 -0
  19. package/dist/human-in-loop/spawn.d.ts +3 -0
  20. package/dist/human-in-loop/spawn.js +16 -0
  21. package/dist/human-in-loop/state-machine.d.ts +4 -0
  22. package/dist/human-in-loop/state-machine.js +10 -0
  23. package/dist/human-in-loop/types.d.ts +41 -0
  24. package/dist/human-in-loop/types.js +13 -0
  25. package/dist/index.compile-check.js +24 -0
  26. package/dist/index.d.ts +32 -13
  27. package/dist/index.js +82 -17
  28. package/dist/json-schema-converter.d.ts +21 -0
  29. package/dist/json-schema-converter.js +432 -0
  30. package/dist/mcp-proxy.d.ts +8 -0
  31. package/dist/mcp-proxy.js +383 -0
  32. package/dist/mcp.compile-check.js +1 -0
  33. package/dist/mcp.d.ts +2 -0
  34. package/dist/mcp.js +103 -11
  35. package/dist/sdk.compile-check.js +77 -0
  36. package/dist/sdk.d.ts +14 -5
  37. package/dist/sdk.js +57 -6
  38. package/dist/user-error.d.ts +3 -0
  39. package/dist/user-error.js +6 -0
  40. package/node_modules/@poe-code/agent-human-in-loop/README.md +42 -0
  41. package/node_modules/@poe-code/agent-human-in-loop/dist/index.d.ts +5 -0
  42. package/node_modules/@poe-code/agent-human-in-loop/dist/index.js +3 -0
  43. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.d.ts +2 -0
  44. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +11 -0
  45. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.d.ts +4 -0
  46. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +40 -0
  47. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.d.ts +6 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +33 -0
  49. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.d.ts +4 -0
  50. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +4 -0
  51. package/node_modules/@poe-code/agent-human-in-loop/dist/types.d.ts +14 -0
  52. package/node_modules/@poe-code/agent-human-in-loop/dist/types.js +1 -0
  53. package/node_modules/@poe-code/agent-human-in-loop/package.json +25 -0
  54. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +6 -0
  55. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +175 -0
  56. package/node_modules/@poe-code/agent-mcp-config/dist/configs.d.ts +22 -0
  57. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +74 -0
  58. package/node_modules/@poe-code/agent-mcp-config/dist/index.d.ts +3 -0
  59. package/node_modules/@poe-code/agent-mcp-config/dist/index.js +2 -0
  60. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +31 -0
  61. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +87 -0
  62. package/node_modules/@poe-code/agent-mcp-config/dist/types.d.ts +25 -0
  63. package/node_modules/@poe-code/agent-mcp-config/dist/types.js +1 -0
  64. package/node_modules/@poe-code/agent-mcp-config/package.json +25 -0
  65. package/node_modules/@poe-code/task-list/README.md +114 -0
  66. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.d.ts +2 -0
  67. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +466 -0
  68. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +8 -0
  69. package/node_modules/@poe-code/task-list/dist/backends/utils.js +58 -0
  70. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.d.ts +2 -0
  71. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +444 -0
  72. package/node_modules/@poe-code/task-list/dist/index.d.ts +4 -0
  73. package/node_modules/@poe-code/task-list/dist/index.js +4 -0
  74. package/node_modules/@poe-code/task-list/dist/open.d.ts +3 -0
  75. package/node_modules/@poe-code/task-list/dist/open.js +34 -0
  76. package/node_modules/@poe-code/task-list/dist/schema/store.schema.json +32 -0
  77. package/node_modules/@poe-code/task-list/dist/schema/task.schema.json +33 -0
  78. package/node_modules/@poe-code/task-list/dist/state-machine.d.ts +16 -0
  79. package/node_modules/@poe-code/task-list/dist/state-machine.js +67 -0
  80. package/node_modules/@poe-code/task-list/dist/state.d.ts +29 -0
  81. package/node_modules/@poe-code/task-list/dist/state.js +61 -0
  82. package/node_modules/@poe-code/task-list/dist/types.d.ts +116 -0
  83. package/node_modules/@poe-code/task-list/dist/types.js +37 -0
  84. package/node_modules/@poe-code/task-list/package.json +26 -0
  85. package/package.json +22 -7
@@ -0,0 +1,466 @@
1
+ import path from "node:path";
2
+ import { acquireFileLock } from "@poe-code/file-lock";
3
+ import { parseDocument, stringify } from "yaml";
4
+ import taskSchema from "../schema/task.schema.json" with { type: "json" };
5
+ import { eventsFromState, findEvent } from "../state-machine.js";
6
+ import { resolveStateMachine } from "../state.js";
7
+ import { InvalidTransitionError, MalformedTaskError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
8
+ import { hasErrorCode, isRecord, sortStrings, sortTasks, statIfExists, validateTaskId, writeAtomically } from "./utils.js";
9
+ const ARCHIVE_DIRECTORY_NAME = "archive";
10
+ const MARKDOWN_EXTENSION = ".md";
11
+ const TASK_KIND = "task";
12
+ const TASK_VERSION = 1;
13
+ const TASK_SCHEMA_ID = taskSchema.$id;
14
+ const RESERVED_FRONTMATTER_KEYS = new Set([
15
+ "$schema",
16
+ "description",
17
+ "kind",
18
+ "name",
19
+ "state",
20
+ "version"
21
+ ]);
22
+ function validateListName(name) {
23
+ if (name.length === 0 ||
24
+ name === ARCHIVE_DIRECTORY_NAME ||
25
+ name.startsWith(".") ||
26
+ name.includes("/") ||
27
+ name.includes("\\") ||
28
+ name.includes("..")) {
29
+ throw new Error(`Invalid task list name "${name}".`);
30
+ }
31
+ return name;
32
+ }
33
+ function parseQualifiedId(qualifiedId) {
34
+ const separatorIndex = qualifiedId.indexOf("/");
35
+ if (separatorIndex <= 0 ||
36
+ separatorIndex !== qualifiedId.lastIndexOf("/") ||
37
+ separatorIndex === qualifiedId.length - 1) {
38
+ throw new Error(`Invalid qualified task id "${qualifiedId}".`);
39
+ }
40
+ return {
41
+ list: validateListName(qualifiedId.slice(0, separatorIndex)),
42
+ id: validateTaskId(qualifiedId.slice(separatorIndex + 1))
43
+ };
44
+ }
45
+ function listPath(rootPath, list) {
46
+ return path.join(rootPath, list);
47
+ }
48
+ function archiveDirectoryPath(rootPath, list) {
49
+ return path.join(listPath(rootPath, list), ARCHIVE_DIRECTORY_NAME);
50
+ }
51
+ function activeTaskPath(rootPath, list, id) {
52
+ return path.join(listPath(rootPath, list), `${id}${MARKDOWN_EXTENSION}`);
53
+ }
54
+ function archivedTaskPath(rootPath, list, id) {
55
+ return path.join(archiveDirectoryPath(rootPath, list), `${id}${MARKDOWN_EXTENSION}`);
56
+ }
57
+ function isMarkdownFile(entryName) {
58
+ return entryName.endsWith(MARKDOWN_EXTENSION);
59
+ }
60
+ function isHiddenEntry(entryName) {
61
+ return entryName.startsWith(".");
62
+ }
63
+ function isLockFile(entryName) {
64
+ return entryName.endsWith(".lock");
65
+ }
66
+ function malformedTask(filePath, field) {
67
+ return new MalformedTaskError(`Malformed task "${filePath}": invalid "${field}".`);
68
+ }
69
+ function stripTrailingCarriageReturn(line) {
70
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
71
+ }
72
+ function splitTaskDocument(content, filePath) {
73
+ const lines = content.split("\n");
74
+ if (lines.length === 0 || stripTrailingCarriageReturn(lines[0]) !== "---") {
75
+ throw malformedTask(filePath, "frontmatter");
76
+ }
77
+ let closingIndex = -1;
78
+ for (let index = 1; index < lines.length; index += 1) {
79
+ if (stripTrailingCarriageReturn(lines[index]) === "---") {
80
+ closingIndex = index;
81
+ break;
82
+ }
83
+ }
84
+ if (closingIndex === -1) {
85
+ throw malformedTask(filePath, "frontmatter");
86
+ }
87
+ const bodyLines = lines.slice(closingIndex + 1);
88
+ if (bodyLines.length > 0 && stripTrailingCarriageReturn(bodyLines[0]) === "") {
89
+ bodyLines.shift();
90
+ }
91
+ return {
92
+ frontmatter: lines.slice(1, closingIndex).join("\n"),
93
+ body: bodyLines.join("\n")
94
+ };
95
+ }
96
+ function readFrontmatter(frontmatterContent, filePath) {
97
+ const document = parseDocument(frontmatterContent);
98
+ if (document.errors.length > 0) {
99
+ throw malformedTask(filePath, "frontmatter");
100
+ }
101
+ const parsed = document.toJS();
102
+ if (!isRecord(parsed)) {
103
+ throw malformedTask(filePath, "frontmatter");
104
+ }
105
+ return parsed;
106
+ }
107
+ function assertValidTaskRecord(frontmatter, filePath, validStates) {
108
+ if ("$schema" in frontmatter && frontmatter.$schema !== TASK_SCHEMA_ID) {
109
+ throw malformedTask(filePath, "$schema");
110
+ }
111
+ if ("kind" in frontmatter && frontmatter.kind !== TASK_KIND) {
112
+ throw malformedTask(filePath, "kind");
113
+ }
114
+ if ("version" in frontmatter) {
115
+ if (typeof frontmatter.version !== "number" ||
116
+ !Number.isInteger(frontmatter.version) ||
117
+ frontmatter.version !== TASK_VERSION) {
118
+ throw malformedTask(filePath, "version");
119
+ }
120
+ }
121
+ if (typeof frontmatter.name !== "string" || frontmatter.name.length === 0) {
122
+ throw malformedTask(filePath, "name");
123
+ }
124
+ if (typeof frontmatter.state !== "string" || !validStates.has(frontmatter.state)) {
125
+ throw malformedTask(filePath, "state");
126
+ }
127
+ if ("description" in frontmatter && typeof frontmatter.description !== "string") {
128
+ throw malformedTask(filePath, "description");
129
+ }
130
+ }
131
+ function metadataFromFrontmatter(frontmatter) {
132
+ const metadata = {};
133
+ for (const [key, value] of Object.entries(frontmatter)) {
134
+ if (!RESERVED_FRONTMATTER_KEYS.has(key)) {
135
+ metadata[key] = value;
136
+ }
137
+ }
138
+ return metadata;
139
+ }
140
+ function createTask(list, id, frontmatter, body) {
141
+ return {
142
+ list,
143
+ id,
144
+ qualifiedId: `${list}/${id}`,
145
+ name: frontmatter.name,
146
+ state: frontmatter.state,
147
+ description: body,
148
+ metadata: metadataFromFrontmatter(frontmatter)
149
+ };
150
+ }
151
+ function serializeTaskDocument(frontmatter, description) {
152
+ return `---\n${stringify(frontmatter)}---\n\n${description}`;
153
+ }
154
+ async function readDirectoryNames(fs, directoryPath) {
155
+ try {
156
+ return sortStrings(await fs.readdir(directoryPath));
157
+ }
158
+ catch (error) {
159
+ if (hasErrorCode(error, "ENOENT")) {
160
+ return [];
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+ async function ensureRootPath(deps) {
166
+ if (deps.create) {
167
+ await deps.fs.mkdir(deps.path, { recursive: true });
168
+ return;
169
+ }
170
+ await deps.fs.stat(deps.path);
171
+ }
172
+ async function readTaskFile(fs, list, id, filePath, validStates) {
173
+ const content = await fs.readFile(filePath, "utf8");
174
+ const document = splitTaskDocument(content, filePath);
175
+ const frontmatter = readFrontmatter(document.frontmatter, filePath);
176
+ assertValidTaskRecord(frontmatter, filePath, validStates);
177
+ return {
178
+ path: filePath,
179
+ frontmatter,
180
+ task: createTask(list, id, frontmatter, document.body)
181
+ };
182
+ }
183
+ async function findTaskLocation(fs, rootPath, list, id) {
184
+ const activePath = activeTaskPath(rootPath, list, id);
185
+ const activeStat = await statIfExists(fs, activePath);
186
+ if (activeStat?.isFile()) {
187
+ return {
188
+ archived: false,
189
+ path: activePath
190
+ };
191
+ }
192
+ const archivedPath = archivedTaskPath(rootPath, list, id);
193
+ const archivedStat = await statIfExists(fs, archivedPath);
194
+ if (archivedStat?.isFile()) {
195
+ return {
196
+ archived: true,
197
+ path: archivedPath
198
+ };
199
+ }
200
+ return undefined;
201
+ }
202
+ async function readTaskAtLocation(fs, rootPath, list, id, validStates) {
203
+ const location = await findTaskLocation(fs, rootPath, list, id);
204
+ if (!location) {
205
+ throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
206
+ }
207
+ return readTaskFile(fs, list, id, location.path, validStates);
208
+ }
209
+ function createdFrontmatter(defaults, input, initialState) {
210
+ const frontmatter = {
211
+ $schema: TASK_SCHEMA_ID,
212
+ kind: TASK_KIND,
213
+ version: TASK_VERSION,
214
+ name: input.name,
215
+ state: initialState
216
+ };
217
+ for (const [key, value] of Object.entries(defaults.metadata)) {
218
+ if (!RESERVED_FRONTMATTER_KEYS.has(key)) {
219
+ frontmatter[key] = value;
220
+ }
221
+ }
222
+ for (const [key, value] of Object.entries(input.metadata ?? {})) {
223
+ if (!RESERVED_FRONTMATTER_KEYS.has(key)) {
224
+ frontmatter[key] = value;
225
+ }
226
+ }
227
+ return frontmatter;
228
+ }
229
+ function updatedFrontmatter(existingFrontmatter, task, patch) {
230
+ const nextFrontmatter = {
231
+ ...existingFrontmatter,
232
+ $schema: existingFrontmatter.$schema ?? TASK_SCHEMA_ID,
233
+ kind: existingFrontmatter.kind ?? TASK_KIND,
234
+ version: existingFrontmatter.version ?? TASK_VERSION,
235
+ name: patch.name ?? task.name,
236
+ state: task.state
237
+ };
238
+ for (const [key, value] of Object.entries(patch.metadata ?? {})) {
239
+ if (!RESERVED_FRONTMATTER_KEYS.has(key)) {
240
+ nextFrontmatter[key] = value;
241
+ }
242
+ }
243
+ return nextFrontmatter;
244
+ }
245
+ function transitionedFrontmatter(existingFrontmatter, task, to) {
246
+ return {
247
+ ...existingFrontmatter,
248
+ $schema: existingFrontmatter.$schema ?? TASK_SCHEMA_ID,
249
+ kind: existingFrontmatter.kind ?? TASK_KIND,
250
+ version: existingFrontmatter.version ?? TASK_VERSION,
251
+ name: task.name,
252
+ state: to
253
+ };
254
+ }
255
+ function firedFrontmatter(existingFrontmatter, task, to, metadataPatch) {
256
+ const nextFrontmatter = transitionedFrontmatter(existingFrontmatter, task, to);
257
+ for (const [key, value] of Object.entries(metadataPatch ?? {})) {
258
+ if (!RESERVED_FRONTMATTER_KEYS.has(key)) {
259
+ nextFrontmatter[key] = value;
260
+ }
261
+ }
262
+ return nextFrontmatter;
263
+ }
264
+ function assertCreateDoesNotSetState(input) {
265
+ if (Object.prototype.hasOwnProperty.call(input, "state")) {
266
+ throw new Error('Tasks.create() does not accept "state"; new tasks always start at stateMachine.initial.');
267
+ }
268
+ }
269
+ function assertUpdateDoesNotSetState(patch) {
270
+ if (Object.prototype.hasOwnProperty.call(patch, "state")) {
271
+ throw new Error('Tasks.update() does not accept "state"; use fire() to change task state.');
272
+ }
273
+ }
274
+ function createTasksView(deps, list) {
275
+ const listDirectoryPath = listPath(deps.path, list);
276
+ const stateMachine = resolveStateMachine(deps.stateMachine);
277
+ const validStates = new Set(stateMachine.states);
278
+ async function listFiles(directoryPath) {
279
+ const entries = await readDirectoryNames(deps.fs, directoryPath);
280
+ const tasks = [];
281
+ for (const entryName of entries) {
282
+ if (isHiddenEntry(entryName) || isLockFile(entryName) || !isMarkdownFile(entryName)) {
283
+ continue;
284
+ }
285
+ const entryPath = path.join(directoryPath, entryName);
286
+ const entryStat = await statIfExists(deps.fs, entryPath);
287
+ if (!entryStat?.isFile()) {
288
+ continue;
289
+ }
290
+ const id = entryName.slice(0, -MARKDOWN_EXTENSION.length);
291
+ tasks.push((await readTaskFile(deps.fs, list, id, entryPath, validStates)).task);
292
+ }
293
+ return sortTasks(tasks);
294
+ }
295
+ async function withTaskLock(id, action) {
296
+ validateTaskId(id);
297
+ const release = await acquireFileLock(activeTaskPath(deps.path, list, id), {
298
+ fs: deps.fs,
299
+ staleMs: deps.lockStaleMs,
300
+ retries: deps.lockRetries
301
+ });
302
+ try {
303
+ return await action();
304
+ }
305
+ finally {
306
+ await release();
307
+ }
308
+ }
309
+ async function getTaskFile(id) {
310
+ validateTaskId(id);
311
+ return readTaskAtLocation(deps.fs, deps.path, list, id, validStates);
312
+ }
313
+ function assertFireableTaskEvent(task, eventName) {
314
+ const event = findEvent(stateMachine, task.state, eventName);
315
+ if (event === undefined) {
316
+ throw new InvalidTransitionError({
317
+ task,
318
+ event: eventName,
319
+ to: stateMachine.events[eventName]?.to,
320
+ reason: `Cannot fire event "${eventName}" from task state "${task.state}".`
321
+ });
322
+ }
323
+ return event;
324
+ }
325
+ return {
326
+ name: list,
327
+ stateMachine,
328
+ async all(filter) {
329
+ const activeTasks = await listFiles(listDirectoryPath);
330
+ const archivedTasks = filter?.includeArchived
331
+ ? await listFiles(archiveDirectoryPath(deps.path, list))
332
+ : [];
333
+ const combinedTasks = [...activeTasks, ...archivedTasks];
334
+ if (filter?.state) {
335
+ return combinedTasks.filter((task) => task.state === filter.state);
336
+ }
337
+ return combinedTasks;
338
+ },
339
+ async get(id) {
340
+ return (await getTaskFile(id)).task;
341
+ },
342
+ async create(input) {
343
+ assertCreateDoesNotSetState(input);
344
+ validateTaskId(input.id);
345
+ await deps.fs.mkdir(listDirectoryPath, { recursive: true });
346
+ return withTaskLock(input.id, async () => {
347
+ const existing = await findTaskLocation(deps.fs, deps.path, list, input.id);
348
+ if (existing) {
349
+ throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
350
+ }
351
+ const targetPath = activeTaskPath(deps.path, list, input.id);
352
+ const frontmatter = createdFrontmatter(deps.defaults, input, stateMachine.initial);
353
+ const description = input.description ?? "";
354
+ await writeAtomically(deps.fs, targetPath, serializeTaskDocument(frontmatter, description));
355
+ return createTask(list, input.id, frontmatter, description);
356
+ });
357
+ },
358
+ async update(id, patch) {
359
+ assertUpdateDoesNotSetState(patch);
360
+ return withTaskLock(id, async () => {
361
+ const existing = await getTaskFile(id);
362
+ const nextFrontmatter = updatedFrontmatter(existing.frontmatter, existing.task, patch);
363
+ const description = patch.description ?? existing.task.description;
364
+ await writeAtomically(deps.fs, existing.path, serializeTaskDocument(nextFrontmatter, description));
365
+ return createTask(list, id, nextFrontmatter, description);
366
+ });
367
+ },
368
+ async fire(id, eventName, opts) {
369
+ return withTaskLock(id, async () => {
370
+ const existing = await getTaskFile(id);
371
+ const event = assertFireableTaskEvent(existing.task, eventName);
372
+ const guardResult = event.guard?.(existing.task) ?? true;
373
+ if (guardResult !== true) {
374
+ throw new InvalidTransitionError({
375
+ task: existing.task,
376
+ event: eventName,
377
+ to: event.to,
378
+ reason: guardResult
379
+ });
380
+ }
381
+ await event.onExit?.(existing.task);
382
+ const nextFrontmatter = firedFrontmatter(existing.frontmatter, existing.task, event.to, opts?.metadataPatch);
383
+ const nextTask = createTask(list, id, nextFrontmatter, existing.task.description);
384
+ const serializedTask = serializeTaskDocument(nextFrontmatter, existing.task.description);
385
+ if (event.to === "archived") {
386
+ const targetPath = archivedTaskPath(deps.path, list, id);
387
+ const archivedTargetExists = await statIfExists(deps.fs, targetPath);
388
+ if (archivedTargetExists?.isFile()) {
389
+ throw new TaskAlreadyExistsError(`Task "${list}/${id}" already exists in archive.`);
390
+ }
391
+ await writeAtomically(deps.fs, existing.path, serializedTask);
392
+ await deps.fs.mkdir(archiveDirectoryPath(deps.path, list), { recursive: true });
393
+ await deps.fs.rename(existing.path, targetPath);
394
+ await event.onEnter?.(nextTask);
395
+ return nextTask;
396
+ }
397
+ await writeAtomically(deps.fs, existing.path, serializedTask);
398
+ await event.onEnter?.(nextTask);
399
+ return nextTask;
400
+ });
401
+ },
402
+ async canFire(id, eventName) {
403
+ const task = (await getTaskFile(id)).task;
404
+ const event = findEvent(stateMachine, task.state, eventName);
405
+ if (event === undefined) {
406
+ return false;
407
+ }
408
+ return (event.guard?.(task) ?? true) === true;
409
+ },
410
+ async events(id) {
411
+ const task = (await getTaskFile(id)).task;
412
+ return eventsFromState(stateMachine, task.state);
413
+ },
414
+ async delete(id) {
415
+ await withTaskLock(id, async () => {
416
+ const location = await findTaskLocation(deps.fs, deps.path, list, id);
417
+ if (!location) {
418
+ throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
419
+ }
420
+ await deps.fs.unlink(location.path);
421
+ });
422
+ }
423
+ };
424
+ }
425
+ export async function markdownDirBackend(deps) {
426
+ await ensureRootPath(deps);
427
+ const list = (name) => {
428
+ const listName = validateListName(name);
429
+ return createTasksView(deps, listName);
430
+ };
431
+ const lists = async () => {
432
+ const entries = await readDirectoryNames(deps.fs, deps.path);
433
+ const result = [];
434
+ for (const entryName of entries) {
435
+ if (entryName === ARCHIVE_DIRECTORY_NAME ||
436
+ isHiddenEntry(entryName) ||
437
+ isLockFile(entryName)) {
438
+ continue;
439
+ }
440
+ const entryPath = path.join(deps.path, entryName);
441
+ const entryStat = await statIfExists(deps.fs, entryPath);
442
+ if (entryStat?.isDirectory()) {
443
+ result.push(entryName);
444
+ }
445
+ }
446
+ return sortStrings(result);
447
+ };
448
+ const allTasks = async (filter) => {
449
+ const allLists = await lists();
450
+ const tasks = [];
451
+ for (const taskListName of allLists) {
452
+ tasks.push(...(await list(taskListName).all(filter)));
453
+ }
454
+ return sortTasks(tasks);
455
+ };
456
+ const get = async (qualifiedId) => {
457
+ const { list: listName, id } = parseQualifiedId(qualifiedId);
458
+ return list(listName).get(id);
459
+ };
460
+ return {
461
+ list,
462
+ lists,
463
+ allTasks,
464
+ get
465
+ };
466
+ }
@@ -0,0 +1,8 @@
1
+ import type { Task, TaskListFs } from "../types.js";
2
+ export declare function hasErrorCode(error: unknown, code: string): boolean;
3
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
4
+ export declare function sortStrings(values: string[]): string[];
5
+ export declare function sortTasks(tasks: Task[]): Task[];
6
+ export declare function validateTaskId(id: string): string;
7
+ export declare function statIfExists(fs: TaskListFs, filePath: string): Promise<Awaited<ReturnType<TaskListFs["stat"]>> | undefined>;
8
+ export declare function writeAtomically(fs: TaskListFs, filePath: string, content: string): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ let tmpFileCounter = 0;
3
+ export function hasErrorCode(error, code) {
4
+ return (!!error &&
5
+ typeof error === "object" &&
6
+ "code" in error &&
7
+ error.code === code);
8
+ }
9
+ export function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ export function sortStrings(values) {
13
+ return [...values].sort((left, right) => left.localeCompare(right));
14
+ }
15
+ export function sortTasks(tasks) {
16
+ return [...tasks].sort((left, right) => left.qualifiedId.localeCompare(right.qualifiedId));
17
+ }
18
+ export function validateTaskId(id) {
19
+ if (id.length === 0 ||
20
+ id.startsWith(".") ||
21
+ id.includes("/") ||
22
+ id.includes("\\") ||
23
+ id.includes("..")) {
24
+ throw new Error(`Invalid task id "${id}".`);
25
+ }
26
+ return id;
27
+ }
28
+ export async function statIfExists(fs, filePath) {
29
+ try {
30
+ return await fs.stat(filePath);
31
+ }
32
+ catch (error) {
33
+ if (hasErrorCode(error, "ENOENT")) {
34
+ return undefined;
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ export async function writeAtomically(fs, filePath, content) {
40
+ const tempPath = `${filePath}.tmp-${process.pid}-${tmpFileCounter}`;
41
+ tmpFileCounter += 1;
42
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
43
+ try {
44
+ await fs.writeFile(tempPath, content, { encoding: "utf8", flag: "wx" });
45
+ await fs.rename(tempPath, filePath);
46
+ }
47
+ catch (error) {
48
+ try {
49
+ await fs.unlink(tempPath);
50
+ }
51
+ catch (unlinkError) {
52
+ if (!hasErrorCode(unlinkError, "ENOENT")) {
53
+ throw unlinkError;
54
+ }
55
+ }
56
+ throw error;
57
+ }
58
+ }
@@ -0,0 +1,2 @@
1
+ import { type BackendDeps, type TaskList } from "../types.js";
2
+ export declare function yamlFileBackend(deps: BackendDeps): Promise<TaskList>;