toolcraft 0.0.23 → 0.0.25

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 (152) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.compile-check.js +1 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +50 -13
  5. package/dist/error-report.js +32 -3
  6. package/dist/human-in-loop/approval-tasks.d.ts +1 -0
  7. package/dist/human-in-loop/approval-tasks.js +7 -5
  8. package/dist/human-in-loop/approvals-commands.js +51 -8
  9. package/dist/human-in-loop/runner.js +24 -19
  10. package/dist/human-in-loop/state-machine.d.ts +3 -3
  11. package/dist/human-in-loop/state-machine.js +13 -5
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.js +6 -1
  14. package/dist/mcp-proxy.js +85 -19
  15. package/dist/mcp.compile-check.js +1 -0
  16. package/dist/mcp.d.ts +1 -0
  17. package/dist/mcp.js +50 -8
  18. package/dist/renderer.js +119 -13
  19. package/dist/sdk.compile-check.js +1 -0
  20. package/dist/sdk.d.ts +1 -0
  21. package/dist/sdk.js +56 -11
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +1 -1
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +22 -11
  24. package/node_modules/@poe-code/agent-defs/package.json +1 -1
  25. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +5 -1
  26. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript.js +1 -1
  27. package/node_modules/@poe-code/agent-human-in-loop/package.json +1 -1
  28. package/node_modules/@poe-code/agent-mcp-config/dist/apply.d.ts +1 -1
  29. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +41 -92
  30. package/node_modules/@poe-code/agent-mcp-config/dist/configs.js +4 -1
  31. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.d.ts +14 -2
  32. package/node_modules/@poe-code/agent-mcp-config/dist/shapes.js +11 -4
  33. package/node_modules/@poe-code/agent-mcp-config/package.json +1 -1
  34. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +200 -22
  35. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +7 -1
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +1 -1
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +11 -7
  38. package/node_modules/@poe-code/config-mutations/dist/formats/object.d.ts +4 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/object.js +27 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +12 -9
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +12 -9
  42. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +11 -1
  43. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +10 -1
  44. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +25 -1
  45. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +12 -2
  46. package/node_modules/@poe-code/config-mutations/package.json +1 -1
  47. package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
  48. package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
  49. package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
  50. package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
  51. package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
  52. package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
  53. package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
  54. package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
  55. package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
  56. package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
  57. package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
  58. package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
  59. package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
  60. package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
  61. package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
  62. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
  63. package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
  64. package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
  65. package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
  66. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
  67. package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
  68. package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
  69. package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
  70. package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
  71. package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
  72. package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
  73. package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
  74. package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
  75. package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
  76. package/node_modules/@poe-code/design-system/dist/index.js +2 -1
  77. package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
  78. package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
  79. package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
  80. package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
  81. package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
  82. package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
  83. package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
  84. package/node_modules/@poe-code/design-system/package.json +2 -1
  85. package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
  86. package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
  87. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +377 -130
  88. package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +78 -10
  89. package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
  90. package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
  91. package/node_modules/@poe-code/process-runner/dist/host/host-execution-env.js +3 -2
  92. package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +21 -5
  93. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  94. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  95. package/node_modules/@poe-code/process-runner/dist/testing/mock-runner.js +30 -8
  96. package/node_modules/@poe-code/process-runner/dist/types.d.ts +6 -0
  97. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +61 -0
  98. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +503 -0
  99. package/node_modules/@poe-code/process-runner/package.json +1 -1
  100. package/node_modules/@poe-code/task-list/README.md +0 -2
  101. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-client.js +3 -0
  102. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +89 -59
  103. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.d.ts +9 -3
  104. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +460 -99
  105. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +156 -154
  106. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  107. package/node_modules/@poe-code/task-list/dist/backends/utils.js +79 -0
  108. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +120 -132
  109. package/node_modules/@poe-code/task-list/dist/index.d.ts +3 -1
  110. package/node_modules/@poe-code/task-list/dist/index.js +2 -0
  111. package/node_modules/@poe-code/task-list/dist/move.d.ts +2 -0
  112. package/node_modules/@poe-code/task-list/dist/move.js +215 -0
  113. package/node_modules/@poe-code/task-list/dist/open.js +3 -4
  114. package/node_modules/@poe-code/task-list/dist/state-machine.js +3 -1
  115. package/node_modules/@poe-code/task-list/dist/state.js +9 -0
  116. package/node_modules/@poe-code/task-list/dist/types.d.ts +48 -13
  117. package/node_modules/@poe-code/task-list/package.json +1 -2
  118. package/node_modules/auth-store/dist/create-secret-store.js +4 -1
  119. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +8 -0
  120. package/node_modules/auth-store/dist/encrypted-file-store.js +104 -8
  121. package/node_modules/auth-store/dist/index.d.ts +1 -1
  122. package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
  123. package/node_modules/auth-store/dist/keychain-store.js +18 -16
  124. package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
  125. package/node_modules/auth-store/dist/provider-store.js +55 -7
  126. package/node_modules/auth-store/dist/types.d.ts +3 -1
  127. package/node_modules/auth-store/package.json +2 -1
  128. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +46 -15
  129. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +49 -12
  130. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +6 -1
  131. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +1 -1
  132. package/node_modules/mcp-oauth/package.json +1 -0
  133. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +1 -1
  134. package/node_modules/tiny-mcp-client/dist/internal.d.ts +9 -4
  135. package/node_modules/tiny-mcp-client/dist/internal.js +244 -66
  136. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +1 -1
  137. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +4 -7
  138. package/node_modules/tiny-mcp-client/package.json +2 -1
  139. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +1 -1
  140. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +46 -0
  141. package/node_modules/tiny-mcp-client/src/internal.ts +287 -76
  142. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +1 -1
  144. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +5 -10
  145. package/node_modules/tiny-mcp-client/src/transports.test.ts +588 -6
  146. package/package.json +10 -12
  147. package/node_modules/@poe-code/file-lock/README.md +0 -52
  148. package/node_modules/@poe-code/file-lock/dist/index.d.ts +0 -1
  149. package/node_modules/@poe-code/file-lock/dist/index.js +0 -1
  150. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +0 -27
  151. package/node_modules/@poe-code/file-lock/dist/lock.js +0 -203
  152. package/node_modules/@poe-code/file-lock/package.json +0 -23
