@method-ai-hq/sdk 0.0.0-stage → 0.14.2
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,127 @@
|
|
|
1
|
+
"""Private line protocol for the pinned browser-use direct controls."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import signal
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
os.environ['ANONYMIZED_TELEMETRY'] = 'false'
|
|
13
|
+
os.environ['BROWSER_USE_LOGGING_LEVEL'] = 'critical'
|
|
14
|
+
os.umask(0o077)
|
|
15
|
+
protocol = sys.stdout
|
|
16
|
+
sys.stdout = sys.stderr
|
|
17
|
+
from browser_use.mcp.server import BrowserUseServer
|
|
18
|
+
from browser_use.browser import BrowserProfile, BrowserSession
|
|
19
|
+
from browser_use.tools.service import Tools
|
|
20
|
+
|
|
21
|
+
CATALOG = json.loads((Path(__file__).parent / 'browser-runtime/tools.json').read_text())
|
|
22
|
+
ALLOWED = {tool['name'] for tool in CATALOG}
|
|
23
|
+
|
|
24
|
+
class BrowserDisconnected(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def require_browser(session):
|
|
29
|
+
if not session or not session.is_cdp_connected:
|
|
30
|
+
raise BrowserDisconnected('Browser connection lost. Chrome closed or disconnected.')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def call_browser(server, name, parameters):
|
|
34
|
+
require_browser(server.browser_session)
|
|
35
|
+
try:
|
|
36
|
+
content = await server._execute_tool(name, parameters)
|
|
37
|
+
except Exception as error:
|
|
38
|
+
require_browser(server.browser_session)
|
|
39
|
+
# Provider exception text can contain cookies or private connection URLs.
|
|
40
|
+
return {'isError': True, 'content': [{'type': 'text', 'text':
|
|
41
|
+
type(error).__name__ + ': browser action failed. Inspect the page and try another action.'}]}
|
|
42
|
+
require_browser(server.browser_session)
|
|
43
|
+
return {'content': [v.model_dump(by_alias=True, exclude_none=True) for v in content]
|
|
44
|
+
if isinstance(content, list) else [{'type': 'text', 'text': content}]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def serve():
|
|
48
|
+
server = BrowserUseServer()
|
|
49
|
+
domains = set()
|
|
50
|
+
owned = False
|
|
51
|
+
copied_profile = None
|
|
52
|
+
async def close_browser():
|
|
53
|
+
try:
|
|
54
|
+
if server.browser_session:
|
|
55
|
+
await asyncio.wait_for(server.browser_session.kill() if owned else server.browser_session.stop(), 5)
|
|
56
|
+
finally:
|
|
57
|
+
if copied_profile:
|
|
58
|
+
shutil.rmtree(copied_profile, ignore_errors=True)
|
|
59
|
+
async def stop():
|
|
60
|
+
with contextlib.suppress(Exception):
|
|
61
|
+
await close_browser()
|
|
62
|
+
os._exit(0)
|
|
63
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
64
|
+
asyncio.get_running_loop().add_signal_handler(sig, lambda: asyncio.create_task(stop()))
|
|
65
|
+
async def save(path):
|
|
66
|
+
state = await server.browser_session.export_storage_state()
|
|
67
|
+
def selected(host):
|
|
68
|
+
host = host.lstrip('.').lower()
|
|
69
|
+
return any(d == host or d.endswith('.' + host) for d in domains)
|
|
70
|
+
state['cookies'] = [c for c in state.get('cookies', []) if selected(c['domain'])]
|
|
71
|
+
state['origins'] = [o for o in state.get('origins', []) if selected(urlparse(o['origin']).hostname or '')]
|
|
72
|
+
target = Path(path)
|
|
73
|
+
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
74
|
+
tmp = target.with_suffix('.tmp')
|
|
75
|
+
tmp.write_text(json.dumps(state)); tmp.chmod(0o600); tmp.replace(target)
|
|
76
|
+
return {'domains': sorted(domains)}
|
|
77
|
+
while line := await asyncio.to_thread(sys.stdin.readline):
|
|
78
|
+
request = json.loads(line)
|
|
79
|
+
try:
|
|
80
|
+
method, args = request['method'], request.get('args', {})
|
|
81
|
+
if method == 'start':
|
|
82
|
+
owned = not bool(args.get('cdp_url'))
|
|
83
|
+
domains.update(args.pop('domains', []))
|
|
84
|
+
# Give the library a value, not our export path: its shutdown save
|
|
85
|
+
# must not overwrite the site-filtered transfer file.
|
|
86
|
+
if isinstance(args.get('storage_state'), str):
|
|
87
|
+
args['storage_state'] = json.loads(Path(args['storage_state']).read_text())
|
|
88
|
+
profile = BrowserProfile(**args, enable_default_extensions=False, args=['--disable-extensions'], keep_alive=not owned)
|
|
89
|
+
if owned and str(profile.user_data_dir) != str(args.get('user_data_dir')) and Path(profile.user_data_dir).name.startswith('browser-use-user-data-dir-'):
|
|
90
|
+
copied_profile = profile.user_data_dir
|
|
91
|
+
server.browser_session = BrowserSession(browser_profile=profile)
|
|
92
|
+
await server.browser_session.start()
|
|
93
|
+
server.tools = Tools()
|
|
94
|
+
result = {'started': True}
|
|
95
|
+
elif method == 'call':
|
|
96
|
+
name = args['name']
|
|
97
|
+
if name not in ALLOWED:
|
|
98
|
+
raise ValueError('Unsupported browser control')
|
|
99
|
+
parameters = args.get('arguments', {})
|
|
100
|
+
if name == 'browser_navigate':
|
|
101
|
+
host = urlparse(parameters['url']).hostname
|
|
102
|
+
if host: domains.add(host.lower())
|
|
103
|
+
result = await call_browser(server, name, parameters)
|
|
104
|
+
# Followed links and sign-in redirects belong to this task's active tab.
|
|
105
|
+
require_browser(server.browser_session)
|
|
106
|
+
page = await server.browser_session.get_current_page()
|
|
107
|
+
if page and name != 'browser_list_tabs':
|
|
108
|
+
host = urlparse(await page.get_url()).hostname
|
|
109
|
+
if host: domains.add(host.lower())
|
|
110
|
+
elif method == 'export':
|
|
111
|
+
result = await save(args['path'])
|
|
112
|
+
elif method == 'close':
|
|
113
|
+
await close_browser()
|
|
114
|
+
result = {'closed': True}
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError('Unknown browser operation')
|
|
117
|
+
protocol.write(json.dumps({'id': request['id'], 'result': result}) + '\n'); protocol.flush()
|
|
118
|
+
if method == 'close': return
|
|
119
|
+
except Exception as error:
|
|
120
|
+
# Avoid exporting exception text that can contain connection URLs or cookies.
|
|
121
|
+
protocol.write(json.dumps({'id': request['id'], 'error': {'code': 'connection_failed', 'message': str(error) if isinstance(error, BrowserDisconnected) else type(error).__name__ + ': browser service failed.'}}) + '\n'); protocol.flush()
|
|
122
|
+
if server.browser_session:
|
|
123
|
+
with contextlib.suppress(Exception):
|
|
124
|
+
await close_browser()
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
asyncio.run(serve())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const browserVersion = "0.13.10";
|
|
2
|
+
export declare function browserName(method: any): string | undefined;
|
|
3
|
+
export declare function browserConfig(method: any, config: any): any;
|
|
4
|
+
export declare const browserDirectory: (run: string) => string;
|
|
5
|
+
export declare function browserBinding(name?: string): string;
|
|
6
|
+
export declare function configureBrowser(name: string, cdp?: string): {
|
|
7
|
+
connection: string;
|
|
8
|
+
configured: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function localChromeProfile(home?: string, platform?: NodeJS.Platform): {
|
|
11
|
+
user_data_dir: string;
|
|
12
|
+
profile_directory: string;
|
|
13
|
+
} | undefined;
|
|
14
|
+
export declare function mergeSessions(previous: any, current: any, domains: string[]): {
|
|
15
|
+
cookies: any[];
|
|
16
|
+
origins: any[];
|
|
17
|
+
};
|
|
18
|
+
export declare class BrowserService {
|
|
19
|
+
private id;
|
|
20
|
+
private pending;
|
|
21
|
+
private stopped;
|
|
22
|
+
private child;
|
|
23
|
+
private queue;
|
|
24
|
+
constructor(python: string);
|
|
25
|
+
private fail;
|
|
26
|
+
request(method: string, args?: any, signal?: AbortSignal): Promise<any>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function openBrowser(method: any, config: any, run: string, signal?: AbortSignal): Promise<{
|
|
30
|
+
connections: {
|
|
31
|
+
[x: string]: {
|
|
32
|
+
call: (tool: string, args: any, signal: AbortSignal) => Promise<any>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
service: BrowserService;
|
|
36
|
+
root: string;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
} | undefined>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { readFileSync, existsSync, mkdirSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { methodCache, prepareRuntime } from './prepare.js';
|
|
9
|
+
import { writePrivateJson } from './files.js';
|
|
10
|
+
import { authoringPath } from './authoring.js';
|
|
11
|
+
export const browserVersion = '0.13.10';
|
|
12
|
+
const assets = fileURLToPath(new URL('./browser-runtime/', import.meta.url));
|
|
13
|
+
const catalog = JSON.parse(readFileSync(join(assets, 'tools.json'), 'utf8'));
|
|
14
|
+
const observers = new Set(['browser_get_state', 'browser_get_html', 'browser_screenshot', 'browser_list_tabs']);
|
|
15
|
+
export function browserName(method) {
|
|
16
|
+
const names = [...new Set(Object.values(method.steps).flatMap((s) => [s.do, s.check]).filter(e => e?.browser).map(e => e.browser.split('.')[1]))];
|
|
17
|
+
if (names.length > 1)
|
|
18
|
+
throw Error('Use one browser connection per run.');
|
|
19
|
+
return names[0];
|
|
20
|
+
}
|
|
21
|
+
export function browserConfig(method, config) {
|
|
22
|
+
const name = browserName(method);
|
|
23
|
+
if (!name)
|
|
24
|
+
return config;
|
|
25
|
+
const tools = { ...config.tools };
|
|
26
|
+
for (const t of catalog) {
|
|
27
|
+
const def = { description: t.description, connection: name, tool: t.name, parameters: t.inputSchema, effects: observers.has(t.name) ? [] : [name] };
|
|
28
|
+
if (tools[t.name] && JSON.stringify(tools[t.name]) !== JSON.stringify(def))
|
|
29
|
+
throw Error(`Browser tool conflicts with custom tool: ${t.name}`);
|
|
30
|
+
tools[t.name] = def;
|
|
31
|
+
}
|
|
32
|
+
return { ...config, environment: { ...config.environment, [name]: config.environment?.[name] ?? 'method-browser:default' }, tools };
|
|
33
|
+
}
|
|
34
|
+
export const browserDirectory = (run) => join(methodCache(), 'browser', 'runs', createHash('sha256').update(resolve(run)).digest('hex'));
|
|
35
|
+
export function browserBinding(name = 'default') {
|
|
36
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name))
|
|
37
|
+
throw Error('Invalid browser connection name.');
|
|
38
|
+
return join(methodCache(), 'browser', 'bindings', name);
|
|
39
|
+
}
|
|
40
|
+
export function configureBrowser(name, cdp) {
|
|
41
|
+
const root = browserBinding(name);
|
|
42
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
43
|
+
if (cdp) {
|
|
44
|
+
const url = new URL(cdp);
|
|
45
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
46
|
+
throw Error('Use a browser endpoint without credentials or query parameters.');
|
|
47
|
+
}
|
|
48
|
+
writePrivateJson(join(root, 'connection.json'), { ...(cdp ? { cdp_url: cdp } : {}) });
|
|
49
|
+
if (existsSync(join(root, 'session.json')))
|
|
50
|
+
unlinkSync(join(root, 'session.json'));
|
|
51
|
+
return { connection: `method-browser:${name}`, configured: true };
|
|
52
|
+
}
|
|
53
|
+
export function localChromeProfile(home = homedir(), platform = process.platform) {
|
|
54
|
+
if (platform !== 'darwin')
|
|
55
|
+
return undefined;
|
|
56
|
+
const root = join(home, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
57
|
+
const file = join(root, 'Local State');
|
|
58
|
+
if (!existsSync(file))
|
|
59
|
+
return undefined;
|
|
60
|
+
const state = JSON.parse(readFileSync(file, 'utf8'));
|
|
61
|
+
const profile = state.profile?.last_used ?? 'Default';
|
|
62
|
+
if (typeof profile !== 'string' || profile !== profile.split(/[\\/]/).pop() || !existsSync(join(root, profile)))
|
|
63
|
+
throw Error('The selected Chrome profile is unavailable. Open Chrome and select a profile, then retry.');
|
|
64
|
+
return { user_data_dir: root, profile_directory: profile };
|
|
65
|
+
}
|
|
66
|
+
export function mergeSessions(previous, current, domains) {
|
|
67
|
+
const selected = (host) => domains.some(d => d === host.replace(/^\./, '') || d.endsWith('.' + host.replace(/^\./, '')));
|
|
68
|
+
return { cookies: [...(previous.cookies ?? []).filter((c) => !selected(c.domain)), ...(current.cookies ?? [])], origins: [...(previous.origins ?? []).filter((o) => !selected(new URL(o.origin).hostname)), ...(current.origins ?? [])] };
|
|
69
|
+
}
|
|
70
|
+
export class BrowserService {
|
|
71
|
+
id = 0;
|
|
72
|
+
pending = new Map();
|
|
73
|
+
stopped = false;
|
|
74
|
+
child;
|
|
75
|
+
queue = Promise.resolve();
|
|
76
|
+
constructor(python) {
|
|
77
|
+
this.child = spawn(python, [fileURLToPath(new URL('./browser-service.py', import.meta.url))], { stdio: ['pipe', 'pipe', 'ignore'], env: { ...process.env, ANONYMIZED_TELEMETRY: 'false' } });
|
|
78
|
+
createInterface({ input: this.child.stdout }).on('line', line => { try {
|
|
79
|
+
const r = JSON.parse(line), p = this.pending.get(r.id);
|
|
80
|
+
if (p) {
|
|
81
|
+
this.pending.delete(r.id);
|
|
82
|
+
r.error ? p.reject(Object.assign(Error(r.error.message), { code: r.error.code })) : p.resolve(r.result);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
this.fail(Error('Invalid browser response'));
|
|
87
|
+
} });
|
|
88
|
+
this.child.stdin.on('error', e => this.fail(e));
|
|
89
|
+
this.child.on('error', e => { this.stopped = true; this.fail(e); });
|
|
90
|
+
this.child.on('exit', () => { this.stopped = true; this.fail(Object.assign(Error('Browser service stopped.'), { code: 'connection_failed' })); });
|
|
91
|
+
}
|
|
92
|
+
fail(e) { for (const p of this.pending.values())
|
|
93
|
+
p.reject(e); this.pending.clear(); }
|
|
94
|
+
request(method, args = {}, signal) {
|
|
95
|
+
const action = async () => {
|
|
96
|
+
if (this.stopped)
|
|
97
|
+
throw Object.assign(Error('Browser service stopped.'), { code: 'connection_failed' });
|
|
98
|
+
if (signal?.aborted)
|
|
99
|
+
throw signal.reason;
|
|
100
|
+
const id = ++this.id;
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const abort = () => { clean(); this.pending.delete(id); this.child.kill('SIGTERM'); reject(signal?.reason ?? Object.assign(Error('Browser operation timed out.'), { code: 'connection_failed' })); };
|
|
103
|
+
const timer = setTimeout(abort, 120_000);
|
|
104
|
+
const clean = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); };
|
|
105
|
+
this.pending.set(id, { resolve: v => { clean(); resolve(v); }, reject: e => { clean(); reject(e); } });
|
|
106
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
107
|
+
this.child.stdin.write(JSON.stringify({ id, method, args }) + '\n');
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
const next = this.queue.then(action);
|
|
111
|
+
this.queue = next.catch(() => { });
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
async close() { if (this.stopped)
|
|
115
|
+
return; try {
|
|
116
|
+
await this.request('close');
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
this.child.kill('SIGTERM');
|
|
120
|
+
} }
|
|
121
|
+
}
|
|
122
|
+
export async function openBrowser(method, config, run, signal) {
|
|
123
|
+
const name = browserName(method);
|
|
124
|
+
if (!name)
|
|
125
|
+
return undefined;
|
|
126
|
+
const root = browserDirectory(run);
|
|
127
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
128
|
+
const binding = String(config.environment[name]);
|
|
129
|
+
if (!binding.startsWith('method-browser:'))
|
|
130
|
+
throw Error('Connect the browser with method browser connect, then use its method-browser connection name.');
|
|
131
|
+
const shared = browserBinding(binding.slice('method-browser:'.length));
|
|
132
|
+
mkdirSync(shared, { recursive: true, mode: 0o700 });
|
|
133
|
+
const selection = join(root, 'connection.json');
|
|
134
|
+
let settings = existsSync(selection) ? JSON.parse(readFileSync(selection, 'utf8')) : existsSync(join(shared, 'connection.json')) ? JSON.parse(readFileSync(join(shared, 'connection.json'), 'utf8')) : {};
|
|
135
|
+
if (!existsSync(selection) && binding === 'method-browser:default' && !settings.cdp_url && !settings.user_data_dir) {
|
|
136
|
+
const profile = localChromeProfile();
|
|
137
|
+
if (profile)
|
|
138
|
+
settings = { ...settings, ...profile };
|
|
139
|
+
}
|
|
140
|
+
if (!existsSync(selection))
|
|
141
|
+
writePrivateJson(selection, settings);
|
|
142
|
+
const state = join(root, 'session.json');
|
|
143
|
+
const profileSource = settings.user_data_dir ? JSON.stringify([settings.user_data_dir, settings.profile_directory]) : 'private';
|
|
144
|
+
if (!existsSync(state) && existsSync(join(shared, 'session.json'))) {
|
|
145
|
+
const saved = JSON.parse(readFileSync(join(shared, 'session.json'), 'utf8'));
|
|
146
|
+
if (profileSource === 'private' || saved.profile_source === profileSource)
|
|
147
|
+
writePrivateJson(state, { cookies: saved.cookies, origins: saved.origins });
|
|
148
|
+
}
|
|
149
|
+
const metadataFile = join(root, 'receipt.json');
|
|
150
|
+
const metadata = existsSync(metadataFile) ? JSON.parse(readFileSync(metadataFile, 'utf8')) : {};
|
|
151
|
+
const prepared = await prepareRuntime(assets, { allow_local_processes: true }, { steps: {} });
|
|
152
|
+
let lock;
|
|
153
|
+
const lockFile = join(shared, 'attached.lock');
|
|
154
|
+
if (settings.cdp_url) {
|
|
155
|
+
if (existsSync(lockFile)) {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(Number(readFileSync(lockFile, 'utf8')), 0);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
if (e.code === 'ESRCH')
|
|
161
|
+
unlinkSync(lockFile);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
lock = openSync(lockFile, 'wx', 0o600);
|
|
166
|
+
writeFileSync(lock, String(process.pid));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw Object.assign(Error('Another Method run controls this Chrome session. Wait for it to finish, or select a separate browser connection.'), { code: 'needs_input' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const release = () => { if (lock !== undefined) {
|
|
173
|
+
closeSync(lock);
|
|
174
|
+
lock = undefined;
|
|
175
|
+
unlinkSync(lockFile);
|
|
176
|
+
} };
|
|
177
|
+
const service = new BrowserService(prepared.config.runtimes.python.command);
|
|
178
|
+
const executable = process.env.METHOD_BROWSER_EXECUTABLE ?? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome'].find(p => existsSync(p));
|
|
179
|
+
try {
|
|
180
|
+
await service.request('start', { user_data_dir: join(root, 'profile'), headless: process.env.METHOD_BROWSER_HEADLESS !== '0', ...(executable ? { executable_path: authoringPath(executable) } : {}), ...settings, ...(existsSync(state) ? { storage_state: state } : {}), domains: metadata.domains ?? [] }, signal);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
await service.close().catch(() => { });
|
|
184
|
+
release();
|
|
185
|
+
throw Object.assign(Error('Browser could not start. If Chrome is open, close it and retry so Method can copy its sign-ins. Otherwise, install Chrome or select a running browser with method browser connect --cdp URL.'), { code: 'needs_input' });
|
|
186
|
+
}
|
|
187
|
+
writePrivateJson(join(run, 'browser-runtime.json'), { provider: 'browser-use', version: browserVersion, connection: name, tools: catalog });
|
|
188
|
+
return { connections: { [name]: { call: (tool, args, signal) => service.request('call', { name: tool, arguments: args }, signal) } }, service, root,
|
|
189
|
+
async close() { try {
|
|
190
|
+
const receipt = await service.request('export', { path: state });
|
|
191
|
+
writePrivateJson(metadataFile, { ...receipt, version: browserVersion, binding });
|
|
192
|
+
const prior = existsSync(join(shared, 'session.json')) ? JSON.parse(readFileSync(join(shared, 'session.json'), 'utf8')) : {};
|
|
193
|
+
writePrivateJson(join(shared, 'session.json'), { ...mergeSessions(prior.profile_source === profileSource || profileSource === 'private' ? prior : {}, JSON.parse(readFileSync(state, 'utf8')), receipt.domains), profile_source: profileSource });
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
try {
|
|
197
|
+
await service.close();
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
release();
|
|
201
|
+
}
|
|
202
|
+
} } };
|
|
203
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Check supported agent access without starting a model request. Custom commands remain operator-owned. */
|
|
2
|
+
export declare function checkAgents(profiles: Record<string, any>): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* The model profiles of a run. With this computer's own model key (method config model-key) and no local agent,
|
|
5
|
+
* hosted profiles call OpenRouter with that key, and classify steps call Jev with it (config.classification).
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveAgentProfiles(method: any, config: any, agent?: string, hostedModel?: string): Promise<any>;
|
|
8
|
+
/** What a run needs from the Method account: hosted models for unconfigured call and agent steps, and classification. */
|
|
9
|
+
export declare function accountNeeds(method: any, config: any, agent?: string): {
|
|
10
|
+
models: boolean;
|
|
11
|
+
classification: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function checkConfiguration(config: any): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolveModels } from '@withmethod/runtime/agents.js';
|
|
2
|
+
import { executable } from '@withmethod/runtime/io.js';
|
|
3
|
+
import { directBackends } from '@withmethod/runtime/model.js';
|
|
4
|
+
import { command } from './prepare.js';
|
|
5
|
+
import { announceOwnKey, loadOwnKey, ownKeyClassification, ownKeyEnv } from './own-model-key.js';
|
|
6
|
+
/** Check supported agent access without starting a model request. Custom commands remain operator-owned. */
|
|
7
|
+
export async function checkAgents(profiles) {
|
|
8
|
+
const checked = new Set();
|
|
9
|
+
for (const profile of Object.values(profiles)) {
|
|
10
|
+
if (!['codex', 'claude'].includes(profile.backend) || profile.command || checked.has(profile.backend))
|
|
11
|
+
continue;
|
|
12
|
+
checked.add(profile.backend);
|
|
13
|
+
try {
|
|
14
|
+
const result = await command(profile.backend, profile.backend === 'codex' ? ['login', 'status'] : ['auth', 'status'], process.cwd());
|
|
15
|
+
if (profile.backend === 'claude' && JSON.parse(result.stdout).loggedIn !== true)
|
|
16
|
+
throw Error('Not signed in.');
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw Object.assign(Error(`${profile.backend} access check failed. Sign in with ${profile.backend}, then continue this run. ${error.message}`), { code: 'needs_input' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The model profiles of a run. With this computer's own model key (method config model-key) and no local agent,
|
|
25
|
+
* hosted profiles call OpenRouter with that key, and classify steps call Jev with it (config.classification).
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveAgentProfiles(method, config, agent, hostedModel) {
|
|
28
|
+
const profiles = await resolveModels(method, config, { agent, hostedModel });
|
|
29
|
+
const env = ownKeyEnv();
|
|
30
|
+
if (!env)
|
|
31
|
+
return profiles;
|
|
32
|
+
const classifies = Object.values(method.steps ?? {}).some((step) => [step?.do, step?.check].some(exec => exec?.kind === 'classify'));
|
|
33
|
+
// A local agent (--agent) runs the model steps; the own key still classifies.
|
|
34
|
+
const hosted = !agent && Object.values(profiles).some((profile) => profile?.backend === 'method');
|
|
35
|
+
if (!hosted && !classifies)
|
|
36
|
+
return profiles;
|
|
37
|
+
if (classifies && !config.classification?.api_key_env)
|
|
38
|
+
config.classification = ownKeyClassification(env);
|
|
39
|
+
// Hosted profiles stay hosted: the run sends their requests with the own key (hostedModelsFor).
|
|
40
|
+
loadOwnKey(env);
|
|
41
|
+
announceOwnKey();
|
|
42
|
+
return profiles;
|
|
43
|
+
}
|
|
44
|
+
/** What a run needs from the Method account: hosted models for unconfigured call and agent steps, and classification. */
|
|
45
|
+
export function accountNeeds(method, config, agent) {
|
|
46
|
+
// Every run asks this first, a resumed one too: the own key's value comes from the device store by name.
|
|
47
|
+
const env = ownKeyEnv();
|
|
48
|
+
if (env)
|
|
49
|
+
loadOwnKey(env, false);
|
|
50
|
+
const steps = Object.values(method.steps ?? {});
|
|
51
|
+
const execs = steps.flatMap(step => [step.do, step.check]).filter(exec => exec?.kind);
|
|
52
|
+
return {
|
|
53
|
+
// A step on a local-agent entry of the Method's models needs no account.
|
|
54
|
+
models: !agent && !config.models?.default && execs.some(exec => ['call', 'agent'].includes(exec.kind) && !config.models?.[exec.model] && !method.models?.[exec.model ?? 'default']?.agent),
|
|
55
|
+
classification: execs.some(exec => exec.kind === 'classify') && !config.classification?.api_key_env && !env,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function checkConfiguration(config) {
|
|
59
|
+
for (const profile of Object.values(config.runtimes ?? {}))
|
|
60
|
+
await executable(profile.command);
|
|
61
|
+
for (const profile of Object.values(config.models ?? {})) {
|
|
62
|
+
if (directBackends.includes(profile.backend) && profile.backend !== 'method' && !process.env[profile.api_key_env])
|
|
63
|
+
throw Error(`Missing environment variable: ${profile.api_key_env}`);
|
|
64
|
+
if (['codex', 'claude'].includes(profile.backend))
|
|
65
|
+
await executable(profile.command ?? profile.backend);
|
|
66
|
+
}
|
|
67
|
+
await checkAgents(config.models ?? {});
|
|
68
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ClassificationProvider } from '@withmethod/runtime';
|
|
2
|
+
import { MethodClient } from './method-client.js';
|
|
3
|
+
/** Use the same Method account and server as the run. Each evaluation is one attempt. */
|
|
4
|
+
export declare function managedClassification(client: MethodClient): ClassificationProvider;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { classificationByteLimit, validateClassification } from '@withmethod/runtime/classification.js';
|
|
3
|
+
const Identity = z.strictObject({ provider: z.literal('typesafe'), model: z.string().trim().min(1) });
|
|
4
|
+
/** Use the same Method account and server as the run. Each evaluation is one attempt. */
|
|
5
|
+
export function managedClassification(client) {
|
|
6
|
+
const options = (signal) => ({ signal, stallMs: 30_000, maxResponseBytes: classificationByteLimit });
|
|
7
|
+
return {
|
|
8
|
+
async resolve(signal) {
|
|
9
|
+
return Identity.parse(await client.request('/api/cli/classifications/model', 'GET', undefined, true, options(signal)));
|
|
10
|
+
},
|
|
11
|
+
async evaluate(request, signal) {
|
|
12
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > classificationByteLimit)
|
|
13
|
+
throw Object.assign(Error('Classification input exceeds request limit.'), { code: 'input_limit' });
|
|
14
|
+
let answer;
|
|
15
|
+
try {
|
|
16
|
+
answer = await client.request('/api/cli/classifications', 'POST', request, true, options(signal));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.status === 409)
|
|
20
|
+
throw Object.assign(Error("This run's classifier version is unavailable. Start a new run to use the current default."), { code: 'classification_version_unavailable' });
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
validateClassification(answer, request, { provider: 'typesafe', model: request.model });
|
|
24
|
+
return answer;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MethodClient } from './method-client.js';
|
|
2
|
+
/** Where the production runs of a Method run in one environment: on the organization's workers, or on Method Cloud. */
|
|
3
|
+
export type Placement = 'workers' | 'cloud';
|
|
4
|
+
export declare const environmentPattern: RegExp;
|
|
5
|
+
/** The placement flags of `method publish`: --cloud or --workers, and --env NAME (default production). */
|
|
6
|
+
export declare function placementFlags(args: string[]): {
|
|
7
|
+
placement: Placement;
|
|
8
|
+
environment: string;
|
|
9
|
+
} | undefined;
|
|
10
|
+
/** Set where the production runs of a Method run in one environment. */
|
|
11
|
+
export declare function setPlacement(client: MethodClient, methodId: string, environment: string, placement: Placement): Promise<{
|
|
12
|
+
environment: string;
|
|
13
|
+
placement: Placement;
|
|
14
|
+
updated_at: string | null;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Publish, then set the placement when --cloud or --workers is given. The flags are checked before the publish. When
|
|
18
|
+
* the organization cannot use Method Cloud, the version stays published and the result says why the placement did not
|
|
19
|
+
* change.
|
|
20
|
+
*/
|
|
21
|
+
export declare function publishWithPlacement<T extends {
|
|
22
|
+
workflow_id: string;
|
|
23
|
+
}>(client: MethodClient, args: string[], publish: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const environmentPattern = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
2
|
+
/** The placement flags of `method publish`: --cloud or --workers, and --env NAME (default production). */
|
|
3
|
+
export function placementFlags(args) {
|
|
4
|
+
const cloud = args.includes('--cloud'), workers = args.includes('--workers');
|
|
5
|
+
if (cloud && workers)
|
|
6
|
+
throw Error('Use --cloud or --workers, not both.');
|
|
7
|
+
const at = args.findIndex(arg => arg === '--env' || arg.startsWith('--env='));
|
|
8
|
+
const environment = at < 0 ? 'production' : args[at].startsWith('--env=') ? args[at].slice(6) : args[at + 1];
|
|
9
|
+
if (!environment || !environmentPattern.test(environment))
|
|
10
|
+
throw Error('Use --env NAME: lowercase letters, digits, and hyphens.');
|
|
11
|
+
if (!cloud && !workers) {
|
|
12
|
+
if (at >= 0)
|
|
13
|
+
throw Error('Use --env with --cloud or --workers.');
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return { placement: cloud ? 'cloud' : 'workers', environment };
|
|
17
|
+
}
|
|
18
|
+
/** Set where the production runs of a Method run in one environment. */
|
|
19
|
+
export async function setPlacement(client, methodId, environment, placement) {
|
|
20
|
+
return client.request(`/api/methods/${encodeURIComponent(methodId)}/placements/${encodeURIComponent(environment)}`, 'PUT', { placement });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Publish, then set the placement when --cloud or --workers is given. The flags are checked before the publish. When
|
|
24
|
+
* the organization cannot use Method Cloud, the version stays published and the result says why the placement did not
|
|
25
|
+
* change.
|
|
26
|
+
*/
|
|
27
|
+
export async function publishWithPlacement(client, args, publish) {
|
|
28
|
+
const wanted = placementFlags(args);
|
|
29
|
+
const published = await publish();
|
|
30
|
+
if (!wanted)
|
|
31
|
+
return published;
|
|
32
|
+
try {
|
|
33
|
+
const placement = await setPlacement(client, published.workflow_id, wanted.environment, wanted.placement);
|
|
34
|
+
return { ...published, placement: { environment: placement.environment, placement: placement.placement } };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
const message = error.code === 'cloud_not_enabled'
|
|
39
|
+
? 'Method Cloud is not on for your organization. Production runs still use your workers.'
|
|
40
|
+
: `The placement did not change: ${error.message}`;
|
|
41
|
+
return { ...published, placement: { environment: wanted.environment, error: message } };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** METHOD_CONFIG_DIR selects another folder for sign-ins and computer settings (tests use a temporary one). */
|
|
3
|
+
export declare const configRoot: () => string;
|
|
4
|
+
export declare const computerFile: () => string;
|
|
5
|
+
declare const Settings: z.ZodObject<{
|
|
6
|
+
agent: z.ZodOptional<z.ZodEnum<["codex", "claude"]>>;
|
|
7
|
+
bindings: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
8
|
+
classification_key_env: z.ZodOptional<z.ZodString>;
|
|
9
|
+
model_key_env: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
bindings: Record<string, Record<string, string>>;
|
|
12
|
+
agent?: "codex" | "claude" | undefined;
|
|
13
|
+
classification_key_env?: string | undefined;
|
|
14
|
+
model_key_env?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
agent?: "codex" | "claude" | undefined;
|
|
17
|
+
bindings?: Record<string, Record<string, string>> | undefined;
|
|
18
|
+
classification_key_env?: string | undefined;
|
|
19
|
+
model_key_env?: string | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type ComputerSettings = z.infer<typeof Settings>;
|
|
22
|
+
/** The settings of this computer: the local agent, connection bindings, and an own model key. Nothing in a Method file. */
|
|
23
|
+
export declare function readComputerSettings(): ComputerSettings;
|
|
24
|
+
export declare function updateComputerSettings(change: (settings: ComputerSettings) => void): ComputerSettings;
|
|
25
|
+
export declare function bindConnectionValue(methodId: string, name: string, value: string): void;
|
|
26
|
+
/** method config: print the computer settings, set the local agent, or turn the own model key on or off. */
|
|
27
|
+
export declare function configCommand(args: string[]): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { writePrivateJson } from './files.js';
|
|
6
|
+
/** METHOD_CONFIG_DIR selects another folder for sign-ins and computer settings (tests use a temporary one). */
|
|
7
|
+
export const configRoot = () => process.env.METHOD_CONFIG_DIR || join(homedir(), '.config', 'method');
|
|
8
|
+
export const computerFile = () => join(configRoot(), 'computer.json');
|
|
9
|
+
// Only settings of this computer. Run limits belong to each Method (format 3.4 limits), never to the computer.
|
|
10
|
+
const Settings = z.object({
|
|
11
|
+
agent: z.enum(['codex', 'claude']).optional(),
|
|
12
|
+
// METHOD_ID -> connection name -> a folder path, or a connection value (URL or method-browser:NAME).
|
|
13
|
+
bindings: z.record(z.string(), z.record(z.string(), z.string())).default({}),
|
|
14
|
+
// The environment variable with this computer's own OpenRouter key: classify then calls Jev directly with it.
|
|
15
|
+
classification_key_env: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
|
|
16
|
+
// The name of this computer's own OpenRouter key (method config model-key): hosted model and classify steps use it.
|
|
17
|
+
model_key_env: z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
|
|
18
|
+
});
|
|
19
|
+
/** The settings of this computer: the local agent, connection bindings, and an own model key. Nothing in a Method file. */
|
|
20
|
+
export function readComputerSettings() {
|
|
21
|
+
const file = computerFile();
|
|
22
|
+
if (!existsSync(file))
|
|
23
|
+
return { bindings: {} };
|
|
24
|
+
try {
|
|
25
|
+
return Settings.parse(JSON.parse(readFileSync(file, 'utf8')));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw Error(`${file} is not valid: ${error.message.slice(0, 300)} Fix or delete the file.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function updateComputerSettings(change) {
|
|
32
|
+
const settings = readComputerSettings();
|
|
33
|
+
change(settings);
|
|
34
|
+
const parsed = Settings.parse(settings);
|
|
35
|
+
writePrivateJson(computerFile(), parsed);
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
export function bindConnectionValue(methodId, name, value) {
|
|
39
|
+
updateComputerSettings(settings => { settings.bindings[methodId] = { ...settings.bindings[methodId], [name]: value }; });
|
|
40
|
+
}
|
|
41
|
+
/** method config: print the computer settings, set the local agent, or turn the own model key on or off. */
|
|
42
|
+
export async function configCommand(args) {
|
|
43
|
+
const [key, value, extra] = args;
|
|
44
|
+
if (!key) {
|
|
45
|
+
process.stdout.write(JSON.stringify({ file: computerFile(), ...readComputerSettings() }, null, 2) + '\n');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (key === 'model-key')
|
|
49
|
+
return (await import('./own-model-key.js')).modelKeyCommand(args.slice(1));
|
|
50
|
+
if (key !== 'agent' || !value || extra !== undefined || !['codex', 'claude', 'none'].includes(value))
|
|
51
|
+
throw Error('Use method config, method config agent codex|claude|none, or method config model-key [--off].');
|
|
52
|
+
const saved = updateComputerSettings(settings => { if (value === 'none')
|
|
53
|
+
delete settings.agent;
|
|
54
|
+
else
|
|
55
|
+
settings.agent = value; });
|
|
56
|
+
process.stdout.write(saved.agent ? `Model steps run with the local ${saved.agent} agent on this computer.\n` : 'Model steps use the Method\'s models and the account default.\n');
|
|
57
|
+
}
|