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 +14 -2
- package/package.json +5 -4
- package/skill/SKILL.md +28 -8
- package/src/index.js +45 -15
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://
|
|
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://
|
|
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.
|
|
4
|
-
"description": "The Speculos toolkit for coding agents
|
|
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://
|
|
31
|
+
"homepage": "https://unified.speculos.ai",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/speculosai/
|
|
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
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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
|
-
|
|
92
|
-
|
|
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://
|
|
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 (
|
|
107
|
-
|
|
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://
|
|
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) {
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
448
|
-
for (const c of conns)
|
|
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;
|