@@ -1,12 +1,11 @@
1
1
  import path from "node:path";
2
- import { acquireFileLock } from "@poe-code/file-lock";
3
2
  import { isMap, parseDocument } from "yaml";
4
3
  import storeSchema from "../schema/store.schema.json" with { type: "json" };
5
4
  import taskSchema from "../schema/task.schema.json" with { type: "json" };
6
5
  import { eventsFromState, findEvent } from "../state-machine.js";
7
6
  import { resolveStateMachine } from "../state.js";
8
7
  import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
9
- import { applyOrder, isRecord, sortStrings, statIfExists, validateTaskId, writeAtomically } from "./utils.js";
8
+ import { applyOrder, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, withFileLock, writeAtomically } from "./utils.js";
10
9
  const STORE_KIND = "task-store";
11
10
  const STORE_SCHEMA_ID = storeSchema.$id;
12
11
  const STORE_VERSION = 1;
@@ -54,7 +53,7 @@ function descriptionFromTaskRecord(taskRecord) {
54
53
  return typeof taskRecord.description === "string" ? taskRecord.description : "";
55
54
  }
56
55
  function metadataFromTaskRecord(taskRecord) {
57
- const metadata = {};
56
+ const metadata = Object.create(null);
58
57
  for (const [key, value] of Object.entries(taskRecord)) {
59
58
  if (!RESERVED_TASK_KEYS.has(key)) {
60
59
  metadata[key] = value;
@@ -84,11 +83,11 @@ function matchesFilter(task, filter) {
84
83
  return true;
85
84
  }
86
85
  function createTaskRecord(defaults, input, initialState) {
87
- const taskRecord = {
86
+ const taskRecord = Object.assign(Object.create(null), {
88
87
  name: input.name,
89
88
  state: initialState,
90
89
  description: input.description ?? ""
91
- };
90
+ });
92
91
  for (const [key, value] of Object.entries(defaults.metadata)) {
93
92
  if (!RESERVED_TASK_KEYS.has(key)) {
94
93
  taskRecord[key] = value;
@@ -118,12 +117,12 @@ function assertUpdateDoesNotSetState(patch) {
118
117
  }
119
118
  }
120
119
  function buildUpdatedTaskRecord(existing, patch) {
121
- const nextTaskRecord = {
120
+ const nextTaskRecord = Object.assign(Object.create(null), existing, {
122
121
  ...existing,
123
122
  name: patch.name ?? existing.name,
124
123
  state: existing.state,
125
124
  description: patch.description ?? descriptionFromTaskRecord(existing)
126
- };
125
+ });
127
126
  for (const [key, value] of Object.entries(patch.metadata ?? {})) {
128
127
  if (!RESERVED_TASK_KEYS.has(key)) {
129
128
  nextTaskRecord[key] = value;
@@ -132,11 +131,11 @@ function buildUpdatedTaskRecord(existing, patch) {
132
131
  return nextTaskRecord;
133
132
  }
134
133
  function buildTransitionedTaskRecord(existing, to) {
135
- return {
134
+ return Object.assign(Object.create(null), existing, {
136
135
  ...existing,
137
136
  state: to,
138
137
  description: descriptionFromTaskRecord(existing)
139
- };
138
+ });
140
139
  }
141
140
  function buildFiredTaskRecord(existing, to, metadataPatch) {
142
141
  const nextTaskRecord = buildTransitionedTaskRecord(existing, to);
@@ -170,7 +169,9 @@ function assertValidStoreRecord(store, filePath) {
170
169
  if (store.kind !== STORE_KIND) {
171
170
  throw malformedStore(filePath, "kind");
172
171
  }
173
- if (typeof store.version !== "number" || !Number.isInteger(store.version) || store.version !== STORE_VERSION) {
172
+ if (typeof store.version !== "number" ||
173
+ !Number.isInteger(store.version) ||
174
+ store.version !== STORE_VERSION) {
174
175
  throw malformedStore(filePath, "version");
175
176
  }
176
177
  if (!isRecord(store.lists)) {
@@ -254,7 +255,9 @@ function getListRecord(store, list) {
254
255
  }
255
256
  function getTaskRecord(store, list, id) {
256
257
  const listRecord = getListRecord(store, list);
257
- const taskRecord = listRecord?.[id];
258
+ const taskRecord = listRecord !== undefined && Object.prototype.hasOwnProperty.call(listRecord, id)
259
+ ? listRecord[id]
260
+ : undefined;
258
261
  return isRecord(taskRecord) ? taskRecord : undefined;
259
262
  }
260
263
  function getTaskOrThrow(store, list, id) {
@@ -280,7 +283,10 @@ function pairKey(pair) {
280
283
  if (typeof key === "string") {
281
284
  return key;
282
285
  }
283
- if (key && typeof key === "object" && "value" in key && typeof key.value === "string") {
286
+ if (key &&
287
+ typeof key === "object" &&
288
+ "value" in key &&
289
+ typeof key.value === "string") {
284
290
  return key.value;
285
291
  }
286
292
  return undefined;
@@ -296,7 +302,10 @@ function activeItemIds(listNode, validStates) {
296
302
  continue;
297
303
  const value = pair.value;
298
304
  let state;
299
- if (value && typeof value === "object" && "get" in value && typeof value.get === "function") {
305
+ if (value &&
306
+ typeof value === "object" &&
307
+ "get" in value &&
308
+ typeof value.get === "function") {
300
309
  state = value.get("state");
301
310
  }
302
311
  else if (isRecord(value)) {
@@ -309,6 +318,7 @@ function activeItemIds(listNode, validStates) {
309
318
  return ids;
310
319
  }
311
320
  async function ensureStorePath(deps) {
321
+ await rejectSymbolicLinkComponents(deps.fs, deps.path);
312
322
  if (!deps.create) {
313
323
  await deps.fs.stat(deps.path);
314
324
  return;
@@ -325,19 +335,6 @@ async function ensureStorePath(deps) {
325
335
  ""
326
336
  ].join("\n"))));
327
337
  }
328
- async function withStoreLock(deps, action) {
329
- const release = await acquireFileLock(deps.path, {
330
- fs: deps.fs,
331
- staleMs: deps.lockStaleMs,
332
- retries: deps.lockRetries
333
- });
334
- try {
335
- return await action();
336
- }
337
- finally {
338
- await release();
339
- }
340
- }
341
338
  function createTasksView(deps, list) {
342
339
  const stateMachine = resolveStateMachine(deps.stateMachine);
343
340
  const validStates = new Set(stateMachine.states);
@@ -382,42 +379,38 @@ function createTasksView(deps, list) {
382
379
  assertCreateDoesNotSetState(input);
383
380
  assertCreateHasId(input);
384
381
  validateTaskId(input.id);
385
- return withStoreLock(deps, async () => {
386
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
387
- if (getTaskRecord(store, list, input.id)) {
388
- throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
389
- }
390
- const taskRecord = createTaskRecord(deps.defaults, input, stateMachine.initial);
391
- document.setIn(["lists", list, input.id], taskRecord);
392
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
393
- return createTask(list, input.id, taskRecord, deps.path);
394
- });
382
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
383
+ if (getTaskRecord(store, list, input.id)) {
384
+ throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
385
+ }
386
+ const taskRecord = createTaskRecord(deps.defaults, input, stateMachine.initial);
387
+ document.setIn(["lists", list, input.id], taskRecord);
388
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
389
+ return createTask(list, input.id, taskRecord, deps.path);
395
390
  },
396
391
  async update(id, patch) {
397
392
  assertUpdateDoesNotSetState(patch);
398
393
  validateTaskId(id);
399
- return withStoreLock(deps, async () => {
400
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
401
- const existing = getTaskOrThrow(store, list, id);
402
- const nextTaskRecord = buildUpdatedTaskRecord(existing, patch);
403
- if (patch.name !== undefined) {
404
- document.setIn(["lists", list, id, "name"], patch.name);
405
- }
406
- if (patch.description !== undefined) {
407
- document.setIn(["lists", list, id, "description"], patch.description);
408
- }
409
- for (const [key, value] of Object.entries(patch.metadata ?? {})) {
410
- if (!RESERVED_TASK_KEYS.has(key)) {
411
- document.setIn(["lists", list, id, key], value);
412
- }
394
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
395
+ const existing = getTaskOrThrow(store, list, id);
396
+ const nextTaskRecord = buildUpdatedTaskRecord(existing, patch);
397
+ if (patch.name !== undefined) {
398
+ document.setIn(["lists", list, id, "name"], patch.name);
399
+ }
400
+ if (patch.description !== undefined) {
401
+ document.setIn(["lists", list, id, "description"], patch.description);
402
+ }
403
+ for (const [key, value] of Object.entries(patch.metadata ?? {})) {
404
+ if (!RESERVED_TASK_KEYS.has(key)) {
405
+ document.setIn(["lists", list, id, key], value);
413
406
  }
414
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
415
- return createTask(list, id, nextTaskRecord, deps.path);
416
- });
407
+ }
408
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
409
+ return createTask(list, id, nextTaskRecord, deps.path);
417
410
  },
418
411
  async fire(id, eventName, opts) {
419
412
  validateTaskId(id);
420
- return withStoreLock(deps, async () => {
413
+ const { event, nextTask } = await withFileLock(deps.fs, `${deps.path}.lock`, async () => {
421
414
  const { document, store } = await readStore(deps.fs, deps.path, validStates);
422
415
  const existing = getTaskOrThrow(store, list, id);
423
416
  const task = createTask(list, id, existing, deps.path);
@@ -440,10 +433,13 @@ function createTasksView(deps, list) {
440
433
  }
441
434
  }
442
435
  await writeAtomically(deps.fs, deps.path, serializeDocument(document));
443
- const nextTask = createTask(list, id, nextTaskRecord, deps.path);
444
- await event.onEnter?.(nextTask);
445
- return nextTask;
436
+ return {
437
+ event,
438
+ nextTask: createTask(list, id, nextTaskRecord, deps.path)
439
+ };
446
440
  });
441
+ await event.onEnter?.(nextTask);
442
+ return nextTask;
447
443
  },
448
444
  async canFire(id, eventName) {
449
445
  validateTaskId(id);
@@ -463,79 +459,73 @@ function createTasksView(deps, list) {
463
459
  },
464
460
  async delete(id) {
465
461
  validateTaskId(id);
466
- await withStoreLock(deps, async () => {
467
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
468
- getTaskOrThrow(store, list, id);
469
- document.deleteIn(["lists", list, id]);
470
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
471
- });
462
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
463
+ getTaskOrThrow(store, list, id);
464
+ document.deleteIn(["lists", list, id]);
465
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
472
466
  },
473
467
  async move(id, anchor) {
474
468
  validateTaskId(id);
475
- return withStoreLock(deps, async () => {
476
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
477
- const taskRecord = getTaskOrThrow(store, list, id);
478
- const listNode = getListNode(document, list);
479
- if (!listNode) {
480
- throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
481
- }
482
- const fromIndex = findItemIndex(listNode, id);
483
- if (fromIndex < 0) {
484
- throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
485
- }
486
- const [movedPair] = listNode.items.splice(fromIndex, 1);
487
- let insertIndex;
488
- if ("position" in anchor) {
489
- insertIndex = anchor.position === "top" ? 0 : listNode.items.length;
490
- }
491
- else {
492
- const anchorId = "before" in anchor ? anchor.before : anchor.after;
493
- const anchorIndex = findItemIndex(listNode, anchorId);
494
- if (anchorIndex < 0) {
495
- listNode.items.splice(fromIndex, 0, movedPair);
496
- throw new AnchorNotFoundError(anchorId);
497
- }
498
- insertIndex = "before" in anchor ? anchorIndex : anchorIndex + 1;
469
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
470
+ const taskRecord = getTaskOrThrow(store, list, id);
471
+ const listNode = getListNode(document, list);
472
+ if (!listNode) {
473
+ throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
474
+ }
475
+ const fromIndex = findItemIndex(listNode, id);
476
+ if (fromIndex < 0) {
477
+ throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
478
+ }
479
+ const [movedPair] = listNode.items.splice(fromIndex, 1);
480
+ let insertIndex;
481
+ if ("position" in anchor) {
482
+ insertIndex = anchor.position === "top" ? 0 : listNode.items.length;
483
+ }
484
+ else {
485
+ const anchorId = "before" in anchor ? anchor.before : anchor.after;
486
+ const anchorIndex = findItemIndex(listNode, anchorId);
487
+ if (anchorIndex < 0) {
488
+ listNode.items.splice(fromIndex, 0, movedPair);
489
+ throw new AnchorNotFoundError(anchorId);
499
490
  }
500
- listNode.items.splice(insertIndex, 0, movedPair);
501
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
502
- return createTask(list, id, taskRecord, deps.path);
503
- });
491
+ insertIndex = "before" in anchor ? anchorIndex : anchorIndex + 1;
492
+ }
493
+ listNode.items.splice(insertIndex, 0, movedPair);
494
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
495
+ return createTask(list, id, taskRecord, deps.path);
504
496
  },
505
497
  async reorder(ids) {
506
498
  for (const id of ids) {
507
499
  validateTaskId(id);
508
500
  }
509
- return withStoreLock(deps, async () => {
510
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
511
- const listNode = getListNode(document, list);
512
- if (!listNode) {
513
- throw new OrderMismatchError({ missing: [...ids], extra: [] });
514
- }
515
- const currentActive = activeItemIds(listNode, validStates);
516
- const currentSet = new Set(currentActive);
517
- const inputSet = new Set(ids);
518
- const missing = currentActive.filter((id) => !inputSet.has(id));
519
- const extra = ids.filter((id) => !currentSet.has(id));
520
- if (missing.length > 0 || extra.length > 0) {
521
- throw new OrderMismatchError({ missing, extra });
501
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
502
+ const listNode = getListNode(document, list);
503
+ if (!listNode) {
504
+ throw new OrderMismatchError({ missing: [...ids], extra: [] });
505
+ }
506
+ const currentActive = activeItemIds(listNode, validStates);
507
+ const currentSet = new Set(currentActive);
508
+ const inputSet = new Set(ids);
509
+ const missing = currentActive.filter((id) => !inputSet.has(id));
510
+ const extra = ids.filter((id) => !currentSet.has(id));
511
+ if (inputSet.size !== ids.length || missing.length > 0 || extra.length > 0) {
512
+ throw new OrderMismatchError({ missing, extra });
513
+ }
514
+ const archivedPairs = listNode.items.filter((pair) => {
515
+ const id = pairKey(pair);
516
+ return id !== undefined && !currentSet.has(id);
517
+ });
518
+ const orderedActive = ids.map((id) => {
519
+ const pair = listNode.items.find((p) => pairKey(p) === id);
520
+ if (!pair) {
521
+ throw new OrderMismatchError({ missing: [id], extra: [] });
522
522
  }
523
- const archivedPairs = listNode.items.filter((pair) => {
524
- const id = pairKey(pair);
525
- return id !== undefined && !currentSet.has(id);
526
- });
527
- const orderedActive = ids.map((id) => {
528
- const pair = listNode.items.find((p) => pairKey(p) === id);
529
- if (!pair) {
530
- throw new OrderMismatchError({ missing: [id], extra: [] });
531
- }
532
- return pair;
533
- });
534
- listNode.items.splice(0, listNode.items.length, ...orderedActive, ...archivedPairs);
535
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
536
- const tasks = ids.map((id) => createTask(list, id, getTaskOrThrow(store, list, id), deps.path));
537
- return tasks;
523
+ return pair;
538
524
  });
525
+ listNode.items.splice(0, listNode.items.length, ...orderedActive, ...archivedPairs);
526
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
527
+ const tasks = ids.map((id) => createTask(list, id, getTaskOrThrow(store, list, id), deps.path));
528
+ return tasks;
539
529
  }
540
530
  };
541
531
  }
@@ -576,20 +566,18 @@ export async function yamlFileBackend(deps) {
576
566
  const moveBetweenLists = async (qualifiedId, targetList) => {
577
567
  const { list: sourceListName, id } = parseQualifiedId(qualifiedId);
578
568
  const targetListName = validateListName(targetList);
579
- return withStoreLock(deps, async () => {
580
- const { document, store } = await readStore(deps.fs, deps.path, validStates);
581
- const taskRecord = getTaskOrThrow(store, sourceListName, id);
582
- if (sourceListName === targetListName) {
583
- return createTask(targetListName, id, taskRecord, deps.path);
584
- }
585
- if (getTaskRecord(store, targetListName, id)) {
586
- throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
587
- }
588
- document.deleteIn(["lists", sourceListName, id]);
589
- document.setIn(["lists", targetListName, id], taskRecord);
590
- await writeAtomically(deps.fs, deps.path, serializeDocument(document));
569
+ const { document, store } = await readStore(deps.fs, deps.path, validStates);
570
+ const taskRecord = getTaskOrThrow(store, sourceListName, id);
571
+ if (sourceListName === targetListName) {
591
572
  return createTask(targetListName, id, taskRecord, deps.path);
592
- });
573
+ }
574
+ if (getTaskRecord(store, targetListName, id)) {
575
+ throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
576
+ }
577
+ document.deleteIn(["lists", sourceListName, id]);
578
+ document.setIn(["lists", targetListName, id], taskRecord);
579
+ await writeAtomically(deps.fs, deps.path, serializeDocument(document));
580
+ return createTask(targetListName, id, taskRecord, deps.path);
593
581
  };
594
582
  return {
595
583
  list,
@@ -1,7 +1,9 @@
1
1
  export { openTaskList } from "./open.js";
2
+ export { moveTasks } from "./move.js";
2
3
  export { assertEvent, assertTransition, defaultStateMachine, type TaskEvent } from "./state.js";
3
4
  export { eventsFromState, findEvent, validateMachine, type EventDef, type StateMachineDef } from "./state-machine.js";
4
- export { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError, type ListFilter, type MoveAnchor, type OpenGhIssuesOptions, type OpenMarkdownDirOptions, type OpenTaskListOptions, type OpenYamlFileOptions, type Task, type TaskCreate, type TaskDefaults, type TaskFireOptions, type TaskList, type TaskListFs, type TaskOrder, type TaskState, type Tasks, type TaskUpdate } from "./types.js";
5
+ export { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError, type ListFilter, type MoveProgressEvent, type MoveResult, type MoveTasksOptions, type MoveAnchor, type OpenGhIssuesOptions, type OpenMarkdownDirOptions, type OpenTaskListOptions, type OpenYamlFileOptions, type Task, type TaskCreate, type TaskDefaults, type TaskFireOptions, type TaskList, type TaskListOptions, type TaskListFs, type TaskOrder, type TaskState, type Tasks, type TaskUpdate } from "./types.js";
6
+ export { resolveAuth } from "./backends/gh-issues-client.js";
5
7
  export type { GhClient, GhClientOptions, ResolveAuthOptions, ResolveEndpointOptions } from "./backends/gh-issues-client.js";
6
8
  export type { GhIssuesBackendDeps } from "./backends/gh-issues.js";
7
9
  export { GhProjectSyncError, syncGhProject, verifyGhProject, type SyncGhProjectOptions, type SyncGhProjectReport, type VerifyGhProjectOptions, type VerifyGhProjectReport } from "./backends/gh-issues-sync.js";
@@ -1,5 +1,7 @@
1
1
  export { openTaskList } from "./open.js";
2
+ export { moveTasks } from "./move.js";
2
3
  export { assertEvent, assertTransition, defaultStateMachine } from "./state.js";
3
4
  export { eventsFromState, findEvent, validateMachine } from "./state-machine.js";
4
5
  export { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "./types.js";
6
+ export { resolveAuth } from "./backends/gh-issues-client.js";
5
7
  export { GhProjectSyncError, syncGhProject, verifyGhProject } from "./backends/gh-issues-sync.js";
@@ -0,0 +1,2 @@
1
+ import type { MoveResult, MoveTasksOptions } from "./types.js";
2
+ export declare function moveTasks(options: MoveTasksOptions): Promise<MoveResult>;
@@ -0,0 +1,215 @@
1
+ import * as fsPromises from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { findEvent } from "./state-machine.js";
4
+ import { openTaskList } from "./open.js";
5
+ export async function moveTasks(options) {
6
+ const rate = options.rate ?? 15;
7
+ if (!Number.isFinite(rate) || rate <= 0) {
8
+ throw new RangeError("moveTasks rate must be a positive number.");
9
+ }
10
+ if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 0)) {
11
+ throw new RangeError("moveTasks limit must be a non-negative integer.");
12
+ }
13
+ const source = await openTaskList(readOnlySourceOptions(options.source));
14
+ const tasks = (await source.allTasks({ includeArchived: true })).slice(0, options.limit);
15
+ const result = { created: 0, skipped: 0, errors: [] };
16
+ if (tasks.length === 0) {
17
+ return result;
18
+ }
19
+ const target = await openTaskList(readOnlyTargetOptions(options.target, options.dryRun));
20
+ const targetLists = new Map();
21
+ const takeToken = createTokenBucket(rate);
22
+ for (const task of tasks) {
23
+ let targetTasks;
24
+ let targetState;
25
+ let createdTarget;
26
+ try {
27
+ targetTasks = await resolveTargetTasks(task, target, targetLists);
28
+ targetState = options.stateMap?.[task.state] ?? targetTasks.stateMachine.initial;
29
+ const transitionEvents = findTransitionEvents(targetTasks, targetTasks.stateMachine.initial, targetState);
30
+ if (targetTasks.stateMachine.initial !== targetState && transitionEvents === undefined) {
31
+ throw new Error(`Cannot migrate task state from "${targetTasks.stateMachine.initial}" to "${targetState}".`);
32
+ }
33
+ if (options.dryRun) {
34
+ result.skipped += 1;
35
+ emitProgress(options, {
36
+ type: "skipped",
37
+ id: task.id,
38
+ source: task,
39
+ targetList: targetTasks.name,
40
+ targetState,
41
+ reason: "dry-run"
42
+ });
43
+ continue;
44
+ }
45
+ await takeToken();
46
+ let created = await targetTasks.create({
47
+ id: task.id,
48
+ name: task.name,
49
+ description: task.description,
50
+ metadata: task.metadata
51
+ });
52
+ createdTarget = created;
53
+ created = await applyState(targetTasks, created, transitionEvents ?? []);
54
+ if (options.deleteSource === true) {
55
+ await source.list(task.list).delete(task.id);
56
+ }
57
+ result.created += 1;
58
+ emitProgress(options, {
59
+ type: "created",
60
+ id: task.id,
61
+ source: task,
62
+ target: created,
63
+ targetList: targetTasks.name,
64
+ targetState
65
+ });
66
+ }
67
+ catch (error) {
68
+ let message = errorMessage(error);
69
+ if (createdTarget !== undefined) {
70
+ try {
71
+ await targetTasks.delete(createdTarget.id);
72
+ }
73
+ catch (rollbackError) {
74
+ message = `${message} Rollback failed: ${errorMessage(rollbackError)}`;
75
+ }
76
+ }
77
+ result.errors.push({ id: task.id, error: message });
78
+ emitProgress(options, { type: "error", id: task.id, source: task, error: message });
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ function emitProgress(options, event) {
84
+ try {
85
+ options.onProgress?.(event);
86
+ }
87
+ catch {
88
+ return;
89
+ }
90
+ }
91
+ async function resolveTargetTasks(sourceTask, target, targetLists) {
92
+ const cached = targetLists.get(sourceTask.list);
93
+ if (cached !== undefined) {
94
+ return cached;
95
+ }
96
+ let tasks;
97
+ try {
98
+ tasks = target.list(sourceTask.list);
99
+ }
100
+ catch (error) {
101
+ const lists = await target.lists();
102
+ if (lists.length !== 1) {
103
+ throw error;
104
+ }
105
+ tasks = target.list(lists[0]);
106
+ }
107
+ targetLists.set(sourceTask.list, tasks);
108
+ return tasks;
109
+ }
110
+ async function applyState(tasks, created, events) {
111
+ let task = created;
112
+ for (const event of events) {
113
+ task = await tasks.fire(task.id, event);
114
+ }
115
+ return task;
116
+ }
117
+ function readOnlySourceOptions(options) {
118
+ if (!("create" in options)) {
119
+ return options;
120
+ }
121
+ return { ...options, create: false };
122
+ }
123
+ function readOnlyTargetOptions(options, dryRun) {
124
+ if (!dryRun || options.type === "gh-issues") {
125
+ return options;
126
+ }
127
+ return {
128
+ ...options,
129
+ create: true,
130
+ fs: createDryRunFs(options.fs ?? fsPromises, options.path, options.type)
131
+ };
132
+ }
133
+ function createDryRunFs(fs, targetPath, targetType) {
134
+ const resolvedTargetPath = path.resolve(targetPath);
135
+ const unexpectedWrite = async () => {
136
+ throw new Error("moveTasks dryRun attempted to write to the target backend.");
137
+ };
138
+ return {
139
+ ...fs,
140
+ async mkdir(directoryPath) {
141
+ if (path.resolve(directoryPath) !== resolvedTargetPath) {
142
+ await unexpectedWrite();
143
+ }
144
+ },
145
+ async stat(filePath) {
146
+ try {
147
+ return await fs.stat(filePath);
148
+ }
149
+ catch (error) {
150
+ if (path.resolve(filePath) !== resolvedTargetPath) {
151
+ throw error;
152
+ }
153
+ return {
154
+ isDirectory: () => targetType === "markdown-dir",
155
+ isFile: () => targetType === "yaml-file",
156
+ mtimeMs: 0
157
+ };
158
+ }
159
+ },
160
+ rename: unexpectedWrite,
161
+ unlink: unexpectedWrite,
162
+ writeFile: unexpectedWrite
163
+ };
164
+ }
165
+ function findTransitionEvents(tasks, fromState, targetState) {
166
+ if (fromState === targetState) {
167
+ return [];
168
+ }
169
+ const queue = [{ state: fromState, events: [] }];
170
+ const visited = new Set([fromState]);
171
+ while (queue.length > 0) {
172
+ const current = queue.shift();
173
+ if (current === undefined) {
174
+ return undefined;
175
+ }
176
+ for (const eventName of Object.keys(tasks.stateMachine.events)) {
177
+ const event = findEvent(tasks.stateMachine, current.state, eventName);
178
+ if (event === undefined) {
179
+ continue;
180
+ }
181
+ const events = [...current.events, eventName];
182
+ if (event.to === targetState) {
183
+ return events;
184
+ }
185
+ if (!visited.has(event.to)) {
186
+ visited.add(event.to);
187
+ queue.push({ state: event.to, events });
188
+ }
189
+ }
190
+ }
191
+ return undefined;
192
+ }
193
+ function createTokenBucket(rate) {
194
+ const intervalMs = 60_000 / rate;
195
+ const capacity = Math.max(1, rate);
196
+ let tokens = capacity;
197
+ let lastRefill = Date.now();
198
+ return async () => {
199
+ while (true) {
200
+ const now = Date.now();
201
+ tokens = Math.min(capacity, tokens + (now - lastRefill) / intervalMs);
202
+ lastRefill = now;
203
+ if (tokens >= 1) {
204
+ tokens -= 1;
205
+ return;
206
+ }
207
+ await new Promise((resolve) => {
208
+ setTimeout(resolve, Math.ceil((1 - tokens) * intervalMs));
209
+ });
210
+ }
211
+ };
212
+ }
213
+ function errorMessage(error) {
214
+ return error instanceof Error ? error.message : String(error);
215
+ }
@@ -5,8 +5,6 @@ import { markdownDirBackend } from "./backends/markdown-dir.js";
5
5
  import { yamlFileBackend } from "./backends/yaml-file.js";
6
6
  import { validateMachine } from "./state-machine.js";
7
7
  import { resolveStateMachine } from "./state.js";
8
- const DEFAULT_LOCK_STALE_MS = 30_000;
9
- const DEFAULT_LOCK_RETRIES = 20;
10
8
  export const backendFactories = {
11
9
  "markdown-dir": markdownDirBackend,
12
10
  "yaml-file": yamlFileBackend
@@ -37,8 +35,6 @@ async function openFileBackend(options) {
37
35
  },
38
36
  singleList: markdownOptions?.singleList,
39
37
  frontmatterMode: markdownOptions?.frontmatterMode ?? "strict",
40
- lockStaleMs: options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS,
41
- lockRetries: options.lockRetries ?? DEFAULT_LOCK_RETRIES,
42
38
  create: options.create ?? false,
43
39
  fs: options.fs ?? createDefaultFs(),
44
40
  stateMachine
@@ -51,6 +47,9 @@ async function openGhIssuesBackend(options) {
51
47
  return ghIssuesBackend({
52
48
  repo: options.repo,
53
49
  project: options.project,
50
+ filter: options.filter,
51
+ state: options.state,
52
+ stateMachine: options.stateMachine,
54
53
  defaults: {
55
54
  metadata: { ...(options.defaults?.metadata ?? {}) }
56
55
  },