@nomac/cli 0.13.0 → 0.13.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/package.json +1 -1
- package/src/mac-tools.mjs +1 -1
- package/src/mac-transport.mjs +13 -6
- package/src/mcp.mjs +1 -1
- package/src/session-ssh.mjs +2 -1
- package/src/ssh-config.mjs +1 -1
package/package.json
CHANGED
package/src/mac-tools.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export const MAC_TOOLS = [
|
|
|
26
26
|
{ 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")) },
|
|
27
27
|
{ name: "get_mac_session", description: "Read live Mac allocation state, cost, funded time and cleanup confirmation, and once ready the credentials installed on it (secrets, signing identities and profiles, by name and path, never values) and account: the Mac's user, whether its login password is set (in $NOMAC_MAC_PASSWORD on the Mac), the access choices, and how to type and click (mac_input). 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))) },
|
|
28
28
|
{ name: "extend_mac", description: "Keep a running Mac going longer: raises its time limit by minutes (default 60), up to 8 hours since it started, and its spend cap with it. Call it when an answer's stop_notice says the Mac stops soon at its time limit or spend cap, or before a build or test that would not finish in time. It adds no credit: when stop_notice's reason is credit, the human adds some (get_mac_checkout). The Mac still bills $0.80 an hour while it runs.", inputSchema: schema({ session_id: session, minutes: { type: "integer", minimum: 1, maximum: 480, default: 60, description: "Minutes to add to the time limit, 1-480. Default 60." } }, ["session_id"]), handler: async ({ session_id, minutes = 60 }) => text(await api("POST", `${base({ session_id })}/extend`, { body: { minutes } })) },
|
|
29
|
-
{ name: "get_mac_drive", description: "Read the account's persistent drive: a disk kept between sessions and mounted at ~/drive on every Mac (
|
|
29
|
+
{ name: "get_mac_drive", description: "Read the account's persistent drive: a disk kept between sessions and mounted at ~/drive on every Mac (20 GB). What is there stays for the next session: keep projects in ~/drive/src for incremental builds; package and build caches (DerivedData, Swift packages, CocoaPods, npm) are kept there already. NoMac saves it when a Mac stops, with nothing to call. Shows the space used, the newest saved version, a save still in progress, recent versions, and the date it is deleted after 90 days without a Mac.", inputSchema: schema({}), handler: async () => text(await api("GET", "/api/v1/mac/drive")) },
|
|
30
30
|
{ name: "reset_mac_drive", description: "Empty the account's persistent drive: the next Mac starts with a new, empty ~/drive, and everything on the old drive is deleted, with no way back. Stop any running session first (stop_mac). For a single Mac without the drive, start_mac with access drive none instead.", inputSchema: schema({}), handler: async () => text(await api("DELETE", "/api/v1/mac/drive")) },
|
|
31
31
|
{ name: "list_secrets", description: "List the account's secrets by name: variables (set in every shell and command on the Mac) and files (with their path on the Mac), whether every Mac receives them or only when asked, sizes, and secrets an agent asked for that the person has not entered yet. Values are never returned.", inputSchema: schema({}), handler: async () => text(await api("GET", "/api/v1/secrets")) },
|
|
32
32
|
{ name: "request_secret", description: "Get a link where the person enters a secret in their browser, so its value never passes through this chat: an API token, a .env value, a .p12 certificate or a config file. kind variable is one line of text, set as an environment variable on the Mac; kind file is stored at destination (under the home folder, like ~/.netrc; default ~/.nomac/secrets/<name>). Give the url to the person who holds the value; they need no NoMac account, and it works once, for 24 hours. Then list_secrets shows it, new Macs receive it, and install_secrets puts it on a running Mac. With a shell on this machine, the person can also run nomac secrets set NAME themselves. Never ask for a secret in the chat.", inputSchema: schema({ name: { type: "string", minLength: 1, maxLength: 128 }, kind: { type: "string", enum: ["variable", "file"] }, description: { type: "string", minLength: 1, maxLength: 500, description: "What the person should enter and why; shown to them" }, destination: { type: "string", maxLength: 256 }, delivery: { type: "string", enum: ["every_mac", "on_request"] } }, ["name", "kind", "description"]), handler: async body => text(await api("POST", "/api/v1/secrets/requests", { body })) },
|
package/src/mac-transport.mjs
CHANGED
|
@@ -28,19 +28,26 @@ export function transportOptions(options = {}) {
|
|
|
28
28
|
return { url, token, agent: proxy ? new HttpsProxyAgent(proxy) : undefined, ca: options.ca };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** How long a busy
|
|
32
|
-
export const GUEST_BUSY_RETRY = { budgetMs:
|
|
31
|
+
/** How long a busy or briefly unreachable Mac is retried before the error reaches the caller. */
|
|
32
|
+
export const GUEST_BUSY_RETRY = { budgetMs: 120_000, pauseMs: 3_000 };
|
|
33
|
+
/**
|
|
34
|
+
* Passing failures: a saturated guest (a first Simulator boot answers slowly for a minute or
|
|
35
|
+
* two), a Mac whose network dropped for a moment (most nights at its provider, October 2026),
|
|
36
|
+
* an answer lost on the way, or the same request still starting (copies of it that waited out
|
|
37
|
+
* an outage reach the Mac together).
|
|
38
|
+
*/
|
|
39
|
+
const PASSING = new Set(["guest_busy", "host_unavailable", "outcome_unknown", "operation_pending"]);
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
42
|
+
* Guest operations are idempotent by job ID, request key and blob offset (a job start always
|
|
43
|
+
* carries its request key), so passing failures are retried with the same request; every other
|
|
44
|
+
* failure is returned for reconciliation.
|
|
38
45
|
*/
|
|
39
46
|
export function macRequest(method, path, options = {}) {
|
|
40
47
|
const started = Date.now();
|
|
41
48
|
// The first attempt runs synchronously, so invalid input still throws immediately.
|
|
42
49
|
const attempt = () => macRequestOnce(method, path, options).catch(async error => {
|
|
43
|
-
if (error?.code
|
|
50
|
+
if (!PASSING.has(error?.code) || Date.now() - started + GUEST_BUSY_RETRY.pauseMs + 15_000 > GUEST_BUSY_RETRY.budgetMs) throw error;
|
|
44
51
|
await new Promise(resolve => setTimeout(resolve, GUEST_BUSY_RETRY.pauseMs));
|
|
45
52
|
return attempt();
|
|
46
53
|
});
|
package/src/mcp.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const INSTRUCTIONS = [
|
|
|
21
21
|
"NoMac gives you a cloud Mac with Xcode, billed from prepaid credit ($0.80 an hour, by the minute).",
|
|
22
22
|
"Use it when work needs macOS that this machine does not have (Windows, Linux or a cloud sandbox): building, running or testing an iOS, iPadOS, watchOS, visionOS or macOS app (Swift, SwiftUI, React Native, Expo, Flutter, Capacitor), the iOS Simulator, Xcode, code signing, TestFlight or App Store submission.",
|
|
23
23
|
"Typical flow: get_mac_credits, start_mac, poll get_mac_session until ready, put code there (exec_mac with git clone into ~/drive/src, write_mac_file, fetch_to_mac, create_mac_upload_link), run exec_mac, get results with read_mac_file or create_mac_download_link, then stop_mac.",
|
|
24
|
-
"The account's drive at ~/drive (
|
|
24
|
+
"The account's drive at ~/drive (20 GB) is on every Mac and kept between sessions; everything outside it is deleted at stop. Clone and build in ~/drive/src/<repo>, so the next session rebuilds only what changed.",
|
|
25
25
|
"When an answer says low_credit (under an hour of Mac time left), tell the human before a long build and offer its buy_url: a Mac stops when the credit runs out. For a refill, suggest 3 packs ($30, +5%).",
|
|
26
26
|
"One Mac runs per account at a time: start_mac while one runs answers session_limit_reached with its id, to use. For work that needs two at once, start_mac with parallel: true starts a second (accounts that bought credit, while NoMac has a Mac to spare); it runs without the drive.",
|
|
27
27
|
"exec_mac returns a quick command's output in the same call; pass wait_seconds: 50 for builds and tests. A command still running comes back as a job: poll get_mac_job with its job_id, then read_mac_output.",
|
package/src/session-ssh.mjs
CHANGED
|
@@ -53,7 +53,8 @@ export function sshArgs(session, { batch = true } = {}) {
|
|
|
53
53
|
...(session.mode === "hosted" ? ["-F", "/dev/null", "-o", `UserKnownHostsFile=${session.knownHosts}`, "-o", "GlobalKnownHostsFile=/dev/null",
|
|
54
54
|
"-o", `HostKeyAlias=nomac-${session.id}`, "-o", `ProxyCommand=${[process.execPath, cliEntry(), "tunnel", session.id].map(shellQuote).join(" ")}`] : []),
|
|
55
55
|
"-o", "StrictHostKeyChecking=yes", "-o", "ForwardAgent=no",
|
|
56
|
-
|
|
56
|
+
// Two minutes of silence: a network drop at the Macs' provider pauses the stream until it resumes.
|
|
57
|
+
"-o", "ConnectTimeout=15", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=8",
|
|
57
58
|
...multiplexArgs(session),
|
|
58
59
|
...(batch ? ["-o", "BatchMode=yes", "-o", "ClearAllForwardings=yes"] : []),
|
|
59
60
|
...(session.port ? ["-p", String(session.port)] : []),
|
package/src/ssh-config.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export function sshConfigBlock(local, alias, project, { platform = process.platf
|
|
|
52
52
|
` ProxyCommand ${quoted(node)} ${quoted(bin)} tunnel ${local.id}`,
|
|
53
53
|
" ForwardAgent no",
|
|
54
54
|
" ServerAliveInterval 15",
|
|
55
|
-
" ServerAliveCountMax
|
|
55
|
+
" ServerAliveCountMax 8",
|
|
56
56
|
`${END} ${alias}`,
|
|
57
57
|
].join("\n");
|
|
58
58
|
}
|