@method-ai-hq/sdk 0.0.0-stage → 0.14.3

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -2
  3. package/dist/packages/contracts/src/identity.d.ts +16 -0
  4. package/dist/packages/contracts/src/identity.js +41 -0
  5. package/dist/packages/contracts/src/method-package.d.ts +71 -0
  6. package/dist/packages/contracts/src/method-package.js +22 -0
  7. package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
  8. package/dist/packages/method-document/src/conversion-report.js +23 -0
  9. package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
  10. package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
  11. package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
  12. package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
  13. package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
  14. package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
  15. package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
  16. package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
  17. package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
  18. package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
  19. package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
  20. package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
  21. package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
  22. package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
  23. package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
  24. package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
  25. package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
  26. package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
  27. package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
  28. package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
  29. package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
  30. package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
  31. package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
  32. package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
  33. package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
  34. package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
  35. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
  36. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
  37. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
  38. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
  39. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
  40. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
  41. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
  42. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
  43. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
  44. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
  45. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
  46. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
  47. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
  48. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
  49. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
  50. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
  51. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
  52. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
  53. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
  54. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
  55. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
  56. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
  57. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
  58. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
  59. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
  60. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
  61. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
  62. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
  63. package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
  64. package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
  65. package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
  66. package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
  67. package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
  68. package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
  69. package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
  70. package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
  71. package/dist/packages/sdk/examples/files.json +126 -0
  72. package/dist/packages/sdk/examples/message-routing/README.md +23 -0
  73. package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
  74. package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
  75. package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
  76. package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
  77. package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
  78. package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
  79. package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
  80. package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
  81. package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
  82. package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
  83. package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
  84. package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
  85. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
  86. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
  87. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
  88. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
  89. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
  90. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
  91. package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
  92. package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
  93. package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
  94. package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
  95. package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
  96. package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
  97. package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
  98. package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
  99. package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
  100. package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
  101. package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
  102. package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
  103. package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
  104. package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
  105. package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
  106. package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
  107. package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
  108. package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
  109. package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
  110. package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
  111. package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
  112. package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
  113. package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
  114. package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
  115. package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
  116. package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
  117. package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
  118. package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
  119. package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
  120. package/dist/packages/sdk/examples/support-triage/README.md +77 -0
  121. package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
  122. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
  123. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
  124. package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
  125. package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
  126. package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
  127. package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
  128. package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
  129. package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
  130. package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
  131. package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
  132. package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
  133. package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
  134. package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
  135. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
  136. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
  137. package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
  138. package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
  139. package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
  140. package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
  141. package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
  142. package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
  143. package/dist/packages/sdk/package.json +232 -0
  144. package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
  145. package/dist/packages/sdk/src/authoring-example.js +50 -0
  146. package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
  147. package/dist/packages/sdk/src/authoring-instructions.js +151 -0
  148. package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
  149. package/dist/packages/sdk/src/authoring-prompt.js +8 -0
  150. package/dist/packages/sdk/src/authoring.d.ts +10 -0
  151. package/dist/packages/sdk/src/authoring.js +287 -0
  152. package/dist/packages/sdk/src/bindings.d.ts +4 -0
  153. package/dist/packages/sdk/src/bindings.js +124 -0
  154. package/dist/packages/sdk/src/bridge.d.ts +2 -0
  155. package/dist/packages/sdk/src/bridge.js +23 -0
  156. package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
  157. package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
  158. package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
  159. package/dist/packages/sdk/src/browser-service.py +127 -0
  160. package/dist/packages/sdk/src/browser.d.ts +38 -0
  161. package/dist/packages/sdk/src/browser.js +203 -0
  162. package/dist/packages/sdk/src/capabilities.d.ts +13 -0
  163. package/dist/packages/sdk/src/capabilities.js +68 -0
  164. package/dist/packages/sdk/src/classification-client.d.ts +4 -0
  165. package/dist/packages/sdk/src/classification-client.js +27 -0
  166. package/dist/packages/sdk/src/cloud.d.ts +23 -0
  167. package/dist/packages/sdk/src/cloud.js +43 -0
  168. package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
  169. package/dist/packages/sdk/src/computer-settings.js +57 -0
  170. package/dist/packages/sdk/src/connect.d.ts +47 -0
  171. package/dist/packages/sdk/src/connect.js +203 -0
  172. package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
  173. package/dist/packages/sdk/src/current-inspection.js +146 -0
  174. package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
  175. package/dist/packages/sdk/src/current-runtime.js +167 -0
  176. package/dist/packages/sdk/src/doctor.d.ts +17 -0
  177. package/dist/packages/sdk/src/doctor.js +68 -0
  178. package/dist/packages/sdk/src/executable.d.ts +2 -0
  179. package/dist/packages/sdk/src/executable.js +22 -0
  180. package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
  181. package/dist/packages/sdk/src/explain-effects.js +192 -0
  182. package/dist/packages/sdk/src/explain.d.ts +115 -0
  183. package/dist/packages/sdk/src/explain.js +441 -0
  184. package/dist/packages/sdk/src/files.d.ts +9 -0
  185. package/dist/packages/sdk/src/files.js +50 -0
  186. package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
  187. package/dist/packages/sdk/src/guard-keys.js +42 -0
  188. package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
  189. package/dist/packages/sdk/src/hosted-models.js +81 -0
  190. package/dist/packages/sdk/src/improve.d.ts +55 -0
  191. package/dist/packages/sdk/src/improve.js +159 -0
  192. package/dist/packages/sdk/src/index.d.ts +13 -0
  193. package/dist/packages/sdk/src/index.js +13 -0
  194. package/dist/packages/sdk/src/inspect.d.ts +6 -0
  195. package/dist/packages/sdk/src/inspect.js +17 -0
  196. package/dist/packages/sdk/src/link.d.ts +12 -0
  197. package/dist/packages/sdk/src/link.js +53 -0
  198. package/dist/packages/sdk/src/local-cli.d.ts +24 -0
  199. package/dist/packages/sdk/src/local-cli.js +146 -0
  200. package/dist/packages/sdk/src/local-setup.d.ts +15 -0
  201. package/dist/packages/sdk/src/local-setup.js +42 -0
  202. package/dist/packages/sdk/src/method-client.d.ts +30 -0
  203. package/dist/packages/sdk/src/method-client.js +250 -0
  204. package/dist/packages/sdk/src/method-files.d.ts +29 -0
  205. package/dist/packages/sdk/src/method-files.js +93 -0
  206. package/dist/packages/sdk/src/method-help.d.ts +19 -0
  207. package/dist/packages/sdk/src/method-help.js +404 -0
  208. package/dist/packages/sdk/src/method-issues.d.ts +86 -0
  209. package/dist/packages/sdk/src/method-issues.js +192 -0
  210. package/dist/packages/sdk/src/method-sync.d.ts +46 -0
  211. package/dist/packages/sdk/src/method-sync.js +293 -0
  212. package/dist/packages/sdk/src/method.d.ts +5 -0
  213. package/dist/packages/sdk/src/method.js +359 -0
  214. package/dist/packages/sdk/src/model-checks.d.ts +96 -0
  215. package/dist/packages/sdk/src/model-checks.js +181 -0
  216. package/dist/packages/sdk/src/outbox.d.ts +120 -0
  217. package/dist/packages/sdk/src/outbox.js +297 -0
  218. package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
  219. package/dist/packages/sdk/src/own-model-key.js +76 -0
  220. package/dist/packages/sdk/src/prepare.d.ts +24 -0
  221. package/dist/packages/sdk/src/prepare.js +245 -0
  222. package/dist/packages/sdk/src/process.d.ts +15 -0
  223. package/dist/packages/sdk/src/process.js +107 -0
  224. package/dist/packages/sdk/src/production.d.ts +182 -0
  225. package/dist/packages/sdk/src/production.js +284 -0
  226. package/dist/packages/sdk/src/progress.d.ts +12 -0
  227. package/dist/packages/sdk/src/progress.js +48 -0
  228. package/dist/packages/sdk/src/proposals.d.ts +73 -0
  229. package/dist/packages/sdk/src/proposals.js +284 -0
  230. package/dist/packages/sdk/src/quality.d.ts +82 -0
  231. package/dist/packages/sdk/src/quality.js +294 -0
  232. package/dist/packages/sdk/src/result-files.d.ts +5 -0
  233. package/dist/packages/sdk/src/result-files.js +149 -0
  234. package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
  235. package/dist/packages/sdk/src/result-transfer.js +37 -0
  236. package/dist/packages/sdk/src/run-data.d.ts +11 -0
  237. package/dist/packages/sdk/src/run-data.js +80 -0
  238. package/dist/packages/sdk/src/run-method.d.ts +2 -0
  239. package/dist/packages/sdk/src/run-method.js +2 -0
  240. package/dist/packages/sdk/src/run-saved.d.ts +5 -0
  241. package/dist/packages/sdk/src/run-saved.js +138 -0
  242. package/dist/packages/sdk/src/run-worker.d.ts +6 -0
  243. package/dist/packages/sdk/src/run-worker.js +131 -0
  244. package/dist/packages/sdk/src/secrets.d.ts +37 -0
  245. package/dist/packages/sdk/src/secrets.js +196 -0
  246. package/dist/packages/sdk/src/update.d.ts +5 -0
  247. package/dist/packages/sdk/src/update.js +39 -0
  248. package/dist/packages/sdk/src/versions.d.ts +58 -0
  249. package/dist/packages/sdk/src/versions.js +144 -0
  250. package/dist/packages/sdk/src/worker.d.ts +120 -0
  251. package/dist/packages/sdk/src/worker.js +443 -0
  252. package/dist/packages/workflow-language/src/execution.d.ts +1 -0
  253. package/dist/packages/workflow-language/src/execution.js +1 -0
  254. package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
  255. package/dist/packages/workflow-language/src/inspection.js +130 -0
  256. package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
  257. package/dist/packages/workflow-language/src/result-files.js +67 -0
  258. package/dist/packages/workflow-language/src/schema.d.ts +207 -0
  259. package/dist/packages/workflow-language/src/schema.js +56 -0
  260. package/dist/packages/workflow-language/src/validate.d.ts +11 -0
  261. package/dist/packages/workflow-language/src/validate.js +77 -0
  262. package/package.json +230 -4
