@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.
- package/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/packages/contracts/src/identity.d.ts +16 -0
- package/dist/packages/contracts/src/identity.js +41 -0
- package/dist/packages/contracts/src/method-package.d.ts +71 -0
- package/dist/packages/contracts/src/method-package.js +22 -0
- package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
- package/dist/packages/method-document/src/conversion-report.js +23 -0
- package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
- package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
- package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
- package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
- package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
- package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
- package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
- package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
- package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
- package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
- package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
- package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
- package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
- package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
- package/dist/packages/sdk/examples/files.json +126 -0
- package/dist/packages/sdk/examples/message-routing/README.md +23 -0
- package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
- package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
- package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
- package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
- package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
- package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
- package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
- package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
- package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
- package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
- package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
- package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
- package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
- package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
- package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
- package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
- package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
- package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
- package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
- package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
- package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
- package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
- package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
- package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
- package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
- package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
- package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
- package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
- package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
- package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
- package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
- package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
- package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
- package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
- package/dist/packages/sdk/examples/support-triage/README.md +77 -0
- package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
- package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
- package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
- package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
- package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
- package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
- package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
- package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
- package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
- package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
- package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
- package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
- package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
- package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
- package/dist/packages/sdk/package.json +232 -0
- package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
- package/dist/packages/sdk/src/authoring-example.js +50 -0
- package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
- package/dist/packages/sdk/src/authoring-instructions.js +151 -0
- package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
- package/dist/packages/sdk/src/authoring-prompt.js +8 -0
- package/dist/packages/sdk/src/authoring.d.ts +10 -0
- package/dist/packages/sdk/src/authoring.js +287 -0
- package/dist/packages/sdk/src/bindings.d.ts +4 -0
- package/dist/packages/sdk/src/bindings.js +124 -0
- package/dist/packages/sdk/src/bridge.d.ts +2 -0
- package/dist/packages/sdk/src/bridge.js +23 -0
- package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
- package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
- package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
- package/dist/packages/sdk/src/browser-service.py +127 -0
- package/dist/packages/sdk/src/browser.d.ts +38 -0
- package/dist/packages/sdk/src/browser.js +203 -0
- package/dist/packages/sdk/src/capabilities.d.ts +13 -0
- package/dist/packages/sdk/src/capabilities.js +68 -0
- package/dist/packages/sdk/src/classification-client.d.ts +4 -0
- package/dist/packages/sdk/src/classification-client.js +27 -0
- package/dist/packages/sdk/src/cloud.d.ts +23 -0
- package/dist/packages/sdk/src/cloud.js +43 -0
- package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
- package/dist/packages/sdk/src/computer-settings.js +57 -0
- package/dist/packages/sdk/src/connect.d.ts +47 -0
- package/dist/packages/sdk/src/connect.js +203 -0
- package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
- package/dist/packages/sdk/src/current-inspection.js +146 -0
- package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
- package/dist/packages/sdk/src/current-runtime.js +167 -0
- package/dist/packages/sdk/src/doctor.d.ts +17 -0
- package/dist/packages/sdk/src/doctor.js +68 -0
- package/dist/packages/sdk/src/executable.d.ts +2 -0
- package/dist/packages/sdk/src/executable.js +22 -0
- package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
- package/dist/packages/sdk/src/explain-effects.js +192 -0
- package/dist/packages/sdk/src/explain.d.ts +115 -0
- package/dist/packages/sdk/src/explain.js +441 -0
- package/dist/packages/sdk/src/files.d.ts +9 -0
- package/dist/packages/sdk/src/files.js +50 -0
- package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
- package/dist/packages/sdk/src/guard-keys.js +42 -0
- package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
- package/dist/packages/sdk/src/hosted-models.js +81 -0
- package/dist/packages/sdk/src/improve.d.ts +55 -0
- package/dist/packages/sdk/src/improve.js +159 -0
- package/dist/packages/sdk/src/index.d.ts +13 -0
- package/dist/packages/sdk/src/index.js +13 -0
- package/dist/packages/sdk/src/inspect.d.ts +6 -0
- package/dist/packages/sdk/src/inspect.js +17 -0
- package/dist/packages/sdk/src/link.d.ts +12 -0
- package/dist/packages/sdk/src/link.js +53 -0
- package/dist/packages/sdk/src/local-cli.d.ts +24 -0
- package/dist/packages/sdk/src/local-cli.js +146 -0
- package/dist/packages/sdk/src/local-setup.d.ts +15 -0
- package/dist/packages/sdk/src/local-setup.js +42 -0
- package/dist/packages/sdk/src/method-client.d.ts +30 -0
- package/dist/packages/sdk/src/method-client.js +250 -0
- package/dist/packages/sdk/src/method-files.d.ts +29 -0
- package/dist/packages/sdk/src/method-files.js +93 -0
- package/dist/packages/sdk/src/method-help.d.ts +19 -0
- package/dist/packages/sdk/src/method-help.js +404 -0
- package/dist/packages/sdk/src/method-issues.d.ts +86 -0
- package/dist/packages/sdk/src/method-issues.js +192 -0
- package/dist/packages/sdk/src/method-sync.d.ts +46 -0
- package/dist/packages/sdk/src/method-sync.js +293 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +359 -0
- package/dist/packages/sdk/src/model-checks.d.ts +96 -0
- package/dist/packages/sdk/src/model-checks.js +181 -0
- package/dist/packages/sdk/src/outbox.d.ts +120 -0
- package/dist/packages/sdk/src/outbox.js +297 -0
- package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
- package/dist/packages/sdk/src/own-model-key.js +76 -0
- package/dist/packages/sdk/src/prepare.d.ts +24 -0
- package/dist/packages/sdk/src/prepare.js +245 -0
- package/dist/packages/sdk/src/process.d.ts +15 -0
- package/dist/packages/sdk/src/process.js +107 -0
- package/dist/packages/sdk/src/production.d.ts +182 -0
- package/dist/packages/sdk/src/production.js +284 -0
- package/dist/packages/sdk/src/progress.d.ts +12 -0
- package/dist/packages/sdk/src/progress.js +48 -0
- package/dist/packages/sdk/src/proposals.d.ts +73 -0
- package/dist/packages/sdk/src/proposals.js +284 -0
- package/dist/packages/sdk/src/quality.d.ts +82 -0
- package/dist/packages/sdk/src/quality.js +294 -0
- package/dist/packages/sdk/src/result-files.d.ts +5 -0
- package/dist/packages/sdk/src/result-files.js +149 -0
- package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
- package/dist/packages/sdk/src/result-transfer.js +37 -0
- package/dist/packages/sdk/src/run-data.d.ts +11 -0
- package/dist/packages/sdk/src/run-data.js +80 -0
- package/dist/packages/sdk/src/run-method.d.ts +2 -0
- package/dist/packages/sdk/src/run-method.js +2 -0
- package/dist/packages/sdk/src/run-saved.d.ts +5 -0
- package/dist/packages/sdk/src/run-saved.js +138 -0
- package/dist/packages/sdk/src/run-worker.d.ts +6 -0
- package/dist/packages/sdk/src/run-worker.js +131 -0
- package/dist/packages/sdk/src/secrets.d.ts +37 -0
- package/dist/packages/sdk/src/secrets.js +196 -0
- package/dist/packages/sdk/src/update.d.ts +5 -0
- package/dist/packages/sdk/src/update.js +39 -0
- package/dist/packages/sdk/src/versions.d.ts +58 -0
- package/dist/packages/sdk/src/versions.js +144 -0
- package/dist/packages/sdk/src/worker.d.ts +120 -0
- package/dist/packages/sdk/src/worker.js +443 -0
- package/dist/packages/workflow-language/src/execution.d.ts +1 -0
- package/dist/packages/workflow-language/src/execution.js +1 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
- package/dist/packages/workflow-language/src/inspection.js +130 -0
- package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
- package/dist/packages/workflow-language/src/result-files.js +67 -0
- package/dist/packages/workflow-language/src/schema.d.ts +207 -0
- package/dist/packages/workflow-language/src/schema.js +56 -0
- package/dist/packages/workflow-language/src/validate.d.ts +11 -0
- package/dist/packages/workflow-language/src/validate.js +77 -0
- package/package.json +230 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { assertCheckpointExecutor } from '@withmethod/runtime/executor-version.js';
|
|
5
|
+
import { restorePackage, runtimeVersion } from './method-files.js';
|
|
6
|
+
import { methodCache, pruneCachedRuns } from './prepare.js';
|
|
7
|
+
import { resolveBindings } from './bindings.js';
|
|
8
|
+
import { runCurrentFile } from './current-runtime.js';
|
|
9
|
+
import { MethodSync } from './method-sync.js';
|
|
10
|
+
import { recordKnownVersion } from './outbox.js';
|
|
11
|
+
import { writePrivateJson } from './files.js';
|
|
12
|
+
import { readDocument } from './authoring.js';
|
|
13
|
+
// Older versions are listed only after their saved-package tests pass.
|
|
14
|
+
const supportedPackageRuntimes = new Set([runtimeVersion, '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
|
|
15
|
+
/** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
|
|
16
|
+
export const finishedRun = (status) => status === 'completed' || status === 'unconfirmed';
|
|
17
|
+
export async function runSaved(saved, flags, client) {
|
|
18
|
+
pruneCachedRuns();
|
|
19
|
+
const directory = resolve(flags['run-dir'] ?? join(methodCache(), 'runs', randomUUID()));
|
|
20
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
21
|
+
const requestFile = join(directory, 'request.json');
|
|
22
|
+
const prior = existsSync(requestFile) ? JSON.parse(readFileSync(requestFile, 'utf8')) : null;
|
|
23
|
+
if (prior && (prior.workflow_id !== saved.workflow_id || prior.version_id !== saved.version_id))
|
|
24
|
+
throw Error('This run belongs to another saved version.');
|
|
25
|
+
const request = prior ?? { run_id: randomUUID(), workflow_id: saved.workflow_id, version_id: saved.version_id, server: client.server };
|
|
26
|
+
if (prior && !flags.resume)
|
|
27
|
+
throw Error('This run already exists. Use --resume.');
|
|
28
|
+
writePrivateJson(requestFile, request);
|
|
29
|
+
const sync = new MethodSync(client, directory, saved.workflow_id, saved.version_id, request.run_id, saved.workflow?.run_data === 'device');
|
|
30
|
+
const completedFile = join(directory, 'summary.json');
|
|
31
|
+
if (flags.resume && existsSync(completedFile)) {
|
|
32
|
+
const completed = JSON.parse(readFileSync(completedFile, 'utf8'));
|
|
33
|
+
if (completed.status === 'completed') {
|
|
34
|
+
const statePath = `/api/cli/methods/${encodeURIComponent(saved.workflow_id)}/state`;
|
|
35
|
+
const shared = await client.request(statePath);
|
|
36
|
+
if (shared.enabled && shared.owner_run === request.run_id) {
|
|
37
|
+
const pending = join(directory, 'state-pending.json');
|
|
38
|
+
if (existsSync(pending))
|
|
39
|
+
await client.request(statePath, 'POST', JSON.parse(readFileSync(pending, 'utf8')));
|
|
40
|
+
await client.request(statePath, 'POST', { action: 'release', run_id: request.run_id });
|
|
41
|
+
}
|
|
42
|
+
await MethodSync.retry(directory, client);
|
|
43
|
+
process.stdout.write(JSON.stringify({ ...completed, sync: 'saved' }) + '\n');
|
|
44
|
+
return completed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const inputFile = join(directory, 'requested-inputs.json');
|
|
48
|
+
const inputs = flags.inputs ? JSON.parse(readFileSync(resolve(flags.inputs), 'utf8')) : existsSync(inputFile) ? JSON.parse(readFileSync(inputFile, 'utf8')) : {};
|
|
49
|
+
writePrivateJson(inputFile, inputs);
|
|
50
|
+
const setupFile = join(directory, 'setup.json');
|
|
51
|
+
const setupEvents = existsSync(setupFile) ? JSON.parse(readFileSync(setupFile, 'utf8')) : [];
|
|
52
|
+
setupEvents.push({ at: new Date().toISOString(), type: 'setup_started' });
|
|
53
|
+
writePrivateJson(setupFile, setupEvents);
|
|
54
|
+
await sync.start(saved.workflow, inputs, {});
|
|
55
|
+
process.stderr.write(`Local run: ${directory}\n`);
|
|
56
|
+
let acquired = false, started = false, revision = 0;
|
|
57
|
+
const statePath = `/api/cli/methods/${encodeURIComponent(saved.workflow_id)}/state`;
|
|
58
|
+
try {
|
|
59
|
+
const resuming = !!flags.resume && existsSync(join(directory, 'checkpoint.json'));
|
|
60
|
+
if (resuming)
|
|
61
|
+
assertCheckpointExecutor(JSON.parse(readFileSync(join(directory, 'checkpoint.json'), 'utf8')));
|
|
62
|
+
else if (saved.package && !supportedPackageRuntimes.has(saved.package.runtime))
|
|
63
|
+
throw Object.assign(new Error(`This package records runtime ${saved.package.runtime}; installed executor: ${runtimeVersion}. Use an SDK release that supports this package runtime.`), { code: 'needs_update' });
|
|
64
|
+
// The version is on the server: its runs need no save first.
|
|
65
|
+
recordKnownVersion(client.server, saved.workflow_id, saved.version_id, saved.version_number);
|
|
66
|
+
if (flags.resume && existsSync(join(directory, 'checkpoint.json')) && !existsSync(join(directory, 'runtime.resolved.json'))) {
|
|
67
|
+
const file = join(directory, 'saved.method');
|
|
68
|
+
writePrivateJson(file, saved.workflow);
|
|
69
|
+
return await runCurrentFile(file, { ...flags, workspace: flags.workspace ?? process.cwd(), 'run-dir': directory }, () => sync, undefined, client);
|
|
70
|
+
}
|
|
71
|
+
const root = saved.package ? join(directory, 'source') : resolve(flags.workspace ?? process.cwd());
|
|
72
|
+
await restorePackage(saved, root, client);
|
|
73
|
+
const file = join(directory, 'saved.method');
|
|
74
|
+
writePrivateJson(file, saved.workflow);
|
|
75
|
+
// An older saved version may carry runtime.json in its package; it is part of that version, not a file on this computer.
|
|
76
|
+
let config = existsSync(join(root, 'runtime.json')) ? readDocument(join(root, 'runtime.json')) : { allow_local_processes: true };
|
|
77
|
+
config = await resolveBindings(client, saved.workflow_id, saved.workflow, config, root, directory);
|
|
78
|
+
const missing = Object.entries(saved.workflow.inputs ?? {}).filter(([key, def]) => !Object.hasOwn(inputs, key) && !Object.hasOwn(def, 'default'));
|
|
79
|
+
if (missing.length && !existsSync(join(directory, 'checkpoint.json')))
|
|
80
|
+
throw Object.assign(new Error('Supply the required Method inputs with --inputs FILE.'), { code: 'needs_input', missing: Object.fromEntries(missing) });
|
|
81
|
+
writePrivateJson(join(directory, 'runtime.json'), config);
|
|
82
|
+
if (resuming)
|
|
83
|
+
started = true;
|
|
84
|
+
let stateFile = flags.state;
|
|
85
|
+
const shared = await client.request(statePath);
|
|
86
|
+
if (shared.enabled) {
|
|
87
|
+
if (flags.state)
|
|
88
|
+
throw Error('This Method uses account state. Do not replace it with --state.');
|
|
89
|
+
const state = await client.request(statePath, 'POST', { action: 'acquire', run_id: request.run_id });
|
|
90
|
+
acquired = true;
|
|
91
|
+
revision = state.revision;
|
|
92
|
+
const pendingFile = join(directory, 'state-pending.json');
|
|
93
|
+
if (flags.resume && existsSync(pendingFile)) {
|
|
94
|
+
const pending = JSON.parse(readFileSync(pendingFile, 'utf8'));
|
|
95
|
+
const result = await client.request(statePath, 'POST', pending);
|
|
96
|
+
revision = result.revision;
|
|
97
|
+
}
|
|
98
|
+
stateFile = join(directory, 'initial-state.json');
|
|
99
|
+
if (!resuming)
|
|
100
|
+
writePrivateJson(stateFile, state.value);
|
|
101
|
+
}
|
|
102
|
+
const executionFlags = { ...flags };
|
|
103
|
+
delete executionFlags.inputs;
|
|
104
|
+
delete executionFlags.state;
|
|
105
|
+
const result = await runCurrentFile(file, { ...executionFlags, ...(!resuming ? { inputs: inputFile } : {}), resume: resuming, workspace: root, 'run-dir': directory, ...(stateFile && !resuming ? { state: stateFile } : {}) }, () => sync, async (event) => {
|
|
106
|
+
if (event.event === 'step.started')
|
|
107
|
+
started = true;
|
|
108
|
+
if (acquired && event.event === 'step.accepted') {
|
|
109
|
+
const commit = { action: 'commit', run_id: request.run_id, revision, commit_id: `${request.run_id}:${event.sequence}`, value: event.state };
|
|
110
|
+
writePrivateJson(join(directory, 'state-pending.json'), commit);
|
|
111
|
+
const result = await client.request(statePath, 'POST', commit);
|
|
112
|
+
revision = result.revision;
|
|
113
|
+
}
|
|
114
|
+
}, client, config);
|
|
115
|
+
// Every step finished (an unconfirmed run too), so the run no longer owns the shared state.
|
|
116
|
+
if (acquired && finishedRun(result.status)) {
|
|
117
|
+
await client.request(statePath, 'POST', { action: 'release', run_id: request.run_id });
|
|
118
|
+
acquired = false;
|
|
119
|
+
}
|
|
120
|
+
return { run_id: request.run_id, directory, ...result };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
writePrivateJson(join(directory, 'setup-error.json'), { code: error.code ?? 'setup_failed', error: error.message, ...(error.missing ? { missing: error.missing } : {}) });
|
|
124
|
+
setupEvents.push({ at: new Date().toISOString(), type: 'setup_failed', detail: error.message });
|
|
125
|
+
writePrivateJson(setupFile, setupEvents);
|
|
126
|
+
await sync.finish(error);
|
|
127
|
+
if (error.code === 'needs_input') {
|
|
128
|
+
process.stdout.write(JSON.stringify({ status: 'needs_input', run_id: request.run_id, run_dir: directory, message: error.message, missing: error.missing, continue: `method run ${saved.workflow_id} --version ${saved.version_id} --resume --run-dir ${JSON.stringify(directory)}` }) + '\n');
|
|
129
|
+
process.exitCode = 2;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (acquired && !started)
|
|
136
|
+
await client.request(statePath, 'POST', { action: 'release', run_id: request.run_id });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function startRunWorker(args: string[], directory: string, background?: boolean): Promise<void>;
|
|
2
|
+
export declare function workerAlive(directory: string): boolean;
|
|
3
|
+
export declare function waitForRun(directory: string): Promise<any>;
|
|
4
|
+
export declare function cancelRun(directory: string): {
|
|
5
|
+
status: string;
|
|
6
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, closeSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { writePrivateJson } from './files.js';
|
|
7
|
+
export async function startRunWorker(args, directory, background = false) {
|
|
8
|
+
directory = resolve(directory);
|
|
9
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
10
|
+
const file = join(directory, 'worker-request.json'), status = join(directory, 'worker.json');
|
|
11
|
+
const lock = join(directory, 'worker-start.lock');
|
|
12
|
+
let lockFd;
|
|
13
|
+
try {
|
|
14
|
+
lockFd = openSync(lock, 'wx', 0o600);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
const owner = Number(readFileSync(lock, 'utf8'));
|
|
18
|
+
if (!Number.isSafeInteger(owner) || owner <= 0)
|
|
19
|
+
throw Error('The worker start record is incomplete. Inspect the run before removing worker-start.lock.');
|
|
20
|
+
try {
|
|
21
|
+
process.kill(owner, 0);
|
|
22
|
+
throw Error('This run is already starting. Wait for its worker status.');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'ESRCH')
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
rmSync(lock);
|
|
29
|
+
lockFd = openSync(lock, 'wx', 0o600);
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(lockFd, String(process.pid));
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(status) && workerAlive(directory))
|
|
34
|
+
throw Error('This run is active. Use method wait RUN_DIRECTORY.');
|
|
35
|
+
rmSync(status, { force: true });
|
|
36
|
+
const identity = join(directory, 'run-id.json');
|
|
37
|
+
if (!existsSync(identity))
|
|
38
|
+
writePrivateJson(identity, { id: randomUUID() });
|
|
39
|
+
const runId = JSON.parse(readFileSync(identity, 'utf8')).id;
|
|
40
|
+
writePrivateJson(file, { args });
|
|
41
|
+
const out = openSync(join(directory, 'worker.stdout.log'), 'a', 0o600), err = openSync(join(directory, 'worker.stderr.log'), 'a', 0o600);
|
|
42
|
+
const suffix = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
43
|
+
const entry = fileURLToPath(new URL(`./method${suffix}`, import.meta.url));
|
|
44
|
+
const child = spawn(process.execPath, [...process.execArgv, entry, '__worker', file], { detached: true, stdio: ['ignore', out, err], env: { ...process.env, METHOD_RUN_WORKER: '1' } });
|
|
45
|
+
closeSync(out);
|
|
46
|
+
closeSync(err);
|
|
47
|
+
await new Promise((yes, no) => { child.once('spawn', yes); child.once('error', no); });
|
|
48
|
+
child.unref();
|
|
49
|
+
// The child owns its status record after startup. Do not overwrite a fast completion.
|
|
50
|
+
process.stdout.write(JSON.stringify({ status: 'started', run_id: runId, run_dir: directory, wait: `method wait ${JSON.stringify(directory)}`, cancel: `method cancel ${JSON.stringify(directory)}` }) + '\n');
|
|
51
|
+
const startupDeadline = Date.now() + 30_000;
|
|
52
|
+
while (!existsSync(status)) {
|
|
53
|
+
if (Date.now() > startupDeadline)
|
|
54
|
+
throw Error('The worker did not write its status. Inspect its error log.');
|
|
55
|
+
await new Promise(r => setTimeout(r, 50));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
closeSync(lockFd);
|
|
60
|
+
rmSync(lock, { force: true });
|
|
61
|
+
}
|
|
62
|
+
if (!background)
|
|
63
|
+
await waitForRun(directory);
|
|
64
|
+
}
|
|
65
|
+
export function workerAlive(directory) {
|
|
66
|
+
const path = join(resolve(directory), 'worker.json');
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return false;
|
|
69
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
70
|
+
if (state.status !== 'running')
|
|
71
|
+
return false;
|
|
72
|
+
if (!Number.isSafeInteger(state.pid) || state.pid <= 0)
|
|
73
|
+
throw Error('The worker record has an invalid process ID.');
|
|
74
|
+
const request = join(resolve(directory), 'worker-request.json');
|
|
75
|
+
try {
|
|
76
|
+
if (process.platform === 'linux')
|
|
77
|
+
return readFileSync(`/proc/${state.pid}/cmdline`, 'utf8').split('\0').includes(request);
|
|
78
|
+
const args = execFileSync('/bin/ps', ['-p', String(state.pid), '-o', 'args='], { encoding: 'utf8' });
|
|
79
|
+
return args.includes(request);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Only a missing process means stopped. An inspection error must not allow
|
|
83
|
+
// another worker to start or make cancellation claim there is nothing to stop.
|
|
84
|
+
try {
|
|
85
|
+
process.kill(state.pid, 0);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === 'ESRCH')
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
throw Error('Could not check the run worker. Its status is unknown; do not start another run.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function waitForRun(directory) {
|
|
95
|
+
directory = resolve(directory);
|
|
96
|
+
let offsets = { out: 0, err: 0 };
|
|
97
|
+
const deadline = Date.now() + 30_000;
|
|
98
|
+
for (;;) {
|
|
99
|
+
for (const [key, name, stream] of [['out', 'worker.stdout.log', process.stdout], ['err', 'worker.stderr.log', process.stderr]]) {
|
|
100
|
+
const file = join(directory, name);
|
|
101
|
+
if (existsSync(file)) {
|
|
102
|
+
const data = readFileSync(file);
|
|
103
|
+
stream.write(data.subarray(offsets[key]));
|
|
104
|
+
offsets[key] = data.length;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const file = join(directory, 'worker.json');
|
|
108
|
+
if (existsSync(file)) {
|
|
109
|
+
const status = JSON.parse(readFileSync(file, 'utf8'));
|
|
110
|
+
if (status.status !== 'running') {
|
|
111
|
+
process.exitCode = status.exit_code ?? 1;
|
|
112
|
+
return status;
|
|
113
|
+
}
|
|
114
|
+
if (!workerAlive(directory)) {
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return { status: 'interrupted', run_dir: directory, message: 'The worker stopped. Inspect its run record before resuming.' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!existsSync(file) && Date.now() > deadline)
|
|
120
|
+
throw Error('The run worker did not start. Inspect worker.stderr.log in the run folder.');
|
|
121
|
+
await new Promise(r => setTimeout(r, 500));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function cancelRun(directory) {
|
|
125
|
+
directory = resolve(directory);
|
|
126
|
+
if (!workerAlive(directory))
|
|
127
|
+
return { status: 'not_running' };
|
|
128
|
+
const state = JSON.parse(readFileSync(join(directory, 'worker.json'), 'utf8'));
|
|
129
|
+
process.kill(-state.pid, 'SIGTERM');
|
|
130
|
+
return { status: 'cancellation_requested' };
|
|
131
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Values for the declared names that have one. */
|
|
2
|
+
export declare function resolveSecrets(names: string[]): Record<string, string>;
|
|
3
|
+
/** Every secret value on this computer (the store, and the shell values of the declared names), to find a leaked value. */
|
|
4
|
+
export declare function deviceSecretValues(declared?: string[]): Record<string, string>;
|
|
5
|
+
export declare function importSecrets(file: string, names: string[]): {
|
|
6
|
+
saved: string[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Find KEY=VALUE files in this folder (3 levels down) and next to it (the parent folder, 2 levels down), and say which
|
|
10
|
+
* key names each one holds. Values are never read into the result, so an agent can choose what to import without
|
|
11
|
+
* opening a key file in the chat.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findSecrets(wanted?: string[], folder?: string): {
|
|
14
|
+
not_found?: string[];
|
|
15
|
+
ask?: string;
|
|
16
|
+
next?: string[];
|
|
17
|
+
secrets?: {
|
|
18
|
+
[k: string]: {
|
|
19
|
+
on_this_computer: boolean;
|
|
20
|
+
in_files: string[];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
files: {
|
|
24
|
+
path: string;
|
|
25
|
+
names: string[];
|
|
26
|
+
empty: string[];
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
export declare function listSecrets(names?: string[]): {
|
|
30
|
+
name: string;
|
|
31
|
+
source: string;
|
|
32
|
+
}[];
|
|
33
|
+
/** Ask for one value in a private browser form on this computer. The value never passes through chat or a terminal. */
|
|
34
|
+
export declare function setSecret(key: string, open?: (url: string) => void, timeoutMs?: number): Promise<{
|
|
35
|
+
saved: string[];
|
|
36
|
+
}>;
|
|
37
|
+
export declare function secretCommand(args: string[]): Promise<boolean>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { writePrivateJson } from './files.js';
|
|
7
|
+
import { openUrl } from './method-client.js';
|
|
8
|
+
/**
|
|
9
|
+
* Secret values stay on this computer, in one private file next to the Method sign-in.
|
|
10
|
+
* Method servers never receive them. A value exported in the shell takes precedence.
|
|
11
|
+
*/
|
|
12
|
+
const storeFile = () => join(homedir(), '.config', 'method', 'secrets.json');
|
|
13
|
+
/** Any environment variable name, except the names that the runtime sets for scripts (PATH, LANG, HOME, METHOD_*). */
|
|
14
|
+
const valid = (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && key.length <= 80 && !['PATH', 'LANG', 'HOME'].includes(key) && !key.startsWith('METHOD_');
|
|
15
|
+
const checkName = (key) => { if (!valid(key))
|
|
16
|
+
throw Error(`Use an environment variable name that the runtime does not set (not PATH, LANG, HOME, or METHOD_...): ${key}`); };
|
|
17
|
+
function readStore() {
|
|
18
|
+
const file = storeFile();
|
|
19
|
+
return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
20
|
+
}
|
|
21
|
+
function save(values) {
|
|
22
|
+
writePrivateJson(storeFile(), { ...readStore(), ...values });
|
|
23
|
+
}
|
|
24
|
+
/** Values for the declared names that have one. */
|
|
25
|
+
export function resolveSecrets(names) {
|
|
26
|
+
const store = readStore();
|
|
27
|
+
return Object.fromEntries(names.flatMap(key => {
|
|
28
|
+
const value = process.env[key] || store[key];
|
|
29
|
+
return value ? [[key, value]] : [];
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
/** Every secret value on this computer (the store, and the shell values of the declared names), to find a leaked value. */
|
|
33
|
+
export function deviceSecretValues(declared = []) {
|
|
34
|
+
const values = { ...readStore() };
|
|
35
|
+
for (const key of declared)
|
|
36
|
+
if (process.env[key])
|
|
37
|
+
values[key] = process.env[key];
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
/** Read KEY=VALUE lines without executing the file. Quotes and a leading export are removed. */
|
|
41
|
+
function parseEnv(text) {
|
|
42
|
+
const values = {};
|
|
43
|
+
for (const line of text.split(/\r?\n/)) {
|
|
44
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
|
45
|
+
if (!match || line.trimStart().startsWith('#'))
|
|
46
|
+
continue;
|
|
47
|
+
let value = match[2];
|
|
48
|
+
if (/^(['"]).*\1$/.test(value))
|
|
49
|
+
value = value.slice(1, -1);
|
|
50
|
+
else
|
|
51
|
+
value = value.replace(/\s+#.*$/, '');
|
|
52
|
+
values[match[1]] = value;
|
|
53
|
+
}
|
|
54
|
+
return values;
|
|
55
|
+
}
|
|
56
|
+
export function importSecrets(file, names) {
|
|
57
|
+
if (!names.length)
|
|
58
|
+
throw Error('Name the secrets to copy: method secret import FILE NAME...');
|
|
59
|
+
names.forEach(checkName);
|
|
60
|
+
const values = parseEnv(readFileSync(resolve(file), 'utf8'));
|
|
61
|
+
const missing = names.filter(key => !values[key]);
|
|
62
|
+
if (missing.length)
|
|
63
|
+
throw Error(`Not found in ${file}: ${missing.join(', ')}. No secret was copied.`);
|
|
64
|
+
save(Object.fromEntries(names.map(key => [key, values[key]])));
|
|
65
|
+
return { saved: names };
|
|
66
|
+
}
|
|
67
|
+
const keyFile = /^(?:\.env(?:\..+)?|.+\.env|secrets?\.env)$/i;
|
|
68
|
+
const skipped = new Set(['node_modules', '.git', '.method-runs', '.venv', 'venv', 'dist', 'build', '__pycache__']);
|
|
69
|
+
function keyFiles(root, depth, found) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = readdirSync(root);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const path = join(root, entry);
|
|
79
|
+
let info;
|
|
80
|
+
try {
|
|
81
|
+
info = statSync(path);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (info.isDirectory()) {
|
|
87
|
+
if (depth > 0 && !skipped.has(entry))
|
|
88
|
+
keyFiles(path, depth - 1, found);
|
|
89
|
+
}
|
|
90
|
+
else if (keyFile.test(entry) && info.size < 1 << 20)
|
|
91
|
+
found.add(path);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Find KEY=VALUE files in this folder (3 levels down) and next to it (the parent folder, 2 levels down), and say which
|
|
96
|
+
* key names each one holds. Values are never read into the result, so an agent can choose what to import without
|
|
97
|
+
* opening a key file in the chat.
|
|
98
|
+
*/
|
|
99
|
+
export function findSecrets(wanted = [], folder = process.cwd()) {
|
|
100
|
+
const found = new Set();
|
|
101
|
+
keyFiles(folder, 3, found);
|
|
102
|
+
keyFiles(dirname(folder), 2, found);
|
|
103
|
+
const files = [...found].sort().map(path => {
|
|
104
|
+
const values = parseEnv(readFileSync(path, 'utf8'));
|
|
105
|
+
return { path: relative(folder, path) || path, names: Object.keys(values).sort(), empty: Object.keys(values).filter(key => !values[key]).sort() };
|
|
106
|
+
});
|
|
107
|
+
const store = readStore();
|
|
108
|
+
const secrets = Object.fromEntries(wanted.map(key => [key, {
|
|
109
|
+
on_this_computer: !!(process.env[key] || store[key]),
|
|
110
|
+
in_files: files.filter(file => file.names.includes(key) && !file.empty.includes(key)).map(file => file.path),
|
|
111
|
+
}]));
|
|
112
|
+
const missing = wanted.filter(key => !secrets[key].on_this_computer);
|
|
113
|
+
// One import per file, preferring the file that holds the most missing names.
|
|
114
|
+
const next = [];
|
|
115
|
+
let left = missing.filter(key => secrets[key].in_files.length);
|
|
116
|
+
while (left.length) {
|
|
117
|
+
const best = files.map(file => ({ file, keys: left.filter(key => secrets[key].in_files.includes(file.path)) })).sort((a, b) => b.keys.length - a.keys.length)[0];
|
|
118
|
+
next.push(`method secret import ${JSON.stringify(best.file.path)} ${best.keys.join(' ')}`);
|
|
119
|
+
left = left.filter(key => !best.keys.includes(key));
|
|
120
|
+
}
|
|
121
|
+
const unfound = missing.filter(key => !secrets[key].in_files.length);
|
|
122
|
+
return { files, ...(wanted.length ? { secrets } : {}), ...(next.length ? { next } : {}),
|
|
123
|
+
...(unfound.length ? { not_found: unfound, ask: `Ask the user to run method secret set ${unfound[0]} for each name in not_found.` } : {}) };
|
|
124
|
+
}
|
|
125
|
+
export function listSecrets(names) {
|
|
126
|
+
const store = readStore();
|
|
127
|
+
const keys = names?.length ? names : Object.keys(store).sort();
|
|
128
|
+
return keys.map(key => ({ name: key, source: process.env[key] ? 'shell' : store[key] ? 'this computer' : 'missing' }));
|
|
129
|
+
}
|
|
130
|
+
/** Ask for one value in a private browser form on this computer. The value never passes through chat or a terminal. */
|
|
131
|
+
export async function setSecret(key, open = openUrl, timeoutMs = 10 * 60_000) {
|
|
132
|
+
checkName(key);
|
|
133
|
+
const token = randomBytes(24).toString('base64url');
|
|
134
|
+
const page = (body) => `<!doctype html><meta charset="utf-8"><title>Method secret</title><body style="font:16px system-ui;max-width:32rem;margin:4rem auto">${body}</body>`;
|
|
135
|
+
return new Promise((done, fail) => {
|
|
136
|
+
const server = createServer(async (request, response) => {
|
|
137
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
138
|
+
const supplied = Buffer.from(url.pathname.slice(1)), expected = Buffer.from(token);
|
|
139
|
+
if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected) || request.headers.host !== `127.0.0.1:${server.address().port}`) {
|
|
140
|
+
response.writeHead(404).end();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (request.method === 'GET') {
|
|
144
|
+
response.writeHead(200, { 'content-type': 'text/html', 'cache-control': 'no-store' });
|
|
145
|
+
response.end(page(`<h1>${key}</h1><p>Paste the value. It is saved only on this computer, in ${storeFile()}.</p><form method="post"><input name="value" type="password" autofocus required style="width:100%;font:inherit;padding:.5rem"><p><button style="font:inherit">Save</button></p></form>`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let body = '';
|
|
149
|
+
for await (const chunk of request) {
|
|
150
|
+
body += chunk;
|
|
151
|
+
if (body.length > 100_000) {
|
|
152
|
+
response.writeHead(413).end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// A pasted value often carries a line break or space at its ends.
|
|
157
|
+
const value = (new URLSearchParams(body).get('value') ?? '').trim();
|
|
158
|
+
if (!value) {
|
|
159
|
+
response.writeHead(400).end(page('<p>Enter a value.</p>'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
save({ [key]: value });
|
|
163
|
+
response.writeHead(200, { 'content-type': 'text/html' }).end(page(`<p>Saved ${key}. You can close this tab.</p>`));
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
server.close();
|
|
166
|
+
done({ saved: [key] });
|
|
167
|
+
});
|
|
168
|
+
const timer = setTimeout(() => { server.close(); fail(Error(`No value was entered for ${key}. Run method secret set ${key} again.`)); }, timeoutMs);
|
|
169
|
+
server.listen(0, '127.0.0.1', () => {
|
|
170
|
+
const url = `http://127.0.0.1:${server.address().port}/${token}`;
|
|
171
|
+
process.stderr.write(`Enter ${key} in your browser: ${url}\n`);
|
|
172
|
+
open(url);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export async function secretCommand(args) {
|
|
177
|
+
const [action, ...rest] = args;
|
|
178
|
+
const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
179
|
+
if (action === 'import' && rest[0])
|
|
180
|
+
return print(importSecrets(rest[0], rest.slice(1)));
|
|
181
|
+
if (action === 'set' && rest.length === 1)
|
|
182
|
+
return print(await setSecret(rest[0]));
|
|
183
|
+
if (action === 'list')
|
|
184
|
+
return print({ secrets: listSecrets(rest) });
|
|
185
|
+
if (action === 'find')
|
|
186
|
+
return print(findSecrets(await declaredSecrets(rest[0])));
|
|
187
|
+
throw Error('Use method secret find [FILE], method secret import FILE NAME..., method secret set NAME, or method secret list.');
|
|
188
|
+
}
|
|
189
|
+
/** The secrets that a Method declares: the named file, or the only Method in this folder. */
|
|
190
|
+
async function declaredSecrets(file) {
|
|
191
|
+
const path = file ?? (() => { const methods = readdirSync(process.cwd(), { withFileTypes: true }).filter(entry => entry.isFile() && entry.name.endsWith('.method')).map(entry => entry.name); return methods.length === 1 ? methods[0] : undefined; })();
|
|
192
|
+
if (!path)
|
|
193
|
+
return [];
|
|
194
|
+
const { readDocument } = await import('@withmethod/runtime/io.js');
|
|
195
|
+
return Object.keys((await readDocument(resolve(path))).secrets ?? {});
|
|
196
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The newest release, from the version pointer that the installer reads, checked at most once a day.
|
|
3
|
+
* Returns a one-line notice when it is newer than this CLI, or nothing. A network problem is never an error.
|
|
4
|
+
*/
|
|
5
|
+
export declare function updateNotice(fetcher?: typeof fetch, now?: number): Promise<string | undefined>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import packageInfo from '../package.json' with { type: 'json' };
|
|
4
|
+
import { methodCache } from './prepare.js';
|
|
5
|
+
import { writePrivateJson } from './files.js';
|
|
6
|
+
const day = 86_400_000;
|
|
7
|
+
const newer = (a, b) => {
|
|
8
|
+
const [x, y] = [a, b].map(v => v.split('.').map(Number));
|
|
9
|
+
for (let i = 0; i < 3; i++)
|
|
10
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
11
|
+
return (x[i] ?? 0) > (y[i] ?? 0);
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The newest release, from the version pointer that the installer reads, checked at most once a day.
|
|
16
|
+
* Returns a one-line notice when it is newer than this CLI, or nothing. A network problem is never an error.
|
|
17
|
+
*/
|
|
18
|
+
export async function updateNotice(fetcher = fetch, now = Date.now()) {
|
|
19
|
+
const cache = join(methodCache(), 'latest.json');
|
|
20
|
+
let latest;
|
|
21
|
+
try {
|
|
22
|
+
const saved = existsSync(cache) ? JSON.parse(readFileSync(cache, 'utf8')) : null;
|
|
23
|
+
if (saved && now - saved.checked_at < day)
|
|
24
|
+
latest = saved.version;
|
|
25
|
+
else {
|
|
26
|
+
const target = `${process.platform === 'darwin' ? 'darwin' : 'linux'}-${process.arch === 'arm64' ? 'arm64' : 'x64'}`;
|
|
27
|
+
const base = process.env.METHOD_DOWNLOAD_URL ?? 'https://app.withmethod.ai/downloads/cli';
|
|
28
|
+
const response = await fetcher(`${base}/latest-${target}.txt`, { signal: AbortSignal.timeout(2000) });
|
|
29
|
+
latest = response.ok ? /^version (\S+)$/m.exec(await response.text())?.[1] : undefined;
|
|
30
|
+
writePrivateJson(cache, { checked_at: now, version: latest ?? null });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (!latest || !newer(latest, packageInfo.version))
|
|
37
|
+
return undefined;
|
|
38
|
+
return `Method ${latest} is available (you have ${packageInfo.version}). Update: curl -fsSL https://app.withmethod.ai/install.sh | sh`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { versionIdFor } from '../../contracts/src/identity.js';
|
|
2
|
+
import type { MethodClient } from './method-client.js';
|
|
3
|
+
import { Outbox } from './outbox.js';
|
|
4
|
+
export declare const methodIdPattern: RegExp;
|
|
5
|
+
/** A new account Method ID, made on this computer. The first save of this ID creates the Method. */
|
|
6
|
+
export declare const newMethodId: () => string;
|
|
7
|
+
export { versionIdFor };
|
|
8
|
+
/** The Method without its `id:` line: the content that a version holds. */
|
|
9
|
+
export declare function methodContent<T extends object>(document: T): Omit<T, 'id'>;
|
|
10
|
+
/**
|
|
11
|
+
* Write `id: METHOD_ID` as the second line of a Method file (after `format:`), and declare method/3.4, the format that
|
|
12
|
+
* has `id`. A JSON file stays JSON. The rest of the file is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeMethodId(file: string, id: string): void;
|
|
15
|
+
/** The file's Method ID; writes a new one when the file has none. */
|
|
16
|
+
export declare function ensureMethodId(file: string): string;
|
|
17
|
+
/** The Method ID and version ID of a file's current content, computed on this computer; undefined without an id: line. */
|
|
18
|
+
export declare function currentVersion(file: string): Promise<{
|
|
19
|
+
method_id: any;
|
|
20
|
+
version_id: string;
|
|
21
|
+
} | undefined>;
|
|
22
|
+
export type PreparedVersion = {
|
|
23
|
+
method_id: string;
|
|
24
|
+
version_id: string;
|
|
25
|
+
url: string;
|
|
26
|
+
unchanged: boolean;
|
|
27
|
+
workflow: any;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Prepare the save of the current file. It sends no request: it computes the version ID from the content and, when
|
|
31
|
+
* this computer has not saved that version yet, puts the files and the version save in the outbox.
|
|
32
|
+
*/
|
|
33
|
+
export declare function prepareVersion(client: MethodClient, outbox: Outbox, file: string, reason?: string): Promise<PreparedVersion>;
|
|
34
|
+
/** The one line a run prints about its version. */
|
|
35
|
+
export declare function savedLine(client: MethodClient, outbox: Outbox, version: PreparedVersion): string;
|
|
36
|
+
/**
|
|
37
|
+
* Publish the current file: save its version (waiting for the upload, with no fixed time limit), check the cases,
|
|
38
|
+
* then mark that version published with the reason.
|
|
39
|
+
*/
|
|
40
|
+
export declare function publish(client: MethodClient, file: string, reason?: string, accepted?: string[]): Promise<{
|
|
41
|
+
published_at: any;
|
|
42
|
+
publish_reason: any;
|
|
43
|
+
accepted_issues?: number;
|
|
44
|
+
issues?: {
|
|
45
|
+
accept?: string;
|
|
46
|
+
message: string;
|
|
47
|
+
fix: string;
|
|
48
|
+
at?: string;
|
|
49
|
+
level: import("./model-checks.js").Level;
|
|
50
|
+
code: string;
|
|
51
|
+
}[];
|
|
52
|
+
cases?: string;
|
|
53
|
+
workflow_id: string;
|
|
54
|
+
version_id: string;
|
|
55
|
+
version_number: any;
|
|
56
|
+
url: string;
|
|
57
|
+
unchanged: boolean;
|
|
58
|
+
}>;
|