@method-ai-hq/sdk 0.14.3 → 0.14.6
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/README.md +1 -1
- package/dist/packages/sdk/package.json +1 -1
- package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
- package/dist/packages/sdk/src/authoring-instructions.js +4 -1
- package/dist/packages/sdk/src/authoring.js +6 -4
- package/dist/packages/sdk/src/current-runtime.d.ts +2 -0
- package/dist/packages/sdk/src/current-runtime.js +12 -2
- package/dist/packages/sdk/src/home.d.ts +46 -0
- package/dist/packages/sdk/src/home.js +137 -0
- package/dist/packages/sdk/src/legacy.d.ts +8 -0
- package/dist/packages/sdk/src/legacy.js +72 -0
- package/dist/packages/sdk/src/local-cli.js +9 -7
- package/dist/packages/sdk/src/method-help.js +6 -7
- package/dist/packages/sdk/src/method-issues.d.ts +0 -5
- package/dist/packages/sdk/src/method-issues.js +10 -19
- package/dist/packages/sdk/src/method-sync.d.ts +4 -0
- package/dist/packages/sdk/src/method-sync.js +13 -2
- package/dist/packages/sdk/src/method.js +26 -3
- package/dist/packages/sdk/src/resume-release.d.ts +5 -0
- package/dist/packages/sdk/src/resume-release.js +30 -0
- package/dist/packages/sdk/src/run-saved.js +1 -1
- package/dist/packages/sdk/src/run-worker.d.ts +2 -1
- package/dist/packages/sdk/src/run-worker.js +8 -4
- package/dist/packages/sdk/src/update.d.ts +8 -3
- package/dist/packages/sdk/src/update.js +32 -10
- package/dist/packages/sdk/src/versions.js +3 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +12 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
MIT-licensed JavaScript API and the `method` CLI. Source and releases: https://github.com/method-ai-hq/method-sdk
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.14.
|
|
6
|
+
npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.14.6/method-ai-hq-sdk-0.14.6.tgz
|
|
7
7
|
npx method --version
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -5,7 +5,7 @@ export declare const designProcedure = "# Choose the design\n\n| Requirement | S
|
|
|
5
5
|
/** The full check rules; in concepts. */
|
|
6
6
|
export declare const checkRules = "# Checks\n\nAdd a check only when it catches a concrete failure that matters to the result. Output types and the runtime already validate shapes; do not repeat them. Do not check wording, headings, keywords, lengths, or counts unless the task requires them: \"write accurate prose\" is not a reason to match strings.\n\nUse the simplest check that proves the fact: a built-in equals, count, present, or file check, a script, or an agent check. Local file writes need no check: the runtime observes files connections itself. A change to a service, an API, email, or a browser that sends needs an effect that reads the result back, because a receipt or a 200 status shows only that the request was accepted. Use a built-in observer (http, sqlite, file) when one fits. When a change cannot or need not be observed, such as a browser step that only reads, write no_effect_reason instead.\n\nExisting checks and tests are implementation choices, not user requirements. Remove a check that is unnecessary, repeats other validation, or enforces a requirement that nobody asked for. Delete the tests and instructions that exist only for it, and do not replace it. Do not change useful output only to satisfy a check.\n";
|
|
7
7
|
/** The rules that make a first Method work quickly. They come before every other part of the guide. */
|
|
8
|
-
export declare const firstMethodRules = "# Build a Method\n\n1. **Sign in first.** Run `method status`. If it is not signed in, run `method login` and let the user approve in the browser. Signed in, model and classification steps need no keys, each run saves a version when the file changed, and runs appear on the dashboard.\n2. **Each model or classifier request is its own step**: `call`, `agent`, or `classify`, with its prompt in the Method, so the user can change one prompt, run again, and compare. A script never calls a model API. When the user already has code that does the work, keep its fixed logic as `run` steps and move each prompt and rubric into its own step; do not wrap the code. \"The same thing\" means the same behavior in steps. If a request cannot become a step because Method lacks a feature, tell the user what is missing.\n3. **Show the design, then build it in the same turn.** Show each step with its type, purpose, and output, and a table of the prompts and rubrics in the user's existing work, each with the step that holds it. Do not wait for approval unless the user asked to approve first. Ask only for information that you cannot find and that would change the design.\n4. **Run early and often.** Write the first steps, validate, run, then add the next steps. Fix errors. Fix each warning, or accept it with the user's reason. Never add a check only to remove a warning. A new run reuses every step whose definition and inputs did not change, so each run executes only what changed. `--rerun STEP` runs a step again anyway; `--fresh` runs every step.\n5. **Keep the inputs of the existing code.** If the code takes an ID and looks up the record, the Method takes the same ID and looks it up the same way.\n6. **Use the defaults without asking.** Model steps use the account's hosted models and `classify` uses Method's classifier, so no model or classifier keys are needed. To keep the model that existing code uses, add `models: {writer: provider/model}` to the Method and use `model: writer` in the steps (or `model: provider/model` on the step); it needs no key. Keep the models that the Method or the existing code names: when a model step fails, show the error and ask before you change its model. Run records go to the user's account. When the user asks that run data stay on this computer or not be uploaded, set `run_data: device`: the run's inputs, outputs, and files stay here, and the account keeps only each step's status and timing. Model steps still send each step's input to the model to answer it; nothing is kept. Tell the user that in one line, then run again.\n7. **Keys stay out of chat.** Declare each key that a script needs under `secrets:` with its purpose; every script of the Method receives all of its declared secrets. Run `method secret find` in the Method's folder: it lists the key files nearby and the names in each, never the values, and prints the import command. Run that command and tell the user which file each key came from: the key stays on this computer, so there is nothing to ask. If it finds nothing, ask the user to run `method secret set NAME`, which opens a private form in their browser. Never open, print, or search a key file (`cat`, `grep`, an editor), including Method's own secret store, and never ask for a value in chat: the values would go into the chat. When a run says Method's model credit is used up, run `method config model-key` yourself: it takes the user's own OpenRouter key from the one nearby key file that holds it, or opens a private form for it. Then run again.\n8. **Choose a sample yourself** from the user's data, and check that it has the sources the Method needs. Ask only when there is no good sample.\n9. **Iterate.** After a good run, show the result and the
|
|
8
|
+
export declare const firstMethodRules = "# Build a Method\n\n1. **Sign in first.** Run `method status`. If it is not signed in, run `method login` and let the user approve in the browser. Signed in, model and classification steps need no keys, each run saves a version when the file changed, and runs appear on the dashboard.\n2. **Each model or classifier request is its own step**: `call`, `agent`, or `classify`, with its prompt in the Method, so the user can change one prompt, run again, and compare. A script never calls a model API. When the user already has code that does the work, keep its fixed logic as `run` steps and move each prompt and rubric into its own step; do not wrap the code. \"The same thing\" means the same behavior in steps. If a request cannot become a step because Method lacks a feature, tell the user what is missing.\n3. **Show the design, then build it in the same turn.** Show each step with its type, purpose, and output, and a table of the prompts and rubrics in the user's existing work, each with the step that holds it. Do not wait for approval unless the user asked to approve first. Ask only for information that you cannot find and that would change the design.\n4. **Run early and often.** Write the first steps, validate, run, then add the next steps. Fix errors. Fix each warning, or accept it with the user's reason. Never add a check only to remove a warning. A new run reuses every step whose definition and inputs did not change, so each run executes only what changed. `--rerun STEP` runs a step again anyway; `--fresh` runs every step.\n5. **Keep the inputs of the existing code.** If the code takes an ID and looks up the record, the Method takes the same ID and looks it up the same way.\n6. **Use the defaults without asking.** Model steps use the account's hosted models and `classify` uses Method's classifier, so no model or classifier keys are needed. To keep the model that existing code uses, add `models: {writer: provider/model}` to the Method and use `model: writer` in the steps (or `model: provider/model` on the step); it needs no key. Keep the models that the Method or the existing code names: when a model step fails, show the error and ask before you change its model. Run records go to the user's account. When the user asks that run data stay on this computer or not be uploaded, set `run_data: device`: the run's inputs, outputs, and files stay here, and the account keeps only each step's status and timing. Model steps still send each step's input to the model to answer it; nothing is kept. Tell the user that in one line, then run again.\n7. **Keys stay out of chat.** Declare each key that a script needs under `secrets:` with its purpose; every script of the Method receives all of its declared secrets. Run `method secret find` in the Method's folder: it lists the key files nearby and the names in each, never the values, and prints the import command. Run that command and tell the user which file each key came from: the key stays on this computer, so there is nothing to ask. If it finds nothing, ask the user to run `method secret set NAME`, which opens a private form in their browser. Never open, print, or search a key file (`cat`, `grep`, an editor), including Method's own secret store, and never ask for a value in chat: the values would go into the chat. When a run says Method's model credit is used up, run `method config model-key` yourself: it takes the user's own OpenRouter key from the one nearby key file that holds it, or opens a private form for it. Then run again.\n8. **Choose a sample yourself** from the user's data, and check that it has the sources the Method needs. Ask only when there is no good sample.\n9. **Iterate.** `method run` prints a `dashboard` link when the run starts and again with its result. Give the user the link as soon as the run starts. After a good run, show the result and the link, and ask what to change. Change the step and run again. Show only results that a run made: never write or edit a result by hand, and fix a wrong result in the Method, not in the data it reads. If you cannot run, say so.\n10. **Publish** with `method publish FILE --reason TEXT` when the user wants to share or schedule a version. It runs the Method's cases first.\n11. **Talk about the work, not about Method.** Unless the user asks, do not mention Method's versions, formats, settings files, or other internal files, and do not ask the user about them, about models, or about which saved version to use. Method updates itself and converts older Method files when it runs them. When the user asks whether a Method works, or asks you to make it work, run it with the inputs of a recent run and show the result and the dashboard link in a few sentences. Say that it works only after a run completed.\n12. **One file for each Method.** Change and run a Method in its own file. Do not copy it to a temporary or other folder to change it: Method does not run or save the copy while the Method's file exists, and names that file.\n13. **Questions in a run are for the user.** When a run stops with `needs_input`, ask the user its `question` in plain words, wait for the answer, and continue the run with it as the output says. Never answer it yourself.\n\nWhen a run fails, read its `fix` and `diagnostics`, change the step, and run again.\n";
|
|
9
9
|
/** How to write the prompt inside a call, agent, or classify step. */
|
|
10
10
|
export declare const promptRules = "# Write each prompt\n\n- **One task per prompt.** If a prompt says \"then\" or has several jobs, make several steps.\n- **Say what to do.** Leave out background that does not change the output.\n- **Show 1 to 3 examples of good output.** Take them from the user's work: examples in an existing prompt, a past output that the user approved, or the first run that the user accepts. Pass a long example as an input, not as text in the prompt.\n- **Put the output shape in `out`**, not in the prompt.\n- **Say what to do when the input does not have the answer**, for example \"leave due empty\". For classify, add an option such as `unclear`.\n\nBefore:\n\n```text\nYou are an expert support analyst with years of experience. Our company values clear\ncommunication. Read the tickets, decide which ones are urgent, write a summary, and\nreturn JSON with a \"summary\" key. Make it good.\n```\n\nAfter (urgency is a classify step before this one; the shape is in `out`):\n\n```text\nWrite a five-sentence summary of this week's support tickets for the team.\nStart with the high-urgency tickets. Quote no customer names.\nWrite it in the style of this summary from an earlier week:\n\n{{example}}\n```\n";
|
|
11
11
|
/** Enough of the format to write a Method without reading the JSON schema. */
|
|
@@ -37,8 +37,11 @@ export const firstMethodRules = `# Build a Method
|
|
|
37
37
|
6. **Use the defaults without asking.** Model steps use the account's hosted models and \`classify\` uses Method's classifier, so no model or classifier keys are needed. To keep the model that existing code uses, add \`models: {writer: provider/model}\` to the Method and use \`model: writer\` in the steps (or \`model: provider/model\` on the step); it needs no key. Keep the models that the Method or the existing code names: when a model step fails, show the error and ask before you change its model. Run records go to the user's account. When the user asks that run data stay on this computer or not be uploaded, set \`run_data: device\`: the run's inputs, outputs, and files stay here, and the account keeps only each step's status and timing. Model steps still send each step's input to the model to answer it; nothing is kept. Tell the user that in one line, then run again.
|
|
38
38
|
7. **Keys stay out of chat.** Declare each key that a script needs under \`secrets:\` with its purpose; every script of the Method receives all of its declared secrets. Run \`method secret find\` in the Method's folder: it lists the key files nearby and the names in each, never the values, and prints the import command. Run that command and tell the user which file each key came from: the key stays on this computer, so there is nothing to ask. If it finds nothing, ask the user to run \`method secret set NAME\`, which opens a private form in their browser. Never open, print, or search a key file (\`cat\`, \`grep\`, an editor), including Method's own secret store, and never ask for a value in chat: the values would go into the chat. When a run says Method's model credit is used up, run \`method config model-key\` yourself: it takes the user's own OpenRouter key from the one nearby key file that holds it, or opens a private form for it. Then run again.
|
|
39
39
|
8. **Choose a sample yourself** from the user's data, and check that it has the sources the Method needs. Ask only when there is no good sample.
|
|
40
|
-
9. **Iterate.** After a good run, show the result and the
|
|
40
|
+
9. **Iterate.** \`method run\` prints a \`dashboard\` link when the run starts and again with its result. Give the user the link as soon as the run starts. After a good run, show the result and the link, and ask what to change. Change the step and run again. Show only results that a run made: never write or edit a result by hand, and fix a wrong result in the Method, not in the data it reads. If you cannot run, say so.
|
|
41
41
|
10. **Publish** with \`method publish FILE --reason TEXT\` when the user wants to share or schedule a version. It runs the Method's cases first.
|
|
42
|
+
11. **Talk about the work, not about Method.** Unless the user asks, do not mention Method's versions, formats, settings files, or other internal files, and do not ask the user about them, about models, or about which saved version to use. Method updates itself and converts older Method files when it runs them. When the user asks whether a Method works, or asks you to make it work, run it with the inputs of a recent run and show the result and the dashboard link in a few sentences. Say that it works only after a run completed.
|
|
43
|
+
12. **One file for each Method.** Change and run a Method in its own file. Do not copy it to a temporary or other folder to change it: Method does not run or save the copy while the Method's file exists, and names that file.
|
|
44
|
+
13. **Questions in a run are for the user.** When a run stops with \`needs_input\`, ask the user its \`question\` in plain words, wait for the answer, and continue the run with it as the output says. Never answer it yourself.
|
|
42
45
|
|
|
43
46
|
When a run fails, read its \`fix\` and \`diagnostics\`, change the step, and run again.
|
|
44
47
|
`;
|
|
@@ -185,11 +185,13 @@ export async function localAuthoring(args) {
|
|
|
185
185
|
const runs = step.ask ? "asks the user"
|
|
186
186
|
: exec.kind === "run" ? `${exec.runtime} ${exec.entrypoint}`
|
|
187
187
|
: exec.kind === "classify" ? (config.classification?.api_key_env || ownKey ? "Jev through OpenRouter with your own key" : "Method's classifier")
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// The computer's agent setting (or --agent) runs every model step, whatever model the Method names.
|
|
189
|
+
: agent && ["call", "agent"].includes(exec.kind) ? agent
|
|
190
|
+
: profile?.backend === "method" ? (ownKey && !agent ? `${profile.model} through OpenRouter with your own key` : `the hosted model ${profile.model}`)
|
|
191
|
+
: profile ? `${profile.backend}${profile.model ? ` ${profile.model}` : ""}`
|
|
192
|
+
: signedIn ? (ownKey && !agent ? "the account's default model through OpenRouter with your own key" : "the account's hosted model") : "a local Codex or Claude agent";
|
|
191
193
|
const needs = [...(exec?.kind === "run" ? missingSecrets.map(name => `secret ${name}`) : []),
|
|
192
|
-
...(!signedIn && (exec?.kind === "classify" && !config.classification?.api_key_env && !ownKey || ["call", "agent"].includes(exec?.kind) && !profile) ? [`method login (for ${exec.kind === "classify" ? "classification" : "hosted models"})`] : [])];
|
|
194
|
+
...(!signedIn && (exec?.kind === "classify" && !config.classification?.api_key_env && !ownKey || ["call", "agent"].includes(exec?.kind) && !profile && !agent) ? [`method login (for ${exec.kind === "classify" ? "classification" : "hosted models"})`] : [])];
|
|
193
195
|
return { id, runs, ...(needs.length ? { needs } : {}) };
|
|
194
196
|
});
|
|
195
197
|
print({ valid: true, definition, local_setup: missingSetup.length ? "needs_preparation" : "valid", ...(missingSetup.length ? { missing_setup: [...new Set(missingSetup)] } : {}), workspace: sourceRoot, files: files.length, steps, executed: false, ...shown });
|
|
@@ -5,6 +5,8 @@ import type { parse } from "./local-cli.js";
|
|
|
5
5
|
export { executeMethod as runCurrentMethod };
|
|
6
6
|
/** Recent run folders on this computer, newest first. The runtime reuses only iterations whose key matches. */
|
|
7
7
|
export declare function recentRuns(exclude: string, limit?: number): string[];
|
|
8
|
+
/** Answers for ask steps: a key without an iteration ("confirm") answers its first iteration ("confirm:0"). */
|
|
9
|
+
export declare function answers(human: any): any;
|
|
8
10
|
/**
|
|
9
11
|
* Run a local Method file. The run starts at once: saving and run records go through the outbox and never stop or fail
|
|
10
12
|
* the run. baseConfig is a configuration the SDK made (a saved version's package); otherwise localSetup builds it.
|
|
@@ -38,6 +38,12 @@ export function recentRuns(exclude, limit = 20) {
|
|
|
38
38
|
}
|
|
39
39
|
return [...found].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([dir]) => dir);
|
|
40
40
|
}
|
|
41
|
+
/** Answers for ask steps: a key without an iteration ("confirm") answers its first iteration ("confirm:0"). */
|
|
42
|
+
export function answers(human) {
|
|
43
|
+
if (!human?.steps)
|
|
44
|
+
return human;
|
|
45
|
+
return { ...human, steps: Object.fromEntries(Object.entries(human.steps).map(([key, value]) => [key.includes(':') ? key : `${key}:0`, value])) };
|
|
46
|
+
}
|
|
41
47
|
/**
|
|
42
48
|
* Run a local Method file. The run starts at once: saving and run records go through the outbox and never stop or fail
|
|
43
49
|
* the run. baseConfig is a configuration the SDK made (a saved version's package); otherwise localSetup builds it.
|
|
@@ -125,7 +131,7 @@ export async function runCurrentFile(file, flags, syncFactory, onEvent, client =
|
|
|
125
131
|
cacheFrom: flags.resume ? [] : recentRuns(authoringPath(flags['run-dir'])),
|
|
126
132
|
fresh: flags.fresh ? true : flags.rerun?.length ? flags.rerun.flatMap(step => step.split(',')).map(step => step.trim()).filter(Boolean) : undefined,
|
|
127
133
|
secrets: { ...resolveSecrets(Object.keys(method.secrets ?? {})), ...Object.fromEntries(Object.entries(runSecrets).filter(([key]) => Object.hasOwn(method.secrets ?? {}, key))) }, ...(hostedModels ? { hostedModels } : {}),
|
|
128
|
-
human: json(flags.human), signal: controller.signal,
|
|
134
|
+
human: answers(json(flags.human)), signal: controller.signal,
|
|
129
135
|
...(browser ? { connections: browser.connections } : {}),
|
|
130
136
|
sourceRoot, ...(classification ? { classification } : {}),
|
|
131
137
|
processPath: prepared.processPath,
|
|
@@ -139,7 +145,11 @@ export async function runCurrentFile(file, flags, syncFactory, onEvent, client =
|
|
|
139
145
|
});
|
|
140
146
|
runFailed = !['completed', 'unconfirmed'].includes(result.status);
|
|
141
147
|
const synced = await sync?.finish();
|
|
142
|
-
|
|
148
|
+
let display = synced ? { ...result, dashboard: sync.dashboard, dashboard_sync: synced } : result;
|
|
149
|
+
// A run that waits for a person shows the question itself, so the agent can ask the user and pass on the answer.
|
|
150
|
+
const question = result.status === 'needs_input' ? (await import('./worker.js')).lastQuestion(authoringPath(flags['run-dir'])) : null;
|
|
151
|
+
if (question)
|
|
152
|
+
display = { ...display, question, fix: `Ask the user this question in plain words and wait for the answer; do not answer it yourself. Then write {"steps": {"${question.step}": {"outputs": {...}}}} with the user's answer (the fields are in question.form) to a file, and run the same command again with --resume --run-dir ${JSON.stringify(flags['run-dir'])} --human FILE.` };
|
|
143
153
|
process.stdout.write(JSON.stringify(display, null, 2) + "\n");
|
|
144
154
|
// 3: every step finished, but an observer could not confirm an external change.
|
|
145
155
|
if (result.status !== "completed")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each Method has one home file on this computer: the file its versions are saved from. A copy in a temporary folder,
|
|
3
|
+
* or in another folder while the home file still exists, is not saved, so one Method never grows two histories.
|
|
4
|
+
* Git worktrees and clones of one repository share a home. The home record also holds what the last save or checkout
|
|
5
|
+
* wrote, so an unchanged home file can take a newer version from the account.
|
|
6
|
+
*/
|
|
7
|
+
export type Home = {
|
|
8
|
+
path: string;
|
|
9
|
+
key: string;
|
|
10
|
+
version_id?: string;
|
|
11
|
+
files?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function readHome(server: string, methodId: string): Home | undefined;
|
|
14
|
+
export declare const temporary: (path: string) => boolean;
|
|
15
|
+
/** The same file in every checkout of one repository has the same key. */
|
|
16
|
+
export declare function homeKey(file: string): string;
|
|
17
|
+
/** What a save or checkout wrote: the Method file and each package file, by hash. */
|
|
18
|
+
export declare function writtenFiles(file: string, packageFiles: Array<{
|
|
19
|
+
path: string;
|
|
20
|
+
sha256: string;
|
|
21
|
+
}>): {
|
|
22
|
+
[x: string]: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function recordHome(server: string, methodId: string, file: string, version?: {
|
|
25
|
+
version_id: string;
|
|
26
|
+
files: Record<string, string>;
|
|
27
|
+
}): void;
|
|
28
|
+
/**
|
|
29
|
+
* Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
|
|
30
|
+
* the call. A home that is gone, holds another Method, or is a temporary copy gives way to a file outside a
|
|
31
|
+
* temporary folder.
|
|
32
|
+
*/
|
|
33
|
+
export declare function claimHome(server: string, methodId: string, file: string): void;
|
|
34
|
+
/** True when every file that the last save or checkout wrote is still unchanged in the home folder. */
|
|
35
|
+
export declare function unchangedSinceSave(home: Home): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Before a run of the home file: when nothing in it changed since the last save or checkout on this computer and the
|
|
38
|
+
* account has a newer version (saved from another computer or folder), write that version here first. Says nothing;
|
|
39
|
+
* any problem leaves the files as they are.
|
|
40
|
+
*/
|
|
41
|
+
export declare function followAccount(client: {
|
|
42
|
+
server: string;
|
|
43
|
+
token(): string | null | undefined;
|
|
44
|
+
request<T>(path: string): Promise<T>;
|
|
45
|
+
transfer(path: string): Promise<Uint8Array>;
|
|
46
|
+
}, file: string): Promise<boolean>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { sha256 } from '../../contracts/src/identity.js';
|
|
5
|
+
import { writePrivateJson } from './files.js';
|
|
6
|
+
import { methodCache } from './prepare.js';
|
|
7
|
+
import { authoringPath, readDocument } from './authoring.js';
|
|
8
|
+
import { fileHash } from './method-files.js';
|
|
9
|
+
const homeFile = (server, methodId) => join(methodCache(), 'homes', `${sha256(`${server}\n${methodId}`).slice(0, 24)}.json`);
|
|
10
|
+
export function readHome(server, methodId) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(homeFile(server, methodId), 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const real = (path) => { try {
|
|
19
|
+
return realpathSync(path);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return resolve(path);
|
|
23
|
+
} };
|
|
24
|
+
const temporaryRoots = () => [...new Set([tmpdir(), '/tmp', '/private/tmp', '/var/folders', '/private/var/folders'].map(real))];
|
|
25
|
+
export const temporary = (path) => temporaryRoots().some(root => real(path).startsWith(root + sep));
|
|
26
|
+
/** The checkout folder that holds path, and the main folder of its Git repository (a worktree points to it). */
|
|
27
|
+
function repository(path) {
|
|
28
|
+
for (let dir = dirname(path);; dir = dirname(dir)) {
|
|
29
|
+
const git = join(dir, '.git');
|
|
30
|
+
if (existsSync(git)) {
|
|
31
|
+
if (statSync(git).isDirectory())
|
|
32
|
+
return { root: dir, main: dir };
|
|
33
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(git, 'utf8'))?.[1]?.trim();
|
|
34
|
+
const main = pointer ? /^(.*)[\\/]\.git[\\/]worktrees[\\/][^\\/]+$/.exec(resolve(dir, pointer))?.[1] : undefined;
|
|
35
|
+
return { root: dir, main: main ?? dir };
|
|
36
|
+
}
|
|
37
|
+
if (dirname(dir) === dir)
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The same file in every checkout of one repository has the same key. */
|
|
42
|
+
export function homeKey(file) {
|
|
43
|
+
const path = real(authoringPath(file)), repo = repository(path);
|
|
44
|
+
return repo ? `${repo.main}:${relative(repo.root, path)}` : path;
|
|
45
|
+
}
|
|
46
|
+
/** What a save or checkout wrote: the Method file and each package file, by hash. */
|
|
47
|
+
export function writtenFiles(file, packageFiles) {
|
|
48
|
+
const path = authoringPath(file);
|
|
49
|
+
return { [relative(dirname(path), path)]: fileHash(readFileSync(path)), ...Object.fromEntries(packageFiles.map(f => [f.path, f.sha256])) };
|
|
50
|
+
}
|
|
51
|
+
export function recordHome(server, methodId, file, version) {
|
|
52
|
+
writePrivateJson(homeFile(server, methodId), { path: authoringPath(file), key: homeKey(file), ...version });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
|
|
56
|
+
* the call. A home that is gone, holds another Method, or is a temporary copy gives way to a file outside a
|
|
57
|
+
* temporary folder.
|
|
58
|
+
*/
|
|
59
|
+
export function claimHome(server, methodId, file) {
|
|
60
|
+
const home = readHome(server, methodId), key = homeKey(file);
|
|
61
|
+
if (home && home.key === key) {
|
|
62
|
+
if (home.path !== authoringPath(file))
|
|
63
|
+
recordHome(server, methodId, file, home.version_id ? { version_id: home.version_id, files: home.files ?? {} } : undefined);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const homeLives = (() => { try {
|
|
67
|
+
return !!home && readDocument(home.path).id === methodId;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
} })();
|
|
72
|
+
if (home && homeLives && (temporary(file) || !temporary(home.path)))
|
|
73
|
+
throw Object.assign(Error(`This Method is saved from ${home.path}. Make the change in that file and run it there; this copy was not run or saved.`), { code: 'other_home', home: home.path });
|
|
74
|
+
recordHome(server, methodId, file);
|
|
75
|
+
}
|
|
76
|
+
/** True when every file that the last save or checkout wrote is still unchanged in the home folder. */
|
|
77
|
+
export function unchangedSinceSave(home) {
|
|
78
|
+
if (!home.version_id || !home.files)
|
|
79
|
+
return false;
|
|
80
|
+
const root = dirname(home.path);
|
|
81
|
+
return Object.entries(home.files).every(([name, hash]) => { try {
|
|
82
|
+
return fileHash(readFileSync(join(root, name))) === hash;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
} });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Before a run of the home file: when nothing in it changed since the last save or checkout on this computer and the
|
|
90
|
+
* account has a newer version (saved from another computer or folder), write that version here first. Says nothing;
|
|
91
|
+
* any problem leaves the files as they are.
|
|
92
|
+
*/
|
|
93
|
+
export async function followAccount(client, file) {
|
|
94
|
+
const path = authoringPath(file);
|
|
95
|
+
try {
|
|
96
|
+
const id = (await import('./authoring.js')).readDocument(path).id;
|
|
97
|
+
if (typeof id !== 'string' || !client.token())
|
|
98
|
+
return false;
|
|
99
|
+
const home = readHome(client.server, id);
|
|
100
|
+
if (!home || home.key !== homeKey(path) || !unchangedSinceSave(home))
|
|
101
|
+
return false;
|
|
102
|
+
const latest = await client.request(`/api/cli/methods/${encodeURIComponent(id)}`);
|
|
103
|
+
if (!latest?.version_id || latest.version_id === home.version_id)
|
|
104
|
+
return false;
|
|
105
|
+
const { mkdtempSync, rmSync, copyFileSync, mkdirSync } = await import('node:fs');
|
|
106
|
+
const { restorePackage } = await import('./method-files.js');
|
|
107
|
+
const staging = mkdtempSync(join(methodCache(), 'follow-'));
|
|
108
|
+
try {
|
|
109
|
+
await restorePackage(latest, staging, client);
|
|
110
|
+
const root = dirname(path), incoming = latest.package?.files ?? [];
|
|
111
|
+
// A local file that the last save did not write, and that differs, is the user's: then nothing changes.
|
|
112
|
+
for (const f of incoming) {
|
|
113
|
+
const local = join(root, f.path);
|
|
114
|
+
if (existsSync(local) && !home.files?.[f.path] && fileHash(readFileSync(local)) !== f.sha256)
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
for (const f of incoming) {
|
|
118
|
+
mkdirSync(dirname(join(root, f.path)), { recursive: true, mode: 0o700 });
|
|
119
|
+
copyFileSync(join(staging, f.path), join(root, f.path));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
rmSync(staging, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
const { writeDocument } = await import('./authoring.js');
|
|
126
|
+
const { methodContent, writeMethodId } = await import('./versions.js');
|
|
127
|
+
writeDocument(path, methodContent(latest.workflow));
|
|
128
|
+
(await import('./legacy.js')).convertLegacy(path);
|
|
129
|
+
writeMethodId(path, id);
|
|
130
|
+
(await import('./outbox.js')).recordCheckout(client.server, id, latest.version_id, latest.version_number, methodContent(latest.workflow));
|
|
131
|
+
recordHome(client.server, id, path, { version_id: latest.version_id, files: writtenFiles(path, latest.package?.files ?? []) });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a Method folder of an older format in place: runtime.json moves into the Method (models, limits, tools),
|
|
3
|
+
* its folder paths become this computer's bindings, and FILE.method.json becomes the id: line. Both old files are
|
|
4
|
+
* deleted. Returns true when it changed something. When the result does not validate, every file is left as it was.
|
|
5
|
+
*/
|
|
6
|
+
export declare function convertLegacy(file: string): boolean;
|
|
7
|
+
/** Convert before a command reads the file. A folder that cannot be converted is a plain error for the agent to fix. */
|
|
8
|
+
export declare function convertLegacyOrThrow(file: string): boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { loadWorkflow, parseDocumentValue, serializeWorkflow } from '../../workflow-language/src/validate.js';
|
|
4
|
+
import { authoringPath } from './authoring.js';
|
|
5
|
+
import { bindConnectionValue } from './computer-settings.js';
|
|
6
|
+
import { newMethodId } from './versions.js';
|
|
7
|
+
const runLimitKeys = ['timeout_ms', 'max_model_requests', 'max_invocations', 'max_tool_calls', 'max_output_bytes', 'max_request_bytes', 'max_concurrency'];
|
|
8
|
+
const stepLimitKeys = ['timeout_ms', 'max_agent_turns', 'max_model_requests'];
|
|
9
|
+
const efforts = ['minimal', 'low', 'medium', 'high'];
|
|
10
|
+
const pick = (value, keys) => Object.fromEntries(keys.filter(key => value?.[key] !== undefined).map(key => [key, value[key]]));
|
|
11
|
+
/** A runtime.json model profile as a format 3.4 models entry. Direct API profiles become the same model, hosted. */
|
|
12
|
+
function modelEntry(profile) {
|
|
13
|
+
if (profile.backend === 'codex' || profile.backend === 'claude')
|
|
14
|
+
return { agent: profile.backend, ...pick(profile, ['model', 'reasoning_effort']) };
|
|
15
|
+
const provider = { 'openai-responses': 'openai/', 'anthropic-messages': 'anthropic/' }[profile.backend] ?? '';
|
|
16
|
+
const model = String(profile.model).includes('/') ? profile.model : `${provider}${profile.model}`;
|
|
17
|
+
const effort = profile.reasoning_effort ?? profile.effort;
|
|
18
|
+
const options = { ...(profile.max_output_tokens ? { max_output_tokens: profile.max_output_tokens } : {}), ...(efforts.includes(effort) ? { reasoning_effort: effort } : {}) };
|
|
19
|
+
return Object.keys(options).length ? { model, ...options } : model;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convert a Method folder of an older format in place: runtime.json moves into the Method (models, limits, tools),
|
|
23
|
+
* its folder paths become this computer's bindings, and FILE.method.json becomes the id: line. Both old files are
|
|
24
|
+
* deleted. Returns true when it changed something. When the result does not validate, every file is left as it was.
|
|
25
|
+
*/
|
|
26
|
+
export function convertLegacy(file) {
|
|
27
|
+
const path = authoringPath(file), folder = dirname(path);
|
|
28
|
+
const runtimeFile = join(folder, 'runtime.json'), sidecarFile = `${path}.method.json`;
|
|
29
|
+
const hasRuntime = existsSync(runtimeFile), hasSidecar = existsSync(sidecarFile);
|
|
30
|
+
if (!hasRuntime && !hasSidecar)
|
|
31
|
+
return false;
|
|
32
|
+
const original = readFileSync(path, 'utf8');
|
|
33
|
+
const document = parseDocumentValue(original);
|
|
34
|
+
const config = hasRuntime ? JSON.parse(readFileSync(runtimeFile, 'utf8')) : {};
|
|
35
|
+
const sidecar = hasSidecar ? JSON.parse(readFileSync(sidecarFile, 'utf8')) : {};
|
|
36
|
+
const { format: _format, id: oldId, ...content } = document;
|
|
37
|
+
// Folder paths become bindings of this computer, which belong to a Method ID.
|
|
38
|
+
const id = oldId ?? sidecar.workflow_id ?? (Object.keys(config.environment ?? {}).length ? newMethodId() : undefined);
|
|
39
|
+
const models = Object.fromEntries(Object.entries(config.models ?? {}).map(([name, profile]) => [name, modelEntry(profile)]));
|
|
40
|
+
const limits = { ...pick(config.limits, runLimitKeys), ...(config.step_defaults ? { step: pick(config.step_defaults, stepLimitKeys) } : {}) };
|
|
41
|
+
const files = (content.files ?? []).filter((name) => name !== 'runtime.json');
|
|
42
|
+
const next = {
|
|
43
|
+
format: 'method/3.4', ...(id ? { id } : {}), ...content,
|
|
44
|
+
...(Object.keys(models).length ? { models: { ...models, ...content.models } } : {}),
|
|
45
|
+
...(Object.keys(limits).length ? { limits: { ...limits, ...content.limits, ...(limits.step || content.limits?.step ? { step: { ...limits.step, ...content.limits?.step } } : {}) } } : {}),
|
|
46
|
+
...(config.tools ? { tools: { ...config.tools, ...content.tools } } : {}),
|
|
47
|
+
};
|
|
48
|
+
if (files.length)
|
|
49
|
+
next.files = files;
|
|
50
|
+
else
|
|
51
|
+
delete next.files;
|
|
52
|
+
const { id: _id, ...checked } = next;
|
|
53
|
+
loadWorkflow(checked);
|
|
54
|
+
writeFileSync(path, /^\s*\{/.test(original) ? JSON.stringify(next, null, 2) + '\n' : serializeWorkflow(next), { mode: 0o600 });
|
|
55
|
+
if (id)
|
|
56
|
+
for (const [name, value] of Object.entries(config.environment ?? {}))
|
|
57
|
+
bindConnectionValue(id, name, isAbsolute(String(value)) ? String(value) : resolve(folder, String(value)));
|
|
58
|
+
if (hasRuntime)
|
|
59
|
+
unlinkSync(runtimeFile);
|
|
60
|
+
if (hasSidecar)
|
|
61
|
+
unlinkSync(sidecarFile);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** Convert before a command reads the file. A folder that cannot be converted is a plain error for the agent to fix. */
|
|
65
|
+
export function convertLegacyOrThrow(file) {
|
|
66
|
+
try {
|
|
67
|
+
return convertLegacy(file);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw Error(`${basename(authoringPath(file))} uses an older format, and its runtime.json could not be moved into it: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -29,16 +29,13 @@ export function parse(args) {
|
|
|
29
29
|
}
|
|
30
30
|
export async function run(target, flags, syncFactory) {
|
|
31
31
|
const path = isWorkflowLink(target) ? (await saveWorkflowLink(target, process.cwd())).path : resolve(target);
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
32
|
+
(await import("./legacy.js")).convertLegacyOrThrow(path);
|
|
33
|
+
if (!syncFactory)
|
|
34
|
+
await (await import("./home.js")).followAccount(new MethodClient(flags.server), path);
|
|
35
35
|
loadWorkflow(methodContent(readDocument(path)));
|
|
36
36
|
const notice = await (await import("./quality.js")).casesNotice(path);
|
|
37
37
|
if (notice)
|
|
38
38
|
process.stderr.write(notice + "\n");
|
|
39
|
-
const update = await (await import("./update.js")).updateNotice();
|
|
40
|
-
if (update)
|
|
41
|
-
process.stderr.write(update + "\n");
|
|
42
39
|
(await import("./prepare.js")).pruneCachedRuns();
|
|
43
40
|
flags = { ...flags, "run-dir": flags["run-dir"] ?? join(process.cwd(), ".method-runs", randomUUID()) };
|
|
44
41
|
const account = syncFactory ? undefined : await accountSync(path, flags);
|
|
@@ -85,9 +82,14 @@ async function accountSync(path, flags) {
|
|
|
85
82
|
return undefined;
|
|
86
83
|
}
|
|
87
84
|
const version = await prepareVersion(client, outbox, path);
|
|
88
|
-
|
|
85
|
+
// A run started by the run worker keeps the ID that its start line linked to.
|
|
86
|
+
const identity = join(runDir, "run-id.json"), runId = existsSync(identity) ? JSON.parse(readFileSync(identity, "utf8")).id : undefined;
|
|
87
|
+
return { client, outbox, version, sync: () => new Sync(client, runDir, version.method_id, version.version_id, runId, method.run_data === "device", outbox) };
|
|
89
88
|
}
|
|
90
89
|
catch (error) {
|
|
90
|
+
// A copy of a Method that is saved from another file must not run: the change belongs in that file.
|
|
91
|
+
if (error.code === 'other_home')
|
|
92
|
+
throw error;
|
|
91
93
|
process.stderr.write(`Not saved to your Method account: ${error.message} The run continues on this computer.\n`);
|
|
92
94
|
return undefined;
|
|
93
95
|
}
|
|
@@ -13,7 +13,7 @@ export const commandHelp = {
|
|
|
13
13
|
"new-id": { usage: "method new-id FILE", purpose: "Make a copied Method file a new Method.", arguments: "Writes a new id: line. A copy that keeps its id: saves to the same Method.", result: "JSON with file and method_id.", errors: "Missing file.", example: "method new-id copy.method" },
|
|
14
14
|
bind: { usage: "method bind FILE_OR_ID NAME (--file FOLDER | --connection URL) [--upload]", purpose: "Save a named input location or connection.", arguments: "Default: this computer only, in ~/.config/method/computer.json under the Method's ID (a FILE gets its id: line). A folder named NAME beside the Method needs no binding. --upload saves only the selected input folder or connection URL in the account. It never scans your disk. Keep credentials in the service's normal sign-in store.", result: "Saved binding and scope.", errors: "Missing input, invalid name, or unsafe path.", example: "method bind METHOD_ID prepared_day --file day-records --upload", remote: true },
|
|
15
15
|
state: { usage: "method state ID [--enable --file state.json | --release RUN_ID]", purpose: "Read or explicitly enable shared account state.", arguments: "Accepted state updates use revision checks. Inspect a stopped run's external actions before releasing its ownership.", result: "Current revision, value, and owner run.", errors: "Concurrent ownership or stale revision. Existing state is never silently overwritten.", example: "method state METHOD_ID --enable --file initial-state.json", remote: true },
|
|
16
|
-
wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output
|
|
16
|
+
wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output, then the final worker status with the dashboard link of a synced run.", errors: "Stopped process or missing run.", example: "method wait .runs/example" },
|
|
17
17
|
'run-status': { usage: "method run-status RUN_DIRECTORY", purpose: "Read worker status without waiting.", arguments: "Local run directory.", result: "Worker status and whether its process is active.", errors: "Unreadable run directory.", example: "method run-status .runs/example" },
|
|
18
18
|
cancel: { usage: "method cancel RUN_DIRECTORY", purpose: "Stop a local run process.", arguments: "Inspect uncertain external actions before any explicit retry.", result: "Cancellation request. The checkpoint remains available.", errors: "Unreadable run directory.", example: "method cancel .runs/example" },
|
|
19
19
|
progress: { usage: "method progress --message TEXT [--completed N --total N --unit NAME] [--child NAME]\nmethod progress --codex --child NAME", purpose: "Report public progress from a running script or relay a child Codex JSON stream.", arguments: "METHOD_PROGRESS_FD is supplied by the executor. --codex reads JSON lines from stdin; --message sends one message. Do not include secrets or source contents.", result: "Writes to the separate progress pipe. No stdout output. No-op outside a Method process.", errors: "Invalid arguments. Malformed Codex events are ignored.", example: "method progress --message 'Rendered 12 of 40 pages' --completed 12 --total 40 --unit pages" },
|
|
@@ -63,9 +63,9 @@ export const commandHelp = {
|
|
|
63
63
|
--resume: continue the same saved run with its saved agent.
|
|
64
64
|
--rerun STEP: run this step again even when an earlier run can be reused. Repeat for more steps.
|
|
65
65
|
--fresh: run every step; reuse nothing.
|
|
66
|
-
--human FILE: answers for ask steps, as {steps: {"STEP
|
|
66
|
+
--human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
|
|
67
67
|
--verbose: print runtime events.
|
|
68
|
-
Use method doctor to check the installed Node and configured tools.`, result: "Progress and final status text; local result.json and run evidence
|
|
68
|
+
Use method doctor to check the installed Node and configured tools.`, result: "When signed in, the start line and the final result have dashboard: the link to the run's page; give it to the user when the run starts. Progress and final status text; local result.json and run evidence. The runtime executes the method's declared scripts, calls, agents, and checks. Executes trusted local processes; changes declarations do not enforce permissions. Runs exit 0 on completion, 1 on failure, 2 when human input is needed, and 3 when an observer could not confirm an external change (unconfirmed).", errors: "Missing inputs/access, failed check, timeout, unsafe resume/version mismatch, upload failure. See recovery. Never retry a business write without inspecting its saved changes.", example: "method run wf_example --version v_example --inputs inputs.json", remote: true },
|
|
69
69
|
observe: { usage: "method observe [RUN_DIRECTORY...] [--pending ROOT]...", purpose: "Make the effect observations that are due for finished runs.", arguments: "Without directories, checks runs with open effects under .method-runs and the Method cache. Runs only observers and judges; never repeats an action. Schedule it, for example hourly, so late evidence such as a bounce reaches the run.", result: "JSON with each run's new verdicts, status, and next observation time. A synced run whose status changed is uploaded again.", errors: "Locked run, changed bundle, or a missing observer credential.", example: "method observe --pending .method-runs" },
|
|
70
70
|
test: { usage: "method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--cases DIR] [--agent codex|claude]", purpose: "Replay recorded cases against this version of a Method. Every case must pass.", arguments: "Cases are in cases/ beside the Method. Unchanged steps return their recorded outputs; changed steps run; files connections are scratch folders, so a changed step that writes files runs safely. A changed step that asks a person or acts on a service makes the case unverifiable. --baseline also runs each case on the old version, to show what the change fixed or broke.", result: "JSON report with a verdict per case and passed:true when no case blocks the change. Exit 0 or 1.", errors: "Unknown case, invalid Method, or setup errors.", example: "method test task.method --baseline task-before.method --new bounce-reported" },
|
|
71
71
|
case: { usage: "method case new|retire|list FILE ...", purpose: "Keep a correction as a recorded case that every later version must pass.", arguments: "Use method help case new, method help case retire, or method help case list.", result: "See the subcommand.", errors: "Unknown action. Choose new, retire, or list.", example: "method help case new" },
|
|
@@ -154,7 +154,7 @@ What this example shows:
|
|
|
154
154
|
|
|
155
155
|
\`\`\`sh
|
|
156
156
|
method validate tickets.method
|
|
157
|
-
method run tickets.method --inputs inputs.json # prints the
|
|
157
|
+
method run tickets.method --inputs inputs.json # prints the run's dashboard link at the start and with the result
|
|
158
158
|
# change the summary prompt, then:
|
|
159
159
|
method run tickets.method --inputs inputs.json # reuses read, urgency and join; runs summary
|
|
160
160
|
\`\`\`
|
|
@@ -331,12 +331,11 @@ const recovery = `# Recovery
|
|
|
331
331
|
For a stopped run, read summary.json, events.jsonl, and checkpoint.json. Resume with the original method, --run-dir DIR, and --resume. Accepted steps and iterations are reused.
|
|
332
332
|
An unfinished action needs --retry STEP:ITERATION after inspection of its external effects. A retry consumes the remaining run budget. Budgets do not reset on resume. Resume always uses the run's saved bundle, so a code fix needs a new run.
|
|
333
333
|
To fix a failed or wrong step, change it and run the Method again. The new run reuses every iteration whose step definition, inputs, model, tools, runtime, and (for script steps) bundle files match an earlier accepted run on this computer, and copies their declared file outputs. Steps with ask or effects, steps that change state or a service, and tools with effects are never reused. Use --rerun STEP to run a step again anyway, or --fresh to reuse nothing.
|
|
334
|
-
For ask, supply --human FILE containing {steps: {"STEP
|
|
334
|
+
For ask, supply --human FILE containing {steps: {"STEP": {outputs: {NAME: VALUE}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat). Use the user's actual answer. Checks still run.
|
|
335
335
|
A save never blocks a run: a run that cannot save says "Will save when online", and any later method command sends the outbox.
|
|
336
336
|
State commits after checks. A local checkpoint cannot roll back an external write. Inspect external state before an explicit retry.
|
|
337
|
-
Two computers can save different content of one Method; each becomes a version with its parent. Going back to earlier content finds its existing version. The id: line keeps a renamed or moved file on its Method; method new-id FILE makes a copy a new Method.
|
|
337
|
+
Two computers can save different content of one Method; each becomes a version with its parent. Going back to earlier content finds its existing version. The id: line keeps a renamed or moved file on its Method; method new-id FILE makes a copy a new Method. On one computer a Method is saved from one file (every checkout of one Git repository counts as one): a copy in another folder or a temporary folder does not run while that file exists, and the error names the file to change. When that file is unchanged since its last save and the account has a newer version, a run takes that version first.
|
|
338
338
|
Use method sync RUN_DIRECTORY to repair a dashboard upload without executing the method again.
|
|
339
|
-
New Methods use format method/3.4. A signed-in run moves a method/3.1 to 3.3 file to 3.4 when it writes the id: line.
|
|
340
339
|
After a failed action with effects, the failure lists what the observers saw. A confirmed effect means the change happened: do not retry the action. A run that is unconfirmed finished all its steps; inspect effects.jsonl and the external system before relying on its result.
|
|
341
340
|
`;
|
|
342
341
|
export const guideTopics = ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
|
|
@@ -13,11 +13,6 @@ export declare function hostedModelUses(method: any): {
|
|
|
13
13
|
}[];
|
|
14
14
|
/** model_not_private: a step names a hosted model that has no private endpoint. Deterministic from the saved list. */
|
|
15
15
|
export declare function privacyIssues(method: any, privateIds: Set<string>): Issue[];
|
|
16
|
-
/**
|
|
17
|
-
* legacy_runtime_json and legacy_sidecar: an old runtime.json or FILE.method.json is next to the Method. Method files
|
|
18
|
-
* no longer read them, so a run would lose their settings. The fix says what to move where; nothing is changed here.
|
|
19
|
-
*/
|
|
20
|
-
export declare function legacyIssues(file: string): Issue[];
|
|
21
16
|
export type IssueReport = {
|
|
22
17
|
issues: Issue[];
|
|
23
18
|
changed?: Set<string>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The issues of a local Method: the runtime's methodIssues (format and code checks, with this computer's secret
|
|
3
3
|
* values and the files that will be uploaded), the model privacy check, and the model checks. Only errors block.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { mkdirSync, openSync, closeSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -68,29 +68,20 @@ export function privacyIssues(method, privateIds) {
|
|
|
68
68
|
fix: 'Choose a model from method models.',
|
|
69
69
|
}));
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
72
|
-
* legacy_runtime_json and legacy_sidecar: an old runtime.json or FILE.method.json is next to the Method. Method files
|
|
73
|
-
* no longer read them, so a run would lose their settings. The fix says what to move where; nothing is changed here.
|
|
74
|
-
*/
|
|
75
|
-
export function legacyIssues(file) {
|
|
76
|
-
const path = authoringPath(file), folder = dirname(path), issues = [];
|
|
77
|
-
if (existsSync(join(folder, 'runtime.json')))
|
|
78
|
-
issues.push({ code: 'legacy_runtime_json', level: 'error',
|
|
79
|
-
message: `runtime.json is next to ${basename(path)}. Methods no longer read runtime.json, so its settings would be lost.`,
|
|
80
|
-
fix: 'Move each part into the Method, then delete runtime.json. models -> top-level models: (NAME: provider/model, or {model, max_output_tokens, reasoning_effort}); a profile with backend codex or claude -> NAME: {agent: codex|claude, model, reasoning_effort} (keep its model and reasoning_effort). limits -> top-level limits:. step_defaults -> limits.step. tools -> top-level tools:. environment paths -> method bind ID NAME --file FOLDER (a folder beside the Method with the connection name needs nothing). classification.api_key_env -> method config model-key. runtimes and allow_local_processes need nothing. Set format: method/3.4 and remove runtime.json from files:.' });
|
|
81
|
-
if (existsSync(`${path}.method.json`))
|
|
82
|
-
issues.push({ code: 'legacy_sidecar', level: 'error',
|
|
83
|
-
message: `${basename(path)}.method.json is next to ${basename(path)}. Methods keep their ID in the file now.`,
|
|
84
|
-
fix: `Put its workflow_id as the line id: wf_... directly after format: method/3.4 in ${basename(path)}, then delete ${basename(path)}.method.json.` });
|
|
85
|
-
return issues;
|
|
86
|
-
}
|
|
87
71
|
/**
|
|
88
72
|
* All issues of a Method file. With changed, model checks run for the steps whose text changed since the last
|
|
89
73
|
* validate (in parallel, waitMs in all; the rest finish in the background). Without it, only cached answers are used.
|
|
90
74
|
*/
|
|
91
75
|
export async function collectIssues(file, options = {}) {
|
|
92
|
-
const path = authoringPath(file)
|
|
93
|
-
|
|
76
|
+
const path = authoringPath(file);
|
|
77
|
+
try {
|
|
78
|
+
(await import('./legacy.js')).convertLegacyOrThrow(path);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { issues: [{ code: 'older_format', level: 'error', message: error.message, fix: 'Fix what the message names in runtime.json or the Method, then validate again.' }], pending: 0, notChecked: [], invalid: true };
|
|
82
|
+
}
|
|
83
|
+
const text = readFileSync(path, 'utf8');
|
|
84
|
+
const first = methodIssues(text);
|
|
94
85
|
if (first.some(issue => issue.level === 'error'))
|
|
95
86
|
return { issues: first, pending: 0, notChecked: [], invalid: true };
|
|
96
87
|
const method = parseDocumentValue(text), read = folderReader(path);
|
|
@@ -17,6 +17,8 @@ export declare class MethodSync {
|
|
|
17
17
|
readonly outbox: Outbox;
|
|
18
18
|
constructor(client: MethodClient, directory: string, workflowId: string, versionId: string, runId?: string, device?: boolean, outbox?: Outbox);
|
|
19
19
|
get device(): boolean;
|
|
20
|
+
/** The run's dashboard page. Before the first record arrives, the page waits for it. */
|
|
21
|
+
get dashboard(): string;
|
|
20
22
|
private get item();
|
|
21
23
|
private save;
|
|
22
24
|
private upload;
|
|
@@ -39,6 +41,8 @@ export declare class MethodSync {
|
|
|
39
41
|
/** Send the final local record of a run. Never runs business actions. */
|
|
40
42
|
static retry(directory: string, client?: MethodClient): Promise<void>;
|
|
41
43
|
}
|
|
44
|
+
/** The dashboard page of the run in a run folder, when the run is linked to an account. */
|
|
45
|
+
export declare function runDashboard(directory: string): string | undefined;
|
|
42
46
|
/**
|
|
43
47
|
* The organization setting "Keep run content on devices". When it cannot be read, the run's content stays on this
|
|
44
48
|
* computer: the server would refuse the content anyway when the setting is on.
|
|
@@ -62,6 +62,10 @@ export class MethodSync {
|
|
|
62
62
|
get device() {
|
|
63
63
|
return this.state.device === true;
|
|
64
64
|
}
|
|
65
|
+
/** The run's dashboard page. Before the first record arrives, the page waits for it. */
|
|
66
|
+
get dashboard() {
|
|
67
|
+
return dashboardUrl(this.state);
|
|
68
|
+
}
|
|
65
69
|
get item() {
|
|
66
70
|
return { kind: "run", server: this.client.server, run_dir: this.directory, version_id: this.state.version_id, pid: process.pid };
|
|
67
71
|
}
|
|
@@ -99,8 +103,6 @@ export class MethodSync {
|
|
|
99
103
|
}
|
|
100
104
|
async send(payload) {
|
|
101
105
|
const result = await retryTransfer(() => this.client.request(`/api/cli/runs/${this.state.id}`, "PUT", payload, true, { stallMs: 60_000 }));
|
|
102
|
-
if (!this.state.dashboard_id)
|
|
103
|
-
process.stderr.write(`Dashboard: ${this.client.server}/runs/${result.id}\n`);
|
|
104
106
|
this.state.dashboard_id = result.id;
|
|
105
107
|
this.save();
|
|
106
108
|
this.warned = false;
|
|
@@ -267,6 +269,15 @@ export class MethodSync {
|
|
|
267
269
|
sync.outbox.remove(sync.item);
|
|
268
270
|
}
|
|
269
271
|
}
|
|
272
|
+
/** The dashboard page of a run: its saved ID, or before the first record, the ID that this computer gave it. */
|
|
273
|
+
function dashboardUrl(state) {
|
|
274
|
+
return `${state.server}/runs/${state.dashboard_id ?? state.id}`;
|
|
275
|
+
}
|
|
276
|
+
/** The dashboard page of the run in a run folder, when the run is linked to an account. */
|
|
277
|
+
export function runDashboard(directory) {
|
|
278
|
+
const path = join(directory, "method-sync.json");
|
|
279
|
+
return existsSync(path) ? dashboardUrl(SyncSchema.parse(JSON.parse(readFileSync(path, "utf8")))) : undefined;
|
|
280
|
+
}
|
|
270
281
|
async function retryTransfer(send) {
|
|
271
282
|
for (let attempt = 0;; attempt++)
|
|
272
283
|
try {
|
|
@@ -41,10 +41,25 @@ async function dispatchRunWorker(args, flags, extra = []) {
|
|
|
41
41
|
const workerArgs = [...args.filter(arg => arg !== '--background'), ...extra];
|
|
42
42
|
if (!flags['run-dir'])
|
|
43
43
|
workerArgs.push('--run-dir', directory);
|
|
44
|
-
|
|
44
|
+
// A new run of a signed-in computer is synced; a resumed run keeps the link that it has.
|
|
45
|
+
let server;
|
|
46
|
+
try {
|
|
47
|
+
const client = new MethodClient(flags.server);
|
|
48
|
+
if (!flags.resume && client.token())
|
|
49
|
+
server = client.server;
|
|
50
|
+
}
|
|
51
|
+
catch { /* Not signed in to this server. */ }
|
|
52
|
+
await startRunWorker(workerArgs, directory, flags.background, server);
|
|
45
53
|
return true;
|
|
46
54
|
}
|
|
47
55
|
export async function methodMain(args = process.argv.slice(2), clientFactory = server => new MethodClient(server)) {
|
|
56
|
+
const resumed = (await import('./resume-release.js')).resumeWithOriginalRelease(args);
|
|
57
|
+
if (resumed !== undefined) {
|
|
58
|
+
process.exitCode = resumed;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// An installed CLI keeps itself current in the background, with no output; this command is not delayed.
|
|
62
|
+
void (await import('./update.js')).updateInBackground();
|
|
48
63
|
const earlyHelp = methodHelp(args);
|
|
49
64
|
if (earlyHelp !== undefined) {
|
|
50
65
|
process.stdout.write(earlyHelp);
|
|
@@ -215,8 +230,7 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
215
230
|
throw error;
|
|
216
231
|
}
|
|
217
232
|
}
|
|
218
|
-
|
|
219
|
-
process.stdout.write(JSON.stringify({ installed: true, server: client.server, signed_in: signedIn, ...(!signedIn ? { next: "Run method login to sign in with browser approval." } : {}), ...(update ? { update } : {}) }, null, 2) + "\n");
|
|
233
|
+
process.stdout.write(JSON.stringify({ installed: true, server: client.server, signed_in: signedIn, ...(!signedIn ? { next: "Run method login to sign in with browser approval." } : {}) }, null, 2) + "\n");
|
|
220
234
|
return;
|
|
221
235
|
}
|
|
222
236
|
// Without sign-in, explain prints one line and stops: script cards need a hosted model.
|
|
@@ -333,8 +347,17 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
333
347
|
// The id: line links the file to its Method; its content is the saved version.
|
|
334
348
|
const { methodContent, writeMethodId } = await import("./versions.js");
|
|
335
349
|
writeDocument(out, methodContent(workflow));
|
|
350
|
+
// An older saved version carries runtime.json; the file gets it in the Method, as in format 3.4.
|
|
351
|
+
(await import("./legacy.js")).convertLegacyOrThrow(out);
|
|
336
352
|
writeMethodId(out, saved.workflow_id);
|
|
337
353
|
(await import("./outbox.js")).recordCheckout(client.server, saved.workflow_id, saved.version_id, saved.version_number, methodContent(workflow));
|
|
354
|
+
// The new file becomes the Method's home on this computer unless another file already is.
|
|
355
|
+
const home = await import("./home.js");
|
|
356
|
+
try {
|
|
357
|
+
home.claimHome(client.server, saved.workflow_id, out);
|
|
358
|
+
home.recordHome(client.server, saved.workflow_id, out, { version_id: saved.version_id, files: home.writtenFiles(out, saved.package?.files ?? []) });
|
|
359
|
+
}
|
|
360
|
+
catch { /* a copy for reading */ }
|
|
338
361
|
print({
|
|
339
362
|
workflow_id: saved.workflow_id,
|
|
340
363
|
version_id: saved.version_id,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run resumes with the runtime that started it. When the CLI updated itself since then, the command runs again with
|
|
3
|
+
* the installed release that has that runtime (releases are kept), and returns its exit code. Undefined: run here.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resumeWithOriginalRelease(args: string[]): number | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import runtimeInfo from '@withmethod/runtime/package.json' with { type: 'json' };
|
|
5
|
+
const read = (path) => { try {
|
|
6
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
} };
|
|
11
|
+
/**
|
|
12
|
+
* A run resumes with the runtime that started it. When the CLI updated itself since then, the command runs again with
|
|
13
|
+
* the installed release that has that runtime (releases are kept), and returns its exit code. Undefined: run here.
|
|
14
|
+
*/
|
|
15
|
+
export function resumeWithOriginalRelease(args) {
|
|
16
|
+
const at = args.indexOf('--run-dir'), releaseRoot = process.env.METHOD_RELEASE_ROOT;
|
|
17
|
+
if (!args.includes('--resume') || at < 0 || !args[at + 1] || !releaseRoot)
|
|
18
|
+
return undefined;
|
|
19
|
+
const wanted = read(join(resolve(args[at + 1]), 'checkpoint.json'))?.executor_version;
|
|
20
|
+
if (typeof wanted !== 'string' || wanted === runtimeInfo.version)
|
|
21
|
+
return undefined;
|
|
22
|
+
const releases = dirname(releaseRoot);
|
|
23
|
+
const match = readdirSync(releases).filter(name => existsSync(join(releases, name, 'method')))
|
|
24
|
+
.filter(name => read(join(releases, name, 'sdk', 'node_modules', '@withmethod', 'runtime', 'package.json'))?.version === wanted)
|
|
25
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))[0];
|
|
26
|
+
if (!match)
|
|
27
|
+
return undefined;
|
|
28
|
+
const result = spawnSync(join(releases, match, 'method'), args, { stdio: 'inherit', env: { ...process.env, METHOD_NO_UPDATE: '1' } });
|
|
29
|
+
return result.status ?? 1;
|
|
30
|
+
}
|
|
@@ -52,7 +52,7 @@ export async function runSaved(saved, flags, client) {
|
|
|
52
52
|
setupEvents.push({ at: new Date().toISOString(), type: 'setup_started' });
|
|
53
53
|
writePrivateJson(setupFile, setupEvents);
|
|
54
54
|
await sync.start(saved.workflow, inputs, {});
|
|
55
|
-
process.stderr.write(`Local run: ${directory}\n`);
|
|
55
|
+
process.stderr.write(`Local run: ${directory}\nDashboard: ${sync.dashboard}\n`);
|
|
56
56
|
let acquired = false, started = false, revision = 0;
|
|
57
57
|
const statePath = `/api/cli/methods/${encodeURIComponent(saved.workflow_id)}/state`;
|
|
58
58
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** server: the signed-in Method server, so that the start line links to the run's dashboard page. */
|
|
2
|
+
export declare function startRunWorker(args: string[], directory: string, background?: boolean, server?: string): Promise<void>;
|
|
2
3
|
export declare function workerAlive(directory: string): boolean;
|
|
3
4
|
export declare function waitForRun(directory: string): Promise<any>;
|
|
4
5
|
export declare function cancelRun(directory: string): {
|
|
@@ -4,7 +4,9 @@ import { existsSync, mkdirSync, openSync, closeSync, readFileSync, rmSync, write
|
|
|
4
4
|
import { resolve, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { writePrivateJson } from './files.js';
|
|
7
|
-
|
|
7
|
+
import { runDashboard } from './method-sync.js';
|
|
8
|
+
/** server: the signed-in Method server, so that the start line links to the run's dashboard page. */
|
|
9
|
+
export async function startRunWorker(args, directory, background = false, server) {
|
|
8
10
|
directory = resolve(directory);
|
|
9
11
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
10
12
|
const file = join(directory, 'worker-request.json'), status = join(directory, 'worker.json');
|
|
@@ -47,7 +49,8 @@ export async function startRunWorker(args, directory, background = false) {
|
|
|
47
49
|
await new Promise((yes, no) => { child.once('spawn', yes); child.once('error', no); });
|
|
48
50
|
child.unref();
|
|
49
51
|
// The child owns its status record after startup. Do not overwrite a fast completion.
|
|
50
|
-
|
|
52
|
+
const dashboard = runDashboard(directory) ?? (server ? `${server}/runs/${runId}` : undefined);
|
|
53
|
+
process.stdout.write(JSON.stringify({ status: 'started', run_id: runId, ...(dashboard ? { dashboard } : {}), run_dir: directory, wait: `method wait ${JSON.stringify(directory)}`, cancel: `method cancel ${JSON.stringify(directory)}` }) + '\n');
|
|
51
54
|
const startupDeadline = Date.now() + 30_000;
|
|
52
55
|
while (!existsSync(status)) {
|
|
53
56
|
if (Date.now() > startupDeadline)
|
|
@@ -107,13 +110,14 @@ export async function waitForRun(directory) {
|
|
|
107
110
|
const file = join(directory, 'worker.json');
|
|
108
111
|
if (existsSync(file)) {
|
|
109
112
|
const status = JSON.parse(readFileSync(file, 'utf8'));
|
|
113
|
+
const dashboard = runDashboard(directory), link = dashboard ? { dashboard } : {};
|
|
110
114
|
if (status.status !== 'running') {
|
|
111
115
|
process.exitCode = status.exit_code ?? 1;
|
|
112
|
-
return status;
|
|
116
|
+
return { ...status, ...link };
|
|
113
117
|
}
|
|
114
118
|
if (!workerAlive(directory)) {
|
|
115
119
|
process.exitCode = 1;
|
|
116
|
-
return { status: 'interrupted', run_dir: directory, message: 'The worker stopped. Inspect its run record before resuming.' };
|
|
120
|
+
return { status: 'interrupted', run_dir: directory, ...link, message: 'The worker stopped. Inspect its run record before resuming.' };
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
if (!existsSync(file) && Date.now() > deadline)
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/** Run the installer in its own process; this command does not wait for it, and it prints nothing. */
|
|
2
|
+
declare function startInstaller(): void;
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* Keep an installed CLI current with no message: at most once a day it reads the newest release from the version
|
|
5
|
+
* pointer that the installer reads, and when that is newer it installs it in the background. The command that started
|
|
6
|
+
* the check continues on this release; the next command uses the new one, and running commands keep their release.
|
|
7
|
+
* Only an installed release updates itself (not a development or npm copy). METHOD_NO_UPDATE=1 turns it off.
|
|
4
8
|
*/
|
|
5
|
-
export declare function
|
|
9
|
+
export declare function updateInBackground(fetcher?: typeof fetch, now?: number, install?: typeof startInstaller): Promise<boolean>;
|
|
10
|
+
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import packageInfo from '../package.json' with { type: 'json' };
|
|
4
5
|
import { methodCache } from './prepare.js';
|
|
5
6
|
import { writePrivateJson } from './files.js';
|
|
6
|
-
const day = 86_400_000;
|
|
7
|
+
const day = 86_400_000, installWindow = 30 * 60_000;
|
|
7
8
|
const newer = (a, b) => {
|
|
8
9
|
const [x, y] = [a, b].map(v => v.split('.').map(Number));
|
|
9
10
|
for (let i = 0; i < 3; i++)
|
|
@@ -11,15 +12,30 @@ const newer = (a, b) => {
|
|
|
11
12
|
return (x[i] ?? 0) > (y[i] ?? 0);
|
|
12
13
|
return false;
|
|
13
14
|
};
|
|
15
|
+
const read = (path) => { try {
|
|
16
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
} };
|
|
21
|
+
/** Run the installer in its own process; this command does not wait for it, and it prints nothing. */
|
|
22
|
+
function startInstaller() {
|
|
23
|
+
const url = process.env.METHOD_INSTALL_URL ?? 'https://app.withmethod.ai/install.sh';
|
|
24
|
+
spawn('/bin/sh', ['-c', 'curl -fsSL "$1" | sh', 'method-update', url], { detached: true, stdio: 'ignore', env: { ...process.env, METHOD_SKIP_PLUGIN: '1' } }).unref();
|
|
25
|
+
}
|
|
14
26
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
27
|
+
* Keep an installed CLI current with no message: at most once a day it reads the newest release from the version
|
|
28
|
+
* pointer that the installer reads, and when that is newer it installs it in the background. The command that started
|
|
29
|
+
* the check continues on this release; the next command uses the new one, and running commands keep their release.
|
|
30
|
+
* Only an installed release updates itself (not a development or npm copy). METHOD_NO_UPDATE=1 turns it off.
|
|
17
31
|
*/
|
|
18
|
-
export async function
|
|
19
|
-
|
|
32
|
+
export async function updateInBackground(fetcher = fetch, now = Date.now(), install = startInstaller) {
|
|
33
|
+
if (process.env.METHOD_NO_UPDATE || !process.env.METHOD_RELEASE_ROOT)
|
|
34
|
+
return false;
|
|
35
|
+
const cache = join(methodCache(), 'latest.json'), lock = join(methodCache(), 'update.json');
|
|
20
36
|
let latest;
|
|
21
37
|
try {
|
|
22
|
-
const saved =
|
|
38
|
+
const saved = read(cache);
|
|
23
39
|
if (saved && now - saved.checked_at < day)
|
|
24
40
|
latest = saved.version;
|
|
25
41
|
else {
|
|
@@ -29,11 +45,17 @@ export async function updateNotice(fetcher = fetch, now = Date.now()) {
|
|
|
29
45
|
latest = response.ok ? /^version (\S+)$/m.exec(await response.text())?.[1] : undefined;
|
|
30
46
|
writePrivateJson(cache, { checked_at: now, version: latest ?? null });
|
|
31
47
|
}
|
|
48
|
+
if (!latest || !newer(latest, packageInfo.version))
|
|
49
|
+
return false;
|
|
50
|
+
// One installer at a time; a failed one is tried again after the window.
|
|
51
|
+
const running = read(lock);
|
|
52
|
+
if (running?.version === latest && now - running.started_at < installWindow)
|
|
53
|
+
return false;
|
|
54
|
+
writePrivateJson(lock, { version: latest, started_at: now });
|
|
55
|
+
install();
|
|
56
|
+
return true;
|
|
32
57
|
}
|
|
33
58
|
catch {
|
|
34
|
-
return
|
|
59
|
+
return false;
|
|
35
60
|
}
|
|
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
61
|
}
|
|
@@ -6,6 +6,7 @@ import { loadWorkflow, parseDocumentValue, serializeWorkflow } from '../../workf
|
|
|
6
6
|
import { buildPackage } from './method-files.js';
|
|
7
7
|
import { authoringPath, readDocument } from './authoring.js';
|
|
8
8
|
import { Outbox, lastSaved, savedVersions } from './outbox.js';
|
|
9
|
+
import { claimHome, recordHome, writtenFiles } from './home.js';
|
|
9
10
|
export const methodIdPattern = /^wf_[a-f0-9]{32}$/;
|
|
10
11
|
/** A new account Method ID, made on this computer. The first save of this ID creates the Method. */
|
|
11
12
|
export const newMethodId = () => `wf_${randomBytes(16).toString('hex')}`;
|
|
@@ -92,10 +93,12 @@ export async function currentVersion(file) {
|
|
|
92
93
|
*/
|
|
93
94
|
export async function prepareVersion(client, outbox, file, reason) {
|
|
94
95
|
const id = ensureMethodId(file);
|
|
96
|
+
claimHome(client.server, id, file);
|
|
95
97
|
const workflow = loadWorkflow(methodContent(readDocument(file)));
|
|
96
98
|
const { pack, sources, tooLarge } = await buildPackage(file, workflow);
|
|
97
99
|
const versionId = versionIdFor(id, pack.digest);
|
|
98
100
|
const url = `${client.server}/methods/${id}?version=${versionId}`;
|
|
101
|
+
recordHome(client.server, id, file, { version_id: versionId, files: writtenFiles(file, pack.files) });
|
|
99
102
|
if (savedVersions(client.server)[versionId])
|
|
100
103
|
return { method_id: id, version_id: versionId, url, unchanged: true, workflow };
|
|
101
104
|
const previous = lastSaved(client.server, id);
|
|
@@ -87,9 +87,9 @@ export declare const RunEventSchema: z.ZodObject<{
|
|
|
87
87
|
kind?: "classify" | undefined;
|
|
88
88
|
child?: string | undefined;
|
|
89
89
|
model?: string | undefined;
|
|
90
|
+
phase?: "check" | "action" | undefined;
|
|
90
91
|
provider?: string | undefined;
|
|
91
92
|
request_id?: string | undefined;
|
|
92
|
-
phase?: "check" | "action" | undefined;
|
|
93
93
|
operation_id?: string | undefined;
|
|
94
94
|
confidence?: number | null | undefined;
|
|
95
95
|
usage?: {
|
|
@@ -114,9 +114,9 @@ export declare const RunEventSchema: z.ZodObject<{
|
|
|
114
114
|
kind?: "classify" | undefined;
|
|
115
115
|
child?: string | undefined;
|
|
116
116
|
model?: string | undefined;
|
|
117
|
+
phase?: "check" | "action" | undefined;
|
|
117
118
|
provider?: string | undefined;
|
|
118
119
|
request_id?: string | undefined;
|
|
119
|
-
phase?: "check" | "action" | undefined;
|
|
120
120
|
operation_id?: string | undefined;
|
|
121
121
|
confidence?: number | null | undefined;
|
|
122
122
|
usage?: {
|
|
@@ -317,9 +317,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
317
317
|
kind?: "classify" | undefined;
|
|
318
318
|
child?: string | undefined;
|
|
319
319
|
model?: string | undefined;
|
|
320
|
+
phase?: "check" | "action" | undefined;
|
|
320
321
|
provider?: string | undefined;
|
|
321
322
|
request_id?: string | undefined;
|
|
322
|
-
phase?: "check" | "action" | undefined;
|
|
323
323
|
operation_id?: string | undefined;
|
|
324
324
|
confidence?: number | null | undefined;
|
|
325
325
|
usage?: {
|
|
@@ -344,9 +344,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
344
344
|
kind?: "classify" | undefined;
|
|
345
345
|
child?: string | undefined;
|
|
346
346
|
model?: string | undefined;
|
|
347
|
+
phase?: "check" | "action" | undefined;
|
|
347
348
|
provider?: string | undefined;
|
|
348
349
|
request_id?: string | undefined;
|
|
349
|
-
phase?: "check" | "action" | undefined;
|
|
350
350
|
operation_id?: string | undefined;
|
|
351
351
|
confidence?: number | null | undefined;
|
|
352
352
|
usage?: {
|
|
@@ -466,9 +466,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
466
466
|
kind?: "classify" | undefined;
|
|
467
467
|
child?: string | undefined;
|
|
468
468
|
model?: string | undefined;
|
|
469
|
+
phase?: "check" | "action" | undefined;
|
|
469
470
|
provider?: string | undefined;
|
|
470
471
|
request_id?: string | undefined;
|
|
471
|
-
phase?: "check" | "action" | undefined;
|
|
472
472
|
operation_id?: string | undefined;
|
|
473
473
|
confidence?: number | null | undefined;
|
|
474
474
|
usage?: {
|
|
@@ -493,9 +493,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
493
493
|
kind?: "classify" | undefined;
|
|
494
494
|
child?: string | undefined;
|
|
495
495
|
model?: string | undefined;
|
|
496
|
+
phase?: "check" | "action" | undefined;
|
|
496
497
|
provider?: string | undefined;
|
|
497
498
|
request_id?: string | undefined;
|
|
498
|
-
phase?: "check" | "action" | undefined;
|
|
499
499
|
operation_id?: string | undefined;
|
|
500
500
|
confidence?: number | null | undefined;
|
|
501
501
|
usage?: {
|
|
@@ -558,9 +558,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
558
558
|
kind?: "classify" | undefined;
|
|
559
559
|
child?: string | undefined;
|
|
560
560
|
model?: string | undefined;
|
|
561
|
+
phase?: "check" | "action" | undefined;
|
|
561
562
|
provider?: string | undefined;
|
|
562
563
|
request_id?: string | undefined;
|
|
563
|
-
phase?: "check" | "action" | undefined;
|
|
564
564
|
operation_id?: string | undefined;
|
|
565
565
|
confidence?: number | null | undefined;
|
|
566
566
|
usage?: {
|
|
@@ -623,9 +623,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
623
623
|
kind?: "classify" | undefined;
|
|
624
624
|
child?: string | undefined;
|
|
625
625
|
model?: string | undefined;
|
|
626
|
+
phase?: "check" | "action" | undefined;
|
|
626
627
|
provider?: string | undefined;
|
|
627
628
|
request_id?: string | undefined;
|
|
628
|
-
phase?: "check" | "action" | undefined;
|
|
629
629
|
operation_id?: string | undefined;
|
|
630
630
|
confidence?: number | null | undefined;
|
|
631
631
|
usage?: {
|
|
@@ -700,9 +700,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
700
700
|
kind?: "classify" | undefined;
|
|
701
701
|
child?: string | undefined;
|
|
702
702
|
model?: string | undefined;
|
|
703
|
+
phase?: "check" | "action" | undefined;
|
|
703
704
|
provider?: string | undefined;
|
|
704
705
|
request_id?: string | undefined;
|
|
705
|
-
phase?: "check" | "action" | undefined;
|
|
706
706
|
operation_id?: string | undefined;
|
|
707
707
|
confidence?: number | null | undefined;
|
|
708
708
|
usage?: {
|
|
@@ -796,9 +796,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
796
796
|
kind?: "classify" | undefined;
|
|
797
797
|
child?: string | undefined;
|
|
798
798
|
model?: string | undefined;
|
|
799
|
+
phase?: "check" | "action" | undefined;
|
|
799
800
|
provider?: string | undefined;
|
|
800
801
|
request_id?: string | undefined;
|
|
801
|
-
phase?: "check" | "action" | undefined;
|
|
802
802
|
operation_id?: string | undefined;
|
|
803
803
|
confidence?: number | null | undefined;
|
|
804
804
|
usage?: {
|
|
@@ -844,9 +844,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
844
844
|
kind?: "classify" | undefined;
|
|
845
845
|
child?: string | undefined;
|
|
846
846
|
model?: string | undefined;
|
|
847
|
+
phase?: "check" | "action" | undefined;
|
|
847
848
|
provider?: string | undefined;
|
|
848
849
|
request_id?: string | undefined;
|
|
849
|
-
phase?: "check" | "action" | undefined;
|
|
850
850
|
operation_id?: string | undefined;
|
|
851
851
|
confidence?: number | null | undefined;
|
|
852
852
|
usage?: {
|
|
@@ -940,9 +940,9 @@ export declare const InspectionSchema: z.ZodObject<{
|
|
|
940
940
|
kind?: "classify" | undefined;
|
|
941
941
|
child?: string | undefined;
|
|
942
942
|
model?: string | undefined;
|
|
943
|
+
phase?: "check" | "action" | undefined;
|
|
943
944
|
provider?: string | undefined;
|
|
944
945
|
request_id?: string | undefined;
|
|
945
|
-
phase?: "check" | "action" | undefined;
|
|
946
946
|
operation_id?: string | undefined;
|
|
947
947
|
confidence?: number | null | undefined;
|
|
948
948
|
usage?: {
|