@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,441 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { parseArgs } from 'node:util';
8
+ import { loadWorkflow } from '../../workflow-language/src/validate.js';
9
+ import { authoringPath } from './authoring.js';
10
+ import { writePrivateJson } from './files.js';
11
+ import { managedModels } from './hosted-models.js';
12
+ import { contained, fileHash } from './method-files.js';
13
+ import { methodCache } from './prepare.js';
14
+ import { scriptLanguage, staticEffects } from './explain-effects.js';
15
+ const runEntrypoints = (method) => new Set(Object.values(method.steps).flatMap(step => [step?.do, step?.check])
16
+ .filter(action => action?.kind === 'run').map(action => action.entrypoint));
17
+ /** The script steps of a Method: steps whose action is a script. */
18
+ export const scriptSteps = (method) => Object.entries(method.steps).filter(([, step]) => step?.do?.kind === 'run').map(([id]) => id);
19
+ /**
20
+ * The files a script step's card describes: its entrypoint and every declared Method file that is not another step's
21
+ * entrypoint (the step cache counts the same helper files, because the runtime does not trace imports).
22
+ */
23
+ export function scriptFiles(method, id) {
24
+ const entrypoints = runEntrypoints(method), entrypoint = method.steps[id].do.entrypoint;
25
+ return [...new Set([entrypoint, ...(method.files ?? []).filter(path => !entrypoints.has(path))])].sort();
26
+ }
27
+ /** sha256 over "<file sha256> <path>\n" lines, sorted by path. The server computes the same value from the package. */
28
+ export function scriptSha256(method, id, hashes) {
29
+ const get = (path) => hashes instanceof Map ? hashes.get(path) : hashes[path];
30
+ const paths = scriptFiles(method, id);
31
+ if (paths.some(path => !get(path)))
32
+ return undefined;
33
+ return createHash('sha256').update(paths.map(path => `${get(path)} ${path}\n`).join('')).digest('hex');
34
+ }
35
+ const numbered = (text) => text.split('\n').map((line, index) => `${String(index + 1).padStart(4)}| ${line}`).join('\n');
36
+ const declaration = (method, id) => {
37
+ const step = method.steps[id];
38
+ return { id, name: step.name, purpose: step.purpose, in: step.in ?? {}, out: step.out ?? {}, secrets: Object.keys(method.secrets ?? {}), changes: step.changes, effects: step.effects };
39
+ };
40
+ // ---------- Entity check ----------
41
+ /**
42
+ * Every number, quoted or backticked name, and host in the card's text must appear in the code or in the step's
43
+ * declaration. A percentage may appear as a fraction (50% as 0.5).
44
+ */
45
+ export function entityProblems(draft, code, step) {
46
+ const haystack = `${code}\n${JSON.stringify(step)}`;
47
+ const words = new Set(haystack.match(/[A-Za-z0-9_.$-]+/g) ?? []);
48
+ const numbers = new Set((haystack.match(/\d+(?:_\d+)*(?:\.\d+)?(?:e-?\d+)?/gi) ?? []).map(value => Number(value.replace(/_/g, ''))));
49
+ const problems = new Set();
50
+ const texts = [draft.summary, ...draft.steps.map(step => step.text), ...draft.not_done];
51
+ for (const text of texts) {
52
+ const names = [...text.matchAll(/`([^`]+)`|"([^"]+)"|'([^'\s][^']*)'(?![a-z])/g)].map(match => (match[1] ?? match[2] ?? match[3]));
53
+ for (const name of names)
54
+ if (!haystack.includes(name))
55
+ problems.add(`"${name}" does not appear in the code.`);
56
+ const plain = text.replace(/`[^`]*`/g, ' ');
57
+ for (const match of plain.matchAll(/\b(?:[a-z0-9-]+\.)+(?:com|org|net|io|ai|dev|app|co|gov|edu|us|uk|de|so|me|cloud)\b/gi))
58
+ if (!words.has(match[0]) && !haystack.toLowerCase().includes(match[0].toLowerCase()))
59
+ problems.add(`Host ${match[0]} does not appear in the code.`);
60
+ for (const match of plain.matchAll(/(?<![\w.])(\d{1,3}(?:,\d{3})+|\d+(?:\.\d+)?)(\s*%)?(?![\w])/g)) {
61
+ const value = Number(match[1].replace(/,/g, ''));
62
+ if (numbers.has(value) || (match[2] && numbers.has(value / 100)))
63
+ continue;
64
+ problems.add(`The number ${match[1]}${match[2] ? '%' : ''} does not appear in the code.`);
65
+ }
66
+ }
67
+ return [...problems];
68
+ }
69
+ // ---------- Hosted model ----------
70
+ const draftSchema = {
71
+ type: 'object', additionalProperties: false, required: ['summary', 'changes', 'steps', 'not_done'],
72
+ properties: {
73
+ summary: { type: 'string' }, changes: { type: 'string', enum: ['nothing', 'data'] },
74
+ steps: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['text', 'lines'], properties: { text: { type: 'string' }, lines: { type: 'array', items: { type: 'integer' } } } } },
75
+ not_done: { type: 'array', items: { type: 'string' } },
76
+ },
77
+ };
78
+ const programSchema = { type: 'object', additionalProperties: false, required: ['code'], properties: { code: { type: 'string' } } };
79
+ const explainRules = `You explain one script of a Method to the person who owns it. Return JSON {summary, changes, steps, not_done}.
80
+ Rules:
81
+ - summary: one sentence in command form (for example "Score each lead and keep those above 50."). changes: "data" when the script writes files, sends data, or changes another system; otherwise "nothing".
82
+ - steps: 3 to 8 numbered plain-English steps in the order the code runs them. Each step has lines [first, last] from the numbered code.
83
+ - Write for a reader who does not read code. Say what happens, not how the code does it: no function calls, expressions, or variable names of the code's internals. Example: write "Keep the links to x.com and twitter.com as handles", not "append urlsplit(url).path to handles".
84
+ - State thresholds, limits, time windows, sort orders, hosts, and file names exactly as the code has them. Name the step's input and output fields in backticks.
85
+ - Add one step that begins with "If" for each branch and each failure path (for example "If \`leads\` is empty, print an empty list."). Join small branches that lead to the same result into one step.
86
+ - not_done: up to 4 edge cases that matter to the result and that the code ignores (for example duplicates, missing fields, time zones). Use an empty list if there are none.
87
+ - Make no claim that is not in the code. Do not describe the effects list again; it is shown separately.`;
88
+ async function ask(models, model, system, user, schema, name, reasoning = { effort: 'low' }) {
89
+ // A reasoning model can spend the whole token budget before it answers a long script, so reasoning is kept low
90
+ // and an empty answer gets one more attempt.
91
+ let data, content;
92
+ for (let attempt = 1; attempt <= 2; attempt++) {
93
+ data = await models.request({
94
+ model, max_tokens: 16000, reasoning, provider: { require_parameters: true },
95
+ messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
96
+ response_format: { type: 'json_schema', json_schema: { name, strict: true, schema } },
97
+ }, AbortSignal.timeout(180_000));
98
+ content = data?.choices?.[0]?.message?.content;
99
+ if (typeof content === 'string' && content.trim())
100
+ break;
101
+ }
102
+ if (typeof content !== 'string' || !content.trim())
103
+ throw Error(`The hosted model returned no answer (finish reason: ${data?.choices?.[0]?.finish_reason ?? 'unknown'}).`);
104
+ const json = content.trim().replace(/^```(?:json)?\s*|\s*```$/g, '');
105
+ return { value: JSON.parse(json), model: typeof data.model === 'string' ? data.model : model };
106
+ }
107
+ function cleanDraft(value, lineCount) {
108
+ const clamp = (n) => Math.min(Math.max(1, Math.trunc(Number(n) || 1)), Math.max(1, lineCount));
109
+ const steps = (Array.isArray(value?.steps) ? value.steps : []).filter((step) => typeof step?.text === 'string' && step.text.trim()).slice(0, 8).map((step) => {
110
+ const [a, b] = [clamp(step.lines?.[0]), clamp(step.lines?.[1] ?? step.lines?.[0])];
111
+ return { text: step.text.trim().slice(0, 2000), lines: [Math.min(a, b), Math.max(a, b)] };
112
+ });
113
+ if (typeof value?.summary !== 'string' || !value.summary.trim() || !steps.length)
114
+ throw Error('The hosted model returned an incomplete explanation.');
115
+ return {
116
+ summary: value.summary.trim().slice(0, 2000), changes: value.changes === 'data' ? 'data' : 'nothing', steps,
117
+ not_done: (Array.isArray(value.not_done) ? value.not_done : []).filter((item) => typeof item === 'string' && item.trim()).slice(0, 20).map((item) => item.trim().slice(0, 2000)),
118
+ };
119
+ }
120
+ // ---------- Round trip ----------
121
+ /** Recorded inputs of a step from local runs of the same entrypoint, newest first, without duplicates. */
122
+ export function recordedInputs(id, entrypoint, roots, limit = 3) {
123
+ const runs = [];
124
+ for (const root of new Set(roots.map(path => resolve(path)))) {
125
+ let names = [];
126
+ try {
127
+ names = readdirSync(root);
128
+ }
129
+ catch {
130
+ continue;
131
+ }
132
+ for (const name of names) {
133
+ const file = join(root, name, 'events.jsonl');
134
+ try {
135
+ const stat = statSync(file);
136
+ if (stat.size < 50_000_000)
137
+ runs.push([file, stat.mtimeMs]);
138
+ }
139
+ catch { /* not a run */ }
140
+ }
141
+ }
142
+ const seen = new Set(), inputs = [];
143
+ for (const [file] of runs.sort((a, b) => b[1] - a[1])) {
144
+ let sameScript = false;
145
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
146
+ if (!line)
147
+ continue;
148
+ let event;
149
+ try {
150
+ event = JSON.parse(line);
151
+ }
152
+ catch {
153
+ continue;
154
+ }
155
+ if (event.event === 'run.started' || event.event === 'run.resumed')
156
+ sameScript = event.method?.steps?.[id]?.do?.entrypoint === entrypoint;
157
+ if (!sameScript || event.event !== 'step.started' || event.step !== id || !event.inputs || typeof event.inputs !== 'object')
158
+ continue;
159
+ const key = canonical(event.inputs);
160
+ if (seen.has(key))
161
+ continue;
162
+ seen.add(key);
163
+ inputs.push(event.inputs);
164
+ if (inputs.length >= limit)
165
+ return inputs;
166
+ }
167
+ }
168
+ return inputs;
169
+ }
170
+ export function canonical(value) {
171
+ if (Array.isArray(value))
172
+ return `[${value.map(canonical).join(',')}]`;
173
+ if (value && typeof value === 'object')
174
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
175
+ return JSON.stringify(value) ?? 'null';
176
+ }
177
+ /** Two outputs agree when their declared fields are JSON-equal after sorting keys (all fields when none are declared). */
178
+ export function sameOutput(a, b, fields) {
179
+ const pick = (value) => fields.length && value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(fields.map(field => [field, value[field] ?? null])) : value;
180
+ return canonical(pick(a)) === canonical(pick(b));
181
+ }
182
+ const pythonGuard = String.raw `import os, runpy, socket, sys
183
+ def _blocked(*args, **kwargs): raise OSError("network disabled by method explain")
184
+ socket.socket.connect = _blocked; socket.socket.connect_ex = _blocked; socket.create_connection = _blocked; socket.getaddrinfo = _blocked
185
+ path = sys.argv[1]; sys.argv = sys.argv[1:]; sys.path.insert(0, os.path.dirname(os.path.abspath(path)))
186
+ runpy.run_path(path, run_name="__main__")`;
187
+ const nodeGuard = `const blocked = () => { throw new Error('network disabled by method explain'); };
188
+ const net = require('node:net'), tls = require('node:tls'), dns = require('node:dns');
189
+ net.Socket.prototype.connect = blocked; net.connect = blocked; net.createConnection = blocked; tls.connect = blocked;
190
+ dns.lookup = (host, options, callback) => (typeof options === 'function' ? options : callback)(new Error('network disabled by method explain'));
191
+ globalThis.fetch = async () => blocked();`;
192
+ /** Run one program on one input with network calls refused. The process gets no secrets and a private temporary folder. */
193
+ export function runProgram(language, file, cwd, input, scratch) {
194
+ const outputDir = mkdtempSync(join(scratch, 'out-'));
195
+ const guard = join(scratch, 'guard.cjs');
196
+ if (language === 'node' && !existsSync(guard))
197
+ writeFileSync(guard, nodeGuard, { mode: 0o600 });
198
+ const [command, args] = language === 'python' ? ['python3', ['-c', pythonGuard, file]] : [process.execPath, ['--require', guard, file]];
199
+ return new Promise(done => {
200
+ const child = spawn(command, [...args], { cwd, env: { PATH: process.env.PATH ?? '', LANG: 'C.UTF-8', HOME: scratch, METHOD_OUTPUT_DIR: outputDir, METHOD_ENVIRONMENT: '{}' }, stdio: ['pipe', 'pipe', 'pipe'] });
201
+ let stdout = '', stderr = '';
202
+ const timer = setTimeout(() => child.kill('SIGKILL'), 30_000);
203
+ child.stdout.on('data', chunk => { stdout += chunk; if (stdout.length > 8_000_000)
204
+ child.kill('SIGKILL'); });
205
+ child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-4000); });
206
+ child.stdin.on('error', () => { });
207
+ child.on('error', error => { clearTimeout(timer); done({ ok: false, error: error.message }); });
208
+ child.on('close', code => {
209
+ clearTimeout(timer);
210
+ if (code !== 0)
211
+ return done({ ok: false, error: stderr.trim().split('\n').at(-1) || `exit ${code}` });
212
+ const lines = stdout.trim().split('\n');
213
+ for (const candidate of [stdout, lines.at(-1) ?? '']) {
214
+ try {
215
+ return done({ ok: true, output: JSON.parse(candidate) });
216
+ }
217
+ catch { /* next */ }
218
+ }
219
+ done({ ok: false, error: 'The program did not print one JSON value.' });
220
+ });
221
+ child.stdin.end(JSON.stringify(input));
222
+ });
223
+ }
224
+ async function roundTrip(o) {
225
+ if (o.effects.network.length)
226
+ return { result: 'not_run', inputs: 0, reason: `the script calls ${o.effects.network.join(', ')}; Method does not call external services twice` };
227
+ if (!o.inputs.length)
228
+ return { result: 'not_run', inputs: 0, reason: 'no recorded inputs of this step on this computer' };
229
+ if (/\.(?:ts|mts|cts)$/i.test(o.entrypoint))
230
+ return { result: 'not_run', inputs: 0, reason: 'TypeScript scripts are not replayed' };
231
+ const language = o.language === 'python' ? 'Python 3 (standard library only)' : 'JavaScript for Node.js (CommonJS, no packages)';
232
+ // The program must follow the text, not the model's own ideas, so it is written without reasoning. A model that
233
+ // cannot write it leaves the card in place with the round trip not run.
234
+ let value;
235
+ try {
236
+ ({ value } = await ask(o.models, o.model, `Write one complete program in ${language}. It reads one JSON object from standard input and prints one JSON object to standard output. Follow the description exactly; it is your only source. Return JSON {code}.`, JSON.stringify({ summary: o.draft.summary, steps: o.draft.steps.map(step => step.text), effects: o.effects }, null, 2), programSchema, 'method_program', { enabled: false }));
237
+ }
238
+ catch (error) {
239
+ return { result: 'not_run', inputs: 0, reason: `no program was written from the steps: ${error.message}` };
240
+ }
241
+ if (typeof value?.code !== 'string' || !value.code.trim())
242
+ return { result: 'failed', inputs: 0, reason: 'the model wrote no program' };
243
+ const scratch = mkdtempSync(join(tmpdir(), 'method-explain-'));
244
+ try {
245
+ const generated = join(scratch, o.language === 'python' ? 'generated.py' : 'generated.cjs');
246
+ writeFileSync(generated, value.code, { mode: 0o600 });
247
+ let replayed = 0;
248
+ for (const input of o.inputs) {
249
+ const original = await runProgram(o.language, contained(o.root, o.entrypoint), o.root, input, scratch);
250
+ if (!original.ok)
251
+ continue;
252
+ replayed++;
253
+ const copy = await runProgram(o.language, generated, mkdtempSync(join(scratch, 'cwd-')), input, scratch);
254
+ if (!copy.ok || !sameOutput(original.output, copy.output, o.effects.output_fields))
255
+ return { result: 'failed', inputs: replayed, reason: copy.ok ? 'a program written from the steps returned another output' : `a program written from the steps failed: ${copy.error}` };
256
+ }
257
+ return replayed ? { result: 'passed', inputs: replayed } : { result: 'not_run', inputs: 0, reason: 'the script failed on every recorded input without network access' };
258
+ }
259
+ finally {
260
+ rmSync(scratch, { recursive: true, force: true });
261
+ }
262
+ }
263
+ export async function makeCard(c) {
264
+ const step = c.method.steps[c.id], entrypoint = step.do.entrypoint;
265
+ const language = scriptLanguage(entrypoint);
266
+ if (!language)
267
+ throw Error(`${entrypoint} is not a Python or JavaScript file.`);
268
+ const { effects, facts } = staticEffects(language, c.sources, step, Object.keys(c.method.secrets ?? {}));
269
+ const code = c.sources.map(source => source.text).join('\n');
270
+ const entry = c.sources.find(source => source.path === entrypoint);
271
+ const model = await c.models.model();
272
+ const prompt = (problems = []) => JSON.stringify({
273
+ step: declaration(c.method, c.id), effects, facts: facts,
274
+ code: c.sources.map(source => ({ path: source.path, numbered: numbered(source.text) })),
275
+ ...(problems.length ? { fix: `Your last answer named things that are not in the code. Remove or correct them: ${problems.join(' ')}` } : {}),
276
+ }, null, 2);
277
+ let answer = await ask(c.models, model, explainRules, prompt(), draftSchema, 'method_script_card');
278
+ let draft = cleanDraft(answer.value, entry.text.split('\n').length);
279
+ let problems = entityProblems(draft, code, declaration(c.method, c.id));
280
+ if (problems.length) {
281
+ answer = await ask(c.models, model, explainRules, prompt(problems), draftSchema, 'method_script_card');
282
+ draft = cleanDraft(answer.value, entry.text.split('\n').length);
283
+ problems = entityProblems(draft, code, declaration(c.method, c.id));
284
+ }
285
+ const trip = c.roundTrip
286
+ ? await roundTrip({ models: c.models, model, language, entrypoint, root: c.root, draft, effects, inputs: recordedInputs(c.id, entrypoint, c.runRoots) })
287
+ : { result: 'not_run', inputs: 0, reason: '--no-round-trip' };
288
+ const changes = draft.changes === 'data' || effects.writes.length || step.changes?.length || step.effects ? 'data' : 'nothing';
289
+ return {
290
+ card: {
291
+ schema: 'method-script-card/1', step_id: c.id, entrypoint, script_sha256: c.scriptHash,
292
+ summary: draft.summary, changes, effects, steps: draft.steps, not_done: draft.not_done,
293
+ checks: { entities: problems.length ? 'failed' : 'passed', entity_problems: problems.slice(0, 100), round_trip: trip.result, round_trip_inputs: trip.inputs },
294
+ generated_at: new Date().toISOString(), model: answer.model,
295
+ },
296
+ ...(trip.reason ? { note: trip.reason } : {}),
297
+ };
298
+ }
299
+ // ---------- Command ----------
300
+ const explainDir = () => join(methodCache(), 'explain');
301
+ const cardCache = () => join(methodCache(), 'script-cards');
302
+ const draftKey = (file) => createHash('sha256').update(resolve(file)).digest('hex').slice(0, 24);
303
+ const cacheKey = (method, id, hash) => createHash('sha256').update(JSON.stringify([hash, declaration(method, id)])).digest('hex');
304
+ /**
305
+ * Make, check, and upload a card for each script step of the saved version of FILE's current content, when the
306
+ * version has no card for the step's current script. A card made earlier for the same script and step is reused.
307
+ */
308
+ export async function explain(client, file, options = {}) {
309
+ const write = options.write ?? (line => process.stdout.write(line + '\n'));
310
+ if (!client.token()) {
311
+ write('Script cards skipped: not signed in. Run method login, then method explain FILE.');
312
+ return { status: 'skipped', cards: [] };
313
+ }
314
+ const path = authoringPath(file), root = dirname(path);
315
+ const current = await (await import('./versions.js')).currentVersion(path);
316
+ if (!current)
317
+ throw Error('This file has no saved version. Run it or publish it while signed in first.');
318
+ const meta = { workflow_id: current.method_id, base_version: current.version_id };
319
+ const versionPath = `/api/cli/methods/${encodeURIComponent(meta.workflow_id)}/versions/${encodeURIComponent(meta.base_version)}/script-cards`;
320
+ const saved = await client.request(`/api/cli/methods/${encodeURIComponent(meta.workflow_id)}?version=${encodeURIComponent(meta.base_version)}`);
321
+ const method = loadWorkflow(saved.workflow);
322
+ const ids = scriptSteps(method).filter(id => !options.step || id === options.step);
323
+ if (options.step && !ids.length)
324
+ throw Error(`Step ${options.step} is not a script step of this version.`);
325
+ const packaged = new Map((saved.package?.files ?? []).map((f) => [f.path, f.sha256]));
326
+ const existing = await client.request(`/api/workspace/methods/${encodeURIComponent(meta.workflow_id)}/versions/${encodeURIComponent(meta.base_version)}/script-cards`);
327
+ const done = new Set(existing.cards.map(card => card.step_id));
328
+ const models = options.models ?? managedModels(client);
329
+ const cards = [], lines = [];
330
+ for (const id of ids) {
331
+ const hash = scriptSha256(method, id, packaged);
332
+ if (!hash) {
333
+ lines.push(`${id}: skipped; the saved version lacks its files.`);
334
+ continue;
335
+ }
336
+ if (done.has(id)) {
337
+ lines.push(`${id}: card exists.`);
338
+ continue;
339
+ }
340
+ const files = scriptFiles(method, id), sources = [];
341
+ for (const name of files) {
342
+ const bytes = readFileSync(contained(root, name));
343
+ if (packaged.get(name) === fileHash(bytes))
344
+ sources.push({ path: name, text: bytes.toString('utf8') });
345
+ }
346
+ if (sources.length !== files.length) {
347
+ lines.push(`${id}: skipped; the local files differ from the saved version. Run method run or method publish to save them.`);
348
+ continue;
349
+ }
350
+ const cached = join(cardCache(), `${cacheKey(method, id, hash)}.json`);
351
+ try {
352
+ let card, note;
353
+ if (existsSync(cached))
354
+ card = JSON.parse(readFileSync(cached, 'utf8'));
355
+ else {
356
+ ({ card, note } = await makeCard({ method, id, root, sources, scriptHash: hash, models, roundTrip: options.roundTrip !== false, runRoots: options.runRoots ?? [join(root, '.method-runs'), join(process.cwd(), '.method-runs'), join(methodCache(), 'runs')] }));
357
+ mkdirSync(cardCache(), { recursive: true, mode: 0o700 });
358
+ writePrivateJson(cached, card);
359
+ }
360
+ cards.push(card);
361
+ const trip = card.checks.round_trip === 'not_run' ? `round trip not run${note ? ` (${note})` : ''}` : `round trip ${card.checks.round_trip} on ${card.checks.round_trip_inputs} input${card.checks.round_trip_inputs === 1 ? '' : 's'}`;
362
+ lines.push(`${id}: ${card.summary} [entities ${card.checks.entities}${card.checks.entity_problems.length ? `: ${card.checks.entity_problems.join(' ')}` : ''}; ${trip}]`);
363
+ }
364
+ catch (error) {
365
+ lines.push(`${id}: no card; ${error.message}`);
366
+ }
367
+ }
368
+ if (cards.length)
369
+ await client.request(versionPath, 'PUT', { cards }, true, { stallMs: 60_000 });
370
+ for (const line of lines)
371
+ write(line);
372
+ const failed = lines.some(line => / no card;| skipped;/.test(line));
373
+ if (!failed && !options.step) {
374
+ mkdirSync(explainDir(), { recursive: true, mode: 0o700 });
375
+ writePrivateJson(join(explainDir(), `done-${meta.base_version}.json`), { at: new Date().toISOString() });
376
+ }
377
+ return { status: 'done', cards };
378
+ }
379
+ export async function explainCommand(client, args) {
380
+ const parsed = parseArgs({ args, allowPositionals: true, options: { step: { type: 'string' }, 'no-round-trip': { type: 'boolean' }, server: { type: 'string' } } });
381
+ const [file] = parsed.positionals;
382
+ if (!file || parsed.positionals.length !== 1)
383
+ throw Error('Use method explain FILE [--step ID] [--no-round-trip].');
384
+ const lock = join(explainDir(), `${draftKey(file)}.json`);
385
+ mkdirSync(explainDir(), { recursive: true, mode: 0o700 });
386
+ if (existsSync(lock) && alive(lock) && JSON.parse(readFileSync(lock, 'utf8')).pid !== process.pid)
387
+ await waitForExplain(file);
388
+ if (!existsSync(lock) || !alive(lock))
389
+ writePrivateJson(lock, { pid: process.pid });
390
+ try {
391
+ await explain(client, file, { step: parsed.values.step, roundTrip: !parsed.values['no-round-trip'] });
392
+ }
393
+ finally {
394
+ try {
395
+ if (JSON.parse(readFileSync(lock, 'utf8')).pid === process.pid)
396
+ rmSync(lock, { force: true });
397
+ }
398
+ catch { /* gone */ }
399
+ }
400
+ }
401
+ function alive(lock) {
402
+ try {
403
+ process.kill(JSON.parse(readFileSync(lock, 'utf8')).pid, 0);
404
+ return true;
405
+ }
406
+ catch {
407
+ return false;
408
+ }
409
+ }
410
+ /**
411
+ * After a signed-in run saved a version with script steps, explain them in a detached process. The run neither waits
412
+ * nor fails because of it. Output goes to a log under the Method cache.
413
+ */
414
+ export function startBackgroundExplain(file, method, versionId, server) {
415
+ try {
416
+ if (process.env.METHOD_NO_EXPLAIN === '1' || !scriptSteps(method).length)
417
+ return;
418
+ const dir = explainDir(), lock = join(dir, `${draftKey(file)}.json`);
419
+ if (existsSync(join(dir, `done-${versionId}.json`)) || (existsSync(lock) && alive(lock)))
420
+ return;
421
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
422
+ const log = join(dir, `${draftKey(file)}.log`), out = openSync(log, 'a', 0o600);
423
+ const suffix = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
424
+ const entry = fileURLToPath(new URL(`./method${suffix}`, import.meta.url));
425
+ const child = spawn(process.execPath, [...process.execArgv, entry, 'explain', resolve(file), '--server', server], { detached: true, stdio: ['ignore', out, out], cwd: process.cwd(), env: { ...process.env, METHOD_RUN_WORKER: '' } });
426
+ closeSync(out);
427
+ child.on('error', () => { });
428
+ if (child.pid)
429
+ writePrivateJson(lock, { pid: child.pid, version_id: versionId, log });
430
+ child.unref();
431
+ }
432
+ catch { /* Script cards never delay or fail a run. */ }
433
+ }
434
+ /** Wait for a background explain of this draft to finish. */
435
+ export async function waitForExplain(file, timeoutMs = 15 * 60_000) {
436
+ const lock = join(explainDir(), `${draftKey(file)}.json`), deadline = Date.now() + timeoutMs;
437
+ while (existsSync(lock) && alive(lock) && Date.now() < deadline)
438
+ await new Promise(r => setTimeout(r, 500));
439
+ }
440
+ /** Used by tests: python3 is required for Python analysis and replay. */
441
+ export const hasPython = () => spawnSync('python3', ['--version']).status === 0;
@@ -0,0 +1,9 @@
1
+ import { type Json } from "../../workflow-language/src/schema.js";
2
+ export declare function writePrivateJson(path: string, value: unknown): void;
3
+ export declare function checkedFile(value: unknown, roots: string[]): {
4
+ path: string;
5
+ sha256: string;
6
+ bytes: Buffer;
7
+ };
8
+ /** Discover only the documented file encoding. Ordinary objects are ordinary evidence. */
9
+ export declare function fileArtifacts(value: Json): Json[];
@@ -0,0 +1,50 @@
1
+ import { closeSync, openSync, renameSync, writeFileSync, fsyncSync, mkdirSync, lstatSync, realpathSync, readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { FileArtifactSchema } from "../../workflow-language/src/schema.js";
5
+ export function writePrivateJson(path, value) {
6
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
7
+ const temporary = `${path}.${randomUUID()}.tmp`;
8
+ const fd = openSync(temporary, "wx", 0o600);
9
+ try {
10
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
11
+ fsyncSync(fd);
12
+ }
13
+ finally {
14
+ closeSync(fd);
15
+ }
16
+ renameSync(temporary, path);
17
+ const parent = openSync(dirname(path), "r");
18
+ try {
19
+ fsyncSync(parent);
20
+ }
21
+ finally {
22
+ closeSync(parent);
23
+ }
24
+ }
25
+ export function checkedFile(value, roots) {
26
+ const artifact = FileArtifactSchema.parse(value);
27
+ if (!isAbsolute(artifact.path))
28
+ throw new Error("ARTIFACT_PATH: file paths must be absolute.");
29
+ const path = realpathSync(artifact.path);
30
+ if (!roots.some(root => {
31
+ const rel = relative(realpathSync(resolve(root)), path);
32
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
33
+ }))
34
+ throw new Error("ARTIFACT_PATH: file is outside the configured resources and run directory.");
35
+ const stat = lstatSync(path);
36
+ if (!stat.isFile() || stat.size > 25_000_000)
37
+ throw new Error("ARTIFACT_SIZE: expected a file no larger than 25 MB.");
38
+ const bytes = readFileSync(path);
39
+ if (createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
40
+ throw new Error("ARTIFACT_CHANGED: file bytes do not match the candidate hash.");
41
+ return { ...artifact, bytes };
42
+ }
43
+ /** Discover only the documented file encoding. Ordinary objects are ordinary evidence. */
44
+ export function fileArtifacts(value) {
45
+ if (!value || typeof value !== "object")
46
+ return [];
47
+ if (FileArtifactSchema.safeParse(value).success)
48
+ return [value];
49
+ return Object.values(value).flatMap(fileArtifacts);
50
+ }
@@ -0,0 +1,5 @@
1
+ export declare function keyFileReason(input: {
2
+ tool_name?: string;
3
+ tool_input?: Record<string, unknown>;
4
+ }): string | undefined;
5
+ export declare function guardKeysCommand(): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { basename } from "node:path";
2
+ /**
3
+ * The Method plugin's check before Claude Code reads a file or runs a command. A key file's values would go into the
4
+ * chat, so opening one needs the person's approval. Only the tool's own input is read; Method's own commands
5
+ * (`method secret import` reads key files without showing values) pass.
6
+ */
7
+ const keyFile = (path) => {
8
+ const name = basename(path.replace(/["']/g, ""));
9
+ return /^\.env(\.[\w-]+)?$/.test(name) && !/^\.env\.(example|sample|template)$/.test(name)
10
+ || name === "secrets.env" || /(^|\/)\.config\/method\/secrets\.json$/.test(path);
11
+ };
12
+ export function keyFileReason(input) {
13
+ const tool = input.tool_input ?? {};
14
+ const reason = "This opens a key file, so its values would go into the chat. To give a key to a Method, run method secret import FILE NAME.";
15
+ if (input.tool_name === "Bash") {
16
+ // A command that runs Method passes, however it is written (Method reads key files without showing values).
17
+ const command = String(tool.command ?? "");
18
+ // No Method command names Method's own secret store; reading it is reading keys.
19
+ if (/\.config\/method\/secrets\.json/.test(command))
20
+ return reason;
21
+ if (/\bmethod\b/i.test(command))
22
+ return undefined;
23
+ return command.split(/[\s;&|()<>]+/).some(word => word && keyFile(word)) ? reason : undefined;
24
+ }
25
+ const path = String(tool.file_path ?? tool.path ?? "");
26
+ return path && keyFile(path) ? reason : undefined;
27
+ }
28
+ export async function guardKeysCommand() {
29
+ let text = "";
30
+ for await (const chunk of process.stdin)
31
+ text += chunk;
32
+ let input = {};
33
+ try {
34
+ input = JSON.parse(text);
35
+ }
36
+ catch {
37
+ return;
38
+ }
39
+ const reason = keyFileReason(input);
40
+ if (reason)
41
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: reason } }) + "\n");
42
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import type { HostedModels } from '@withmethod/runtime';
3
+ import { MethodClient } from './method-client.js';
4
+ /** Where a run's hosted model requests go: this computer's own OpenRouter key when it has one, else the account. */
5
+ export declare function hostedModelsFor(client: MethodClient): Promise<HostedModels>;
6
+ /** Hosted models through the signed-in Method account. The account's model credit pays for them. */
7
+ export declare function managedModels(client: MethodClient): HostedModels & {
8
+ model(): Promise<string>;
9
+ };
10
+ declare const PrivateModels: z.ZodObject<{
11
+ models: z.ZodArray<z.ZodObject<{
12
+ id: z.ZodString;
13
+ providers: z.ZodArray<z.ZodString, "many">;
14
+ }, "strip", z.ZodTypeAny, {
15
+ id: string;
16
+ providers: string[];
17
+ }, {
18
+ id: string;
19
+ providers: string[];
20
+ }>, "many">;
21
+ default: z.ZodOptional<z.ZodNullable<z.ZodString>>;
22
+ checked_at: z.ZodString;
23
+ }, "strip", z.ZodTypeAny, {
24
+ models: {
25
+ id: string;
26
+ providers: string[];
27
+ }[];
28
+ checked_at: string;
29
+ default?: string | null | undefined;
30
+ }, {
31
+ models: {
32
+ id: string;
33
+ providers: string[];
34
+ }[];
35
+ checked_at: string;
36
+ default?: string | null | undefined;
37
+ }>;
38
+ export type PrivateModelList = z.infer<typeof PrivateModels>;
39
+ /**
40
+ * The hosted models that work with the private options (no training, zero data retention), from the server. The list
41
+ * is kept for a day under the Method cache; undefined when it cannot be fetched and no copy is saved.
42
+ */
43
+ export declare function privateModels(client: MethodClient, options?: {
44
+ refresh?: boolean;
45
+ waitMs?: number;
46
+ }): Promise<PrivateModelList | undefined>;
47
+ /** method models [--refresh] [--server URL]: the hosted models that work with the private options. */
48
+ export declare function modelsCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
49
+ export {};