@open-agent-toolkit/cli 0.2.76 → 0.2.77

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 (28) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/public-package-versions.json +4 -4
  3. package/assets/skills/explainer-kit/SKILL.md +1 -1
  4. package/assets/skills/explainer-kit/scripts/bundle.mjs +26 -2
  5. package/assets/skills/explainer-kit/scripts/record.mjs +26 -3
  6. package/assets/skills/explainer-kit/scripts/verify.mjs +26 -3
  7. package/assets/skills/oat-explainer-kit/SKILL.md +1 -1
  8. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +25 -5
  9. package/assets/skills/oat-project-complete/SKILL.md +1 -1
  10. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +26 -6
  11. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +26 -6
  12. package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +7 -0
  13. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +26 -5
  14. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +26 -5
  15. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +26 -6
  16. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +26 -6
  17. package/assets/skills/oat-project-complete/scripts/resolve-synced-archive-entry.mjs +26 -5
  18. package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +7 -0
  19. package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs +126 -95
  20. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  21. package/assets/skills/oat-project-implement/scripts/capture-dirty-tree.mjs +3 -0
  22. package/assets/skills/recon/SKILL.md +1 -1
  23. package/assets/skills/recon/scripts/create-review-brief.mjs +25 -5
  24. package/assets/skills/recon/scripts/prepare-routing.mjs +26 -2
  25. package/assets/skills/recon/scripts/render-packet.mjs +25 -5
  26. package/assets/skills/recon/scripts/validate-artifact.mjs +25 -5
  27. package/assets/skills/recon/scripts/validate-packet.mjs +25 -5
  28. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "oatVersion": "0.2.76"
3
+ "oatVersion": "0.2.77"
4
4
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.2.76",
3
- "docs-config": "0.2.76",
4
- "docs-theme": "0.2.76",
5
- "docs-transforms": "0.2.76"
2
+ "cli": "0.2.77",
3
+ "docs-config": "0.2.77",
4
+ "docs-theme": "0.2.77",
5
+ "docs-transforms": "0.2.77"
6
6
  }
@@ -4,7 +4,7 @@ description: Use when building destination-neutral visual explainer artifacts fr
4
4
  user-invocable: true
5
5
  allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Agent, mcp__*
6
6
  metadata:
7
- version: 3.0.2
7
+ version: 3.0.3
8
8
  ---
9
9
 
