@nomac/cli 0.13.3 → 0.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @nomac/cli
2
+
3
+ A real cloud Mac for AI agents and for you: macOS on Apple silicon with Xcode and the iOS Simulator, administrator access and nothing blocked, billed from prepaid credit at $0.80 an hour by the minute. Build, run and test iOS and macOS apps from Linux, Windows or a cloud sandbox, then sign, send to TestFlight and publish.
4
+
5
+ ```sh
6
+ npm install -g @nomac/cli
7
+ nomac login # approve the code in your browser
8
+ nomac start # a Mac, usually ready within a minute
9
+ nomac sync . # upload this project (to ~/drive/src, kept between sessions)
10
+ nomac exec --wait 600 -- xcodebuild -scheme App -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
11
+ nomac pull ./build # download results
12
+ nomac stop # the Mac and its files are deleted, except ~/drive; billing ends
13
+ ```
14
+
15
+ ## For agents
16
+
17
+ `nomac mcp` is an MCP server over stdio for Claude Code, Codex, Cursor and others:
18
+
19
+ ```sh
20
+ claude mcp add nomac -- npx @nomac/cli mcp
21
+ codex mcp add nomac -- npx @nomac/cli mcp
22
+ ```
23
+
24
+ Hosted MCP needs no install: `https://mcp.nomac.app/mcp` (the Mac) and `https://mcp.nomac.app/appstore` (signing, TestFlight and App Store Connect), one sign-in for both.
25
+
26
+ ## What else
27
+
28
+ `nomac --help` lists every command: SSH (`nomac ssh`, `nomac ssh-config`), the desktop in your browser, screenshots and screen recordings, real keyboard and mouse input, secrets that reach every Mac, App Store signing (`nomac sign`), TestFlight (`nomac testflight`), App Review (`nomac publish`) and notarization (`nomac notarize`).
29
+
30
+ The full guide for agents is https://nomac.app/llms.txt; the CLI reference is https://nomac.app/docs/cli and the API https://nomac.app/docs/api. The CLI updates itself in the background (`NOMAC_NO_AUTO_UPDATE=1` turns that off).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomac/cli",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "A real cloud Mac for AI agents: Xcode, the iOS Simulator, signing, TestFlight and App Store publishing, by the minute, over MCP, SSH and HTTPS.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,7 +22,6 @@
22
22
  "https-proxy-agent": "^9.1.0",
23
23
  "ignore": "^7.0.0",
24
24
  "proxy-from-env": "^2.1.0",
25
- "tar": "^7.4.0",
26
25
  "ws": "^8.21.3"
27
26
  },
