toolcraft 0.0.50 → 0.0.52

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 (47) hide show
  1. package/node_modules/@poe-code/agent-defs/README.md +35 -0
  2. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +9 -2
  3. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +3 -1
  4. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +44 -2
  5. package/node_modules/@poe-code/agent-human-in-loop/package.json +0 -1
  6. package/node_modules/@poe-code/agent-mcp-config/README.md +54 -0
  7. package/node_modules/@poe-code/agent-mcp-config/package.json +0 -2
  8. package/node_modules/@poe-code/config-mutations/README.md +55 -0
  9. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +1 -0
  10. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +65 -0
  11. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +4 -11
  12. package/node_modules/@poe-code/config-mutations/package.json +0 -1
  13. package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +17 -0
  14. package/node_modules/@poe-code/frontmatter/dist/fences.js +33 -5
  15. package/node_modules/@poe-code/frontmatter/dist/parse.js +86 -14
  16. package/node_modules/@poe-code/frontmatter/dist/stringify.js +13 -0
  17. package/node_modules/@poe-code/process-runner/dist/docker/args.js +14 -1
  18. package/node_modules/@poe-code/process-runner/dist/docker/build-context.d.ts +5 -0
  19. package/node_modules/@poe-code/process-runner/dist/docker/build-context.js +37 -0
  20. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +29 -29
  21. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  22. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  23. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +49 -3
  24. package/node_modules/@poe-code/process-runner/package.json +8 -1
  25. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +7 -0
  26. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +34 -7
  27. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +75 -19
  28. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  29. package/node_modules/@poe-code/task-list/dist/backends/utils.js +23 -2
  30. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +16 -12
  31. package/node_modules/@poe-code/task-list/dist/state-machine.js +9 -0
  32. package/node_modules/@poe-code/task-list/package.json +0 -1
  33. package/node_modules/auth-store/dist/create-secret-store.js +4 -3
  34. package/node_modules/auth-store/dist/encrypted-file-store.js +49 -2
  35. package/node_modules/auth-store/dist/keychain-store.js +11 -4
  36. package/node_modules/auth-store/package.json +0 -1
  37. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +69 -12
  38. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +100 -68
  39. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +19 -18
  40. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +37 -31
  41. package/node_modules/mcp-oauth/package.json +0 -1
  42. package/node_modules/tiny-mcp-client/dist/internal.js +96 -10
  43. package/node_modules/tiny-mcp-client/package.json +0 -3
  44. package/node_modules/tiny-mcp-client/src/internal.ts +120 -18
  45. package/node_modules/tiny-mcp-client/src/transports.test.ts +231 -2
  46. package/node_modules/toolcraft-design/package.json +0 -1
  47. package/package.json +3 -2
@@ -36,7 +36,8 @@ export function buildDockerRunArgs(input) {
36
36
  const volume = `${path.resolve(mount.source)}:${mount.target}${mount.readonly ? ":ro" : ""}`;
37
37
  args.push("-v", volume);
38
38
  }
