@nomac/cli 0.5.6 → 0.5.8
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/package.json +1 -1
- package/skill/SKILL.md +8 -147
- package/src/cli.mjs +3 -6
- package/src/desktop.mjs +26 -0
- package/src/hosted-sessions.mjs +8 -1
- package/src/mac-tools.mjs +7 -0
- package/src/mac-transport.mjs +3 -1
- package/src/mcp.mjs +15 -6
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -3,8 +3,7 @@ name: nomac
|
|
|
3
3
|
description: >
|
|
4
4
|
Use a full cloud Mac over SSH, HTTPS or MCP for macOS tools, Xcode and iOS
|
|
5
5
|
development. Start sessions, sync files, run commands, retrieve work and
|
|
6
|
-
stop.
|
|
7
|
-
App Store publishing, signing, screenshots, metadata and review replies.
|
|
6
|
+
stop.
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
# nomac — a Mac for your agent
|
|
@@ -40,155 +39,17 @@ Keep the same request key and arguments after uncertain responses; inspect
|
|
|
40
39
|
the original job before retrying. Output retrieval failure is not job failure.
|
|
41
40
|
Managed jobs have a 600-second limit; SSH can run longer within session limits.
|
|
42
41
|
CLI sync skips symlinks and preserves remote extra files; use native SSH/rsync
|
|
43
|
-
when needed. GUI
|
|
42
|
+
when needed. Agents have no GUI control: drive the Simulator with simctl, and
|
|
43
|
+
give the human `open_mac_desktop` when a screen needs a person. Persistent disks
|
|
44
|
+
are not currently offered.
|
|
44
45
|
Report failures using `report_issue` with `session_id`, optional `job_id` and
|
|
45
46
|
a description without secrets. Read https://nomac.app/llms.txt for full limits.
|
|
46
47
|
|
|
47
|
-
##
|
|
48
|
+
## Builder (existing subscribers only)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## Setup check
|
|
54
|
-
|
|
55
|
-
1. MCP tools available (`connect_status`, `push_project`, `build`, …)? Use them.
|
|
56
|
-
2. Otherwise: `npx @nomac/cli whoami`. If not logged in, run
|
|
57
|
-
`npx @nomac/cli login`; give the human the device URL and code to approve.
|
|
58
|
-
Then `claude mcp add nomac -- npx @nomac/cli mcp`.
|
|
59
|
-
3. `connect_status` must show a healthy Apple connection. If not, the human
|
|
60
|
-
connects at the dashboard (60 seconds, one .p8 upload). Don't guess —
|
|
61
|
-
verify, then proceed.
|
|
62
|
-
|
|
63
|
-
## Ship to TestFlight
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
push_project # after every code change
|
|
67
|
-
build workflow=release # ~3 min; smoke = fast compile-only check
|
|
68
|
-
status # poll ~30s until ready | failed
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- `ready` → tell the human: "open TestFlight on your phone." Their taps are
|
|
72
|
-
the test run; read `get_feedback` for crashes/comments afterwards.
|
|
73
|
-
- `failed` → `get_failure`: kind=project means you fix source and re-push;
|
|
74
|
-
kind=account means relay the link to the human. Don't burn release builds
|
|
75
|
-
debugging compile errors — use `build workflow=smoke`.
|
|
76
|
-
|
|
77
|
-
## Submit for App Store review
|
|
78
|
-
|
|
79
|
-
1. `review_lint` → fix every red. Apply returned patches (e.g. generated
|
|
80
|
-
`PrivacyInfo.xcprivacy`) directly to the source. Yellow "signals"
|
|
81
|
-
(4.3 distinctiveness, thin listing) are yours to judge and improve.
|
|
82
|
-
2. `set_metadata` — you write the copy; check `get_metadata_schema` for
|
|
83
|
-
limits. Age rating + data usage answers are legal attestations: ask the
|
|
84
|
-
human, relay their answers. `upload_screenshots` needs exact dimensions
|
|
85
|
-
(in the schema). It runs in the background: poll `get_screenshot_upload`
|
|
86
|
-
until complete before publishing. A rolled-back or paused replacement
|
|
87
|
-
needs the returned recovery instructions before starting another upload.
|
|
88
|
-
3. `publish` without confirm stages real Apple drafts/build attachments but
|
|
89
|
-
does not submit. Fix the returned blockers; retain the exact build/submission IDs.
|
|
90
|
-
4. `publish confirm=true` sends the app to Apple. Ensure the human authorized
|
|
91
|
-
this app/version; honor authorization already given. New versions use manual
|
|
92
|
-
release, while existing versions retain their Apple release policy.
|
|
93
|
-
|
|
94
|
-
If metadata or publish returns pending, keep its `request_key` and repeat
|
|
95
|
-
the original arguments with that key. `get_store_operation` recovers the key
|
|
96
|
-
and progress after a lost response. A new key starts a separate operation;
|
|
97
|
-
do not use one to bypass an unconfirmed Apple request. Staging and confirmation
|
|
98
|
-
remain separate invocations. For screenshots, keep the original `request_key`
|
|
99
|
-
too, and use `get_screenshot_upload` to find or poll the existing operation.
|
|
100
|
-
|
|
101
|
-
## Fix review rejections and release
|
|
102
|
-
|
|
103
|
-
- `get_review` lists live Apple submissions, including ones created outside
|
|
104
|
-
nomac. Pass `asc_submission_id` for all item states and exact version/build IDs.
|
|
105
|
-
- Apple does not expose App Review messages through its public API. Use the
|
|
106
|
-
returned App Store Connect handoff to have the human provide the actual text.
|
|
107
|
-
Never invent a rejection reason from a status code or public customer review.
|
|
108
|
-
- Fix metadata/screenshots on the exact `version_string`. A binary change needs
|
|
109
|
-
push/build, then `publish` with the exact `build_id` and `asc_submission_id`.
|
|
110
|
-
`resolve_rejection:true` explicitly asserts that this version's issue is fixed.
|
|
111
|
-
- `prepare_review_reply` prepares your authored text but always returns `sent:false`.
|
|
112
|
-
The human sends it and any message attachments in App Store Connect. Changing
|
|
113
|
-
review_contact.notes is not a reply; sending a reply is not resubmission.
|
|
114
|
-
- `update_review` supports `resolve_item`, `remove_item`, `cancel`, `resubmit`,
|
|
115
|
-
and `release`. Preview without confirm; confirm the exact requested action.
|
|
116
|
-
Fix issues before marking items resolved. Every rejected item must be resolved
|
|
117
|
-
or removed before resubmitting. Removed items cannot be added back to that submission.
|
|
118
|
-
- Poll `get_review`; approval and release are separate. Release requires an
|
|
119
|
-
app-version item in Pending Developer Release. Edit subscription/IAP products,
|
|
120
|
-
events and custom pages through the store catalog below. Apple/account/legal
|
|
121
|
-
decisions still require the relevant Apple workflow.
|
|
122
|
-
|
|
123
|
-
## External TestFlight
|
|
124
|
-
|
|
125
|
-
`ready` from a nomac build means internal TestFlight readiness. External testing
|
|
126
|
-
requires its own review and distribution:
|
|
127
|
-
|
|
128
|
-
1. `get_testflight` lists groups, test information and recent Apple builds. Use
|
|
129
|
-
`asc_build_id` for a specific build's beta-review/external state, or `group_id`
|
|
130
|
-
to list testers. Never infer external approval from internal readiness.
|
|
131
|
-
2. `manage_testflight action=metadata` sets app_info (description/feedbackEmail/URLs),
|
|
132
|
-
review_contact (Apple contact/demo fields), locale, and build whats_new.
|
|
133
|
-
3. Select/create an external group. `invite_tester`/`remove_tester` use an explicit
|
|
134
|
-
email/tester ID and group_id. `update_group` can enable an optional public link.
|
|
135
|
-
4. `distribute` requires asc_build_id and explicit group_ids. It submits beta
|
|
136
|
-
review if needed, then assigns those groups. auto_notify defaults false;
|
|
137
|
-
`notify` starts testing after approval. `remove_build` affects one group;
|
|
138
|
-
`expire` stops testing for the build across all groups.
|
|
139
|
-
5. All actions preview without confirm and apply with confirm:true. Pending
|
|
140
|
-
requests use their original key/body; get_store_operation recovers them.
|
|
141
|
-
6. Relay an accurate human-provided uses_non_exempt_encryption with action=encryption
|
|
142
|
-
when appropriate. Never guess. Encryption declarations/documents use the
|
|
143
|
-
store catalog; rejected beta messages require App Store Connect. Fix the cause
|
|
144
|
-
before resubmitting a rejected binary.
|
|
145
|
-
|
|
146
|
-
## Pricing, products, customer reviews and the wider store API
|
|
147
|
-
|
|
148
|
-
1. `get_store_capabilities` lists operations by category/search with next_offset.
|
|
149
|
-
Pass `operation` to get that operation's exact Apple request/query schema.
|
|
150
|
-
Categories: listing, releases, pricing, purchases, subscriptions, reviews,
|
|
151
|
-
marketing, assets, testflight, analytics, compliance, webhooks.
|
|
152
|
-
2. `read_store` executes listed GET operations. Begin with an `apps_*` relationship
|
|
153
|
-
and `scope:[]` for the linked app. Every child has a reusable `scope`; copy it
|
|
154
|
-
into the next read/write. Keep the query when following `next_cursor`.
|
|
155
|
-
3. `update_store` takes the operation, scope and exact JSON:API `body`. For creation,
|
|
156
|
-
scope the parent. Every existing relationship ID needs its own verified scope
|
|
157
|
-
in `references`; the target and its ancestors are already verified. Territories
|
|
158
|
-
and categories are global. Inline creates use local IDs such as `${price1}`.
|
|
159
|
-
4. Preview without confirm. `confirm:true` applies real paid prices, availability,
|
|
160
|
-
products/offers, public responses, release settings or event subscriptions.
|
|
161
|
-
Honor the human's existing authorization; do not ask again for authorized work.
|
|
162
|
-
Do not invent prices, legal answers or factual claims in public replies.
|
|
163
|
-
5. `customerReviewResponses_createInstance` replies to a public customer review.
|
|
164
|
-
It does not send a message to Apple reviewers. Read their actual messages in
|
|
165
|
-
App Store Connect; `prepare_review_reply` only drafts the response.
|
|
166
|
-
6. Product management covers subscriptions/IAP versions, localization, prices,
|
|
167
|
-
availability, grace periods and offers. First IAP approval may require the
|
|
168
|
-
app version submission. Draft review submission items can attach products;
|
|
169
|
-
use `publish` / `update_review` to submit, resolve, resubmit and release.
|
|
170
|
-
7. Events, custom pages, experiments, accessibility and phased releases use their
|
|
171
|
-
catalog operations. Apple still validates account permissions and valid states.
|
|
172
|
-
Analytics report segments supply download URLs; offer-code CSV exports return
|
|
173
|
-
bounded base64 downloads. Webhooks send app events to an owner-selected receiver;
|
|
174
|
-
that receiver is responsible for running an agent when events arrive.
|
|
175
|
-
|
|
176
|
-
`upload_store_asset` handles new Apple file resources: previews, screenshot files,
|
|
177
|
-
purchase images, supplemental review attachments and encryption documents. Choose
|
|
178
|
-
the asset create operation, provide its parent relationship/body, and pass a local
|
|
179
|
-
`file_path` (CLI, up to 500 MiB) or `data_base64` (hosted, up to 2 MiB). Apple also
|
|
180
|
-
has asset-specific limits. The tool reserves, uploads each requested byte range,
|
|
181
|
-
then commits its checksum. Keep the same request_key and original bytes to resume.
|
|
182
|
-
`uploaded:true` is not processing success: poll `read_store` until
|
|
183
|
-
`assetDeliveryState.state` is `COMPLETE`. FAILED requires addressing Apple's errors.
|
|
184
|
-
This adds a new file; use `upload_screenshots` for managed screenshot replacement.
|
|
185
|
-
|
|
186
|
-
Keep pending write keys and unchanged bodies. `get_store_operation` exposes captured
|
|
187
|
-
targets and receipts. Unknown Apple outcomes remain pending and block other writes
|
|
188
|
-
to the app until reconciled; never use a fresh key to bypass one. Corrected inputs
|
|
189
|
-
or a new intentional action require a fresh key. Correspondence, appeals, App Privacy
|
|
190
|
-
questionnaire answers and account-wide financial/admin APIs are not exposed.
|
|
191
|
-
This catalog does not run simulators or Xcode Cloud jobs.
|
|
50
|
+
The separate iOS Builder publishing product is closed to new subscriptions.
|
|
51
|
+
If `get_billing_status` shows a Builder plan, its tools are listed and
|
|
52
|
+
https://nomac.app/llms.txt describes them. Otherwise use the cloud Mac above.
|
|
192
53
|
|
|
193
54
|
## When stuck
|
|
194
55
|
|
package/src/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@ Nomac 2.0 cloud Macs:
|
|
|
13
13
|
nomac exec --request-key KEY -- command args
|
|
14
14
|
nomac job JOB_ID inspect a submitted command
|
|
15
15
|
nomac output JOB_ID read resumable command output
|
|
16
|
+
nomac desktop the Mac's full desktop in your browser
|
|
16
17
|
nomac start --help session limits and usage
|
|
17
18
|
|
|
18
19
|
Existing Mac workspaces:
|
|
@@ -28,14 +29,9 @@ Nomac 2.0 transport probe:
|
|
|
28
29
|
nomac tunnel <session> SSH through the configured port-443 gateway
|
|
29
30
|
nomac transport --help HTTPS jobs, resumable output and binary probes
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Account and agents:
|
|
32
33
|
nomac login connect this device (approve in the browser)
|
|
33
34
|
nomac login <api-key> store an nmk_ key directly (CI / power users)
|
|
34
|
-
nomac push pack this directory and upload a snapshot
|
|
35
|
-
nomac build [--smoke] start a release (or smoke) build
|
|
36
|
-
nomac build --testflight release build; waits until TestFlight-ready
|
|
37
|
-
nomac status [build-id] show latest (or given) build status
|
|
38
|
-
nomac logs <build-id> show build logs
|
|
39
35
|
nomac mcp run the stdio MCP server (for agents)
|
|
40
36
|
nomac whoami show org + usage
|
|
41
37
|
|
|
@@ -66,6 +62,7 @@ export async function main(argv) {
|
|
|
66
62
|
case "output":
|
|
67
63
|
case "sync":
|
|
68
64
|
case "ssh":
|
|
65
|
+
case "desktop":
|
|
69
66
|
case "pull":
|
|
70
67
|
case "stop":
|
|
71
68
|
case "session": {
|
package/src/desktop.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { loadApiUrl } from "./config.mjs";
|
|
2
|
+
import { openUrl } from "./mac-credits.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `nomac desktop`: the Mac's full macOS desktop in the browser. The page on
|
|
6
|
+
* nomac.app connects through the gateway to the VM's own display, so there is
|
|
7
|
+
* nothing to install and no password to type. macOS has no RDP.
|
|
8
|
+
*/
|
|
9
|
+
export const DESKTOP_HELP = `nomac desktop [--no-open] [--json]
|
|
10
|
+
Open the Mac's full macOS desktop in your browser: mouse and keyboard,
|
|
11
|
+
nothing to install. Sign in to nomac.app if asked. (macOS has no RDP.)
|
|
12
|
+
The session stays busy while the desktop is open; the page disconnects after
|
|
13
|
+
15 minutes without input. Credit is used while the Mac is ready: nomac stop when done.`;
|
|
14
|
+
|
|
15
|
+
export const desktopUrl = id => `${loadApiUrl()}/mac/desktop/${encodeURIComponent(id)}`;
|
|
16
|
+
|
|
17
|
+
export function desktop(local, { values = {}, print = console.log, open = openUrl } = {}) {
|
|
18
|
+
const url = desktopUrl(local.id);
|
|
19
|
+
const opened = values["no-open"] ? false : open(url);
|
|
20
|
+
if (values.json) return print(JSON.stringify({ session_id: local.id, url, opened_in_browser: opened }));
|
|
21
|
+
print([
|
|
22
|
+
`Mac desktop: ${url}`,
|
|
23
|
+
opened ? "Opened in your browser. Sign in to nomac.app if asked." : "Open this link in your browser and sign in to nomac.app.",
|
|
24
|
+
"Credit is used while the Mac is ready: nomac stop when done.",
|
|
25
|
+
].join("\n"));
|
|
26
|
+
}
|
package/src/hosted-sessions.mjs
CHANGED
|
@@ -7,12 +7,14 @@ import { sessionStore } from "./session-store.mjs";
|
|
|
7
7
|
import { forwardingArg, runProcess, sshArgs, workspaceCommand } from "./session-ssh.mjs";
|
|
8
8
|
import { pullProject, syncProject } from "./session-transfer.mjs";
|
|
9
9
|
import { macRequest, sessionId, transportOptions } from "./mac-transport.mjs";
|
|
10
|
+
import { desktop, DESKTOP_HELP } from "./desktop.mjs";
|
|
10
11
|
|
|
11
12
|
const HELP = `Nomac cloud Mac sessions
|
|
12
13
|
nomac start [--wait 300] [--max-duration 7200] [--idle-timeout 600] [--max-cost 5] [--json]
|
|
13
14
|
nomac sync . [--dry-run] upload changed files
|
|
14
15
|
nomac ssh [--forward 8080:3000] full Mac shell and port forwarding
|
|
15
16
|
nomac ssh -- command args run any command over SSH
|
|
17
|
+
nomac desktop [--no-open] [--json] the full macOS desktop in your browser
|
|
16
18
|
nomac exec --request-key KEY -- command args
|
|
17
19
|
nomac job JOB_ID reconcile a submitted command
|
|
18
20
|
nomac output JOB_ID [--cursor 0] [--stream stdout|stderr]
|
|
@@ -33,6 +35,7 @@ const options = {
|
|
|
33
35
|
"max-cost": { type: "string" }, "request-key": { type: "string" }, timeout: { type: "string" },
|
|
34
36
|
overwrite: { type: "boolean" }, "dry-run": { type: "boolean" }, from: { type: "string" },
|
|
35
37
|
forward: { type: "string", multiple: true }, cursor: { type: "string" }, stream: { type: "string" },
|
|
38
|
+
"no-open": { type: "boolean" },
|
|
36
39
|
};
|
|
37
40
|
function budget(value = "5") {
|
|
38
41
|
if (!/^(?:0|[1-9][0-9]{0,5})(?:\.[0-9]{1,6})?$/.test(value)) throw new CliError("--max-cost must be a positive USD amount, for example 5 or 2.50.");
|
|
@@ -69,7 +72,7 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
69
72
|
try { parsed = parseArgs({ args: argv, options, allowPositionals: true }); }
|
|
70
73
|
catch (error) { throw new CliError(error.message); }
|
|
71
74
|
const { values, positionals } = parsed;
|
|
72
|
-
if (values.help) return print(HELP);
|
|
75
|
+
if (values.help) return print(command === "desktop" ? DESKTOP_HELP : HELP);
|
|
73
76
|
const wait = ["start", "stop"].includes(command) ? integer(values.wait, command === "start" ? 300 : 60, 0, command === "start" ? 600 : 300) : 0;
|
|
74
77
|
if (command === "start" && values["request-key"] !== undefined && !validRequestKey(values["request-key"])) throw new CliError("Use a request key of 8–128 letters, digits, underscores, periods, colons or hyphens.");
|
|
75
78
|
const output = result => {
|
|
@@ -122,6 +125,10 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
122
125
|
}
|
|
123
126
|
return output(await hostedRequest(local.id, method, path, params, { request, transport }));
|
|
124
127
|
}
|
|
128
|
+
if (command === "desktop") {
|
|
129
|
+
if (positionals.length) throw new CliError("Usage: nomac desktop [--no-open] [--json]");
|
|
130
|
+
return desktop(await ready(localSession()), { values, print });
|
|
131
|
+
}
|
|
125
132
|
if (command === "ssh") {
|
|
126
133
|
if (values.json) throw new CliError("SSH streams bytes. Use session status --json for metadata.");
|
|
127
134
|
const local = await ready(localSession());
|
package/src/mac-tools.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { api } from "./api.mjs";
|
|
2
2
|
import { openUrl } from "./mac-credits.mjs";
|
|
3
|
+
import { desktopUrl } from "./desktop.mjs";
|
|
3
4
|
const text = value => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
4
5
|
const session = { type: "string", pattern: "^ses_[A-Za-z0-9_-]{1,80}$" };
|
|
5
6
|
const job = { type: "string", pattern: "^job_[a-f0-9]{40}$" };
|
|
@@ -16,6 +17,12 @@ export const MAC_TOOLS = [
|
|
|
16
17
|
{ name: "start_mac", description: "Allocate a full cloud Mac VM, billed from prepaid credit at $0.80 an hour by the minute. If it returns insufficient_credit, call get_mac_checkout, have the human pay, then retry with the same request_key. Supply a stable request_key; SSH public_key is optional for HTTPS-only agents; reuse both after a lost response. Inspect the returned offer and limits. Poll get_mac_session until ready. No project pipeline is imposed; stop_mac deletes the VM and files.", inputSchema: schema({ public_key: { type: "string" }, request_key: key, limits: { type: "object", properties: { max_duration_seconds: { type: "integer", minimum: 60, maximum: 28800 }, idle_timeout_seconds: { type: "integer", minimum: 60, maximum: 3600 }, spend_cap_microunits: { type: "string", pattern: "^[1-9][0-9]{0,17}$" } }, additionalProperties: false } }, ["request_key"]), handler: async ({ request_key, ...body }) => text(await api("POST", "/api/v1/mac/sessions", { body, idempotencyKey: request_key })) },
|
|
17
18
|
{ name: "list_mac_sessions", description: "Find existing Mac sessions, including after losing a response. Shows state, cost, limits and cleanup.", inputSchema: schema({}), handler: async () => text(await api("GET", "/api/v1/mac/sessions")) },
|
|
18
19
|
{ name: "get_mac_session", description: "Read live Mac allocation state, cost, funded time and cleanup confirmation. queued/provisioning means wait; stopping is not yet confirmed cleanup.", inputSchema: schema({ session_id: session }, ["session_id"]), handler: async args => text(await api("GET", base(args))) },
|
|
20
|
+
{ name: "open_mac_desktop", description: "Open the Mac's full macOS desktop in the human's browser on this machine, with mouse and keyboard and nothing to install. Use it when the human wants to see or click something on the Mac, for example Xcode, the Simulator or a sign-in window. The session must be ready. If opened_in_browser is false, give the human the returned url; it opens after they sign in to nomac.app. The session stays busy while the desktop is open and the page disconnects after 15 minutes without input. To show the iOS Simulator there, boot a device with simctl, then run open \"$(xcode-select -p)/Applications/Simulator.app\" with exec_mac. macOS has no RDP.", inputSchema: schema({ session_id: session, open: { type: "boolean", default: true, description: "Open the link in this machine's browser." } }, ["session_id"]), handler: async ({ session_id, open = true }) => {
|
|
21
|
+
const view = await api("GET", base({ session_id }));
|
|
22
|
+
if (view?.session?.state !== "ready") return text({ error: { code: "session_not_ready", message: "The Mac is not ready. Wait until get_mac_session shows ready, then call open_mac_desktop again." } });
|
|
23
|
+
const url = desktopUrl(session_id), opened = open ? openUrl(url) : false;
|
|
24
|
+
return text({ session_id, url, opened_in_browser: opened, message: opened ? "The desktop opened in the human's browser; they may need to sign in to nomac.app." : "Give the human this link; it opens after they sign in to nomac.app." });
|
|
25
|
+
} },
|
|
19
26
|
{ name: "stop_mac", description: "Stop billing by shutting down the VM and deleting its files. Retrieve files first. Poll get_mac_session until cleanup_confirmed; stopping only acknowledges the request.", inputSchema: schema({ session_id: session }, ["session_id"]), handler: async args => text(await api("DELETE", base(args))) },
|
|
20
27
|
{ name: "exec_mac", description: "Run any argv command in your cloud Mac workspace. Returns a durable job ID; poll get_mac_job and read_mac_output. Reuse request_key and identical arguments after lost responses; never submit a new key blindly. Use bash -lc explicitly for shell syntax. timeout_seconds defaults to 300 and caps at 600; raise it for slow work such as a first simulator boot, which needs about 90 seconds. A command (argv plus env) can be at most 64 KiB; to put larger files on the Mac, fetch them there (git clone, curl) or write them in parts of up to about 40 KiB (e.g. base64 -d >> file).", inputSchema: schema({ session_id: session, request_key: key, argv: { type: "array", items: { type: "string" }, minItems: 1 }, timeout_seconds: { type: "integer", minimum: 1, maximum: 600 } }, ["session_id", "request_key", "argv"]), handler: async ({ session_id, request_key, ...body }) => text(await api("POST", `${base({ session_id })}/jobs`, { body, idempotencyKey: request_key })) },
|
|
21
28
|
{ name: "get_mac_job", description: "Inspect the existing command without rerunning it. Retain its job ID until completion and retrieve output before stopping the VM.", inputSchema: schema({ session_id: session, job_id: job }, ["session_id", "job_id"]), handler: async args => text(await api("GET", `${base(args)}/jobs/${encodeURIComponent(args.job_id)}`)) },
|
package/src/mac-transport.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { getProxyForUrl } from "proxy-from-env";
|
|
|
5
5
|
import WebSocket, { createWebSocketStream } from "ws";
|
|
6
6
|
import { CliError } from "./api.mjs";
|
|
7
7
|
import { clientHeaders } from "./client-info.mjs";
|
|
8
|
+
// Bytes in flight per relay hop. 64 KiB capped a ~190 ms transatlantic path near 0.3 MB/s.
|
|
9
|
+
const RELAY_BUFFER = 1024 * 1024;
|
|
8
10
|
|
|
9
11
|
export function sessionId(value) {
|
|
10
12
|
const id = String(value ?? "").replace(/^nomac-/, "");
|
|
@@ -105,7 +107,7 @@ export function tunnel(value, { input = process.stdin, output = process.stdout,
|
|
|
105
107
|
handshakeTimeout: 15_000, perMessageDeflate: false, maxPayload: 1024 * 1024,
|
|
106
108
|
followRedirects: false,
|
|
107
109
|
});
|
|
108
|
-
const stream = createWebSocketStream(ws, { highWaterMark:
|
|
110
|
+
const stream = createWebSocketStream(ws, { highWaterMark: RELAY_BUFFER });
|
|
109
111
|
let failure, closedCode, settled = false;
|
|
110
112
|
const finish = () => {
|
|
111
113
|
if (settled) return;
|
package/src/mcp.mjs
CHANGED
|
@@ -40,11 +40,6 @@ export const TOOLS = [
|
|
|
40
40
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
41
41
|
handler: async () => text(await api("GET", "/api/v1/billing/status")),
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
name: "get_checkout_link", description: "Return an org-bound checkout link; the human chooses a plan and pays explicitly. This tool does not charge them. Reuses pending checkout; existing subscribers get billing management instead.",
|
|
45
|
-
inputSchema: { type: "object", properties: { plan: { type: "string", enum: ["starter", "pro"] } }, additionalProperties: false },
|
|
46
|
-
handler: async args => text(await api("POST", "/api/v1/billing/checkout", { body: args })),
|
|
47
|
-
},
|
|
48
43
|
{
|
|
49
44
|
name: "get_refund_status", description: "Read the progress of a refund the account owner requested at nomac.app/usage. Agents cannot request refunds; point the human to that page. Does not submit a refund or cancel anything.",
|
|
50
45
|
inputSchema: { type: "object", properties: { request_id: { type: "string" } }, required: ["request_id"], additionalProperties: false },
|
|
@@ -383,6 +378,20 @@ export const TOOLS = [
|
|
|
383
378
|
},
|
|
384
379
|
];
|
|
385
380
|
|
|
381
|
+
/** Shown to everyone: the Mac tools and support. The rest serve Builder subscribers. */
|
|
382
|
+
const PUBLIC_TOOLS = new Set([...MAC_TOOLS.map(t => t.name), "report_issue"]);
|
|
383
|
+
let subscriber;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The Builder (publishing) plans are closed to new subscriptions, so their tools
|
|
387
|
+
* are listed only for accounts that still have one. Hidden tools still answer
|
|
388
|
+
* when called, so existing subscribers' agents keep working.
|
|
389
|
+
*/
|
|
390
|
+
export async function listedTools(isSubscriber = () => (subscriber ??= api("GET", "/api/v1/billing/status").then(s => Boolean(s?.tier) && s.tier !== "none", () => false))) {
|
|
391
|
+
const legacy = await isSubscriber();
|
|
392
|
+
return TOOLS.filter(t => legacy || PUBLIC_TOOLS.has(t.name));
|
|
393
|
+
}
|
|
394
|
+
|
|
386
395
|
export async function runMcpServer() {
|
|
387
396
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
388
397
|
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
@@ -417,7 +426,7 @@ export async function runMcpServer() {
|
|
|
417
426
|
break;
|
|
418
427
|
case "tools/list":
|
|
419
428
|
reply({
|
|
420
|
-
tools:
|
|
429
|
+
tools: (await listedTools()).map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
421
430
|
});
|
|
422
431
|
break;
|
|
423
432
|
case "tools/call": {
|