speculos-toolkit 1.1.0 → 1.1.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/README.md CHANGED
@@ -17,9 +17,21 @@ host never runs your build.
17
17
 
18
18
  > **Frontend hosting is free; every Speculos account includes one backend app free.** Sign in
19
19
  > once with `speculos-toolkit login` and a detected backend deploys too (sign up at
20
- > https://deploy.speculos.ai). Without an account the backend is skipped and the frontend still
20
+ > https://unified.speculos.ai). Without an account the backend is skipped and the frontend still
21
21
  > ships free.
22
22
 
23
+ ## Who can see your app
24
+
25
+ | | |
26
+ |---|---|
27
+ | **No account** | the app is **public** — anyone with the link. A private app checks who the visitor is when they open it, and there is no account to check against yet. |
28
+ | **Signed in** | a new app is **private** by default. `--public` shares it, `--org` opens it to your org. |
29
+ | **Already deployed anonymously?** | `speculos-toolkit login` links this machine and your existing apps come with it — then redeploy with `--private`, or flip it on the [dashboard](https://unified.speculos.ai/?tab=deploys). |
30
+
31
+ `--private` or `--org` without an account fails the deploy (`LOGIN_REQUIRED`)
32
+ rather than publishing something public that you asked to keep private.
33
+ A redeploy never changes the visibility of an app that is already live.
34
+
23
35
  ## What it prints
24
36
 
25
37
  The last line of stdout is a single JSON object:
@@ -67,6 +79,6 @@ speculos-toolkit teardown --slug <s> remove a deployment
67
79
  --env KEY=VAL --env-file <file> --api <url> --timeout <sec> --json
68
80
  ```
69
81
 
70
- Docs: https://deploy.speculos.ai · Source: https://github.com/speculosai/agent_deploy_infra
82
+ Docs: https://unified.speculos.ai · Source: https://github.com/speculosai/unified_platform
71
83
 
72
84
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "speculos-toolkit",
3
- "version": "1.1.0",
4
- "description": "The Speculos toolkit for coding agents deploy any frontend/backend to a live URL and build against your linked data connectors (BigQuery, Postgres, Snowflake, Salesforce, ). Built for Claude Code, Codex, Cursor, and friends.",
3
+ "version": "1.1.2",
4
+ "description": "The Speculos toolkit for coding agents \u2014 deploy any frontend/backend to a live URL and build against your linked data connectors (BigQuery, Postgres, Snowflake, Salesforce, \u2026). Built for Claude Code, Codex, Cursor, and friends.",
5
5
  "bin": {
6
6
  "speculos-toolkit": "bin/speculos-toolkit.js"
7
7
  },
@@ -28,9 +28,10 @@
28
28
  "speculos"
29
29
  ],
30
30
  "license": "MIT",
31
- "homepage": "https://deploy.speculos.ai",
31
+ "homepage": "https://unified.speculos.ai",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "git+https://github.com/speculosai/agent_deploy_infra.git"
34
+ "url": "git+https://github.com/speculosai/unified_platform.git",
35
+ "directory": "deploy_plane/packages/cli"
35
36
  }
36
37
  }
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: speculos-toolkit
3
- description: Deploy the current project to a live public URL with Speculos, and build against the user's linked data sources (connectors). Builds the frontend locally, hosts it, wires its API calls to the deployed backend, and reports the URLs. Use when the user says "deploy", "ship it", "publish", "put it live", "get me a URL", "deploy to speculos", "deploy the frontend/backend", or wants an app built on their connected data (BigQuery, Postgres, Snowflake, Salesforce, ...). Handles plain static sites and Vite / Next / CRA / Angular / Svelte frontends; deploys Node/Python/Bun backends once the user has signed in (`speculos-toolkit login`) — one backend app is included with every Speculos account (frontend-only needs no account).
3
+ description: Deploy the current project to a live URL with Speculos (public by default with no account; private once signed in), and build against the user's linked data sources (connectors). Builds the frontend locally, hosts it, wires its API calls to the deployed backend, and reports the URLs. Use when the user says "deploy", "ship it", "publish", "put it live", "get me a URL", "deploy to speculos", "deploy the frontend/backend", or wants an app built on their connected data (BigQuery, Postgres, Snowflake, Salesforce, ...). Handles plain static sites and Vite / Next / CRA / Angular / Svelte frontends; deploys Node/Python/Bun backends once the user has signed in (`speculos-toolkit login`) — one backend app is included with every Speculos account (frontend-only needs no account).
4
4
  ---
5
5
 
6
6
  # Speculos Toolkit
@@ -29,7 +29,7 @@ with `--frontend`/`--backend` in step 4 (or force a build dir to serve as-is wit
29
29
  ### If the project has a backend, ASK the user how to deploy (before you build)
30
30
 
31
31
  Frontends are **free** and need no account; **every Speculos account includes one backend
32
- app free** (sign in at https://deploy.speculos.ai). So when `detect` finds a backend, DON'T silently
32
+ app free** (sign in at https://unified.speculos.ai). So when `detect` finds a backend, DON'T silently
33
33
  skip it — ask the user with your question UI (e.g. AskUserQuestion). Tailor the “what won't
34
34
  work” line to THIS app (a counter button, a form, login, saved data…). For example:
35
35
 
@@ -146,9 +146,12 @@ npx -y speculos-toolkit@latest connectors list --json
146
146
  The last stdout line is `{ ok, brokerUrl, connectors: [{ alias, name, kind, accountIdentifier, tools: [...] }] }`.
147
147
 
148
148
  - `ok:false` + `code:"NO_TOKEN"` → the device isn't logged in; run `login` (step 4) first.
149
+ - `ok:false` + `code:"LOGIN_REQUIRED"` on a deploy → `--private`/`--org` was asked for on a
150
+ machine with no account. Nothing was published. Either run `login` and deploy again, or
151
+ drop the flag (the app ships public) and lock it down after linking.
149
152
  - `ok:true` with empty `connectors` → nothing linked (or nothing granted to this user).
150
153
  If the app clearly wants external data, tell the user to link a source (or ask their org
151
- admin for access) at **https://deploy.speculos.ai/dashboard**, then **re-run the list** —
154
+ admin for access) at **https://unified.speculos.ai/?tab=deploys**, then **re-run the list** —
152
155
  access is resolved server-side on every call, so a source linked or granted seconds ago
153
156
  shows up immediately with no re-login and no session restart. Never block a deploy on this.
154
157
  - A `403 NO_ACCESS` on execute means an org admin hasn't granted that source to this user —
@@ -289,6 +292,23 @@ Builds run locally (this machine already has the toolchain); only static output
289
292
  - Useful flags: `--slug <name>`, `--env KEY=VAL` (repeatable, backend env), `--build`
290
293
  (force the frontend through its build step), `--env-file <file>`.
291
294
 
295
+ ### Who can see the deployed app
296
+
297
+ - **No account -> the app is PUBLIC**, anyone with the link. That is the only
298
+ thing it can be: a private app checks who the visitor is when they open it,
299
+ and there is no account to check against yet. Frontend-only deploys need no
300
+ account and this is the normal path.
301
+ - **Once the machine is linked** (`speculos-toolkit login`), a NEW app is
302
+ **private by default**. Pass `--public` to share it, `--org` for the whole org.
303
+ - `--private` / `--org` from an UNLINKED machine **fails the deploy** with
304
+ `code:"LOGIN_REQUIRED"` before anything is published. It does not quietly
305
+ deploy something public that the user asked to be private. Recovery: run
306
+ `speculos-toolkit login`, then deploy again.
307
+ - Already deployed anonymously and now want it private? `speculos-toolkit login`
308
+ links this machine and its existing apps come with it — then redeploy with
309
+ `--private`, or flip it at https://unified.speculos.ai/?tab=deploys.
310
+ - A redeploy never changes the visibility of an app that is already live.
311
+
292
312
  The **last line of stdout is one JSON object**:
293
313
  ```json
294
314
  { "ok": true, "userId": "...", "urls": { "frontend": "https://user-deployed.speculos.ai/...", "backend": "https://...daytonaproxy01.net" } }
@@ -300,16 +320,16 @@ On `ok:false`, read `error`/`logTail`, fix the cause **once**, and re-run. Do no
300
320
  > limit (one is included; more come with Team plans). In both cases the deploy still **ships
301
321
  > the frontend** and returns `ok:true` with a `backendNote` — so the user already has a live
302
322
  > URL. To ship the backend: for `BACKEND_DISABLED`, sign in; for `TOO_MANY`, take an app
303
- > offline at https://deploy.speculos.ai/dashboard, redeploy an existing backend app, or talk
323
+ > offline at https://unified.speculos.ai/?tab=deploys, redeploy an existing backend app, or talk
304
324
  > to our team at https://calendar.app.google/VMGTvK3FmyDMAsix6 about more capacity. Always write the URL as
305
- > **https://deploy.speculos.ai** — never `speculos.ai`. Don't retry the backend; once signed
325
+ > **https://unified.speculos.ai** — never `speculos.ai`. Don't retry the backend; once signed
306
326
  > in / under the limit, re-running `deploy` ships it. Redeploying an app that already has a
307
327
  > backend never counts against the limit (it reuses its sandbox).
308
328
 
309
329
  ## 5. Report + verify
310
330
 
311
331
  - Give the user `urls.frontend` (and `urls.backend` if deployed). They can manage their
312
- published apps and take them offline anytime at https://deploy.speculos.ai/dashboard. The public URL is
332
+ published apps and take them offline anytime at https://unified.speculos.ai/?tab=deploys. The public URL is
313
333
  `user-deployed.speculos.ai/<username>/<app-slug>/`, and BOTH segments are renameable there:
314
334
  "Edit link" changes an app's slug (the second segment), and the account's "your link name"
315
335
  changes the first segment (default a random id) for every app on that device. Accounts on
@@ -329,10 +349,10 @@ On `ok:false`, read `error`/`logTail`, fix the cause **once**, and re-run. Do no
329
349
  ## Notes
330
350
 
331
351
  - Until the user signs in, the backend is skipped (frontend-only). Run `speculos-toolkit login`,
332
- or point users to https://deploy.speculos.ai.
352
+ or point users to https://unified.speculos.ai.
333
353
  - Permission is granted once at skill install, so the deploy command runs without prompting.
334
354
  - To remove a deployment: `npx -y speculos-toolkit@latest teardown --slug <slug>` (or use the
335
- dashboard at https://deploy.speculos.ai/dashboard).
355
+ dashboard at https://unified.speculos.ai/?tab=deploys).
336
356
  - **Already have this skill from before connectors existed?** Re-run
337
357
  `npx -y speculos-toolkit@latest install-skill` to refresh it (safe to run repeatedly — it
338
358
  overwrites the skill file and re-grants the command). The CLI itself is always current
package/src/index.js CHANGED
@@ -87,11 +87,22 @@ USAGE
87
87
  run one connector tool through the broker
88
88
  (read-only discovery, live queries)
89
89
 
90
- Frontends are hosted FREE at user-deployed.speculos.ai/<userId>/<slugUuid>.
91
- A NEW app is PRIVATE — deploy with --public (or flip it on the dashboard) to
92
- share it. A redeploy never changes the visibility of an app that is already live.
90
+ Frontends are hosted FREE at user-deployed.speculos.ai/<userId>/<slugUuid>,
91
+ with no account and no sign-up.
92
+
93
+ WHO CAN SEE IT
94
+ No account the app is PUBLIC — anyone with the link. That is the only thing
95
+ it can be: a private app checks who you are when someone opens
96
+ it, and there is no account to check against yet.
97
+ Linked a NEW app is PRIVATE by default. --public to share it.
98
+ Already
99
+ deployed? run 'speculos-toolkit login' to link this machine — your existing
100
+ apps come with it — then deploy again with --private, or flip it
101
+ on https://unified.speculos.ai/?tab=deploys.
102
+
103
+ A redeploy never changes the visibility of an app that is already live.
93
104
  Every Speculos account includes one backend app free (an isolated sandbox per app) —
94
- sign up at https://deploy.speculos.ai and run 'speculos-toolkit login'.
105
+ sign up at https://unified.speculos.ai and run 'speculos-toolkit login'.
95
106
  Without an account a detected backend is skipped (frontend still ships free).
96
107
 
97
108
  OPTIONS
@@ -103,8 +114,9 @@ OPTIONS
103
114
  --build force the frontend through its build step (vs serve as static)
104
115
  --static serve the frontend dir as-is even if it has a build script
105
116
  --no-backend frontend-only: skip backend even if one is detected
106
- --private only you can open the deployed app (DEFAULT for a new app)
107
- --org anyone in your org can open it
117
+ --private only you can open the deployed app (needs an account;
118
+ the DEFAULT for a new app once this machine is linked)
119
+ --org anyone in your org can open it (needs an account)
108
120
  --public anyone with the link can open it, no sign-in
109
121
  --no-frontend backend-only: skip frontend even if one is detected
110
122
  --output <dir> frontend build output dir (dist/build/out — auto-detected)
@@ -132,6 +144,11 @@ VITE_API_URL / NEXT_PUBLIC_API_URL / API_URL (build) or window.SPECULOS_API_URL.
132
144
 
133
145
  async function cmdDetect(root, opts) {
134
146
  const d = detect(root, opts);
147
+ // `detect` derives a slug from the directory name; a project that has already
148
+ // been deployed has its real slug in .speculos.json, and deploy honours it.
149
+ // Reporting the derived one made `detect` disagree with what `deploy` would do.
150
+ const saved = creds.load(root);
151
+ if (!opts.slug && saved && saved.slug) d.slug = saved.slug;
135
152
  if (!opts.json) {
136
153
  log(opts, `slug: ${d.slug}`);
137
154
  log(opts, `frontend: ${d.frontend ? `${path.relative(root, d.frontend.dir) || "."} (${d.frontend.kind}${d.frontend.framework ? "/" + d.frontend.framework : ""})` : "none"}`);
@@ -172,7 +189,7 @@ async function cmdDeploy(root, opts) {
172
189
  d.backend = null;
173
190
  }
174
191
  if (!d.frontend && !d.backend) {
175
- if (hadBackend) { emit({ ok: false, error: `nothing to deploy: Speculos hosts frontends free, but this project is backend-only — sign in to deploy the backend (every Speculos account includes one free backend app). Run \`speculos-toolkit login\` (https://deploy.speculos.ai), or add a frontend.`, code: "BETA_BACKEND_ONLY" }); return 2; }
192
+ if (hadBackend) { emit({ ok: false, error: `nothing to deploy: Speculos hosts frontends free, but this project is backend-only — sign in to deploy the backend (every Speculos account includes one free backend app). Run \`speculos-toolkit login\` (https://unified.speculos.ai), or add a frontend.`, code: "BETA_BACKEND_ONLY" }); return 2; }
176
193
  emit({ ok: false, error: `could not detect a frontend in ${root}. Pass --frontend <dir>.`, code: "DETECT" });
177
194
  return 2;
178
195
  }
@@ -187,7 +204,13 @@ async function cmdDeploy(root, opts) {
187
204
  let alloc;
188
205
  try {
189
206
  alloc = await client.allocate({ userId: identity && identity.userId, userKey: identity && identity.userKey, slug: d.slug, visibility: opts.visibility }, opts);
190
- } catch (e) { emit({ ok: false, error: e.message, code: e.code || "ALLOCATE" }); return 1; }
207
+ } catch (e) {
208
+ // Readable first: emit() is JSON for the agent case, but a person who typed
209
+ // --private with no account needs to see WHY in plain words.
210
+ log(opts, `\u2717 ${e.message}`);
211
+ emit({ ok: false, error: e.message, code: e.code || "ALLOCATE" });
212
+ return 1;
213
+ }
191
214
  if (alloc.userKey) { // freshly minted on the server
192
215
  identity = { userId: alloc.userId, userKey: alloc.userKey };
193
216
  creds.saveIdentity(identity);
@@ -287,6 +310,7 @@ async function cmdDeploy(root, opts) {
287
310
  if (alloc && alloc.visibilityNote) log(opts, ` note: ${alloc.visibilityNote}`);
288
311
  const out = { ok: true, slug: d.slug, userId, urls };
289
312
  if (visibility) out.visibility = visibility;
313
+ if (alloc && alloc.visibilityNote) out.visibilityNote = alloc.visibilityNote;
290
314
  if (backendNote) out.backendNote = backendNote;
291
315
  emit({ ...out });
292
316
  return 0;
@@ -325,7 +349,7 @@ async function cmdLogin(opts) {
325
349
  // to a different account, incl. moving a personal-linked device onto an org).
326
350
  const existing = creds.loadIdentity();
327
351
  if (existing && existing.accountToken && !opts.relink && !opts.pasteToken) {
328
- log(opts, `✓ this device is already linked to your Speculos account. Pass --relink to link a different account. Manage deployments at https://deploy.speculos.ai/dashboard.`);
352
+ log(opts, `✓ this device is already linked to your Speculos account. Pass --relink to link a different account. Manage deployments at https://unified.speculos.ai/?tab=deploys.`);
329
353
  emit({ ok: true, alreadyLinked: true });
330
354
  return 0;
331
355
  }
@@ -351,7 +375,7 @@ async function cmdLogin(opts) {
351
375
  } catch { /* links on next deploy via the token */ }
352
376
  }
353
377
  if (reparented) process.stderr.write(`\n⚠ This device's deployments moved from ${previousEmail || "another account"} to ${who.email || "this account"}.\n`);
354
- log(opts, `✓ this device is linked${who.email ? ` to ${who.email}` : ""}${who.org ? ` (org: ${who.org})` : ""} — manage at https://deploy.speculos.ai/dashboard.`);
378
+ log(opts, `✓ this device is linked${who.email ? ` to ${who.email}` : ""}${who.org ? ` (org: ${who.org})` : ""} — manage at https://unified.speculos.ai/?tab=deploys.`);
355
379
  emit({ ok: true, linked: true, account: who.email || null, org: who.org || null, reparented, previousAccount: previousEmail });
356
380
  return 0;
357
381
  }
@@ -365,7 +389,7 @@ async function cmdLogin(opts) {
365
389
  // And on stderr (even in --json) for a human watching.
366
390
  process.stderr.write(`\nLink this machine to your Speculos account to enable backend hosting:\n`);
367
391
  process.stderr.write(`\n 1. Open: ${start.url}\n`);
368
- process.stderr.write(` (or go to https://deploy.speculos.ai/link and enter code ${start.code})\n`);
392
+ process.stderr.write(` (or go to https://unified.speculos.ai/link and enter code ${start.code})\n`);
369
393
  process.stderr.write(` 2. Sign in / sign up, then click Approve.\n\n`);
370
394
  process.stderr.write(`Waiting for approval…\n`);
371
395
 
@@ -393,7 +417,7 @@ async function cmdLogin(opts) {
393
417
  if (reparented) {
394
418
  process.stderr.write(`\n⚠ This device's deployments moved from ${previousEmail || "another account"} to ${approvedEmail || "this account"}. They now appear only in the new account's dashboard.\n`);
395
419
  }
396
- log(opts, `✓ this device is linked${approvedEmail ? ` to ${approvedEmail}` : ""} — manage your deployments at https://deploy.speculos.ai/dashboard.`);
420
+ log(opts, `✓ this device is linked${approvedEmail ? ` to ${approvedEmail}` : ""} — manage your deployments at https://unified.speculos.ai/?tab=deploys.`);
397
421
  emit({ ok: true, linked, account: approvedEmail, reparented, previousAccount: previousEmail });
398
422
  return 0;
399
423
  }
@@ -433,7 +457,7 @@ async function cmdConnectors(opts) {
433
457
  const identity = creds.loadIdentity();
434
458
  const token = identity && identity.accountToken;
435
459
  if (!token) {
436
- emit({ ok: false, code: "NO_TOKEN", error: "not signed in — run `npx -y speculos-toolkit@latest login`, then link data sources at https://deploy.speculos.ai/dashboard" });
460
+ emit({ ok: false, code: "NO_TOKEN", error: "not signed in — run `npx -y speculos-toolkit@latest login`, then link data sources at https://unified.speculos.ai/?tab=deploys" });
437
461
  return 1;
438
462
  }
439
463
  const sub = opts._[1] || "list";
@@ -444,8 +468,14 @@ async function cmdConnectors(opts) {
444
468
  catch (e) { emit({ ok: false, error: e.message, code: e.code || "CONNECTORS" }); return 1; }
445
469
  const conns = r.connectors || [];
446
470
  if (!opts.json) {
447
- if (!conns.length) log(opts, "no data sources linked — link one at https://deploy.speculos.ai/dashboard");
448
- for (const c of conns) log(opts, ` ${c.alias} (${c.name}${c.accountIdentifier ? " · " + c.accountIdentifier : ""}) — ${(c.tools || []).length} tools`);
471
+ if (!conns.length) log(opts, "no data sources linked — link one at https://unified.speculos.ai/?tab=deploys");
472
+ for (const c of conns) {
473
+ // Say which product a source came from: the deploy plane and the
474
+ // Cloud console hold separate connections, often to the same SaaS,
475
+ // and running a tool against the wrong one is silent and wrong.
476
+ const scope = c.orgWide ? " · org" : "";
477
+ log(opts, ` ${c.alias} (${c.name}${c.accountIdentifier ? " · " + c.accountIdentifier : ""}${scope}) — ${(c.tools || []).length} tools`);
478
+ }
449
479
  }
450
480
  emit({ ok: true, brokerUrl: client.base(opts) + "/api/connectors", connectors: conns });
451
481
  return 0;