@@ -0,0 +1,294 @@
1
+ /** Effect observation, recorded cases, the save gate, and library observers. */
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createHash } from "node:crypto";
7
+ import { parseArgs } from "node:util";
8
+ import { observeRun, pendingRuns } from "@withmethod/runtime/observe.js";
9
+ import { createCase, retireCase, listCases, testSuite } from "@withmethod/runtime/cases.js";
10
+ import { authoringPath, editDocument, readDocument } from "./authoring.js";
11
+ import { localSetup } from "./local-setup.js";
12
+ import { resolveAgentProfiles, checkAgents, accountNeeds } from "./capabilities.js";
13
+ import { hostedModelsFor, managedModels } from "./hosted-models.js";
14
+ import { managedClassification } from "./classification-client.js";
15
+ import { MethodClient } from "./method-client.js";
16
+ import { prepareRuntime, methodCache } from "./prepare.js";
17
+ import { writePrivateJson } from "./files.js";
18
+ const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + "\n");
19
+ const json = (path) => path ? JSON.parse(readFileSync(authoringPath(path), "utf8")) : undefined;
20
+ /** The same runtime preparation as method run, without starting a run. Hosted models are used when signed in. */
21
+ export async function preparedConfig(file, flags, options = {}) {
22
+ const setup = await localSetup(file, flags);
23
+ const method = readDocument(file);
24
+ const config = setup.config;
25
+ const client = new MethodClient();
26
+ let hostedModel;
27
+ const signedIn = (() => { try {
28
+ return !!client.token();
29
+ }
30
+ catch {
31
+ return false;
32
+ } })();
33
+ // A rubric judge is a model step too: without a local agent, a signed-in computer judges with the account's model.
34
+ const judge = options.judge && !setup.agent && !config.models?.judge;
35
+ if ((accountNeeds(method, config, setup.agent).models || judge) && signedIn) {
36
+ try {
37
+ hostedModel = await managedModels(client).model();
38
+ }
39
+ catch (error) {
40
+ process.stderr.write(`Hosted models are unavailable (${error.message}). Using a local agent.\n`);
41
+ }
42
+ }
43
+ config.models = await resolveAgentProfiles(method, config, setup.agent, hostedModel);
44
+ if (judge && hostedModel)
45
+ config.models.judge = { backend: "method", model: hostedModel };
46
+ // Classify steps that run live use the account's classifier, as method run does.
47
+ const classification = accountNeeds(method, config, setup.agent).classification && signedIn ? managedClassification(client) : undefined;
48
+ if (classification)
49
+ config.classification = await classification.resolve(new AbortController().signal);
50
+ await checkAgents(config.models);
51
+ const prepared = await prepareRuntime(setup.sourceRoot, config, method);
52
+ const hostedModels = Object.values(prepared.config.models ?? {}).some((profile) => profile.backend === 'method') ? await hostedModelsFor(client) : undefined;
53
+ return { config: prepared.config, sourceRoot: setup.sourceRoot, processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, ...(hostedModels ? { hostedModels } : {}), ...(classification ? { classification } : {}) };
54
+ }
55
+ /** Run directories that this computer's Method runs use by default. */
56
+ export const defaultRunRoots = () => [resolve(".method-runs"), join(methodCache(), "runs")];
57
+ export async function observeCommand(args) {
58
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { pending: { type: "string", multiple: true }, sync: { type: "boolean" } } });
59
+ const dirs = positionals.map(dir => authoringPath(dir));
60
+ if (!dirs.length)
61
+ for (const root of values.pending ?? defaultRunRoots())
62
+ dirs.push(...await pendingRuns(authoringPath(root)));
63
+ const runs = [];
64
+ for (const dir of dirs) {
65
+ try {
66
+ const result = await observeRun(dir, {});
67
+ let dashboard;
68
+ // A late observation can change a synced run; upload the new record so the dashboard shows it.
69
+ if (result.changed && existsSync(join(dir, "method-sync.json")) && values.sync !== false) {
70
+ try {
71
+ const { MethodSync } = await import("./method-sync.js");
72
+ await MethodSync.retry(dir);
73
+ dashboard = "saved";
74
+ }
75
+ catch (error) {
76
+ dashboard = `not saved: ${error.message}. Retry with method sync ${dir}`;
77
+ }
78
+ }
79
+ runs.push({ run_dir: dir, status: result.status, changed: result.changed, observed: result.observed.map((e) => ({ effect: e.effect, verdict: e.verdict, reason: e.reason })),
80
+ next_observation_at: result.effects?.next_observation_at ?? null, ...(dashboard ? { dashboard } : {}) });
81
+ }
82
+ catch (error) {
83
+ runs.push({ run_dir: dir, error: error.message, code: error.code ?? "observe_failed" });
84
+ }
85
+ }
86
+ print({ runs, note: dirs.length ? undefined : "No run has open effects." });
87
+ if (runs.some(run => "error" in run))
88
+ process.exitCode = 1;
89
+ }
90
+ /** A fingerprint of a Method and the files it runs, to tell whether its cases were checked since the last change. */
91
+ export function methodFingerprint(file) {
92
+ const folder = dirname(file), method = readDocument(file);
93
+ const parts = [readFileSync(file, "utf8")];
94
+ const paths = new Set(method.files ?? []);
95
+ for (const step of Object.values(method.steps ?? {})) {
96
+ for (const exec of [step.do, step.check])
97
+ if (exec?.kind === "run")
98
+ paths.add(exec.entrypoint);
99
+ for (const effect of Object.values(step.effects ?? {}))
100
+ for (const exec of [effect.observe, effect.judge])
101
+ if (exec?.kind === "run")
102
+ paths.add(exec.entrypoint);
103
+ }
104
+ for (const path of [...paths].sort()) {
105
+ try {
106
+ parts.push(path, readFileSync(join(folder, path), "utf8"));
107
+ }
108
+ catch {
109
+ parts.push(path, "missing");
110
+ }
111
+ }
112
+ return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
113
+ }
114
+ const checkedFile = (file) => join(methodCache(), "checked", createHash("sha256").update(file).digest("hex") + ".json");
115
+ function recordChecked(file, report) {
116
+ mkdirSync(dirname(checkedFile(file)), { recursive: true, mode: 0o700 });
117
+ writePrivateJson(checkedFile(file), { file, fingerprint: methodFingerprint(file), passed: report.passed, cases: report.cases.length, at: new Date().toISOString() });
118
+ }
119
+ /** One line for method run when the Method changed since its cases last passed. */
120
+ export async function casesNotice(file) {
121
+ const cases = (await listCases(file)).filter((c) => c.status === "active");
122
+ if (!cases.length)
123
+ return undefined;
124
+ let checked;
125
+ try {
126
+ checked = JSON.parse(readFileSync(checkedFile(file), "utf8"));
127
+ }
128
+ catch { /* never checked */ }
129
+ if (checked?.passed && checked.fingerprint === methodFingerprint(file))
130
+ return undefined;
131
+ return `${cases.length} case${cases.length === 1 ? "" : "s"} not checked since this Method changed: method test ${file}`;
132
+ }
133
+ /** The Method's declared secrets from this computer or the environment, as method run gives them. */
134
+ async function declaredSecrets(file) {
135
+ const { resolveSecrets } = await import("./secrets.js");
136
+ return resolveSecrets(Object.keys(readDocument(file)?.secrets ?? {}));
137
+ }
138
+ /** Run the cases of a Method with the same preparation as method run. */
139
+ export async function runCases(file, flags, options = {}) {
140
+ const prepared = await preparedConfig(file, flags, { judge: true });
141
+ const report = await testSuite(file, prepared.config, { ...options,
142
+ runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}), ...(flags.agent ? { agent: flags.agent } : {}) } });
143
+ if (!options.ids && !options.casesDir)
144
+ recordChecked(file, report);
145
+ return report;
146
+ }
147
+ export async function testCommand(args) {
148
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, agent: { type: "string" } } });
149
+ const file = positionals[0];
150
+ if (!file || positionals.length !== 1)
151
+ throw Error("Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]...");
152
+ const report = await runCases(authoringPath(file), values, { casesDir: values.cases ? authoringPath(values.cases) : undefined, ids: values.case,
153
+ baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [] });
154
+ print(report);
155
+ process.exitCode = report.passed ? 0 : 1;
156
+ }
157
+ /**
158
+ * The save gate: a version that breaks an approved case is refused, unless each failing case is accepted with a
159
+ * reason. Returns a line for the version's record.
160
+ */
161
+ export async function caseGate(file, accept = [], reason) {
162
+ const cases = (await listCases(file)).filter((c) => c.status === "active");
163
+ if (!cases.length)
164
+ return { line: undefined, report: undefined };
165
+ // A save checks every case; say so first, because live model steps and judges take time and cost money.
166
+ process.stderr.write(`Checking ${cases.length} case${cases.length === 1 ? "" : "s"} before saving. Cases whose model steps changed run them live, up to three times each.\n`);
167
+ const report = await runCases(file, {});
168
+ const t = report.totals;
169
+ process.stderr.write(`Cases checked in ${Math.round(t.duration_ms / 1000)} s: ${t.live_model_steps} live model step${t.live_model_steps === 1 ? "" : "s"}, ${t.judge_calls} judge call${t.judge_calls === 1 ? "" : "s"}.\n`);
170
+ const failing = report.cases.filter((c) => !["pass", "fixed"].includes(c.verdict));
171
+ const unaccepted = failing.filter((c) => !accept.includes(c.id));
172
+ if (unaccepted.length)
173
+ throw Object.assign(Error(`This version breaks ${unaccepted.length} approved case${unaccepted.length === 1 ? "" : "s"}: ${unaccepted.map((c) => `${c.id} (${c.verdict}: ${c.note})`).join("; ")}. Fix the Method and run method test, or retire a case whose rule no longer applies with method case retire. To publish anyway, add --accept-failing-case ${unaccepted.map((c) => c.id).join(",")} with --reason.`), { code: "cases_failed" });
174
+ if (failing.length && !reason)
175
+ throw Error("Give --reason for accepting a failing case.");
176
+ return { report, line: failing.length ? `Cases: ${report.cases.length - failing.length} of ${report.cases.length} passed; accepted failing ${failing.map((c) => c.id).join(", ")}.` : `Cases: ${report.cases.length} of ${report.cases.length} passed.` };
177
+ }
178
+ export async function caseCommand(args) {
179
+ const text = { type: "string" };
180
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { run: text, "passing-run": text, id: text, note: text, rubric: { type: "string", multiple: true }, ref: text, context: { type: "string", multiple: true }, expect: text,
181
+ observations: text, redact: text, runs: text, "min-pass": text, author: text, reason: text, by: text, cases: text, workspace: text, agent: text,
182
+ "retention-days": text, supersedes: { type: "string", multiple: true } } });
183
+ const [action, file, id] = positionals;
184
+ if (!file || !["new", "retire", "list"].includes(action ?? ""))
185
+ throw Error("Use method case new|retire|list FILE. See method help case new.");
186
+ const methodFile = authoringPath(file);
187
+ const casesDir = values.cases ? authoringPath(values.cases) : undefined;
188
+ const integer = (value, name) => { if (value === undefined)
189
+ return undefined; const n = Number(value); if (!Number.isSafeInteger(n))
190
+ throw Error(`${name} must be an integer.`); return n; };
191
+ if (action === "list")
192
+ return print((await listCases(methodFile, casesDir)).map(({ id, status, note, created, superseded_by, retention_until }) => ({ id, status, note, created, superseded_by, retention_until })));
193
+ if (action === "retire") {
194
+ if (!id || !values.reason)
195
+ throw Error("Supply the case ID and --reason.");
196
+ await retireCase(methodFile, id, { by: values.by ?? null, reason: values.reason, casesDir });
197
+ return print({ retired: id });
198
+ }
199
+ if (!values.id || !values.note || (!values.run && !values["passing-run"]))
200
+ throw Error('Supply --id, --note "the person\'s words", and --run (the run that went wrong) or --passing-run (a run that was right).');
201
+ if (!values.rubric?.length && !values.expect)
202
+ throw Error('Say what must be true with --rubric "plain sentence" (repeatable), or give --expect FILE.');
203
+ const prepared = await preparedConfig(methodFile, { ...(values.workspace ? { workspace: values.workspace } : {}), ...(values.agent ? { agent: values.agent } : {}) }, { judge: true });
204
+ const privacy = casesPrivacyWarning(methodFile, casesDir);
205
+ if (privacy)
206
+ process.stderr.write(privacy + "\n");
207
+ print(await createCase({ methodFile, runDir: values.run ? authoringPath(values.run) : undefined, passingRun: values["passing-run"] ? authoringPath(values["passing-run"]) : undefined,
208
+ id: values.id, note: values.note, author: values.author ?? null, rubric: values.rubric ?? [], ref: values.ref, context: values.context ?? [], expect: json(values.expect) ?? [],
209
+ observations: json(values.observations), redact: json(values.redact), runs: integer(values.runs, "--runs"), minPass: integer(values["min-pass"], "--min-pass"),
210
+ retentionDays: integer(values["retention-days"], "--retention-days"), supersedes: values.supersedes ?? [], casesDir, config: prepared.config,
211
+ options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}), ...(values.agent ? { agent: values.agent } : {}) } } }));
212
+ }
213
+ /**
214
+ * A case copies run data (inputs, outputs, written files) into cases/. When that folder would be committed to a Git
215
+ * repository, say so once per case: the repository may be shared.
216
+ */
217
+ export function casesPrivacyWarning(methodFile, casesDir) {
218
+ const folder = casesDir ?? join(dirname(methodFile), "cases");
219
+ try {
220
+ execFileSync("git", ["-C", dirname(methodFile), "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
221
+ }
222
+ catch {
223
+ return undefined;
224
+ }
225
+ try {
226
+ execFileSync("git", ["-C", dirname(methodFile), "check-ignore", "-q", join(folder, "x", "recording.json")], { stdio: "ignore" });
227
+ return undefined;
228
+ }
229
+ catch { /* not ignored */ }
230
+ return `Note: ${folder} keeps copies of run data (inputs, outputs and written files) and is inside a Git repository. Commit it only if that data may be shared with everyone who can read the repository. To keep the data local, add this line to .gitignore: ${relative(dirname(methodFile), folder)}/*/recording.json, and the same for files/ and artifacts/; or use --redact.`;
231
+ }
232
+ const library = () => fileURLToPath(new URL("../observers/", import.meta.url));
233
+ export function observerLibrary() {
234
+ return readdirSync(library()).filter(name => existsSync(join(library(), name, "observer.json"))).sort()
235
+ .map(name => ({ name, ...JSON.parse(readFileSync(join(library(), name, "observer.json"), "utf8")) }));
236
+ }
237
+ /** Copy a reviewed observer into the Method folder and declare the effect on a step. */
238
+ export async function effectCommand(args) {
239
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { observer: { type: "string" }, connection: { type: "string" }, intent: { type: "string" }, in: { type: "string", multiple: true }, set: { type: "string", multiple: true }, horizon: { type: "string" }, blocking: { type: "boolean" } } });
240
+ const [action, file, stepId, name] = positionals;
241
+ if (action === "list")
242
+ return print(observerLibrary().map(({ name, description, needs }) => ({ name, description, needs })));
243
+ if (action !== "add" || !file || !stepId || !name || !values.observer)
244
+ throw Error("Use method effect add FILE STEP NAME --observer OBSERVER [--connection NAME] [--in ALIAS=REF]... [--set KEY=VALUE]... or method effect list.");
245
+ const entry = observerLibrary().find(item => item.name === values.observer);
246
+ if (!entry)
247
+ throw Error(`Unknown observer ${values.observer}. Use method effect list.`);
248
+ const methodFile = authoringPath(file), folder = dirname(methodFile), target = join(folder, "observers", entry.name);
249
+ const connection = values.connection ?? entry.connection.name;
250
+ const bindings = Object.fromEntries((values.in ?? []).map(pair => { const at = pair.indexOf("="); if (at < 1)
251
+ throw Error("Use --in ALIAS=REFERENCE."); return [pair.slice(0, at), pair.slice(at + 1)]; }));
252
+ for (const required of entry.inputs ?? [])
253
+ if (!Object.hasOwn(bindings, required.alias))
254
+ throw Error(`The ${entry.name} observer needs --in ${required.alias}=REFERENCE: ${required.description}`);
255
+ const settings = { connection, ...Object.fromEntries((values.set ?? []).map(pair => { const at = pair.indexOf("="); if (at < 1)
256
+ throw Error("Use --set KEY=VALUE."); return [pair.slice(0, at), pair.slice(at + 1)]; })) };
257
+ // Fill the observer's own placeholders; {token} and {inputs.NAME} stay for the observer to fill at run time.
258
+ const observerArgs = (entry.effect.args ?? []).map((arg) => arg.replace(/\{([a-z_]+)\}/g, (match, key) => {
259
+ if (key === "token")
260
+ return match;
261
+ if (!Object.hasOwn(settings, key))
262
+ throw Error(`The ${entry.name} observer needs --set ${key}=VALUE. ${entry.needs}`);
263
+ return settings[key];
264
+ }));
265
+ const result = editDocument(methodFile, doc => {
266
+ const step = doc.steps?.[stepId];
267
+ if (!step)
268
+ throw Error(`Step not found: ${stepId}.`);
269
+ if (!(step.changes ?? []).some((c) => c.startsWith("environment.")))
270
+ throw Error(`${stepId} changes no environment. Declare the changed connection in changes first.`);
271
+ if (doc.format !== "method/3.4")
272
+ doc.format = "method/3.4";
273
+ doc.environment ??= {};
274
+ const existing = doc.environment[connection];
275
+ if (existing && existing.role !== "observer")
276
+ throw Error(`Environment ${connection} exists and is not an observer connection. Choose --connection NAME.`);
277
+ doc.environment[connection] = { type: entry.connection.type, description: entry.connection.description, role: "observer" };
278
+ const path = (file) => relative(folder, join(target, file)).split("\\").join("/");
279
+ step.effects = { ...step.effects, [name]: {
280
+ intent: values.intent ?? entry.effect.intent, in: bindings,
281
+ observe: { kind: "run", runtime: entry.effect.observe_runtime ?? "node", entrypoint: path("fetch.mjs"), ...(observerArgs.length ? { args: observerArgs } : {}) },
282
+ judge: { kind: "run", runtime: "node", entrypoint: path("judge.mjs") }, fixtures: path("fixtures"),
283
+ schedule: { ...entry.effect.schedule, ...(values.horizon ? { horizon: values.horizon } : {}) }, confirm: entry.effect.confirm,
284
+ ...(values.blocking ? { blocking: true } : {}),
285
+ } };
286
+ return doc;
287
+ });
288
+ if (!existsSync(target)) {
289
+ mkdirSync(dirname(target), { recursive: true });
290
+ cpSync(join(library(), entry.name), target, { recursive: true });
291
+ }
292
+ print({ file: methodFile, step: stepId, effect: name, observer: entry.name, copied_to: target, connection,
293
+ setup: entry.setup, workflow: result });
294
+ }
@@ -0,0 +1,5 @@
1
+ import { type AttachedFile } from "../../workflow-language/src/result-files.js";
2
+ import { type Workflow } from "../../workflow-language/src/schema.js";
3
+ import type { RunInspection } from "../../workflow-language/src/inspection.js";
4
+ /** Read only declared file outputs and explicitly listed website assets. Never scan a directory. */
5
+ export declare function attachResultFiles(workflow: Workflow, invocations: RunInspection["invocations"], roots: string[], artifactRoot?: string, referencesOnly?: boolean): AttachedFile[];
@@ -0,0 +1,149 @@
1
+ import { effectiveOutputs } from '../../workflow-language/src/schema.js';
2
+ import { gzipSync, gunzipSync } from "node:zlib";
3
+ import { Buffer } from "node:buffer";
4
+ import { dirname, join, resolve, sep, extname } from "node:path";
5
+ import { checkedFile } from "./files.js";
6
+ import { WebsiteManifestSchema, MAX_RESULT_FILES, MAX_RESULT_BYTES, MAX_RESULT_TRANSFER_BYTES, } from "../../workflow-language/src/result-files.js";
7
+ import { FileArtifactSchema, } from "../../workflow-language/src/schema.js";
8
+ const mediaTypes = {
9
+ ".html": "text/html",
10
+ ".htm": "text/html",
11
+ ".md": "text/markdown",
12
+ ".txt": "text/plain",
13
+ ".json": "application/json",
14
+ ".csv": "text/csv",
15
+ ".css": "text/css",
16
+ ".js": "text/javascript",
17
+ ".mjs": "text/javascript",
18
+ ".svg": "image/svg+xml",
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".webp": "image/webp",
23
+ ".pdf": "application/pdf",
24
+ ".zip": "application/zip",
25
+ ".woff2": "font/woff2",
26
+ };
27
+ /** Read only declared file outputs and explicitly listed website assets. Never scan a directory. */
28
+ export function attachResultFiles(workflow, invocations, roots, artifactRoot, referencesOnly = false) {
29
+ const files = [], seen = new Set();
30
+ let remaining = MAX_RESULT_BYTES, transferRemaining = MAX_RESULT_TRANSFER_BYTES;
31
+ const observed_at = new Date().toISOString();
32
+ const attach = (reference, title, mediaType) => {
33
+ const identity = JSON.stringify(reference);
34
+ if (seen.has(identity)) {
35
+ const file = files.find((file) => file.path === reference.path && file.sha256 === reference.sha256);
36
+ return {
37
+ file,
38
+ ...(file.data !== undefined
39
+ ? { bytes: file.encoding === "gzip" ? gunzipSync(Buffer.from(file.data, "base64"), { maxOutputLength: MAX_RESULT_BYTES }) : Buffer.from(file.data, "base64") }
40
+ : {}),
41
+ };
42
+ }
43
+ if (files.length >= MAX_RESULT_FILES)
44
+ return;
45
+ seen.add(identity);
46
+ const file = {
47
+ ...reference,
48
+ title: title.slice(0, 160),
49
+ media_type: mediaType ??
50
+ mediaTypes[extname(reference.path).toLowerCase()] ??
51
+ "application/octet-stream",
52
+ status: "unavailable",
53
+ observed_at,
54
+ };
55
+ files.push(file);
56
+ try {
57
+ if (files.length > MAX_RESULT_FILES || remaining <= 0 || transferRemaining <= 0)
58
+ throw Error("The run attachment limit was reached (20,000 files, 100 MB decoded, or 20 MB transferred).");
59
+ const localPath = artifactRoot
60
+ ? resolve(artifactRoot, reference.path)
61
+ : reference.path;
62
+ if (artifactRoot &&
63
+ (!localPath.startsWith(artifactRoot + sep) ||
64
+ reference.path.startsWith("/")))
65
+ throw Error("The output is outside the run artifacts folder.");
66
+ const result = checkedFile({ ...reference, path: localPath }, roots);
67
+ if (result.bytes.length > remaining)
68
+ throw Error("The file exceeds the remaining 100 MB run attachment limit.");
69
+ if (referencesOnly) {
70
+ remaining -= result.bytes.length;
71
+ file.status = "verified";
72
+ file.bytes = result.bytes.length;
73
+ return { file, bytes: result.bytes };
74
+ }
75
+ const compressed = gzipSync(result.bytes);
76
+ const useGzip = compressed.length < result.bytes.length;
77
+ const payload = useGzip ? compressed : result.bytes;
78
+ if (payload.length > transferRemaining)
79
+ throw Error("The file exceeds the remaining 20 MB transfer limit.");
80
+ transferRemaining -= payload.length;
81
+ remaining -= result.bytes.length;
82
+ file.status = "verified";
83
+ file.bytes = result.bytes.length;
84
+ file.data = Buffer.from(payload).toString("base64");
85
+ if (useGzip)
86
+ file.encoding = "gzip";
87
+ // Text is decoded by the reader from the attached bytes, avoiding duplicate payloads.
88
+ return { file, bytes: result.bytes };
89
+ }
90
+ catch (error) {
91
+ file.reason =
92
+ error instanceof Error
93
+ ? error.message
94
+ : "The file could not be attached.";
95
+ return { file };
96
+ }
97
+ };
98
+ const visit = (shape, value, title) => {
99
+ const def = typeof shape === "string" ? { type: shape } : shape;
100
+ if (def.type === "file") {
101
+ const ref = FileArtifactSchema.safeParse(value);
102
+ if (!ref.success)
103
+ return;
104
+ const attached = attach(ref.data, title);
105
+ if (!attached)
106
+ return;
107
+ if ("format" in def &&
108
+ def.format === "method-website" &&
109
+ attached.bytes) {
110
+ try {
111
+ const manifest = WebsiteManifestSchema.parse(JSON.parse(new TextDecoder().decode(attached.bytes)));
112
+ const members = manifest.files.map((member) => ({
113
+ ...member,
114
+ path: join(dirname(ref.data.path), member.path),
115
+ }));
116
+ attached.file.title = manifest.title;
117
+ attached.file.media_type = "application/vnd.method.website+json";
118
+ attached.file.website = {
119
+ entrypoint: join(dirname(ref.data.path), manifest.entrypoint),
120
+ files: members,
121
+ };
122
+ for (const member of members)
123
+ attach({ path: member.path, sha256: member.sha256 }, member.path, member.media_type);
124
+ }
125
+ catch (error) {
126
+ attached.file.reason = "The website file list is invalid: " + (error instanceof Error ? error.message : String(error));
127
+ }
128
+ }
129
+ }
130
+ else if (def.type === "record" &&
131
+ "fields" in def &&
132
+ value &&
133
+ typeof value === "object") {
134
+ for (const [name, child] of Object.entries(def.fields ?? {}))
135
+ visit(child, value[name], name.replaceAll("_", " "));
136
+ }
137
+ else if (def.type === "list" && Array.isArray(value)) {
138
+ for (const item of value)
139
+ if ("fields" in def && def.fields)
140
+ visit({ type: "record", fields: def.fields }, item, title);
141
+ else if ("items" in def && def.items)
142
+ visit(def.items, item, title);
143
+ }
144
+ };
145
+ for (const row of Object.values(invocations))
146
+ for (const [name, shape] of Object.entries(effectiveOutputs(workflow.steps[row.step_id] ?? {})))
147
+ visit(shape, row.outputs?.[name], name.replaceAll("_", " "));
148
+ return files;
149
+ }
@@ -0,0 +1,21 @@
1
+ import type { MethodClient } from './method-client.js';
2
+ import type { AttachedFile } from '../../workflow-language/src/result-files.js';
3
+ /** Transfer declared artifacts independently; server receipts make retries resumable. */
4
+ export declare function transferResults(client: MethodClient, directory: string, files: AttachedFile[]): Promise<{
5
+ stored: boolean;
6
+ sha256: string;
7
+ status: "verified" | "unavailable";
8
+ path: string;
9
+ observed_at: string;
10
+ bytes?: number | undefined;
11
+ media_type?: string | undefined;
12
+ title?: string | undefined;
13
+ website?: {
14
+ files: {
15
+ sha256: string;
16
+ path: string;
17
+ media_type: string;
18
+ }[];
19
+ entrypoint: string;
20
+ } | undefined;
21
+ }[]>;
@@ -0,0 +1,37 @@
1
+ import { join } from 'node:path';
2
+ import { checkedFile } from './files.js';
3
+ /** Transfer declared artifacts independently; server receipts make retries resumable. */
4
+ export async function transferResults(client, directory, files) {
5
+ const local = files.filter(f => !f.stored);
6
+ const invalid = local.find(f => f.status !== 'verified');
7
+ if (invalid)
8
+ throw Error(`Result upload pending: ${invalid.path}: ${invalid.reason}`);
9
+ const unique = [...new Map(local.map(f => [f.sha256, f])).values()];
10
+ const missing = new Set();
11
+ for (let offset = 0; offset < unique.length; offset += 256) {
12
+ const result = await client.request('/api/cli/result-files/missing', 'POST', {
13
+ files: unique.slice(offset, offset + 256).map(f => ({ sha256: f.sha256, bytes: f.bytes })),
14
+ });
15
+ result.missing.forEach(hash => missing.add(hash));
16
+ }
17
+ const send = async (file) => {
18
+ const bytes = checkedFile({ path: join(directory, 'artifacts', file.path), sha256: file.sha256 }, [join(directory, 'artifacts')]).bytes;
19
+ await client.transfer(`/api/cli/result-files/${file.sha256}`, bytes);
20
+ };
21
+ const needed = unique.filter(f => missing.has(f.sha256));
22
+ // Avoid concurrent large buffers on the receiving Worker.
23
+ for (const file of needed.filter(f => (f.bytes ?? 0) > 4_000_000))
24
+ await send(file);
25
+ const queue = needed.filter(f => (f.bytes ?? 0) <= 4_000_000);
26
+ let next = 0;
27
+ const results = await Promise.allSettled(Array.from({ length: Math.min(6, queue.length) }, async () => {
28
+ while (next < queue.length) {
29
+ const file = queue[next++];
30
+ await send(file);
31
+ }
32
+ }));
33
+ const failed = results.find((result) => result.status === "rejected");
34
+ if (failed)
35
+ throw failed.reason;
36
+ return files.map(({ data, encoding, text, reason, ...file }) => ({ ...file, stored: true }));
37
+ }
@@ -0,0 +1,11 @@
1
+ export type SealedKind = "inputs" | "question" | "answer" | "snapshot";
2
+ /** Seal a JSON value. */
3
+ export declare function seal(credential: string, kind: SealedKind, methodId: string, value: unknown): string;
4
+ /** Open a sealed JSON value. Fails with code wrong_key when another credential sealed it. */
5
+ export declare function open<T = unknown>(credential: string, kind: SealedKind, methodId: string, sealed: string): T;
6
+ export declare function sealBytes(credential: string, methodId: string, bytes: Uint8Array): Buffer;
7
+ export declare function openBytes(credential: string, methodId: string, sealed: Uint8Array): Buffer;
8
+ /** The idempotency digest of sealed inputs: equal inputs give an equal digest, and the server cannot reverse it. */
9
+ export declare function inputsDigest(credential: string, methodId: string, inputs: unknown): string;
10
+ /** `method __run-data seal|open KIND METHOD_ID`: the Python library seals with this (stdin JSON in, stdout out). */
11
+ export declare function runDataCommand(args: string[]): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { createCipheriv, createDecipheriv, createHmac, hkdfSync, randomBytes } from "node:crypto";
2
+ // Sealed run content for device runs in the production queue. The key is derived on the client from the credential
3
+ // that starts the run (a service key, or the CLI sign-in): HKDF-SHA256 of the credential, then AES-256-GCM. The server
4
+ // has only the SHA-256 of the credential, so it cannot derive the key and cannot open what it stores.
5
+ //
6
+ // Text: "v1." + base64url(12-byte IV) + "." + base64url(ciphertext and 16-byte tag)
7
+ // Bytes: "MRS1" + IV + tag + ciphertext (a run folder)
8
+ // The additional data names what is sealed and for which Method, so one sealed value cannot stand in for another.
9
+ const SALT = Buffer.from("method-run-data/1");
10
+ const encryptionKey = (credential) => Buffer.from(hkdfSync("sha256", Buffer.from(credential, "utf8"), SALT, "aes-256-gcm", 32));
11
+ const digestKey = (credential) => Buffer.from(hkdfSync("sha256", Buffer.from(credential, "utf8"), SALT, "hmac-sha256", 32));
12
+ const aad = (kind, methodId) => Buffer.from(`method-run-data/1:${kind}:${methodId}`);
13
+ /** Seal a JSON value. */
14
+ export function seal(credential, kind, methodId, value) {
15
+ const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", encryptionKey(credential), iv);
16
+ cipher.setAAD(aad(kind, methodId));
17
+ const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final(), cipher.getAuthTag()]);
18
+ return `v1.${iv.toString("base64url")}.${body.toString("base64url")}`;
19
+ }
20
+ /** Open a sealed JSON value. Fails with code wrong_key when another credential sealed it. */
21
+ export function open(credential, kind, methodId, sealed) {
22
+ const match = sealed.match(/^v1\.([A-Za-z0-9_-]{16})\.([A-Za-z0-9_-]+)$/);
23
+ if (!match)
24
+ throw Object.assign(Error("This is not sealed run data."), { code: "invalid_sealed" });
25
+ const body = Buffer.from(match[2], "base64url");
26
+ try {
27
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey(credential), Buffer.from(match[1], "base64url"));
28
+ decipher.setAAD(aad(kind, methodId));
29
+ decipher.setAuthTag(body.subarray(body.length - 16));
30
+ return JSON.parse(Buffer.concat([decipher.update(body.subarray(0, body.length - 16)), decipher.final()]).toString("utf8"));
31
+ }
32
+ catch {
33
+ throw Object.assign(Error("This run's content was sealed with another key. Use the METHOD_API_KEY that started the run."), { code: "wrong_key" });
34
+ }
35
+ }
36
+ export function sealBytes(credential, methodId, bytes) {
37
+ const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", encryptionKey(credential), iv);
38
+ cipher.setAAD(aad("snapshot", methodId));
39
+ const body = Buffer.concat([cipher.update(bytes), cipher.final()]);
40
+ return Buffer.concat([Buffer.from("MRS1"), iv, cipher.getAuthTag(), body]);
41
+ }
42
+ export function openBytes(credential, methodId, sealed) {
43
+ const data = Buffer.from(sealed);
44
+ if (data.subarray(0, 4).toString("latin1") !== "MRS1")
45
+ throw Object.assign(Error("This is not a sealed run folder."), { code: "invalid_sealed" });
46
+ try {
47
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey(credential), data.subarray(4, 16));
48
+ decipher.setAAD(aad("snapshot", methodId));
49
+ decipher.setAuthTag(data.subarray(16, 32));
50
+ return Buffer.concat([decipher.update(data.subarray(32)), decipher.final()]);
51
+ }
52
+ catch {
53
+ throw Object.assign(Error("This run folder was sealed with another key. Use the METHOD_API_KEY that started the run."), { code: "wrong_key" });
54
+ }
55
+ }
56
+ /** The idempotency digest of sealed inputs: equal inputs give an equal digest, and the server cannot reverse it. */
57
+ export function inputsDigest(credential, methodId, inputs) {
58
+ return createHmac("sha256", digestKey(credential)).update(`${methodId}\n${canonical(inputs)}`).digest("hex");
59
+ }
60
+ function canonical(value) {
61
+ if (Array.isArray(value))
62
+ return `[${value.map(canonical).join(",")}]`;
63
+ if (value && typeof value === "object")
64
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
65
+ return JSON.stringify(value);
66
+ }
67
+ /** `method __run-data seal|open KIND METHOD_ID`: the Python library seals with this (stdin JSON in, stdout out). */
68
+ export async function runDataCommand(args) {
69
+ const [action, kind, methodId] = args;
70
+ const credential = process.env.METHOD_API_KEY;
71
+ if (!credential || !methodId || !["inputs", "question", "answer"].includes(kind ?? "") || !["seal", "open", "digest"].includes(action ?? ""))
72
+ throw Error("Use METHOD_API_KEY=… method __run-data seal|open|digest inputs|question|answer METHOD_ID < VALUE");
73
+ let text = "";
74
+ for await (const chunk of process.stdin)
75
+ text += chunk;
76
+ const result = action === "seal" ? seal(credential, kind, methodId, JSON.parse(text))
77
+ : action === "digest" ? inputsDigest(credential, methodId, JSON.parse(text))
78
+ : open(credential, kind, methodId, text.trim());
79
+ process.stdout.write(JSON.stringify(result) + "\n");
80
+ }
@@ -0,0 +1,2 @@
1
+ /** Execute current Methods with the single public runtime. */
2
+ export { runMethod } from "@withmethod/runtime/runner.js";
@@ -0,0 +1,2 @@
1
+ /** Execute current Methods with the single public runtime. */
2
+ export { runMethod } from "@withmethod/runtime/runner.js";
@@ -0,0 +1,5 @@
1
+ import { MethodClient } from './method-client.js';
2
+ import type { parse } from './local-cli.js';
3
+ /** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
4
+ export declare const finishedRun: (status: string) => status is "completed" | "unconfirmed";
5
+ export declare function runSaved(saved: any, flags: ReturnType<typeof parse>['values'], client: MethodClient): Promise<any>;