39
- for (const port of input.ports) {
39
+ for (const [index, port] of input.ports.entries()) {
40
+ assertValidDockerPortMapping(port, index);
40
41
  const mapping = `${port.host}:${port.container}${port.protocol === undefined || port.protocol === "tcp" ? "" : `/${port.protocol}`}`;
41
42
  args.push("-p", mapping);
42
43
  }
@@ -46,3 +47,15 @@ export function buildDockerRunArgs(input) {
46
47
  args.push(...input.extraArgs, input.image, input.command, ...input.args);
47
48
  return args;
48
49
  }
50
+ function assertValidDockerPortMapping(port, index) {
51
+ assertValidPortNumber(port.host, `ports[${index}].host`);
52
+ assertValidPortNumber(port.container, `ports[${index}].container`);
53
+ if (port.protocol !== undefined && port.protocol !== "tcp" && port.protocol !== "udp") {
54
+ throw new Error(`Invalid Docker port mapping ${index}: protocol must be tcp or udp.`);
55
+ }
56
+ }
57
+ function assertValidPortNumber(value, field) {
58
+ if (!Number.isSafeInteger(value) || value < 1 || value > 65_535) {
59
+ throw new Error(`Invalid Docker port mapping ${field}: port must be an integer from 1 to 65535.`);
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ export interface DockerBuildContextFile {
2
+ relativePath: string;
3
+ bytes: Buffer;
4
+ }
5
+ export declare function readDockerBuildContextFiles(buildContext: string): Promise<DockerBuildContextFile[]>;
@@ -0,0 +1,37 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ const require = createRequire(import.meta.url);
5
+ const createIgnore = require("ignore");
6
+ export async function readDockerBuildContextFiles(buildContext) {
7
+ const rootEntries = await readdir(buildContext, { withFileTypes: true });
8
+ const dockerignoreEntry = rootEntries.find((entry) => entry.isFile() && String(entry.name) === ".dockerignore");
9
+ const ignored = createIgnore();
10
+ if (dockerignoreEntry !== undefined) {
11
+ ignored.add(String(await readFile(path.join(buildContext, ".dockerignore"))));
12
+ }
13
+ const files = [];
14
+ await collectBuildContextFiles(buildContext, "", files, ignored, rootEntries);
15
+ return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
16
+ }
17
+ async function collectBuildContextFiles(buildContext, relativeDir, files, ignored, entries) {
18
+ const absoluteDir = path.join(buildContext, relativeDir);
19
+ const dirEntries = entries ?? (await readdir(absoluteDir, { withFileTypes: true }));
20
+ for (const entry of dirEntries) {
21
+ const relativePath = path.join(relativeDir, String(entry.name));
22
+ const dockerPath = relativePath.split(path.sep).join("/");
23
+ if (entry.isDirectory()) {
24
+ if (!ignored.ignores(`${dockerPath}/`)) {
25
+ await collectBuildContextFiles(buildContext, relativePath, files, ignored);
26
+ }
27
+ continue;
28
+ }
29
+ if (!entry.isFile() || (dockerPath !== ".dockerignore" && ignored.ignores(dockerPath))) {
30
+ continue;
31
+ }
32
+ files.push({
33
+ relativePath: dockerPath,
34
+ bytes: await readFile(path.join(buildContext, relativePath))
35
+ });
36
+ }
37
+ }
@@ -1,9 +1,10 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
- import { readdir, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { readFile, realpath, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
7
+ import { readDockerBuildContextFiles } from "./build-context.js";
7
8
  import { buildContextArgs, detectContext } from "./context.js";
8
9
  import { detectEngine } from "./engine.js";
9
10
  import { createDockerEnvFile } from "./env-file.js";
@@ -185,7 +186,7 @@ export async function buildDockerRuntimeTemplate(input) {
185
186
  const context = detectContext();
186
187
  const { dockerfilePath, buildContext } = await resolveRuntimeBuildPaths(input.cwd, input.runtime);
187
188
  const dockerfileBytes = await readFile(dockerfilePath);
188
- const buildContextFiles = await readBuildContextFiles(buildContext);
189
+ const buildContextFiles = await readDockerBuildContextFiles(buildContext);
189
190
  const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
190
191
  const cached = input.force ? null : await input.state?.templates.get("docker", hash);
191
192
  if (cached?.image !== undefined && (await imageExists(runner, engine, context, cached.image))) {
@@ -262,29 +263,6 @@ function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engin
262
263
  }
263
264
  return hash.digest("hex");
264
265
  }
265
- async function readBuildContextFiles(buildContext) {
266
- const files = [];
267
- await collectBuildContextFiles(buildContext, "", files);
268
- return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
269
- }
270
- async function collectBuildContextFiles(buildContext, relativeDir, files) {
271
- const absoluteDir = path.join(buildContext, relativeDir);
272
- const entries = await readdir(absoluteDir, { withFileTypes: true });
273
- for (const entry of entries) {
274
- const relativePath = path.join(relativeDir, entry.name);
275
- if (entry.isDirectory()) {
276
- await collectBuildContextFiles(buildContext, relativePath, files);
277
- continue;
278
- }
279
- if (!entry.isFile()) {
280
- continue;
281
- }
282
- files.push({
283
- relativePath: relativePath.split(path.sep).join("/"),
284
- bytes: await readFile(path.join(buildContext, relativePath))
285
- });
286
- }
287
- }
288
266
  async function imageExists(runner, engine, context, image) {
289
267
  const handle = runner.exec({
290
268
  command: engine,
@@ -602,8 +580,7 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
602
580
  });
603
581
  const stdout = await readStream(handle.stdout);
604
582
  const result = await handle.result;
605
- const exitCode = Number.parseInt(stdout.trim(), 10);
606
- return { exitCode: Number.isNaN(exitCode) ? result.exitCode : exitCode };
583
+ return { exitCode: parseDockerWaitExitCode(stdout, result.exitCode) };
607
584
  },
608
585
  async kill(signal) {
609
586
  const args = signal === undefined || signal === "SIGTERM"
@@ -618,6 +595,29 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
618
595
  }
619
596
  };
620
597
  }
598
+ function parseDockerWaitExitCode(stdout, fallbackExitCode) {
599
+ const trimmed = stdout.trim();
600
+ if (trimmed.length === 0) {
601
+ return fallbackExitCode;
602
+ }
603
+ return parseCompleteDecimalExitCode(trimmed, "docker wait");
604
+ }
605
+ function parseCompleteDecimalExitCode(value, source) {
606
+ if (value.length === 0) {
607
+ throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
608
+ }
609
+ for (let index = 0; index < value.length; index += 1) {
610
+ const code = value.charCodeAt(index);
611
+ if (code < 48 || code > 57) {
612
+ throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
613
+ }
614
+ }
615
+ const exitCode = Number(value);
616
+ if (!Number.isSafeInteger(exitCode)) {
617
+ throw new Error(`${source} returned an invalid exit code: ${JSON.stringify(value)}.`);
618
+ }
619
+ return exitCode;
620
+ }
621
621
  function completeUtf8PrefixLength(contents) {
622
622
  if (contents.length === 0) {
623
623
  return 0;
@@ -671,8 +671,8 @@ async function readDetachedExitCode(containerId, jobId, runner, engine, context)
671
671
  if (result.exitCode !== 0) {
672
672
  return null;
673
673
  }
674
- const exitCode = Number.parseInt(stdout.trim(), 10);
675
- return Number.isNaN(exitCode) ? null : exitCode;
674
+ const text = stdout.trim();
675
+ return text.length === 0 ? null : parseCompleteDecimalExitCode(text, "detached exit marker");
676
676
  }
677
677
  function createAttachedSpec(cwd = "/workspace") {
678
678
  return {
@@ -1,4 +1,5 @@
1
1
  export { buildContextArgs, detectContext } from "./docker/context.js";
2
+ export { readDockerBuildContextFiles, type DockerBuildContextFile } from "./docker/build-context.js";
2
3
  export { detectEngine, isEngineAvailable } from "./docker/engine.js";
3
4
  export { createDockerRunner } from "./docker/docker-runner.js";
4
5
  export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
@@ -1,4 +1,5 @@
1
1
  export { buildContextArgs, detectContext } from "./docker/context.js";
2
+ export { readDockerBuildContextFiles } from "./docker/build-context.js";
2
3
  export { detectEngine, isEngineAvailable } from "./docker/engine.js";
3
4
  export { createDockerRunner } from "./docker/docker-runner.js";
4
5
  export { buildDockerRuntimeTemplate, dockerExecutionEnvFactory } from "./docker/docker-execution-env.js";
@@ -6,13 +6,13 @@ export async function uploadWorkspace(env, opts) {
6
6
  const localFs = env.fs ?? nodeFs;
7
7
  const remoteFs = env.remoteFs ?? localFs;
8
8
  const workspaceDir = env.workspaceDir ?? "/workspace";
9
- const maxBytes = (opts.uploadMaxFileMb ?? opts.runner?.upload_max_file_mb ?? 100) * 1024 * 1024;
9
+ const maxBytes = resolveUploadMaxBytes(opts);
10
10
  const warn = opts.warn ?? console.warn;
11
11
  const allFiles = await listFiles(localFs, env.cwd);
12
12
  const state = new Map();
13
13
  const gitignore = await readGitignoreRules(localFs, env.cwd, allFiles);
14
14
  const poeCodeIgnore = await readIgnoreFile(localFs, env.cwd, ".poe-code-ignore", false);
15
- const workspaceExclude = parseIgnoreLines([...(opts.runner?.workspace?.exclude ?? []), ...(opts.workspaceExclude ?? [])], false);
15
+ const workspaceExclude = parseIgnoreLines([".git/", ...(opts.runner?.workspace?.exclude ?? []), ...(opts.workspaceExclude ?? [])], false);
16
16
  const entries = [];
17
17
  const skipped = [];
18
18
  for (const file of allFiles) {
@@ -122,6 +122,13 @@ export async function downloadWorkspace(env, opts) {
122
122
  }
123
123
  return { files, bytes, conflicts };
124
124
  }
125
+ function resolveUploadMaxBytes(opts) {
126
+ const uploadMaxFileMb = opts.uploadMaxFileMb ?? opts.runner?.upload_max_file_mb ?? 100;
127
+ if (!Number.isFinite(uploadMaxFileMb) || uploadMaxFileMb <= 0) {
128
+ throw new Error("runner.upload_max_file_mb must be a finite positive number.");
129
+ }
130
+ return uploadMaxFileMb * 1024 * 1024;
131
+ }
125
132
  async function listFilesIfExists(fs, root, options = {}) {
126
133
  try {
127
134
  return await listFiles(fs, root, options);
@@ -205,17 +212,50 @@ function parseIgnoreLines(lines, allowNegation, basePath = "") {
205
212
  return rules;
206
213
  }
207
214
  function isIgnoredByGit(relativePath, rules) {
215
+ if (isPathIgnoredByGitRules(relativePath, rules, "path")) {
216
+ return true;
217
+ }
218
+ for (const parent of parentDirectories(relativePath)) {
219
+ if (isPathIgnoredByGitRules(parent, rules, "directory")) {
220
+ return true;
221
+ }
222
+ }
223
+ return false;
224
+ }
225
+ function isPathIgnoredByGitRules(relativePath, rules, targetKind) {
208
226
  let ignored = false;
209
227
  for (const rule of rules) {
210
- if (matchesRule(relativePath, rule)) {
228
+ if (matchesGitRule(relativePath, rule, targetKind)) {
211
229
  ignored = !rule.negate;
212
230
  }
213
231
  }
214
232
  return ignored;
215
233
  }
234
+ function parentDirectories(relativePath) {
235
+ const segments = normalizeRelativePath(relativePath).split("/");
236
+ const parents = [];
237
+ for (let index = 1; index < segments.length; index += 1) {
238
+ parents.push(segments.slice(0, index).join("/"));
239
+ }
240
+ return parents;
241
+ }
216
242
  function isIgnoredAdditively(relativePath, rules) {
217
243
  return rules.some((rule) => matchesRule(relativePath, rule));
218
244
  }
245
+ function matchesGitRule(relativePath, rule, targetKind) {
246
+ if (!rule.directoryOnly) {
247
+ return matchesRule(relativePath, rule);
248
+ }
249
+ const normalizedPath = normalizeRelativePath(relativePath);
250
+ const scopedPath = pathWithinRuleScope(normalizedPath, rule.basePath);
251
+ if (scopedPath === null) {
252
+ return false;
253
+ }
254
+ if (targetKind === "directory") {
255
+ return pathMatchesDirectorySelf(scopedPath, normalizeRelativePath(rule.pattern), rule.anchored);
256
+ }
257
+ return !rule.negate && matchesRule(relativePath, rule);
258
+ }
219
259
  function matchesRule(relativePath, rule) {
220
260
  const normalizedPath = normalizeRelativePath(relativePath);
221
261
  const scopedPath = pathWithinRuleScope(normalizedPath, rule.basePath);
@@ -250,6 +290,12 @@ function pathMatchesDirectory(relativePath, pattern, anchored) {
250
290
  return matchSegment(segment, pattern) && index < segments.length - 1;
251
291
  });
252
292
  }
293
+ function pathMatchesDirectorySelf(relativePath, pattern, anchored) {
294
+ if (anchored || pattern.includes("/")) {
295
+ return relativePath === pattern;
296
+ }
297
+ return relativePath.split("/").some((segment) => matchSegment(segment, pattern));
298
+ }
253
299
  function matchPathSegments(pathSegments, patternSegments) {
254
300
  return matchPathFrom(0, 0);
255
301
  function matchPathFrom(pathIndex, patternIndex) {
@@ -10,6 +10,10 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
12
  },
13
+ "./docker/build-context": {
14
+ "types": "./dist/docker/build-context.d.ts",
15
+ "import": "./dist/docker/build-context.js"
16
+ },
13
17
  "./testing": {
14
18
  "types": "./dist/testing/index.d.ts",
15
19
  "import": "./dist/testing/index.js"
@@ -23,5 +27,8 @@
23
27
  },
24
28
  "files": [
25
29
  "dist"
26
- ]
30
+ ],
31
+ "dependencies": {
32
+ "ignore": "^5.3.2"
33
+ }
27
34
  }
@@ -51,6 +51,7 @@ export class GhProjectSyncError extends Error {
51
51
  }
52
52
  }
53
53
  export async function verifyGhProject(opts) {
54
+ validateRequiredStates(opts.requiredStates);
54
55
  const client = resolveGhClient(opts);
55
56
  const lookup = await lookupProject(client, opts.owner, opts.number);
56
57
  return buildVerifyReport(lookup, opts);
@@ -124,6 +125,7 @@ function buildVerifyReport(lookup, opts) {
124
125
  };
125
126
  }
126
127
  export async function syncGhProject(opts) {
128
+ validateRequiredStates(opts.requiredStates);
127
129
  const client = resolveGhClient(opts);
128
130
  let lookup = await lookupProject(client, opts.owner, opts.number);
129
131
  const initialReport = buildVerifyReport(lookup, opts);
@@ -184,6 +186,11 @@ export async function syncGhProject(opts) {
184
186
  updated: []
185
187
  };
186
188
  }
189
+ function validateRequiredStates(requiredStates) {
190
+ if (requiredStates.some((state) => state.trim().length === 0)) {
191
+ throw new Error("requiredStates must not contain empty state names.");
192
+ }
193
+ }
187
194
  async function createProject(client, opts) {
188
195
  const target = `${opts.owner}/${opts.number}`;
189
196
  try {
@@ -1,4 +1,4 @@
1
- import { eventsFromState, findEvent } from "../state-machine.js";
1
+ import { eventsFromState, findEvent, validateMachine } from "../state-machine.js";
2
2
  import { AnchorNotFoundError, InvalidTransitionError, OrderMismatchError, TaskNotFoundError } from "../types.js";
3
3
  import { createGhClient } from "./gh-issues-client.js";
4
4
  import { applyOrder, sortTasks } from "./utils.js";
@@ -251,6 +251,9 @@ export async function ghIssuesBackend(deps) {
251
251
  if (deps.state?.labelPrefix === "") {
252
252
  throw new Error("gh-issues state.labelPrefix must be a non-empty string when configured.");
253
253
  }
254
+ if (deps.stateMachine !== undefined) {
255
+ validateMachine(deps.stateMachine);
256
+ }
254
257
  const client = createGhClient({
255
258
  token: deps.token,
256
259
  endpoint: deps.endpoint,
@@ -446,7 +449,9 @@ function createTasksView(name, session, context) {
446
449
  listName: name,
447
450
  session
448
451
  });
449
- return session.labelPrefix === undefined ? task : { ...task, state: session.stateMachine.initial };
452
+ return session.labelPrefix === undefined
453
+ ? task
454
+ : { ...task, state: session.stateMachine.initial };
450
455
  },
451
456
  async update(id, patch) {
452
457
  const task = await fetchIssueTask(id, name, session, context);
@@ -646,7 +651,9 @@ async function resolveProjectItemId(id, listName, session, context) {
646
651
  return projectItem.id;
647
652
  }
648
653
  const pageInfo = issue.projectItems?.pageInfo;
649
- if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
654
+ if (pageInfo?.hasNextPage !== true ||
655
+ pageInfo.endCursor === undefined ||
656
+ pageInfo.endCursor === null) {
650
657
  throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
651
658
  }
652
659
  after = pageInfo.endCursor;
@@ -711,7 +718,9 @@ async function updateIssueStateLabel(id, listName, state, session, context) {
711
718
  break;
712
719
  }
713
720
  const pageInfo = currentIssue.projectItems?.pageInfo;
714
- if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
721
+ if (pageInfo?.hasNextPage !== true ||
722
+ pageInfo.endCursor === undefined ||
723
+ pageInfo.endCursor === null) {
715
724
  break;
716
725
  }
717
726
  after = pageInfo.endCursor;
@@ -883,12 +892,15 @@ async function fetchIssueTask(id, listName, session, context) {
883
892
  break;
884
893
  }
885
894
  projectItem =
886
- currentIssue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ?? null;
895
+ currentIssue.projectItems?.nodes?.find((item) => item.project?.id === session.projectId) ??
896
+ null;
887
897
  if (projectItem !== null) {
888
898
  break;
889
899
  }
890
900
  const pageInfo = currentIssue.projectItems?.pageInfo;
891
- if (pageInfo?.hasNextPage !== true || pageInfo.endCursor === undefined || pageInfo.endCursor === null) {
901
+ if (pageInfo?.hasNextPage !== true ||
902
+ pageInfo.endCursor === undefined ||
903
+ pageInfo.endCursor === null) {
892
904
  throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
893
905
  }
894
906
  after = pageInfo.endCursor;
@@ -902,12 +914,27 @@ async function fetchIssueTask(id, listName, session, context) {
902
914
  });
903
915
  }
904
916
  function parseIssueNumber(id, listName) {
917
+ if (!isCanonicalDecimalIssueId(id)) {
918
+ throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
919
+ }
905
920
  const issueNumber = Number(id);
906
- if (!Number.isInteger(issueNumber) || issueNumber < 1) {
921
+ if (!Number.isSafeInteger(issueNumber) || issueNumber < 1) {
907
922
  throw new TaskNotFoundError(`Task "${listName}/${id}" not found.`);
908
923
  }
909
924
  return issueNumber;
910
925
  }
926
+ function isCanonicalDecimalIssueId(id) {
927
+ if (id.length === 0 || id[0] === "0") {
928
+ return false;
929
+ }
930
+ for (let index = 0; index < id.length; index += 1) {
931
+ const charCode = id.charCodeAt(index);
932
+ if (charCode < 48 || charCode > 57) {
933
+ return false;
934
+ }
935
+ }
936
+ return true;
937
+ }
911
938
  function mapProjectItemToTask(item, listName, session) {
912
939
  const content = item.content;
913
940
  if (!isIssueNode(content)) {
@@ -4,7 +4,7 @@ import taskSchema from "../schema/task.schema.json" with { type: "json" };
4
4
  import { eventsFromState, findEvent } from "../state-machine.js";
5
5
  import { resolveStateMachine } from "../state.js";
6
6
  import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
7
- import { applyOrder, hasErrorCode, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, withFileLock, writeAtomically } from "./utils.js";
7
+ import { applyOrder, hasErrorCode, isTrimmedPrintableIdentifier, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, validateTaskName, withFileLock, writeAtomically } from "./utils.js";
8
8
  const ARCHIVE_DIRECTORY_NAME = "archive";
9
9
  const MARKDOWN_EXTENSION = ".md";
10
10
  const TASK_KIND = "task";
@@ -25,7 +25,7 @@ function resolveListLayout(deps) {
25
25
  return deps.singleList ? { kind: "single", name: deps.singleList } : { kind: "multi" };
26
26
  }
27
27
  function validateListName(name) {
28
- if (name.length === 0 ||
28
+ if (!isTrimmedPrintableIdentifier(name) ||
29
29
  name === ARCHIVE_DIRECTORY_NAME ||
30
30
  name.startsWith(".") ||
31
31
  name.includes("/") ||
@@ -239,11 +239,9 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
239
239
  task: createTask(list, id, frontmatter, document.body, mode, filePath)
240
240
  };
241
241
  }
242
- const parsedFilename = parseActiveFilename(path.basename(filePath));
243
- const defaultName = parsedFilename?.id ?? id;
244
242
  const effectiveFrontmatter = {
245
243
  ...frontmatter,
246
- name: typeof frontmatter.name === "string" ? frontmatter.name : defaultName,
244
+ name: typeof frontmatter.name === "string" ? frontmatter.name : id,
247
245
  state: typeof frontmatter.state === "string" && validStates.has(frontmatter.state)
248
246
  ? frontmatter.state
249
247
  : initialState
@@ -254,22 +252,72 @@ async function readTaskFile(fs, list, id, filePath, validStates, initialState, m
254
252
  task: createTask(list, id, effectiveFrontmatter, document.body, mode, filePath)
255
253
  };
256
254
  }
257
- async function findActiveTaskFilename(fs, listDirectoryPath, id) {
255
+ async function readPassthroughFrontmatter(fs, filePath, mode) {
256
+ if (mode !== "passthrough") {
257
+ return {};
258
+ }
259
+ const content = await fs.readFile(filePath, "utf8");
260
+ const document = splitTaskDocument(content, filePath, mode);
261
+ if (document.frontmatter.trim().length === 0) {
262
+ return {};
263
+ }
264
+ return readFrontmatter(document.frontmatter, filePath);
265
+ }
266
+ async function resolveActiveFilenameEntry(fs, entryName, entryPath, parsed, mode) {
267
+ if (mode !== "passthrough" || parsed.order === null) {
268
+ return parsed;
269
+ }
270
+ const frontmatter = await readPassthroughFrontmatter(fs, entryPath, mode);
271
+ if (Object.keys(frontmatter).length === 0 &&
272
+ orderedFilenamePrefixLength(entryName) <= MIN_PREFIX_WIDTH) {
273
+ return parsed;
274
+ }
275
+ if (typeof frontmatter.state === "string" ||
276
+ hasOwnTaskField(frontmatter, "$schema") ||
277
+ hasOwnTaskField(frontmatter, "kind") ||
278
+ hasOwnTaskField(frontmatter, "version")) {
279
+ return parsed;
280
+ }
281
+ return {
282
+ id: entryName.slice(0, -MARKDOWN_EXTENSION.length),
283
+ order: null,
284
+ filename: parsed.filename
285
+ };
286
+ }
287
+ function orderedFilenamePrefixLength(entryName) {
288
+ const stem = entryName.slice(0, -MARKDOWN_EXTENSION.length);
289
+ const separatorIndex = stem.indexOf("-");
290
+ if (separatorIndex <= 0) {
291
+ return Number.POSITIVE_INFINITY;
292
+ }
293
+ for (let index = 0; index < separatorIndex; index += 1) {
294
+ const code = stem.charCodeAt(index);
295
+ if (code < 48 || code > 57) {
296
+ return Number.POSITIVE_INFINITY;
297
+ }
298
+ }
299
+ return separatorIndex;
300
+ }
301
+ async function findActiveTaskFilename(fs, listDirectoryPath, id, mode) {
258
302
  const entries = await readDirectoryNames(fs, listDirectoryPath);
259
303
  for (const entryName of entries) {
260
304
  if (isHiddenEntry(entryName))
261
305
  continue;
262
306
  const parsed = parseActiveFilename(entryName);
263
- if (parsed?.id === id) {
307
+ if (!parsed)
308
+ continue;
309
+ const entryPath = path.join(listDirectoryPath, entryName);
310
+ const resolved = await resolveActiveFilenameEntry(fs, entryName, entryPath, { id: parsed.id, order: parsed.order, filename: entryName }, mode);
311
+ if (resolved.id === id) {
264
312
  return entryName;
265
313
  }
266
314
  }
267
315
  return undefined;
268
316
  }
269
- async function findTaskLocation(fs, rootPath, layout, list, id) {
317
+ async function findTaskLocation(fs, rootPath, layout, list, id, mode) {
270
318
  const listDirectoryPath = listPath(rootPath, layout, list);
271
319
  await rejectSymbolicLinkComponents(fs, listDirectoryPath);
272
- const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id);
320
+ const activeName = await findActiveTaskFilename(fs, listDirectoryPath, id, mode);
273
321
  if (activeName) {
274
322
  const activePath = path.join(listDirectoryPath, activeName);
275
323
  await rejectSymbolicLinkComponents(fs, activePath);
@@ -288,7 +336,7 @@ async function findTaskLocation(fs, rootPath, layout, list, id) {
288
336
  return undefined;
289
337
  }
290
338
  async function readTaskAtLocation(fs, rootPath, layout, list, id, validStates, initialState, mode) {
291
- const location = await findTaskLocation(fs, rootPath, layout, list, id);
339
+ const location = await findTaskLocation(fs, rootPath, layout, list, id, mode);
292
340
  if (!location) {
293
341
  throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
294
342
  }
@@ -404,7 +452,7 @@ function createTasksView(deps, layout, list) {
404
452
  const entryStat = await statIfExists(deps.fs, entryPath);
405
453
  if (!entryStat?.isFile())
406
454
  continue;
407
- result.push({ id: parsed.id, order: parsed.order, filename: entryName });
455
+ result.push(await resolveActiveFilenameEntry(deps.fs, entryName, entryPath, { id: parsed.id, order: parsed.order, filename: entryName }, deps.frontmatterMode));
408
456
  }
409
457
  result.sort((left, right) => {
410
458
  const leftOrder = left.order ?? Number.POSITIVE_INFINITY;
@@ -460,7 +508,12 @@ function createTasksView(deps, layout, list) {
460
508
  const targetPath = path.join(listDirectoryPath, desiredFilename);
461
509
  try {
462
510
  await deps.fs.rename(fromPath, stagingPath);
463
- staged.push({ original: fromPath, staging: stagingPath, target: targetPath, finalized: false });
511
+ staged.push({
512
+ original: fromPath,
513
+ staging: stagingPath,
514
+ target: targetPath,
515
+ finalized: false
516
+ });
464
517
  }
465
518
  catch (error) {
466
519
  for (const stagedEntry of staged.reverse()) {
@@ -598,8 +651,7 @@ function createTasksView(deps, layout, list) {
598
651
  return false;
599
652
  return true;
600
653
  });
601
- const orderedActiveTasks = applyOrder(orderedActive, filter?.order);
602
- return [...orderedActiveTasks, ...filteredArchived.map((entry) => entry.task)];
654
+ return applyOrder([...orderedActive, ...filteredArchived], filter?.order);
603
655
  },
604
656
  async get(id) {
605
657
  return (await getTaskFile(id)).task;
@@ -608,9 +660,10 @@ function createTasksView(deps, layout, list) {
608
660
  assertCreateDoesNotSetState(input);
609
661
  assertCreateHasId(input);
610
662
  validateTaskId(input.id);
663
+ validateTaskName(input.name);
611
664
  await rejectSymbolicLinkComponents(deps.fs, listDirectoryPath);
612
665
  return withFileLock(deps.fs, path.join(listDirectoryPath, ".transition.lock"), async () => {
613
- const existing = await findTaskLocation(deps.fs, deps.path, layout, list, input.id);
666
+ const existing = await findTaskLocation(deps.fs, deps.path, layout, list, input.id, deps.frontmatterMode);
614
667
  if (existing) {
615
668
  throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
616
669
  }
@@ -629,6 +682,9 @@ function createTasksView(deps, layout, list) {
629
682
  async update(id, patch) {
630
683
  assertUpdateDoesNotSetState(patch);
631
684
  validateTaskId(id);
685
+ if (patch.name !== undefined) {
686
+ validateTaskName(patch.name);
687
+ }
632
688
  const existing = await getTaskFile(id);
633
689
  const nextFrontmatter = updatedFrontmatter(existing.frontmatter, existing.task, patch, deps.frontmatterMode);
634
690
  const description = patch.description ?? existing.task.description;
@@ -676,7 +732,7 @@ function createTasksView(deps, layout, list) {
676
732
  };
677
733
  if (stateMachine.events[eventName]?.to === "archived") {
678
734
  validateTaskId(id);
679
- const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
735
+ const location = await findTaskLocation(deps.fs, deps.path, layout, list, id, deps.frontmatterMode);
680
736
  if (!location) {
681
737
  throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
682
738
  }
@@ -700,7 +756,7 @@ function createTasksView(deps, layout, list) {
700
756
  },
701
757
  async delete(id) {
702
758
  validateTaskId(id);
703
- const location = await findTaskLocation(deps.fs, deps.path, layout, list, id);
759
+ const location = await findTaskLocation(deps.fs, deps.path, layout, list, id, deps.frontmatterMode);
704
760
  if (!location) {
705
761
  throw new TaskNotFoundError(`Task "${list}/${id}" not found.`);
706
762
  }
@@ -808,11 +864,11 @@ export async function markdownDirBackend(deps) {
808
864
  const targetListDir = listPath(deps.path, layout, targetListName);
809
865
  await rejectSymbolicLinkComponents(deps.fs, targetListDir);
810
866
  return withFileLock(deps.fs, path.join(targetListDir, ".transition.lock"), async () => {
811
- const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id);
867
+ const targetExisting = await findTaskLocation(deps.fs, deps.path, layout, targetListName, id, deps.frontmatterMode);
812
868
  if (targetExisting) {
813
869
  throw new TaskAlreadyExistsError(`Task "${targetListName}/${id}" already exists.`);
814
870
  }
815
- const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id);
871
+ const sourceLocation = await findTaskLocation(deps.fs, deps.path, layout, sourceListName, id, deps.frontmatterMode);
816
872
  if (!sourceLocation) {
817
873
  throw new TaskNotFoundError(`Task "${sourceListName}/${id}" not found.`);
818
874
  }
@@ -9,7 +9,9 @@ export declare function hasErrorCode(error: unknown, code: string): boolean;
9
9
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
10
10
  export declare function sortStrings(values: string[]): string[];
11
11
  export declare function sortTasks(tasks: Task[]): Task[];
12
+ export declare function isTrimmedPrintableIdentifier(value: string): boolean;
12
13
  export declare function validateTaskId(id: string): string;
14
+ export declare function validateTaskName(name: string): string;
13
15
  export declare function statIfExists(fs: TaskListFs, filePath: string): Promise<Awaited<ReturnType<TaskListFs["stat"]>> | undefined>;
14
16
  export declare function rejectSymbolicLinkComponents(fs: TaskListFs, filePath: string): Promise<void>;
15
17
  export declare function writeAtomically(fs: TaskListFs, filePath: string, content: string): Promise<void>;