28
27
  "devDependencies": {
package/skill/SKILL.md CHANGED
@@ -160,13 +160,6 @@ A step only a person can take (creating the app in App Store Connect,
160
160
  agreements, an App Group) comes back as needs_human with its link: give it to
161
161
  the human, then call again.
162
162
 
163
- ## Builder (existing subscribers only)
164
-
165
- The separate iOS Builder publishing product is closed to new subscriptions.
166
- If `get_billing_status` shows a Builder plan, its tools are listed and
167
- https://nomac.app/llms-legacy.txt describes them. Otherwise use the cloud Mac
168
- above.
169
-
170
163
  ## When stuck
171
164
 
172
165
  `report_issue` with the failing `ref_id` files a support ticket with full
package/src/cli.mjs CHANGED
@@ -1,8 +1,5 @@
1
- import { basename } from "node:path";
2
1
  import { api, CliError } from "./api.mjs";
3
- import { loadProjectState, saveApiKey, saveProjectState } from "./config.mjs";
4
- import { packTarball } from "./pack.mjs";
5
- import { uploadSourceSnapshot } from "./upload.mjs";
2
+ import { saveApiKey } from "./config.mjs";
6
3
 
7
4
  const HELP = `nomac: a cloud Mac for you and your agents ($0.80 an hour, by the minute)
8
5
 
@@ -46,27 +43,12 @@ Agents:
46
43
  More: nomac start --help, nomac whoami, nomac --version, nomac update, nomac start --host user@mac (a Mac you own)
47
44
  Environment: NOMAC_API_KEY, NOMAC_API_URL, NOMAC_NO_AUTO_UPDATE=1 (the CLI updates itself otherwise)`;
48
45
 
49
- // The Builder commands act at once (a release build uses one of the plan's builds),
50
- // so they take only their own flags and print usage for --help instead of running.
51
- const LEGACY_HELP = {
52
- push: "Usage: nomac push\n Upload this folder as a project snapshot (respects .gitignore; secrets are excluded).",
53
- build: "Usage: nomac build [--smoke] [--testflight | --wait]\n Start a signed release build for TestFlight (uses one build from your plan).\n --smoke unsigned compile check instead\n --testflight wait until the build is on TestFlight (--wait for a smoke build)",
54
- status: "Usage: nomac status [build-id]\n Show a build's state as JSON (the last build in this folder by default).",
55
- logs: "Usage: nomac logs [build-id]\n Show a build's steps and failure excerpt (the last build in this folder by default).",
46
+ const COMMAND_HELP = {
56
47
  mcp: "Usage: nomac mcp\n Run the NoMac MCP server on stdin/stdout. Add it to your agent's MCP settings as the command `nomac mcp`.",
57
48
  };
58
49
  const isHelp = (args) => args.some((a) => a === "--help" || a === "-h" || a === "help");
59
- function onlyFlags(args, allowed, command) {
60
- const unknown = args.find((a) => !allowed.includes(a));
61
- if (unknown !== undefined) throw new CliError(`unknown option: ${unknown}`, `Unknown option "${unknown}" for nomac ${command}.\n\n${LEGACY_HELP[command]}`);
62
- }
63
- /** A build ID argument: bld_..., or none for the folder's last build. Never a flag. */
64
- function buildIdArg(args, command) {
65
- const [first, ...more] = args.filter((a) => a !== "--json");
66
- if (first === undefined) return undefined;
67
- if (!/^bld_[A-Za-z0-9]+$/.test(first) || more.length) throw new CliError(`not a build id: ${first}`, `"${[first, ...more].join(" ")}" is not a build ID (bld_...).\n\n${LEGACY_HELP[command]}`);
68
- return first;
69
- }
50
+ /** The Builder's commands, removed with it (October 2026). */
51
+ const RETIRED = "was removed with the Builder, retired in October 2026. Build on a NoMac Mac instead: nomac start, nomac sync, nomac exec -- xcodebuild ..., then nomac sign, nomac testflight App.ipa and nomac publish.";
70
52
 
71
53
  export async function main(argv) {
72
54
  const [cmd, ...rest] = argv;
@@ -99,12 +81,6 @@ export async function main(argv) {
99
81
  return hostedSessionCommand(cmd, rest, { store: sessionStore() });
100
82
  }
101
83
  case "status":
102
- // A legacy build ID, or a folder that only ever built with the old pipeline, keeps build status.
103
- if (/^bld_/.test(rest[0] ?? "") || (loadProjectState().last_build && !(await import("./session-store.mjs")).sessionStore().load())) {
104
- if (isHelp(rest)) return console.log(LEGACY_HELP.status);
105
- return status(buildIdArg(rest, "status"));
106
- }
107
- // falls through: everyone else sees their Mac
108
84
  case "start":
109
85
  case "attach":
110
86
  case "extend":
@@ -178,19 +154,12 @@ export async function main(argv) {
178
154
  return deviceLogin();
179
155
  }
180
156
  case "push":
181
- if (isHelp(rest)) return console.log(LEGACY_HELP.push);
182
- onlyFlags(rest, [], "push");
183
- return push();
184
157
  case "build":
185
- if (isHelp(rest)) return console.log(LEGACY_HELP.build);
186
- onlyFlags(rest, ["--smoke", "--testflight", "--wait"], "build");
187
- return build(rest);
188
158
  case "logs":
189
- if (isHelp(rest)) return console.log(LEGACY_HELP.logs);
190
- return logs(buildIdArg(rest, "logs"));
159
+ throw new CliError("retired", `nomac ${cmd} ${RETIRED}`);
191
160
  case "mcp": {
192
161
  // Agents start this as a stdio server; --help must not start it and wait on stdin.
193
- if (isHelp(rest)) return console.log(LEGACY_HELP.mcp);
162
+ if (isHelp(rest)) return console.log(COMMAND_HELP.mcp);
194
163
  const { runMcpServer } = await import("./mcp.mjs");
195
164
  return runMcpServer();
196
165
  }
@@ -252,129 +221,3 @@ export function describeApple(view) {
252
221
  return lines.join("\n");
253
222
  }
254
223
 
255
- async function ensureProject() {
256
- const state = loadProjectState();
257
- if (state.project_id) return state;
258
- const name = basename(process.cwd());
259
- const created = await api("POST", "/api/v1/projects", { body: { name } });
260
- const next = { project_id: created.id, name };
261
- saveProjectState(next);
262
- console.log(`created project ${created.id} ("${name}")`);
263
- return next;
264
- }
265
-
266
- async function push() {
267
- const state = await ensureProject();
268
- console.log("packing working tree (respecting .gitignore, secrets excluded)…");
269
- const { tarball, files } = await packTarball(process.cwd());
270
- console.log(`${files.length} files, ${(tarball.length / 1024).toFixed(0)} KB compressed`);
271
-
272
- const res = await uploadSourceSnapshot(state.project_id, tarball);
273
- console.log(`✅ snapshot ${res.snapshot_id} (commit ${res.commit.slice(0, 8)})`);
274
- const d = res.detected ?? {};
275
- const framework = d.project_kind
276
- ? ` · ${d.project_kind}${d.flutter_sdk_version ? ` ${d.flutter_sdk_version}` : ""}`
277
- : "";
278
- console.log(
279
- ` detected: ${d.xcodeproj ?? "?"} · scheme ${d.scheme ?? "?"} · ${d.bundle_id ?? "no bundle id"}${d.marketing_version ? ` · v${d.marketing_version}` : ""}${framework}`,
280
- );
281
- if (res.lint) {
282
- const icon = res.lint.grade === "green" ? "🟢" : res.lint.grade === "yellow" ? "🟡" : "🔴";
283
- const n = res.lint.findings ?? 0;
284
- console.log(
285
- ` review lint: ${icon} ${res.lint.grade}${n ? ` · ${n} finding${n === 1 ? "" : "s"}` : ""}`,
286
- );
287
- if (res.lint.grade === "red") {
288
- console.log(" ⚠ red findings will block publish: fix them before `nomac build`/publish.");
289
- }
290
- }
291
- if (res.warning) console.log(`⚠ ${res.warning}`);
292
- saveProjectState({ ...state, last_snapshot: res.snapshot_id });
293
- }
294
-
295
- async function build(rest) {
296
- const state = loadProjectState();
297
- if (!state.project_id) {
298
- throw new CliError("no project", "No project here. Run `nomac push` first.");
299
- }
300
- const smoke = rest.includes("--smoke");
301
- const wait = rest.includes("--testflight") || rest.includes("--wait");
302
- const res = await api("POST", "/api/v1/builds", {
303
- body: { project_id: state.project_id, workflow: smoke ? "smoke" : "release" },
304
- idempotencyKey: `cli-${Date.now()}-${Math.random().toString(36).slice(2)}`,
305
- });
306
- console.log(`✅ build ${res.id} started (${res.workflow}, #${res.build_number})`);
307
- saveProjectState({ ...state, last_build: res.id });
308
- if (!wait) {
309
- console.log(` follow with: nomac status ${res.id}`);
310
- return;
311
- }
312
- await waitForBuild(res.id);
313
- }
314
-
315
- const SPINNER_STATES = {
316
- queued: "queued",
317
- mirrored: "mirrored",
318
- dispatched: "runner starting",
319
- building: "building on macOS",
320
- uploading: "uploading to Apple",
321
- processing: "Apple is processing",
322
- };
323
-
324
- async function waitForBuild(id) {
325
- const started = Date.now();
326
- let last = "";
327
- let lastNotice = "";
328
- for (;;) {
329
- const b = await api("GET", `/api/v1/builds/${id}`);
330
- if (b.state !== last) {
331
- const t = Math.round((Date.now() - started) / 1000);
332
- console.log(` [${t}s] ${SPINNER_STATES[b.state] ?? b.state}`);
333
- last = b.state;
334
- }
335
- const notice = b.error?.stage === "testflight" ? b.error.raw : "";
336
- if (notice && notice !== lastNotice) {
337
- console.log(` ${notice}`);
338
- if (b.error.url) console.log(` ${b.error.url}`);
339
- }
340
- lastNotice = notice;
341
- if (b.state === "ready") {
342
- console.log(
343
- b.workflow === "smoke"
344
- ? "\n🎉 smoke build passed: the project compiles clean on macOS."
345
- : "\n🎉 build ready: open TestFlight on your phone, the new build is live.",
346
- );
347
- return;
348
- }
349
- if (b.state === "failed") {
350
- const err = b.error ?? {};
351
- console.log(`\n❌ build failed (${err.code ?? "unknown"})`);
352
- if (err.raw) console.log(` ${err.raw}`);
353
- console.log(` details: nomac logs ${id}`);
354
- const e = new CliError("build failed", "");
355
- e.friendly = "";
356
- throw e;
357
- }
358
- await new Promise((r) => setTimeout(r, 10_000));
359
- }
360
- }
361
-
362
- async function status(buildId) {
363
- const state = loadProjectState();
364
- const id = buildId ?? state.last_build;
365
- if (!id) throw new CliError("no build", "No build id. Usage: nomac status <build-id>");
366
- const b = await api("GET", `/api/v1/builds/${id}`);
367
- console.log(JSON.stringify(b, null, 2));
368
- }
369
-
370
- async function logs(buildId) {
371
- const state = loadProjectState();
372
- const id = buildId ?? state.last_build;
373
- if (!id) throw new CliError("no build", "No build id. Usage: nomac logs <build-id>");
374
- const res = await api("GET", `/api/v1/builds/${id}/logs`);
375
- for (const step of res.steps ?? []) {
376
- console.log(`\n═══ ${step.name} [${step.status ?? "?"}] ═══`);
377
- if (step.log) console.log(step.log);
378
- }
379
- if (res.excerpt) console.log(`\n═══ failure excerpt ═══\n${res.excerpt}`);
380
- }
package/src/mcp.mjs CHANGED
@@ -9,9 +9,7 @@ import { createInterface } from "node:readline";
9
9
  import { api } from "./api.mjs";
10
10
  import { CLI_VERSION, mcpClientName, mcpContext, toolScope } from "./client-info.mjs";
11
11
  import { argumentProblem } from "./mcp-args.mjs";
12
- import { loadProjectState, saveProjectState } from "./config.mjs";
13
- import { packTarball } from "./pack.mjs";
14
- import { uploadSourceSnapshot } from "./upload.mjs";
12
+ import { loadProjectState } from "./config.mjs";
15
13
  import { uploadStoreAsset } from "./store-upload.mjs";
16
14
  import { MAC_TOOLS } from "./mac-tools.mjs";
17
15
 
@@ -25,7 +23,7 @@ const INSTRUCTIONS = [
25
23
  "For the iOS Simulator, simulator-use is on the Mac (run it with exec_mac): simulator-use look reads the screen as text, simulator-use act 'tap \"Sign In\"' taps and types by label, and simulator-use run builds, installs and launches the app; ~/NOMAC.md has the rest.",
26
24
  "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%).",
27
25
  "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.",
28
- "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.",
26
+ "exec_mac returns a quick command's output in the same call; pass wait_seconds: 40 for builds and tests (a call answers within 45 s). A command still running comes back as a job: poll get_mac_job with its job_id, then read_mac_output.",
29
27
  "Never rerun a command to get its output; after a lost response, repeat the call with the same request_key and arguments.",
30
28
  "Write files with write_mac_file, not heredocs in exec_mac: a command is at most 64 KiB.",
31
29
  "Shipping without the Apple key on the Mac: set_up_signing on the project returns the files and commands that sign a build with the agent's own tools; send_to_testflight sends the signed .ipa to TestFlight; publish submits it for App Review; notarize notarizes and staples a Developer ID-signed Mac app. Never ask for a secret in the chat: request_secret gives the human a link, and every Mac receives the account's secrets.",
@@ -44,26 +42,11 @@ async function resolveProjectId(args) {
44
42
  if (args?.project_id) return args.project_id;
45
43
  const state = loadProjectState(args?.directory ?? process.cwd());
46
44
  if (state.project_id) return state.project_id;
47
- throw new Error("no project here: run push_project first (or pass project_id)");
45
+ throw new Error("no project here: pass project_id (send_to_testflight answers with it, and connect_status lists the account's projects)");
48
46
  }
49
47
 
50
48
  export const TOOLS = [
51
49
  ...MAC_TOOLS,
52
- {
53
- name: "check_project", description: "Free source-readiness check of the latest uploaded snapshot. No Apple credentials or paid compute. Run push_project first or after code changes; this does not prove compilation or TestFlight delivery.",
54
- inputSchema: { type: "object", properties: { project_id: { type: "string" } }, additionalProperties: false },
55
- handler: async args => text(await api("POST", "/api/v1/check-project", { body: { project_id: await resolveProjectId(args) } })),
56
- },
57
- {
58
- name: "get_billing_status", description: "Read confirmed payment, contracted prices, quota/reset, renewal and first-purchase refund eligibility. After payment, resume a blocked build once with a fresh request_key only if the earlier response said build_started:false. Reconcile unknown outcomes first.",
59
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
60
- handler: async () => text(await api("GET", "/api/v1/billing/status")),
61
- },
62
- {
63
- 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.",
64
- inputSchema: { type: "object", properties: { request_id: { type: "string" } }, required: ["request_id"], additionalProperties: false },
65
- handler: async args => text(await api("GET", `/api/v1/billing/refund?request_id=${encodeURIComponent(args.request_id)}`)),
66
- },
67
50
  {
68
51
  name: "upload_store_asset",
69
52
  description: "Upload a new Apple screenshot, preview, purchase image, supplemental reviewer attachment or encryption document from file_path (up to 500 MiB, subject to Apple limits) or data_base64 (2 MiB). Discover the createInstance operation/body and parent scope with get_store_capabilities/read_store. Preview by default; confirm:true reserves, uploads and commits. Preserve request_key and identical bytes/body to resume. ready:true requires Apple processing COMPLETE. Message attachments require App Store Connect.",
@@ -117,97 +100,6 @@ export const TOOLS = [
117
100
  },
118
101
  handler: async args => text(await api("PATCH", `/api/v1/projects/${await resolveProjectId(args)}`, { body: { connection_id: args.connection_id } })),
119
102
  },
120
- {
121
- name: "push_project",
122
- description:
123
- "Pack the project directory (respects .gitignore; secrets never leave the machine) and upload a snapshot. Auto-detects the Xcode project, scheme and bundle id, runs source-level review lint, and links the App Store Connect app record. Run after every code change, before build.",
124
- inputSchema: {
125
- type: "object",
126
- properties: {
127
- directory: { type: "string", description: "project directory (default: cwd)" },
128
- },
129
- additionalProperties: false,
130
- },
131
- handler: async (args) => {
132
- const dir = args?.directory ?? process.cwd();
133
- let state = loadProjectState(dir);
134
- if (!state.project_id) {
135
- const name = dir.split("/").filter(Boolean).pop() ?? "app";
136
- const created = await api("POST", "/api/v1/projects", { body: { name } });
137
- state = { project_id: created.id, name };
138
- saveProjectState(state, dir);
139
- }
140
- const { tarball, files } = await packTarball(dir);
141
- const res = await uploadSourceSnapshot(state.project_id, tarball);
142
- saveProjectState({ ...state, last_snapshot: res.snapshot_id }, dir);
143
- return text({ project_id: state.project_id, files_packed: files.length, ...res });
144
- },
145
- },
146
- {
147
- name: "build",
148
- description:
149
- "Start a build. workflow=release (signed, uploads to TestFlight; costs one build from the plan quota) or smoke (unsigned compile check, fastest way to verify the project builds on macOS). Returns a build_id; poll it with status.",
150
- inputSchema: {
151
- type: "object",
152
- properties: {
153
- project_id: { type: "string" },
154
- workflow: { type: "string", enum: ["release", "smoke"], default: "release" },
155
- request_key: { type: "string", description: "Reuse this key to reconcile an unknown response. Use a fresh key after a confirmed build_started:false and payment/setup completion." },
156
- },
157
- additionalProperties: false,
158
- },
159
- handler: async (args) => {
160
- const project_id = await resolveProjectId(args);
161
- const res = await api("POST", "/api/v1/builds", {
162
- body: { project_id, workflow: args?.workflow ?? "release" },
163
- idempotencyKey: args?.request_key ?? `mcp-${randomUUID()}`,
164
- });
165
- let warning;
166
- try {
167
- const state = loadProjectState();
168
- if (state?.project_id === project_id) {
169
- saveProjectState({ ...state, last_build: res.id });
170
- } else {
171
- warning = "Build started for a project outside this directory's saved project. Pass build_id to status.";
172
- }
173
- } catch {
174
- // The paid attempt already exists. Return its ID even when the local
175
- // directory is read-only, so the agent can poll without starting again.
176
- warning = "Build started, but its ID could not be saved locally. Pass build_id to status.";
177
- }
178
- return text({
179
- ...res,
180
- ...(warning ? { warning } : {}),
181
- note: "poll `status` every ~30s; a release build reaching `ready` means the phone can install it from TestFlight",
182
- });
183
- },
184
- },
185
- {
186
- name: "status",
187
- description: "Build status (state machine position). States: queued→mirrored→dispatched→building→uploading→processing→ready|failed.",
188
- inputSchema: {
189
- type: "object",
190
- properties: { build_id: { type: "string", description: "defaults to the last build started here" } },
191
- additionalProperties: false,
192
- },
193
- handler: async (args) => {
194
- const id = args?.build_id ?? loadProjectState().last_build;
195
- if (!id) return errText("no build_id given and no build started from this directory");
196
- return text(await api("GET", `/api/v1/builds/${id}`));
197
- },
198
- },
199
- {
200
- name: "get_failure",
201
- description:
202
- "Structured failure detail for a failed build: {stage, code, kind, error_lines, hints, raw_excerpt}. kind=project → you fix the source and re-push; kind=account → relay the deep link to your human.",
203
- inputSchema: {
204
- type: "object",
205
- properties: { build_id: { type: "string" } },
206
- required: ["build_id"],
207
- additionalProperties: false,
208
- },
209
- handler: async (args) => text(await api("GET", `/api/v1/builds/${args.build_id}/failure`)),
210
- },
211
103
  {
212
104
  name: "review_lint",
213
105
  description:
@@ -403,18 +295,15 @@ export const TOOLS = [
403
295
  },
404
296
  ];
405
297
 
406
- /** The Builder's own tools: its plans are closed, so they are listed only for accounts that still have one. */
407
- const BUILDER_TOOLS = new Set(["check_project", "get_billing_status", "get_refund_status", "push_project", "build", "status", "get_failure"]);
408
- let subscriber;
298
+ /** The Builder's tools, removed with it (October 2026): a call to one says what does its job now. */
299
+ export const RETIRED_TOOLS = Object.fromEntries(["check_project", "push_project", "build", "status", "get_failure", "get_billing_status", "get_refund_status"].map(name => [name,
300
+ name === "get_billing_status" ? "get_billing_status was removed with the Builder, retired in October 2026: get_mac_credits shows the account's Mac credit."
301
+ : name === "get_refund_status" ? "get_refund_status was removed: refunds are the account owner's to ask, at support@nomac.app."
302
+ : `${name} was removed with the Builder, retired in October 2026: build and ship on a NoMac Mac instead. start_mac, build with exec_mac (xcodebuild), then set_up_signing, send_to_testflight and publish.`]));
409
303
 
410
- /**
411
- * Everyone gets the Mac, App Store and support tools (publish follows send_to_testflight for
412
- * every account); the Builder's own tools are listed only for its remaining subscribers.
413
- * Hidden tools still answer when called, so existing subscribers' agents keep working.
414
- */
415
- export async function listedTools(isSubscriber = () => (subscriber ??= api("GET", "/api/v1/billing/status").then(s => Boolean(s?.tier) && s.tier !== "none", () => false))) {
416
- const legacy = await isSubscriber();
417
- return TOOLS.filter(t => legacy || !BUILDER_TOOLS.has(t.name));
304
+ /** Every tool is listed for every account. */
305
+ export async function listedTools() {
306
+ return TOOLS;
418
307
  }
419
308
 
420
309
  export async function runMcpServer() {
@@ -457,6 +346,10 @@ export async function runMcpServer() {
457
346
  break;
458
347
  case "tools/call": {
459
348
  const tool = TOOLS.find((t) => t.name === params?.name);
349
+ if (!tool && Object.hasOwn(RETIRED_TOOLS, params?.name ?? "")) {
350
+ reply({ content: [{ type: "text", text: JSON.stringify({ error: { code: "tool_moved", message: RETIRED_TOOLS[params.name] } }, null, 2) }], isError: true });
351
+ break;
352
+ }
460
353
  if (!tool) return fail(-32602, `unknown tool: ${params?.name}`);
461
354
  const problem = argumentProblem(tool.inputSchema, params?.arguments ?? {});
462
355
  if (problem) {
package/src/pack.mjs DELETED
@@ -1,77 +0,0 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { join, relative, sep } from "node:path";
3
- import ignoreFactory from "ignore";
4
- import * as tar from "tar";
5
-
6
- // Pack the working tree: respect .gitignore, always exclude the deny-list
7
- // (secrets never leave the machine), gzip in memory.
8
-
9
- const ALWAYS_IGNORE = [
10
- ".git/",
11
- "node_modules/",
12
- ".nomac.json",
13
- "*.p8",
14
- "*.p12",
15
- "*.mobileprovision",
16
- "*.pem",
17
- ".env",
18
- ".env.*",
19
- ".DS_Store",
20
- "build/",
21
- "DerivedData/",
22
- "*.ipa",
23
- "*.xcarchive",
24
- ];
25
-
26
- export function collectFiles(dir) {
27
- const denied = ignoreFactory().add(ALWAYS_IGNORE);
28
- const relativePath = (base, path) => relative(base, path).split(sep).join("/");
29
- const files = [];
30
- const walk = (current, inherited = []) => {
31
- let scopes = inherited;
32
- try {
33
- const rules = readFileSync(join(current, ".gitignore"), "utf8");
34
- scopes = [...inherited, { base: current, ignore: ignoreFactory().add(rules) }];
35
- } catch (error) {
36
- if (error.code !== "ENOENT") throw error;
37
- }
38
- for (const entry of readdirSync(current, { withFileTypes: true })) {
39
- const full = join(current, entry.name);
40
- const rel = relativePath(dir, full);
41
- const relForIgnore = entry.isDirectory() ? `${rel}/` : rel;
42
- if (denied.ignores(relForIgnore)) continue;
43
- let ignored = false;
44
- for (const scope of scopes) {
45
- const scoped = relativePath(scope.base, full) + (entry.isDirectory() ? "/" : "");
46
- const result = scope.ignore.test(scoped);
47
- if (result.ignored) ignored = true;
48
- else if (result.unignored) ignored = false;
49
- }
50
- if (ignored) continue;
51
- if (entry.isDirectory()) walk(full, scopes);
52
- else if (entry.isFile()) files.push(rel);
53
- }
54
- };
55
- walk(dir);
56
- return files.sort();
57
- }
58
-
59
- export async function packTarball(dir) {
60
- const files = collectFiles(dir);
61
- if (files.length === 0) {
62
- throw new Error("nothing to pack (is this the right directory?)");
63
- }
64
- const chunks = [];
65
- await new Promise((resolve, reject) => {
66
- tar
67
- .create({ gzip: true, cwd: dir, portable: true }, files)
68
- .on("data", (c) => chunks.push(c))
69
- .on("end", resolve)
70
- .on("error", reject);
71
- });
72
- return { tarball: Buffer.concat(chunks), files };
73
- }
74
-
75
- export function totalSize(dir, files) {
76
- return files.reduce((n, f) => n + statSync(join(dir, f)).size, 0);
77
- }
package/src/upload.mjs DELETED
@@ -1,36 +0,0 @@
1
- import { api, CliError } from "./api.mjs";
2
-
3
- /** Shared by the CLI and stdio MCP so both bypass the API's 4 MiB body limit. */
4
- export async function uploadSourceSnapshot(projectId, tarball) {
5
- const path = `/api/v1/projects/${projectId}/snapshots`;
6
- let grant;
7
- try {
8
- grant = await api("POST", `${path}/uploads`);
9
- } catch (error) {
10
- // Older servers have no grant route. Outages and authorization failures
11
- // should remain actionable, not silently retry through another transport.
12
- if (!(error instanceof CliError) || error.status !== 404 || (error.code && error.code !== "bad_response")) throw error;
13
- if (tarball.length > 4 * 1024 * 1024) {
14
- throw new CliError("direct uploads unavailable", "This server does not support direct source uploads. Update the server or reduce the compressed archive to 4 MiB.");
15
- }
16
- return api("POST", path, { raw: tarball });
17
- }
18
- if (typeof grant.upload_url !== "string" || typeof grant.upload_key !== "string" ||
19
- !Number.isSafeInteger(grant.max_bytes) || grant.max_bytes <= 0) {
20
- throw new CliError("invalid source upload grant");
21
- }
22
- if (tarball.length > grant.max_bytes) {
23
- throw new CliError(
24
- "tarball too large",
25
- `Packed source is ${(tarball.length / 1048576).toFixed(1)} MiB; the limit is ${(grant.max_bytes / 1048576).toFixed(0)} MiB. Check .gitignore: build artifacts are recreated on the build VM.`,
26
- );
27
- }
28
- const uploaded = await fetch(grant.upload_url, {
29
- method: "PUT",
30
- headers: { "content-type": "application/gzip", "content-length": String(tarball.length) },
31
- body: tarball,
32
- });
33
- await uploaded.body?.cancel();
34
- if (!uploaded.ok) throw new CliError(`upload failed (HTTP ${uploaded.status})`);
35
- return api("POST", path, { body: { upload_key: grant.upload_key } });
36
- }