speculos-toolkit 1.2.1 → 1.2.3
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 +11 -1
- package/package.json +1 -1
- package/skill/SKILL.md +5 -0
- package/src/client.js +2 -1
- package/src/index.js +32 -14
package/README.md
CHANGED
|
@@ -73,12 +73,22 @@ speculos-toolkit [deploy] detect, pack, deploy ./
|
|
|
73
73
|
speculos-toolkit detect show what would deploy (no upload)
|
|
74
74
|
speculos-toolkit status <jobId> poll a deployment
|
|
75
75
|
speculos-toolkit teardown --slug <s> remove a deployment
|
|
76
|
+
speculos-toolkit login [--token …] link this machine (browser approval at
|
|
77
|
+
https://unified.speculos.ai/link)
|
|
78
|
+
speculos-toolkit logout unlink it (revokes this device's token)
|
|
79
|
+
speculos-toolkit connectors [list|exec] your linked data sources, and one tool call
|
|
80
|
+
speculos-toolkit install-skill install the Claude Code skill
|
|
76
81
|
|
|
77
82
|
--frontend <dir> --backend <dir> --slug <name>
|
|
78
|
-
--runtime node|python --start "<cmd>" --build --output <dir>
|
|
83
|
+
--runtime node|python|bun --start "<cmd>" --build --static --output <dir>
|
|
84
|
+
--private | --org | --public
|
|
79
85
|
--env KEY=VAL --env-file <file> --api <url> --timeout <sec> --json
|
|
80
86
|
```
|
|
81
87
|
|
|
88
|
+
`--api` defaults to `https://unified-api.speculos.ai/toolkit` (the platform gateway,
|
|
89
|
+
which proxies to the deploy orchestrator and records the deploy in the console's
|
|
90
|
+
history); `SPECULOS_API` overrides it.
|
|
91
|
+
|
|
82
92
|
Docs: https://unified.speculos.ai · Source: https://github.com/speculosai/unified_platform
|
|
83
93
|
|
|
84
94
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speculos-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
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"
|
package/skill/SKILL.md
CHANGED
|
@@ -222,6 +222,11 @@ export async function connector(alias, tool, args) {
|
|
|
222
222
|
}
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
(The `x-speculos-gate` line is belt-and-braces: the deployed page also loads an
|
|
226
|
+
injected `speculos-env.js` that patches `fetch`/`XMLHttpRequest` to add that header
|
|
227
|
+
on every call to the backend or broker origin, so an app written WITHOUT this helper
|
|
228
|
+
still works when it is private. Keeping it costs nothing and makes the flow explicit.)
|
|
229
|
+
|
|
225
230
|
Rules (and state to the user):
|
|
226
231
|
- **Read-only only.** The broker rejects any non-read tool on this path (`WRITE_BLOCKED`).
|
|
227
232
|
Use `LIST_`/`GET_`/`SEARCH_`/`QUERY` tools; `POSTGRES_QUERY` runs in a read-only transaction.
|
package/src/client.js
CHANGED
|
@@ -36,7 +36,8 @@ async function linkPoll(code, opts = {}) { return post(base(opts) + "/api/cli/li
|
|
|
36
36
|
// verify a pasted account token (login --token) and get its account/org
|
|
37
37
|
async function whoami(token, opts = {}) { return get(base(opts) + "/api/account/whoami", token); }
|
|
38
38
|
async function linkMachine(token, payload, opts = {}) { return post(base(opts) + "/api/account/link", payload, token); }
|
|
39
|
-
|
|
39
|
+
// The machine identity rides along so the server can unlink it too.
|
|
40
|
+
async function logout(token, machine = {}, opts = {}) { return post(base(opts) + "/api/account/logout", { userId: machine.userId, userKey: machine.userKey }, token); }
|
|
40
41
|
// poll a backend job (owner-authenticated)
|
|
41
42
|
// The machine credentials travel as headers, not a query string: a query
|
|
42
43
|
// string is written to every access log between here and the orchestrator.
|
package/src/index.js
CHANGED
|
@@ -100,14 +100,16 @@ WHO CAN SEE IT
|
|
|
100
100
|
apps come with it — then deploy again with --private, or flip it
|
|
101
101
|
on https://unified.speculos.ai/?tab=deploys.
|
|
102
102
|
|
|
103
|
-
A redeploy
|
|
103
|
+
A redeploy with no visibility flag never changes a live app's visibility;
|
|
104
|
+
an explicit --private / --org / --public applies immediately, both ways.
|
|
104
105
|
Every Speculos account includes one backend app free (an isolated sandbox per app) —
|
|
105
106
|
sign up at https://unified.speculos.ai and run 'speculos-toolkit login'.
|
|
106
107
|
Without an account a detected backend is skipped (frontend still ships free).
|
|
107
108
|
|
|
108
109
|
OPTIONS
|
|
109
110
|
--frontend <dir> frontend dir (auto-detected: frontend/web/client/… or ./ if index.html)
|
|
110
|
-
--backend <dir> backend dir (auto-detected: backend/api/server/…) — needs
|
|
111
|
+
--backend <dir> backend dir (auto-detected: backend/api/server/…) — needs a linked
|
|
112
|
+
account (one backend app is included) or --override
|
|
111
113
|
--slug <slug> deployment slug (default: <folder>-<hash>)
|
|
112
114
|
--runtime <r> backend runtime: node | python | bun (auto-detected)
|
|
113
115
|
--start <cmd> backend start command (auto-detected; must bind 0.0.0.0:$PORT)
|
|
@@ -130,7 +132,7 @@ OPTIONS
|
|
|
130
132
|
--args-file <f> connectors exec: tool arguments from a JSON file (preferred
|
|
131
133
|
in agents — inline JSON with $ ( ) etc. may need approval)
|
|
132
134
|
--api <url> platform API base (default https://unified-api.speculos.ai/toolkit)
|
|
133
|
-
--timeout <sec> max seconds to wait for the backend (default
|
|
135
|
+
--timeout <sec> max seconds to wait for the backend (default 960)
|
|
134
136
|
--json machine-readable only (auto-on when non-TTY/CI)
|
|
135
137
|
|
|
136
138
|
Identity: the first deploy mints a machine-global { userId, userKey } saved to
|
|
@@ -184,11 +186,16 @@ async function cmdDeploy(root, opts) {
|
|
|
184
186
|
const accountToken = identity && identity.accountToken;
|
|
185
187
|
if (accountToken) opts.token = accountToken; // sent as Bearer on allocate/backend
|
|
186
188
|
const hadBackend = !!d.backend;
|
|
189
|
+
let backendNote = null;
|
|
187
190
|
if (d.backend && !override && !accountToken) {
|
|
188
|
-
|
|
191
|
+
// Said in the JSON too: an agent reads only stdout, and "ok: true" with a
|
|
192
|
+
// frontend URL used to hide that the API it built never shipped.
|
|
193
|
+
backendNote = "backend detected but skipped: this machine is not linked to a Speculos account (every account includes one backend app). Run `speculos-toolkit login`, then deploy again.";
|
|
194
|
+
log(opts, `↪ ${backendNote}`);
|
|
189
195
|
d.backend = null;
|
|
190
196
|
}
|
|
191
197
|
if (!d.frontend && !d.backend) {
|
|
198
|
+
if (opts.noFrontend && opts.noBackend) { emit({ ok: false, error: "both --no-frontend and --no-backend were given - nothing to deploy", code: "NOTHING_SELECTED" }); return 2; }
|
|
192
199
|
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; }
|
|
193
200
|
emit({ ok: false, error: `could not detect a frontend in ${root}. Pass --frontend <dir>.`, code: "DETECT" });
|
|
194
201
|
return 2;
|
|
@@ -220,7 +227,7 @@ async function cmdDeploy(root, opts) {
|
|
|
220
227
|
creds.save(root, { slug: d.slug, slugUuid });
|
|
221
228
|
|
|
222
229
|
// ---- backend first (so its URL can be baked into the frontend) ----
|
|
223
|
-
let backendUrl = null,
|
|
230
|
+
let backendUrl = null, jobId = null, historyIds = [];
|
|
224
231
|
if (d.backend) {
|
|
225
232
|
if (opts.envFileError) { emit({ ok: false, error: opts.envFileError, code: "ENV_FILE" }); log(opts, `✗ ${opts.envFileError}`); return 1; }
|
|
226
233
|
log(opts, `→ packing backend (${path.relative(root, d.backend.dir) || "."}, ${d.backend.runtime})`);
|
|
@@ -240,6 +247,8 @@ async function cmdDeploy(root, opts) {
|
|
|
240
247
|
else { emit({ ok: false, error: e.message, code: e.code || "BACKEND" }); return 1; }
|
|
241
248
|
}
|
|
242
249
|
if (created) {
|
|
250
|
+
jobId = created.jobId;
|
|
251
|
+
if (created.historyId) historyIds.push(created.historyId);
|
|
243
252
|
const st = await pollBackend(created.jobId, { userId, userKey }, opts);
|
|
244
253
|
if (st.status !== "success" || !(st.urls && st.urls.backend)) {
|
|
245
254
|
emit({ ok: false, slug: d.slug, jobId: created.jobId, status: "error", error: st.error || "backend failed", logTail: st.logTail });
|
|
@@ -290,6 +299,7 @@ async function cmdDeploy(root, opts) {
|
|
|
290
299
|
try { fe = await client.putFrontend({ userId, userKey, slug: d.slug, slugUuid, tarB64: feTar.base64, backendUrl }, opts); }
|
|
291
300
|
catch (e) { emit({ ok: false, error: e.message, code: e.code || "FRONTEND" }); return 1; }
|
|
292
301
|
frontendUrl = fe.frontendUrl;
|
|
302
|
+
if (fe.historyId) historyIds.push(fe.historyId);
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
const urls = {};
|
|
@@ -309,6 +319,10 @@ async function cmdDeploy(root, opts) {
|
|
|
309
319
|
else if (visibility === "public") log(opts, ` visibility: public — anyone with the link.`);
|
|
310
320
|
if (alloc && alloc.visibilityNote) log(opts, ` note: ${alloc.visibilityNote}`);
|
|
311
321
|
const out = { ok: true, slug: d.slug, userId, urls };
|
|
322
|
+
// What an agent needs to follow up without a second lookup: the backend
|
|
323
|
+
// job id for `status`, and the console history ids the gateway filed.
|
|
324
|
+
if (jobId) out.jobId = jobId;
|
|
325
|
+
if (historyIds.length) out.historyIds = historyIds;
|
|
312
326
|
if (visibility) out.visibility = visibility;
|
|
313
327
|
if (alloc && alloc.visibilityNote) out.visibilityNote = alloc.visibilityNote;
|
|
314
328
|
if (backendNote) out.backendNote = backendNote;
|
|
@@ -338,8 +352,9 @@ async function cmdTeardown(root, opts) {
|
|
|
338
352
|
if (identity.accountToken) opts.token = identity.accountToken;
|
|
339
353
|
const r = await client.teardown({ userId: identity.userId, userKey: identity.userKey, slug }, opts);
|
|
340
354
|
if (saved && saved.slug === slug) creds.remove(root);
|
|
355
|
+
if (r.notFound) log(opts, `! nothing was deployed under "${slug}" from this machine - nothing to tear down`);
|
|
341
356
|
emit({ ok: true, ...r });
|
|
342
|
-
return 0;
|
|
357
|
+
return r.notFound ? 3 : 0;
|
|
343
358
|
} catch (e) { emit({ ok: false, error: e.message, code: e.code || "TEARDOWN" }); return 1; }
|
|
344
359
|
}
|
|
345
360
|
|
|
@@ -428,12 +443,14 @@ async function cmdLogout(opts) {
|
|
|
428
443
|
const identity = creds.loadIdentity();
|
|
429
444
|
const token = identity && identity.accountToken;
|
|
430
445
|
if (!token) { emit({ ok: true, alreadyLoggedOut: true }); return 0; }
|
|
431
|
-
let revoked = false;
|
|
432
|
-
try {
|
|
433
|
-
|
|
446
|
+
let revoked = false, unlinked = false;
|
|
447
|
+
try {
|
|
448
|
+
const r = await client.logout(token, { userId: identity.userId, userKey: identity.userKey }, opts);
|
|
449
|
+
revoked = !!r.revoked; unlinked = !!r.unlinked;
|
|
450
|
+
} catch { /* revoke best-effort; still clear locally */ }
|
|
434
451
|
creds.clearAccountToken();
|
|
435
|
-
log(opts, `✓ this device is signed out of your Speculos account${revoked ? " (token revoked)" : ""}.`);
|
|
436
|
-
emit({ ok: true, loggedOut: true, revoked });
|
|
452
|
+
log(opts, `✓ this device is signed out of your Speculos account${revoked ? " (token revoked)" : ""}${unlinked ? "; the machine is no longer linked to it - `login` links it again" : ""}.`);
|
|
453
|
+
emit({ ok: true, loggedOut: true, revoked, unlinked });
|
|
437
454
|
return 0;
|
|
438
455
|
}
|
|
439
456
|
|
|
@@ -471,9 +488,10 @@ async function cmdConnectors(opts) {
|
|
|
471
488
|
if (!opts.json) {
|
|
472
489
|
if (!conns.length) log(opts, "no data sources linked — link one at https://unified.speculos.ai/?tab=deploys");
|
|
473
490
|
for (const c of conns) {
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
491
|
+
// Mark the org-wide sources: a person in an org sees those plus their
|
|
492
|
+
// own, and which one a tool runs against matters. (Console and toolkit
|
|
493
|
+
// now share ONE Composio workspace, so there is no "which product"
|
|
494
|
+
// left to disambiguate — a source linked in the console is this source.)
|
|
477
495
|
const scope = c.orgWide ? " · org" : "";
|
|
478
496
|
log(opts, ` ${c.alias} (${c.name}${c.accountIdentifier ? " · " + c.accountIdentifier : ""}${scope}) — ${(c.tools || []).length} tools`);
|
|
479
497
|
}
|