10
10
  # Explainer Kit
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash } from 'node:crypto';
4
+ import { realpathSync } from 'node:fs';
4
5
  import {
5
6
  lstat,
6
7
  mkdir,
@@ -20,8 +21,9 @@ import {
20
21
  isAbsolute,
21
22
  join,
22
23
  relative,
24
+ resolve,
23
25
  } from 'node:path';
24
- import { pathToFileURL } from 'node:url';
26
+ import { fileURLToPath } from 'node:url';
25
27
 
26
28
  import {
27
29
  harvestNumericTokens,
@@ -704,7 +706,29 @@ function bundleError(message) {
704
706
  return error;
705
707
  }
706
708
 
707
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
709
+ /**
710
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
711
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
712
+ * that exits 0 whenever the skill is reached through a symlinked install root,
713
+ * and canonicalizing only one side has the same effect under
714
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
715
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
716
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
717
+ * `false`; it never masks a direct run.
718
+ */
719
+ function isDirectInvocation(invokedPath) {
720
+ if (!invokedPath) return false;
721
+ try {
722
+ return (
723
+ realpathSync(fileURLToPath(import.meta.url)) ===
724
+ realpathSync(resolve(invokedPath))
725
+ );
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+
731
+ if (isDirectInvocation(process.argv[1])) {
708
732
  runBundle(process.argv.slice(2)).catch((error) => {
709
733
  console.error(error.message);
710
734
  process.exitCode = 1;
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
+ import { realpathSync } from 'node:fs';
4
5
  import { access, readFile, readdir, rename, writeFile } from 'node:fs/promises';
5
- import { join } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
6
+ import { join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
7
8
 
8
9
  import { validateContract } from './lib/contracts.mjs';
9
10
  import {
@@ -327,7 +328,29 @@ function recordError(code, message) {
327
328
  return error;
328
329
  }
329
330
 
330
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
331
+ /**
332
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
333
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
334
+ * that exits 0 whenever the skill is reached through a symlinked install root,
335
+ * and canonicalizing only one side has the same effect under
336
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
337
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
338
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
339
+ * `false`; it never masks a direct run.
340
+ */
341
+ function isDirectInvocation(invokedPath) {
342
+ if (!invokedPath) return false;
343
+ try {
344
+ return (
345
+ realpathSync(fileURLToPath(import.meta.url)) ===
346
+ realpathSync(resolve(invokedPath))
347
+ );
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+
353
+ if (isDirectInvocation(process.argv[1])) {
331
354
  runRecord(process.argv.slice(2)).catch((error) => {
332
355
  console.error(`${error.code}: ${error.message}`);
333
356
  process.exitCode = 1;
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash } from 'node:crypto';
4
+ import { realpathSync } from 'node:fs';
4
5
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
- import { join } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
6
+ import { join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
7
8
 
8
9
  import { writeFailure } from './bundle.mjs';
9
10
  import {
@@ -640,7 +641,29 @@ function verifyError(code, message) {
640
641
  return error;
641
642
  }
642
643
 
643
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
644
+ /**
645
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
646
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
647
+ * that exits 0 whenever the skill is reached through a symlinked install root,
648
+ * and canonicalizing only one side has the same effect under
649
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
650
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
651
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
652
+ * `false`; it never masks a direct run.
653
+ */
654
+ function isDirectInvocation(invokedPath) {
655
+ if (!invokedPath) return false;
656
+ try {
657
+ return (
658
+ realpathSync(fileURLToPath(import.meta.url)) ===
659
+ realpathSync(resolve(invokedPath))
660
+ );
661
+ } catch {
662
+ return false;
663
+ }
664
+ }
665
+
666
+ if (isDirectInvocation(process.argv[1])) {
644
667
  runVerify(process.argv.slice(2)).catch((error) => {
645
668
  console.error(`${error.code ?? 'verify-error'}: ${error.message}`);
646
669
  process.exitCode = 1;
@@ -5,7 +5,7 @@ disable-model-invocation: false
5
5
  user-invocable: true
6
6
  allowed-tools: Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion
7
7
  metadata:
8
- version: 1.0.9
8
+ version: 1.0.10
9
9
  ---
10
10
 
11
11
  # OAT Explainer Kit
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { realpathSync } from 'node:fs';
2
3
  import { lstat, readFile, realpath } from 'node:fs/promises';
3
4
  import {
4
5
  basename,
@@ -9,7 +10,7 @@ import {
9
10
  resolve,
10
11
  sep,
11
12
  } from 'node:path';
12
- import { pathToFileURL } from 'node:url';
13
+ import { fileURLToPath } from 'node:url';
13
14
 
14
15
  import { validateContract } from '../../explainer-kit/scripts/lib/contracts.mjs';
15
16
  import { isFlowFailureStage } from '../../explainer-kit/scripts/lib/failure.mjs';
@@ -324,10 +325,29 @@ function recapOutcomeError(message) {
324
325
  return error;
325
326
  }
326
327
 
327
- if (
328
- process.argv[1] &&
329
- import.meta.url === pathToFileURL(process.argv[1]).href
330
- ) {
328
+ /**
329
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
330
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
331
+ * that exits 0 whenever the skill is reached through a symlinked install root,
332
+ * and canonicalizing only one side has the same effect under
333
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
334
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
335
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
336
+ * `false`; it never masks a direct run.
337
+ */
338
+ function isDirectInvocation(invokedPath) {
339
+ if (!invokedPath) return false;
340
+ try {
341
+ return (
342
+ realpathSync(fileURLToPath(import.meta.url)) ===
343
+ realpathSync(resolve(invokedPath))
344
+ );
345
+ } catch {
346
+ return false;
347
+ }
348
+ }
349
+
350
+ if (isDirectInvocation(process.argv[1])) {
331
351
  main(process.argv.slice(2))
332
352
  .then((result) => {
333
353
  process.stdout.write(`${JSON.stringify(result)}\n`);
@@ -5,7 +5,7 @@ disable-model-invocation: true
5
5
  user-invocable: true
6
6
  allowed-tools: Read, Write, Bash, AskUserQuestion
7
7
  metadata:
8
- version: 1.7.12
8
+ version: 1.7.13
9
9
  ---
10
10
 
11
11
  # Complete Project
@@ -1,6 +1,7 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import { readdir, realpath } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
3
+ import { join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
 
5
6
  import { readPersistedIntent } from '../../oat-explainer-kit/scripts/persist-intent.mjs';
6
7
 
@@ -110,10 +111,29 @@ async function main(argv) {
110
111
  return consumePersistedRecapIntent({ projectPath: argv[0] });
111
112
  }
112
113
 
113
- if (
114
- process.argv[1] &&
115
- import.meta.url === pathToFileURL(process.argv[1]).href
116
- ) {
114
+ /**
115
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
116
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
117
+ * that exits 0 whenever the skill is reached through a symlinked install root,
118
+ * and canonicalizing only one side has the same effect under
119
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
120
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
121
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
122
+ * `false`; it never masks a direct run.
123
+ */
124
+ function isDirectInvocation(invokedPath) {
125
+ if (!invokedPath) return false;
126
+ try {
127
+ return (
128
+ realpathSync(fileURLToPath(import.meta.url)) ===
129
+ realpathSync(resolve(invokedPath))
130
+ );
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ if (isDirectInvocation(process.argv[1])) {
117
137
  main(process.argv.slice(2))
118
138
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
119
139
  .catch((error) => {
@@ -1,7 +1,8 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
2
3
  import { readFile } from 'node:fs/promises';
3
- import { isAbsolute, posix, relative, sep } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
4
+ import { isAbsolute, posix, relative, sep, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { promisify } from 'node:util';
6
7
 
7
8
  import { validateSyncedArchiveTerminalReport } from './finalize-synced-archive.mjs';
@@ -261,10 +262,29 @@ async function main(argv) {
261
262
  });
262
263
  }
263
264
 
264
- if (
265
- process.argv[1] &&
266
- import.meta.url === pathToFileURL(process.argv[1]).href
267
- ) {
265
+ /**
266
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
267
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
268
+ * that exits 0 whenever the skill is reached through a symlinked install root,
269
+ * and canonicalizing only one side has the same effect under
270
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
271
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
272
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
273
+ * `false`; it never masks a direct run.
274
+ */
275
+ function isDirectInvocation(invokedPath) {
276
+ if (!invokedPath) return false;
277
+ try {
278
+ return (
279
+ realpathSync(fileURLToPath(import.meta.url)) ===
280
+ realpathSync(resolve(invokedPath))
281
+ );
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ if (isDirectInvocation(process.argv[1])) {
268
288
  main(process.argv.slice(2))
269
289
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
270
290
  .catch((error) => {
@@ -92,6 +92,10 @@ async function main(argv) {
92
92
  projectName,
93
93
  getArchiveReport: async () => {
94
94
  try {
95
+ // Stdin is read to EOF with no timeout. Every caller pipes the archive
96
+ // report in (`printf '%s\n' "$ARCHIVE_OUTPUT" | node ...`), so EOF
97
+ // always arrives; a timeout would turn a slow producer into a false
98
+ // failure, and a caller that leaves stdin open is a caller bug.
95
99
  return JSON.parse(readFileSync(0, 'utf8'));
96
100
  } catch (error) {
97
101
  throw finalizationError(
@@ -119,6 +123,9 @@ async function main(argv) {
119
123
  * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
120
124
  * way the caller reads "exited 0" as "verified" — the fail-open shape this
121
125
  * finalizer exists to prevent.
126
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
127
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
128
+ * `false`; it never masks a direct run.
122
129
  */
123
130
  function isDirectInvocation(invokedPath) {
124
131
  if (!invokedPath) return false;
@@ -1,4 +1,6 @@
1
- import { pathToFileURL } from 'node:url';
1
+ import { realpathSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
 
3
5
  const FULL_SHA = /^[0-9a-f]{40}$/;
4
6
  const PR_ARTIFACT = /^pr\/project-pr-[^/\s]+\.md$/;
@@ -217,10 +219,29 @@ function main(argv) {
217
219
  return parseCompletionRetryFields(argv[0]);
218
220
  }
219
221
 
220
- if (
221
- process.argv[1] &&
222
- import.meta.url === pathToFileURL(process.argv[1]).href
223
- ) {
222
+ /**
223
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
224
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
225
+ * that exits 0 whenever the skill is reached through a symlinked install root,
226
+ * and canonicalizing only one side has the same effect under
227
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
228
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
229
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
230
+ * `false`; it never masks a direct run.
231
+ */
232
+ function isDirectInvocation(invokedPath) {
233
+ if (!invokedPath) return false;
234
+ try {
235
+ return (
236
+ realpathSync(fileURLToPath(import.meta.url)) ===
237
+ realpathSync(resolve(invokedPath))
238
+ );
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
244
+ if (isDirectInvocation(process.argv[1])) {
224
245
  try {
225
246
  process.stdout.write(`${main(process.argv.slice(2))}\n`);
226
247
  } catch (error) {
@@ -1,4 +1,6 @@
1
- import { pathToFileURL } from 'node:url';
1
+ import { realpathSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
 
3
5
  function fieldsError(message) {
4
6
  const error = new Error(message);
@@ -92,10 +94,29 @@ function main(argv) {
92
94
  return formatShellAssignments(parseSyncedArchiveResumeFields(result));
93
95
  }
94
96
 
95
- if (
96
- process.argv[1] &&
97
- import.meta.url === pathToFileURL(process.argv[1]).href
98
- ) {
97
+ /**
98
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
99
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
100
+ * that exits 0 whenever the skill is reached through a symlinked install root,
101
+ * and canonicalizing only one side has the same effect under
102
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
103
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
104
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
105
+ * `false`; it never masks a direct run.
106
+ */
107
+ function isDirectInvocation(invokedPath) {
108
+ if (!invokedPath) return false;
109
+ try {
110
+ return (
111
+ realpathSync(fileURLToPath(import.meta.url)) ===
112
+ realpathSync(resolve(invokedPath))
113
+ );
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ if (isDirectInvocation(process.argv[1])) {
99
120
  try {
100
121
  process.stdout.write(`${main(process.argv.slice(2))}\n`);
101
122
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { basename } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
2
+ import { realpathSync } from 'node:fs';
3
+ import { basename, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { promisify } from 'node:util';
5
6
 
6
7
  const execFileAsync = promisify(execFile);
@@ -692,10 +693,29 @@ async function main(argv) {
692
693
  return recoverCompletionReceipts(options);
693
694
  }
694
695
 
695
- if (
696
- process.argv[1] &&
697
- import.meta.url === pathToFileURL(process.argv[1]).href
698
- ) {
696
+ /**
697
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
698
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
699
+ * that exits 0 whenever the skill is reached through a symlinked install root,
700
+ * and canonicalizing only one side has the same effect under
701
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
702
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
703
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
704
+ * `false`; it never masks a direct run.
705
+ */
706
+ function isDirectInvocation(invokedPath) {
707
+ if (!invokedPath) return false;
708
+ try {
709
+ return (
710
+ realpathSync(fileURLToPath(import.meta.url)) ===
711
+ realpathSync(resolve(invokedPath))
712
+ );
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+
718
+ if (isDirectInvocation(process.argv[1])) {
699
719
  main(process.argv.slice(2))
700
720
  .then((result) => {
701
721
  process.stdout.write(`${JSON.stringify(result)}\n`);
@@ -1,6 +1,7 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import { lstat, readdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
3
+ import { join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
 
5
6
  import {
6
7
  detectCompletionReceiptCandidate,
@@ -116,10 +117,29 @@ async function main(argv) {
116
117
  return resolveCompletionRetry(parseArguments(argv));
117
118
  }
118
119
 
119
- if (
120
- process.argv[1] &&
121
- import.meta.url === pathToFileURL(process.argv[1]).href
122
- ) {
120
+ /**
121
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
122
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
123
+ * that exits 0 whenever the skill is reached through a symlinked install root,
124
+ * and canonicalizing only one side has the same effect under
125
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
126
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
127
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
128
+ * `false`; it never masks a direct run.
129
+ */
130
+ function isDirectInvocation(invokedPath) {
131
+ if (!invokedPath) return false;
132
+ try {
133
+ return (
134
+ realpathSync(fileURLToPath(import.meta.url)) ===
135
+ realpathSync(resolve(invokedPath))
136
+ );
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ if (isDirectInvocation(process.argv[1])) {
123
143
  main(process.argv.slice(2))
124
144
  .then((result) => {
125
145
  process.stdout.write(`${JSON.stringify(result)}\n`);
@@ -1,6 +1,8 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
2
3
  import { readFile } from 'node:fs/promises';
3
- import { pathToFileURL } from 'node:url';
4
+ import { resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import { promisify } from 'node:util';
5
7
 
6
8
  const execFile = promisify(execFileCallback);
@@ -149,10 +151,29 @@ async function main(argv) {
149
151
  return resolveSyncedArchiveEntry({ ...options, record });
150
152
  }
151
153
 
152
- if (
153
- process.argv[1] &&
154
- import.meta.url === pathToFileURL(process.argv[1]).href
155
- ) {
154
+ /**
155
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
156
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
157
+ * that exits 0 whenever the skill is reached through a symlinked install root,
158
+ * and canonicalizing only one side has the same effect under
159
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
160
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
161
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
162
+ * `false`; it never masks a direct run.
163
+ */
164
+ function isDirectInvocation(invokedPath) {
165
+ if (!invokedPath) return false;
166
+ try {
167
+ return (
168
+ realpathSync(fileURLToPath(import.meta.url)) ===
169
+ realpathSync(resolve(invokedPath))
170
+ );
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ if (isDirectInvocation(process.argv[1])) {
156
177
  main(process.argv.slice(2))
157
178
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
158
179
  .catch((error) => {
@@ -134,6 +134,10 @@ async function main(argv) {
134
134
  if (options.mode === 'receipt') {
135
135
  let report;
136
136
  try {
137
+ // Stdin is read to EOF with no timeout. Every caller pipes the archive
138
+ // report in (`printf '%s\n' "$ARCHIVE_OUTPUT" | node ...`), so EOF
139
+ // always arrives; a timeout would turn a slow producer into a false
140
+ // failure, and a caller that leaves stdin open is a caller bug.
137
141
  report = JSON.parse(readFileSync(0, 'utf8'));
138
142
  } catch (error) {
139
143
  throw receiptError(
@@ -158,6 +162,9 @@ async function main(argv) {
158
162
  * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
159
163
  * way the caller reads "exited 0" as "verified" — the fail-open shape this
160
164
  * validator exists to prevent.
165
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
166
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
167
+ * `false`; it never masks a direct run.
161
168
  */
162
169
  function isDirectInvocation(invokedPath) {
163
170
  if (!invokedPath) return false;
@@ -11,6 +11,7 @@ import {
11
11
  resolve,
12
12
  sep,
13
13
  } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
14
15
 
15
16
  function fail(message) {
16
17
  process.stderr.write(`Invalid non-archive lifecycle receipt: ${message}\n`);
@@ -57,104 +58,134 @@ function gitSucceeds(cwd, args) {
57
58
  }
58
59
  }
59
60
 
60
- const [recordArgument, commit, projectName] = process.argv.slice(2);
61
- if (!recordArgument || !/^[0-9a-f]{40}$/.test(commit ?? '') || !projectName) {
62
- fail('expected <record-path> <full-commit-sha> <project-name>.');
63
- }
61
+ function main(argv) {
62
+ const [recordArgument, commit, projectName] = argv;
63
+ if (!recordArgument || !/^[0-9a-f]{40}$/.test(commit ?? '') || !projectName) {
64
+ fail('expected <record-path> <full-commit-sha> <project-name>.');
65
+ }
64
66
 
65
- const unresolvedRecordPath = resolve(recordArgument);
66
- if (!existsSync(unresolvedRecordPath)) {
67
- fail('record path does not exist; restore the synced discovery record.');
68
- }
69
- const recordPath = realpathSync(unresolvedRecordPath);
70
- const repoRoot = realpathSync(
71
- git(dirname(recordPath), ['rev-parse', '--show-toplevel']),
72
- );
73
- const recordRelative = relative(repoRoot, recordPath);
74
- if (
75
- recordRelative === '' ||
76
- recordRelative === '..' ||
77
- recordRelative.startsWith(`..${sep}`) ||
78
- isAbsolute(recordRelative)
79
- ) {
80
- fail('record path is outside the repository root.');
81
- }
82
- const gitRecordPath = recordRelative.split(sep).join('/');
83
- if (basename(gitRecordPath, extname(gitRecordPath)) !== projectName) {
84
- fail('record filename must match the project name.');
85
- }
86
- if (!gitSucceeds(repoRoot, ['merge-base', '--is-ancestor', commit, 'HEAD'])) {
87
- fail('receipt commit must be an ancestor of the current repository HEAD.');
88
- }
89
- const changedPaths = git(repoRoot, [
90
- 'diff-tree',
91
- '--root',
92
- '--no-commit-id',
93
- '--name-only',
94
- '-r',
95
- '--format=',
96
- commit,
97
- ])
98
- .split('\n')
99
- .filter(Boolean);
100
- if (changedPaths.length !== 1 || changedPaths[0] !== gitRecordPath) {
101
- fail('receipt commit must change exactly the synced discovery record path.');
102
- }
103
- const expectedSubject = `chore(oat): complete synced project ${projectName}`;
104
- const subject = git(repoRoot, ['show', '-s', '--format=%s', commit]);
105
- if (subject !== expectedSubject) {
106
- fail(`commit subject must be "${expectedSubject}".`);
107
- }
67
+ const unresolvedRecordPath = resolve(recordArgument);
68
+ if (!existsSync(unresolvedRecordPath)) {
69
+ fail('record path does not exist; restore the synced discovery record.');
70
+ }
71
+ const recordPath = realpathSync(unresolvedRecordPath);
72
+ const repoRoot = realpathSync(
73
+ git(dirname(recordPath), ['rev-parse', '--show-toplevel']),
74
+ );
75
+ const recordRelative = relative(repoRoot, recordPath);
76
+ if (
77
+ recordRelative === '' ||
78
+ recordRelative === '..' ||
79
+ recordRelative.startsWith(`..${sep}`) ||
80
+ isAbsolute(recordRelative)
81
+ ) {
82
+ fail('record path is outside the repository root.');
83
+ }
84
+ const gitRecordPath = recordRelative.split(sep).join('/');
85
+ if (basename(gitRecordPath, extname(gitRecordPath)) !== projectName) {
86
+ fail('record filename must match the project name.');
87
+ }
88
+ if (!gitSucceeds(repoRoot, ['merge-base', '--is-ancestor', commit, 'HEAD'])) {
89
+ fail('receipt commit must be an ancestor of the current repository HEAD.');
90
+ }
91
+ const changedPaths = git(repoRoot, [
92
+ 'diff-tree',
93
+ '--root',
94
+ '--no-commit-id',
95
+ '--name-only',
96
+ '-r',
97
+ '--format=',
98
+ commit,
99
+ ])
100
+ .split('\n')
101
+ .filter(Boolean);
102
+ if (changedPaths.length !== 1 || changedPaths[0] !== gitRecordPath) {
103
+ fail(
104
+ 'receipt commit must change exactly the synced discovery record path.',
105
+ );
106
+ }
107
+ const expectedSubject = `chore(oat): complete synced project ${projectName}`;
108
+ const subject = git(repoRoot, ['show', '-s', '--format=%s', commit]);
109
+ if (subject !== expectedSubject) {
110
+ fail(`commit subject must be "${expectedSubject}".`);
111
+ }
108
112
 
109
- let committedRecordContents;
110
- let record;
111
- try {
112
- committedRecordContents = gitRaw(repoRoot, [
113
- 'show',
114
- `${commit}:${gitRecordPath}`,
113
+ let committedRecordContents;
114
+ let record;
115
+ try {
116
+ committedRecordContents = gitRaw(repoRoot, [
117
+ 'show',
118
+ `${commit}:${gitRecordPath}`,
119
+ ]);
120
+ record = JSON.parse(committedRecordContents);
121
+ } catch (error) {
122
+ fail(
123
+ `committed record is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
124
+ );
125
+ }
126
+ if (readFileSync(recordPath, 'utf8') !== committedRecordContents) {
127
+ fail('working-tree record content must exactly match the receipt commit.');
128
+ }
129
+ const expectedKeys = new Set([
130
+ 'archiveSnapshot',
131
+ 'completedAt',
132
+ 'createdAt',
133
+ 'ref',
134
+ 'remote',
135
+ 'schemaVersion',
136
+ 'scope',
137
+ 'slug',
138
+ 'status',
115
139
  ]);
116
- record = JSON.parse(committedRecordContents);
117
- } catch (error) {
118
- fail(
119
- `committed record is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
120
- );
121
- }
122
- if (readFileSync(recordPath, 'utf8') !== committedRecordContents) {
123
- fail('working-tree record content must exactly match the receipt commit.');
140
+ if (
141
+ !record ||
142
+ typeof record !== 'object' ||
143
+ Array.isArray(record) ||
144
+ Object.keys(record).some((key) => !expectedKeys.has(key)) ||
145
+ record.schemaVersion !== 1 ||
146
+ record.slug !== projectName ||
147
+ record.scope !== 'synced' ||
148
+ record.ref !== `refs/oat/projects/${projectName}` ||
149
+ record.remote !== 'origin' ||
150
+ record.status !== 'complete' ||
151
+ typeof record.createdAt !== 'string' ||
152
+ Number.isNaN(Date.parse(record.createdAt)) ||
153
+ typeof record.completedAt !== 'string' ||
154
+ Number.isNaN(Date.parse(record.completedAt)) ||
155
+ (record.archiveSnapshot !== undefined &&
156
+ (typeof record.archiveSnapshot !== 'string' ||
157
+ record.archiveSnapshot.length === 0))
158
+ ) {
159
+ fail(
160
+ 'committed record must be a complete synced record with the exact project identity and lifecycle content.',
161
+ );
162
+ }
163
+
164
+ process.stdout.write(`${commit}\n`);
124
165
  }
125
- const expectedKeys = new Set([
126
- 'archiveSnapshot',
127
- 'completedAt',
128
- 'createdAt',
129
- 'ref',
130
- 'remote',
131
- 'schemaVersion',
132
- 'scope',
133
- 'slug',
134
- 'status',
135
- ]);
136
- if (
137
- !record ||
138
- typeof record !== 'object' ||
139
- Array.isArray(record) ||
140
- Object.keys(record).some((key) => !expectedKeys.has(key)) ||
141
- record.schemaVersion !== 1 ||
142
- record.slug !== projectName ||
143
- record.scope !== 'synced' ||
144
- record.ref !== `refs/oat/projects/${projectName}` ||
145
- record.remote !== 'origin' ||
146
- record.status !== 'complete' ||
147
- typeof record.createdAt !== 'string' ||
148
- Number.isNaN(Date.parse(record.createdAt)) ||
149
- typeof record.completedAt !== 'string' ||
150
- Number.isNaN(Date.parse(record.completedAt)) ||
151
- (record.archiveSnapshot !== undefined &&
152
- (typeof record.archiveSnapshot !== 'string' ||
153
- record.archiveSnapshot.length === 0))
154
- ) {
155
- fail(
156
- 'committed record must be a complete synced record with the exact project identity and lifecycle content.',
157
- );
166
+
167
+ /**
168
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
169
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
170
+ * that exits 0 whenever the skill is reached through a symlinked install root,
171
+ * and canonicalizing only one side has the same effect under
172
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
173
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
174
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
175
+ * `false`; it never masks a direct run.
176
+ */
177
+ function isDirectInvocation(invokedPath) {
178
+ if (!invokedPath) return false;
179
+ try {
180
+ return (
181
+ realpathSync(fileURLToPath(import.meta.url)) ===
182
+ realpathSync(resolve(invokedPath))
183
+ );
184
+ } catch {
185
+ return false;
186
+ }
158
187
  }
159
188
 
160
- process.stdout.write(`${commit}\n`);
189
+ if (isDirectInvocation(process.argv[1])) {
190
+ main(process.argv.slice(2));
191
+ }
@@ -7,7 +7,7 @@ disable-model-invocation: true
7
7
  user-invocable: true
8
8
  allowed-tools: Read, Write, Bash(git:*), Bash(oat:*), Bash(oat project log:*), Glob, Grep, AskUserQuestion, Task
9
9
  metadata:
10
- version: 2.3.9
10
+ version: 2.3.10
11
11
  ---
12
12
 
13
13
  # Implementation Phase
@@ -1051,6 +1051,9 @@ async function main(argv) {
1051
1051
  * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`. Either
1052
1052
  * way the caller reads "exited 0" as "verified" — the fail-open shape the
1053
1053
  * contracts exist to prevent.
1054
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
1055
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
1056
+ * `false`; it never masks a direct run.
1054
1057
  */
1055
1058
  function isDirectInvocation(invokedPath) {
1056
1059
  if (!invokedPath) return false;
@@ -6,7 +6,7 @@ disable-model-invocation: true
6
6
  user-invocable: true
7
7
  allowed-tools: Read, Write, Glob, Grep, Bash, AskUserQuestion, Agent, mcp__*
8
8
  metadata:
9
- version: 1.1.2
9
+ version: 1.1.3
10
10
  ---
11
11
 
12
12
  # Recon
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { realpathSync } from 'node:fs';
3
4
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
5
  import { dirname, relative, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
8
9
  import { issue, isObject, validateArtifactShape } from './lib/contracts.mjs';
@@ -441,10 +442,29 @@ async function main(argv) {
441
442
  process.stdout.write(`${JSON.stringify(reference, null, 2)}\n`);
442
443
  }
443
444
 
444
- if (
445
- process.argv[1] &&
446
- import.meta.url === pathToFileURL(process.argv[1]).href
447
- ) {
445
+ /**
446
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
447
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
448
+ * that exits 0 whenever the skill is reached through a symlinked install root,
449
+ * and canonicalizing only one side has the same effect under
450
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
451
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
452
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
453
+ * `false`; it never masks a direct run.
454
+ */
455
+ function isDirectInvocation(invokedPath) {
456
+ if (!invokedPath) return false;
457
+ try {
458
+ return (
459
+ realpathSync(fileURLToPath(import.meta.url)) ===
460
+ realpathSync(resolve(invokedPath))
461
+ );
462
+ } catch {
463
+ return false;
464
+ }
465
+ }
466
+
467
+ if (isDirectInvocation(process.argv[1])) {
448
468
  main(process.argv.slice(2)).catch((error) => {
449
469
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
450
470
  process.exitCode = 1;
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { realpathSync } from 'node:fs';
3
4
  import { readFile } from 'node:fs/promises';
4
- import { pathToFileURL } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
5
7
 
6
8
  import {
7
9
  checkApprovedWaveTarget,
@@ -79,7 +81,29 @@ export async function main(argv = process.argv.slice(2)) {
79
81
  );
80
82
  }
81
83
 
82
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
84
+ /**
85
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
86
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
87
+ * that exits 0 whenever the skill is reached through a symlinked install root,
88
+ * and canonicalizing only one side has the same effect under
89
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
90
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
91
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
92
+ * `false`; it never masks a direct run.
93
+ */
94
+ function isDirectInvocation(invokedPath) {
95
+ if (!invokedPath) return false;
96
+ try {
97
+ return (
98
+ realpathSync(fileURLToPath(import.meta.url)) ===
99
+ realpathSync(resolve(invokedPath))
100
+ );
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ if (isDirectInvocation(process.argv[1])) {
83
107
  main().catch((error) => {
84
108
  const code = error instanceof RoutingContractError ? error.code : 'ERROR';
85
109
  process.stderr.write(`${code}: ${error.message}\n`);
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
+ import { realpathSync } from 'node:fs';
4
5
  import { lstat, open, rename, rm } from 'node:fs/promises';
5
6
  import { join, resolve } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
7
+ import { fileURLToPath } from 'node:url';
7
8
 
8
9
  import { hashFile } from './lib/canonical-json.mjs';
9
10
  import {
@@ -436,10 +437,29 @@ async function main(argv) {
436
437
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
437
438
  }
438
439
 
439
- if (
440
- process.argv[1] &&
441
- import.meta.url === pathToFileURL(process.argv[1]).href
442
- ) {
440
+ /**
441
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
442
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
443
+ * that exits 0 whenever the skill is reached through a symlinked install root,
444
+ * and canonicalizing only one side has the same effect under
445
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
446
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
447
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
448
+ * `false`; it never masks a direct run.
449
+ */
450
+ function isDirectInvocation(invokedPath) {
451
+ if (!invokedPath) return false;
452
+ try {
453
+ return (
454
+ realpathSync(fileURLToPath(import.meta.url)) ===
455
+ realpathSync(resolve(invokedPath))
456
+ );
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ if (isDirectInvocation(process.argv[1])) {
443
463
  main(process.argv.slice(2)).catch((error) => {
444
464
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
445
465
  process.exitCode = 1;
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { realpathSync } from 'node:fs';
3
4
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { basename, join, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  import { validateArtifactShape } from './lib/contracts.mjs';
8
9
  import {
@@ -85,10 +86,29 @@ async function main(argv) {
85
86
  process.exitCode = result.valid ? 0 : 1;
86
87
  }
87
88
 
88
- if (
89
- process.argv[1] &&
90
- import.meta.url === pathToFileURL(process.argv[1]).href
91
- ) {
89
+ /**
90
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
91
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
92
+ * that exits 0 whenever the skill is reached through a symlinked install root,
93
+ * and canonicalizing only one side has the same effect under
94
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
95
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
96
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
97
+ * `false`; it never masks a direct run.
98
+ */
99
+ function isDirectInvocation(invokedPath) {
100
+ if (!invokedPath) return false;
101
+ try {
102
+ return (
103
+ realpathSync(fileURLToPath(import.meta.url)) ===
104
+ realpathSync(resolve(invokedPath))
105
+ );
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ if (isDirectInvocation(process.argv[1])) {
92
112
  main(process.argv.slice(2)).catch((error) => {
93
113
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
94
114
  process.exitCode = 2;
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { realpathSync } from 'node:fs';
3
4
  import { readFile, rm } from 'node:fs/promises';
4
5
  import { join, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
6
+ import { fileURLToPath } from 'node:url';
6
7
 
7
8
  import { hashCanonicalJson, hashFile, sha256 } from './lib/canonical-json.mjs';
8
9
  import {
@@ -2598,10 +2599,29 @@ async function main(argv) {
2598
2599
  process.exitCode = result.publishable ? 0 : 1;
2599
2600
  }
2600
2601
 
2601
- if (
2602
- process.argv[1] &&
2603
- import.meta.url === pathToFileURL(process.argv[1]).href
2604
- ) {
2602
+ /**
2603
+ * Direct invocation, compared as canonical paths on both sides. Comparing a raw
2604
+ * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
2605
+ * that exits 0 whenever the skill is reached through a symlinked install root,
2606
+ * and canonicalizing only one side has the same effect under
2607
+ * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
2608
+ * A path that cannot be canonicalized is not a module Node loaded as the entry
2609
+ * point, so a thrown `realpathSync` means "not invoked directly" and returns
2610
+ * `false`; it never masks a direct run.
2611
+ */
2612
+ function isDirectInvocation(invokedPath) {
2613
+ if (!invokedPath) return false;
2614
+ try {
2615
+ return (
2616
+ realpathSync(fileURLToPath(import.meta.url)) ===
2617
+ realpathSync(resolve(invokedPath))
2618
+ );
2619
+ } catch {
2620
+ return false;
2621
+ }
2622
+ }
2623
+
2624
+ if (isDirectInvocation(process.argv[1])) {
2605
2625
  main(process.argv.slice(2)).catch((error) => {
2606
2626
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
2607
2627
  process.exitCode = 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.2.76"
37
+ "@open-agent-toolkit/control-plane": "0.2.77"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",