@wirl/mcp 0.2.0 → 0.2.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 +10 -6
- package/bin.js +670 -140
- package/package.json +1 -1
package/bin.js
CHANGED
|
@@ -15,10 +15,14 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// packages/core/src/types.ts
|
|
18
|
-
var AppKind;
|
|
18
|
+
var AppRole, AppKind;
|
|
19
19
|
var init_types = __esm({
|
|
20
20
|
"packages/core/src/types.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
+
AppRole = {
|
|
23
|
+
VIEWER: "viewer",
|
|
24
|
+
EDITOR: "editor"
|
|
25
|
+
};
|
|
22
26
|
AppKind = {
|
|
23
27
|
STATIC: "static",
|
|
24
28
|
SERVICE: "service",
|
|
@@ -189,9 +193,33 @@ function validateSlug(input) {
|
|
|
189
193
|
function isWellFormedSlug(input) {
|
|
190
194
|
return typeof input === "string" && input.length >= SLUG_MIN && input.length <= SLUG_MAX && SLUG_RE.test(input) && !input.includes(HOST_SEPARATOR);
|
|
191
195
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
function slugBase(input) {
|
|
197
|
+
return input.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, SLUG_MAX).replace(/-$/, "");
|
|
198
|
+
}
|
|
199
|
+
function previewSlugFor(raw) {
|
|
200
|
+
const base = slugBase(raw);
|
|
201
|
+
switch (validateSlug(base).problem) {
|
|
202
|
+
case void 0:
|
|
203
|
+
return { ok: true, slug: base };
|
|
204
|
+
case "reserved":
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
problem: "reserved",
|
|
208
|
+
message: `The preview name "${base}" is reserved. The reserved preview names are: ${[...RESERVED_SLUGS].sort().join(", ")}. Pick another name.`
|
|
209
|
+
};
|
|
210
|
+
case "too_short":
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
problem: "too_short",
|
|
214
|
+
message: `That name is too short: it makes a preview name of one character, and a preview name needs at least ${String(SLUG_MIN)}.`
|
|
215
|
+
};
|
|
216
|
+
default:
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
problem: "no_characters",
|
|
220
|
+
message: "That name has no usable characters: a preview name is made of the letters a to z and the digits 0 to 9 in what you give it, and none are left."
|
|
221
|
+
};
|
|
222
|
+
}
|
|
195
223
|
}
|
|
196
224
|
function appLabel(orgSlug, appSlug) {
|
|
197
225
|
return `${orgSlug}${HOST_SEPARATOR}${appSlug}`;
|
|
@@ -1794,6 +1822,9 @@ var init_migrations = __esm({
|
|
|
1794
1822
|
leaseMs: 6e5,
|
|
1795
1823
|
orgApplies: 2,
|
|
1796
1824
|
// per org: the limit one tenant's applies meet
|
|
1825
|
+
// Per app, its live database and its previews' together, inside the org's
|
|
1826
|
+
// two, so one app's deployers can never take both (OIM-927).
|
|
1827
|
+
appApplies: 1,
|
|
1797
1828
|
// Per control-plane process. The lease holds no pooled connection, so this
|
|
1798
1829
|
// bounds the process's work, not its pool; at `orgApplies` two each,
|
|
1799
1830
|
// filling it takes eight orgs (T5b-6).
|
|
@@ -2695,7 +2726,7 @@ var init_runtime = __esm({
|
|
|
2695
2726
|
});
|
|
2696
2727
|
|
|
2697
2728
|
// packages/core/src/broker-stream.ts
|
|
2698
|
-
var BROKER_LIMITS, STREAMED_MEDIA_TYPES;
|
|
2729
|
+
var BROKER_LIMITS, STREAMED_MEDIA_TYPES, MIB;
|
|
2699
2730
|
var init_broker_stream = __esm({
|
|
2700
2731
|
"packages/core/src/broker-stream.ts"() {
|
|
2701
2732
|
"use strict";
|
|
@@ -2712,15 +2743,132 @@ var init_broker_stream = __esm({
|
|
|
2712
2743
|
streamIdleMs: 4 * 6e4,
|
|
2713
2744
|
/** Bytes in any one answer, streamed or read whole. */
|
|
2714
2745
|
answerBytes: 25 * 1024 * 1024,
|
|
2746
|
+
/**
|
|
2747
|
+
* Bytes in the body of any one call an app makes. The body is read whole
|
|
2748
|
+
* before the call is sent, and before its grant is looked up, so a body
|
|
2749
|
+
* past this never opens a key and never reaches a vendor (OIM-931).
|
|
2750
|
+
* 100 MiB: Cloudflare publishes 100 MB as the request body its Free and
|
|
2751
|
+
* Pro plans take, and a deployed app's call is first held whole by the
|
|
2752
|
+
* egress Worker, in an isolate Cloudflare gives 128 MB of memory. Nothing
|
|
2753
|
+
* else bounds a caller that holds a broker token and reaches the control
|
|
2754
|
+
* plane itself: Railway sets no size on a body, only five minutes to send it.
|
|
2755
|
+
*/
|
|
2756
|
+
requestBytes: 100 * 1024 * 1024,
|
|
2757
|
+
/**
|
|
2758
|
+
* Bytes of call bodies one app's calls hold at once, its previews' with
|
|
2759
|
+
* them (OIM-951): counted as each chunk arrives, from a call's first byte
|
|
2760
|
+
* until the broker has answered it (broker-bodies.ts). At least
|
|
2761
|
+
* `requestBytes`, so one call at the cap always fits an app that holds
|
|
2762
|
+
* nothing else; a quarter of `heldBodyBytes`, so four apps of four owners
|
|
2763
|
+
* at their share fill the whole (one owner's apps cannot: `ownerHeldBodyBytes`).
|
|
2764
|
+
*/
|
|
2765
|
+
appHeldBodyBytes: 128 * 1024 * 1024,
|
|
2766
|
+
/**
|
|
2767
|
+
* Bytes of call bodies the calls of every app one owner has hold at once,
|
|
2768
|
+
* their previews' with them (OIM-967): the person `apps.owner_user_id`
|
|
2769
|
+
* names, whichever organisations their apps are in. Apps and
|
|
2770
|
+
* organisations cost nothing to create (no cap on either), so a share per
|
|
2771
|
+
* app let one person with four apps fill the whole, and one per
|
|
2772
|
+
* organisation would let them do it with four organisations; a person
|
|
2773
|
+
* costs a sign-in. Two apps at their share, so one app that runs
|
|
2774
|
+
* away still leaves its owner's other apps room, and half of
|
|
2775
|
+
* `heldBodyBytes`, so one owner can never take more than half, and a
|
|
2776
|
+
* second sign-in is what it takes to fill it. At least `requestBytes`, so
|
|
2777
|
+
* one call at the cap always fits an owner whose apps hold nothing else.
|
|
2778
|
+
*/
|
|
2779
|
+
ownerHeldBodyBytes: 256 * 1024 * 1024,
|
|
2780
|
+
/**
|
|
2781
|
+
* Bytes of call bodies every app's calls hold at once, in this process.
|
|
2782
|
+
* A call's reader starts at 16 KiB and doubles its buffer as the body
|
|
2783
|
+
* arrives, so the buffers behind this hold at most twice it, 1 GiB, and
|
|
2784
|
+
* 16 KiB a call beside (the ruling in OIM-951).
|
|
2785
|
+
*/
|
|
2786
|
+
heldBodyBytes: 512 * 1024 * 1024,
|
|
2787
|
+
/** The `Retry-After` of a call refused for either. */
|
|
2788
|
+
heldBodyRetryAfterSeconds: 10,
|
|
2789
|
+
/**
|
|
2790
|
+
* How long a call's body may send nothing before its read is stopped:
|
|
2791
|
+
* an upload's (OIM-935). A deployed app's body comes from the egress
|
|
2792
|
+
* Worker, which holds it whole before it sends it, and `wirl dev` reads it
|
|
2793
|
+
* whole first, so no ordinary sender ever pauses (fix round 1, R1-F2).
|
|
2794
|
+
*/
|
|
2795
|
+
heldBodyStallMs: 3e4,
|
|
2796
|
+
/**
|
|
2797
|
+
* How long a call's body may take to arrive whole: 100 MiB in 120 seconds
|
|
2798
|
+
* is about 7 Mbit/s, which a body between Cloudflare and the control plane
|
|
2799
|
+
* passes many times over, and well under Node's 300 seconds a request,
|
|
2800
|
+
* after which the body would be cut anyway (fix round 1, R1-F2).
|
|
2801
|
+
*/
|
|
2802
|
+
heldBodyArrivalMs: 12e4,
|
|
2803
|
+
/**
|
|
2804
|
+
* Bytes of answers read whole that one app's calls hold at once, its
|
|
2805
|
+
* previews' with them (OIM-966): counted as each chunk arrives from the
|
|
2806
|
+
* vendor, from an answer's first byte until wirl's server has taken the
|
|
2807
|
+
* last of it, or the app has gone (broker-bodies.ts). A vendor's event
|
|
2808
|
+
* stream is not counted: it is passed on as the app reads it, and holds
|
|
2809
|
+
* about 285 to 441 KiB at a time, measured end to end through the control
|
|
2810
|
+
* plane's server (`passOn` in broker.ts; OIM-966 fix round 1, R1-F3,
|
|
2811
|
+
* R2-F2). More than two `answerBytes`, so two answers at the cap fit an
|
|
2812
|
+
* app that holds nothing else; a quarter of `heldAnswerBytes`, so four apps
|
|
2813
|
+
* of four owners at their share fill the whole (one owner's apps cannot:
|
|
2814
|
+
* `ownerHeldAnswerBytes`).
|
|
2815
|
+
*/
|
|
2816
|
+
appHeldAnswerBytes: 64 * 1024 * 1024,
|
|
2817
|
+
/**
|
|
2818
|
+
* Bytes of answers read whole that the calls of every app one owner has
|
|
2819
|
+
* hold at once, their previews' with them (OIM-967), for the reasons
|
|
2820
|
+
* `ownerHeldBodyBytes` gives: two apps at their share, half of
|
|
2821
|
+
* `heldAnswerBytes`, and more than two `answerBytes` and than the room a
|
|
2822
|
+
* call needs, so an owner whose apps hold nothing else can always send.
|
|
2823
|
+
*/
|
|
2824
|
+
ownerHeldAnswerBytes: 128 * 1024 * 1024,
|
|
2825
|
+
/**
|
|
2826
|
+
* Bytes of answers read whole that every app's calls hold at once, in this
|
|
2827
|
+
* process. An answer's reader doubles its buffer as it fills, as a body's
|
|
2828
|
+
* does, and never past `answerBytes`, so while answers arrive the buffers
|
|
2829
|
+
* behind this hold at most twice it, 512 MiB, and 16 KiB an answer beside;
|
|
2830
|
+
* an answer read whole is copied to exactly its size before it is handed
|
|
2831
|
+
* on, so from then its count is its memory (the ruling in OIM-966, and its
|
|
2832
|
+
* fix round 1, R1-F2).
|
|
2833
|
+
*/
|
|
2834
|
+
heldAnswerBytes: 256 * 1024 * 1024,
|
|
2835
|
+
/**
|
|
2836
|
+
* The room an app's share of answers, and the whole, must each have left
|
|
2837
|
+
* for a call to be sent at all: one answer at the cap. With less, the call
|
|
2838
|
+
* is refused with a 503 before it leaves wirl, rather than run at its
|
|
2839
|
+
* vendor and its answer cut (OIM-966 fix round 1, R1-F1).
|
|
2840
|
+
*/
|
|
2841
|
+
heldAnswerRoomBytes: 25 * 1024 * 1024,
|
|
2842
|
+
/** The `Retry-After` of a call refused for that room, before it was sent. */
|
|
2843
|
+
heldAnswerRetryAfterSeconds: 10,
|
|
2844
|
+
/**
|
|
2845
|
+
* How long an answer read whole may wait for wirl's server to take the
|
|
2846
|
+
* last of it once it is handed back: past this it is cut, and its count
|
|
2847
|
+
* goes back, so no app holds its share, or the whole, by not reading.
|
|
2848
|
+
* Under Cloudflare's measured 125 seconds with nothing moving, which ends
|
|
2849
|
+
* a deployed app's unread answer anyway (OIM-966 fix round 1, R1-F1).
|
|
2850
|
+
*/
|
|
2851
|
+
heldAnswerHandOutMs: 12e4,
|
|
2715
2852
|
/**
|
|
2716
2853
|
* Streamed answers one app may have open at once. Each holds a vendor
|
|
2717
2854
|
* socket and an inbound connection on one control-plane instance for up
|
|
2718
2855
|
* to fourteen minutes, so one app cannot be allowed an unbounded number.
|
|
2719
2856
|
* Counted in memory, per instance, like the control plane's other limits.
|
|
2720
2857
|
*/
|
|
2721
|
-
openStreamsPerApp: 20
|
|
2858
|
+
openStreamsPerApp: 20,
|
|
2859
|
+
/**
|
|
2860
|
+
* How long an event stream wirl cut waits for its closing event to leave
|
|
2861
|
+
* the server before it ends the stream with its error anyway (OIM-837 fix
|
|
2862
|
+
* round 1). The event is on its way when the server's queue of what it has
|
|
2863
|
+
* to write has emptied; a reader that is slow but reading empties a full
|
|
2864
|
+
* kernel buffer, a few hundred kilobytes, well inside this. Past it the
|
|
2865
|
+
* reader is not reading, and the stream ends as it always did; the write
|
|
2866
|
+
* stall that holds a connection longer than this is OIM-968's.
|
|
2867
|
+
*/
|
|
2868
|
+
cutEventSentMs: 5e3
|
|
2722
2869
|
};
|
|
2723
2870
|
STREAMED_MEDIA_TYPES = Object.freeze(["text/event-stream", "application/x-ndjson"]);
|
|
2871
|
+
MIB = 1024 * 1024;
|
|
2724
2872
|
}
|
|
2725
2873
|
});
|
|
2726
2874
|
|
|
@@ -2788,20 +2936,21 @@ arrives: pass \`response.body\` straight on. There are no WebSockets. A front en
|
|
|
2788
2936
|
\`"static": "dist"\` serves that folder's files at their own paths (\`/\` is its \`index.html\`) and the module
|
|
2789
2937
|
answers everything else; add \`"spa": true\` for a client-side router. Nothing outside that folder is served.
|
|
2790
2938
|
|
|
2791
|
-
Preview before you deploy: \`wirl preview\`
|
|
2792
|
-
|
|
2793
|
-
\`wirl_preview_fetch\` tool) shows you what it serves.
|
|
2939
|
+
Preview before you deploy: \`wirl preview\` gives a folder its own address and database (at first only what
|
|
2940
|
+
\`migrations/\` makes). \`wirl fetch <path> --preview <name>\` or \`wirl_preview_fetch\` shows what it answers. Every request a preview's module gets names the preview in \`x-wirl-preview\`, and wirl marks its pages as previews.
|
|
2794
2941
|
|
|
2795
2942
|
npm packages work: install them in the project and the deploy bundles them in. Nothing is installed on the
|
|
2796
2943
|
server: native code will not run. Node built-ins work (\`node:crypto\`, \`node:path\`, ...; \`Buffer\` and
|
|
2797
2944
|
\`process\` are global), but what \`node:fs\` writes is gone by the next request: use \`env.DB\`.
|
|
2798
2945
|
|
|
2799
2946
|
\`env\` holds \`DB\` and nothing else. \`env.DB\` is Cloudflare D1: prepared statements with \`.bind()\`, at most
|
|
2800
|
-
100 bound parameters a statement. \`exec()\` runs one statement per line
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2947
|
+
100 bound parameters a statement. \`exec()\` runs one statement per line, as does each file in \`migrations/\`,
|
|
2948
|
+
which wirl applies in order to a preview and before going live; a failing one fails the deploy. Never edit
|
|
2949
|
+
an applied file. Dropping or renaming a table or column needs \`"migrations": { "destructive": [...] }\`, its
|
|
2950
|
+
own live deploy and a person's yes. Declare a host under \`connections\`, then \`fetch\` the vendor's real URL:
|
|
2951
|
+
wirl attaches the key. An undeclared host is refused; a public API needing no key uses \`"kind": "none"\`.
|
|
2952
|
+
Declaring one deploys before anyone approves it; the deploy says per connection whether it is usable now or
|
|
2953
|
+
waiting on an admin. A vendor has 60 seconds to answer, whole, unless it streams: ask a model for a stream.
|
|
2805
2954
|
|
|
2806
2955
|
Six headers say who is asking: \`x-wirl-user-id\`, \`x-wirl-user-email\`, \`x-wirl-user-name\`,
|
|
2807
2956
|
\`x-wirl-role\`, \`x-wirl-app-id\`, \`x-wirl-org\`. Read the name with
|
|
@@ -2811,10 +2960,9 @@ Schedules POST to \`/__wirl/schedule/<name>\`; a run gets 5 minutes unless that
|
|
|
2811
2960
|
says otherwise, and at most 15. A \`public_paths\` path takes requests with no sign-in.
|
|
2812
2961
|
Work that outlives one request (waiting for a person, trying tomorrow, a long loop) is a flow: see Flows.
|
|
2813
2962
|
|
|
2814
|
-
A new app defaults to org-wide visibility when its org has a company email domain,
|
|
2815
|
-
|
|
2816
|
-
Sharing by email adds one named person
|
|
2817
|
-
sets the level. Public is only ever a person's click on the app's Access page.
|
|
2963
|
+
A new app defaults to org-wide visibility when its org has a company email domain, and to private
|
|
2964
|
+
otherwise, including one made with \`wirl orgs create\`; \`wirl visibility private|org\` sets the level.
|
|
2965
|
+
Sharing by email adds one named person. Public is only ever a person's click on the app's Access page.
|
|
2818
2966
|
|
|
2819
2967
|
Porting an existing project: route it through the \`fetch\` handler, replace ORM calls with
|
|
2820
2968
|
\`env.DB.prepare(...).bind(...)\`, turn timers into \`schedules\`, and move keys into \`connections\`. Keep the
|
|
@@ -2846,18 +2994,67 @@ early, repeated or late one gets 409, or 404 once the run ends; check \`x-wirl-u
|
|
|
2846
2994
|
}
|
|
2847
2995
|
});
|
|
2848
2996
|
|
|
2997
|
+
// packages/core/src/version.ts
|
|
2998
|
+
function wirlVersion() {
|
|
2999
|
+
return BUILT_VERSION ?? WORKSPACE_VERSION;
|
|
3000
|
+
}
|
|
3001
|
+
var WORKSPACE_VERSION, BUILT_VERSION;
|
|
3002
|
+
var init_version = __esm({
|
|
3003
|
+
"packages/core/src/version.ts"() {
|
|
3004
|
+
"use strict";
|
|
3005
|
+
WORKSPACE_VERSION = "0.0.0";
|
|
3006
|
+
BUILT_VERSION = true ? "0.2.2" : null;
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
|
|
2849
3010
|
// packages/core/src/install.ts
|
|
3011
|
+
function fallbackWhen(controlPlane = "this control plane") {
|
|
3012
|
+
return `If your npm registry does not carry wirl, or to run exactly the build ${controlPlane} serves,`;
|
|
3013
|
+
}
|
|
3014
|
+
function namedControlPlane(controlOrigin) {
|
|
3015
|
+
return `the wirl control plane (${controlOrigin.replace(/^https?:\/\//, "").replace(/\/+$/, "")})`;
|
|
3016
|
+
}
|
|
2850
3017
|
function installFor(controlOrigin) {
|
|
2851
|
-
if (NPM_PUBLISHED) return { cli: "wirl", mcp: "@wirl/mcp" };
|
|
2852
3018
|
const origin = controlOrigin.replace(/\/+$/, "");
|
|
2853
|
-
|
|
3019
|
+
const tarball = { cli: `${origin}/cli/wirl.tgz`, mcp: `${origin}/cli/wirl-mcp.tgz` };
|
|
3020
|
+
if (NPM_PUBLISHED) return { cli: "wirl", mcp: "@wirl/mcp", tarball };
|
|
3021
|
+
return { ...tarball, tarball };
|
|
2854
3022
|
}
|
|
2855
|
-
var NPM_PUBLISHED, WIRL_BIN;
|
|
3023
|
+
var NPM_PUBLISHED, WIRL_BIN, FALLBACK_WHEN, hasFallback;
|
|
2856
3024
|
var init_install = __esm({
|
|
2857
3025
|
"packages/core/src/install.ts"() {
|
|
2858
3026
|
"use strict";
|
|
2859
|
-
NPM_PUBLISHED =
|
|
3027
|
+
NPM_PUBLISHED = true;
|
|
2860
3028
|
WIRL_BIN = NPM_PUBLISHED ? "npx wirl" : "wirl";
|
|
3029
|
+
FALLBACK_WHEN = fallbackWhen();
|
|
3030
|
+
hasFallback = (install) => install.cli !== install.tarball.cli;
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
// packages/core/src/share-role.ts
|
|
3035
|
+
function readShareRole(value) {
|
|
3036
|
+
if (value === void 0 || value === null) return { ok: true, role: void 0 };
|
|
3037
|
+
if (value === AppRole.VIEWER || value === AppRole.EDITOR) return { ok: true, role: value };
|
|
3038
|
+
return { ok: false, message: SHARE_ROLE_REFUSAL };
|
|
3039
|
+
}
|
|
3040
|
+
var SHARE_ROLE_REFUSAL;
|
|
3041
|
+
var init_share_role = __esm({
|
|
3042
|
+
"packages/core/src/share-role.ts"() {
|
|
3043
|
+
"use strict";
|
|
3044
|
+
init_types();
|
|
3045
|
+
SHARE_ROLE_REFUSAL = "role must be viewer or editor.";
|
|
3046
|
+
}
|
|
3047
|
+
});
|
|
3048
|
+
|
|
3049
|
+
// packages/core/src/connect.ts
|
|
3050
|
+
var CLAUDE_CODE_SIGN_IN, CLAUDE_CODE_LOCAL_SIGN_IN, CLI_LOGIN_DOES;
|
|
3051
|
+
var init_connect = __esm({
|
|
3052
|
+
"packages/core/src/connect.ts"() {
|
|
3053
|
+
"use strict";
|
|
3054
|
+
init_install();
|
|
3055
|
+
CLAUDE_CODE_SIGN_IN = "`claude mcp add` only saves the server's configuration. Then sign in: run `/mcp` inside Claude Code and follow the steps in the browser, or run `claude mcp login wirl` in a terminal.";
|
|
3056
|
+
CLAUDE_CODE_LOCAL_SIGN_IN = `The local server signs in the first time a wirl tool needs it: the agent calls \`wirl_login\`, which returns a link and a code, and the agent shows both to the person. The person approves the code in the browser, and the agent's next wirl tool call works. A machine already signed in with \`${WIRL_BIN} login\` skips this.`;
|
|
3057
|
+
CLI_LOGIN_DOES = "prints a link and a code to approve in the browser";
|
|
2861
3058
|
}
|
|
2862
3059
|
});
|
|
2863
3060
|
|
|
@@ -3081,6 +3278,66 @@ var init_porting = __esm({
|
|
|
3081
3278
|
}
|
|
3082
3279
|
});
|
|
3083
3280
|
|
|
3281
|
+
// packages/core/src/throttle-report.ts
|
|
3282
|
+
var THROTTLE_REPORT, EDGE_REFUSALS_NOTE;
|
|
3283
|
+
var init_throttle_report = __esm({
|
|
3284
|
+
"packages/core/src/throttle-report.ts"() {
|
|
3285
|
+
"use strict";
|
|
3286
|
+
init_public_path();
|
|
3287
|
+
THROTTLE_REPORT = {
|
|
3288
|
+
/**
|
|
3289
|
+
* One report per app a minute at most from each gate isolate, and what
|
|
3290
|
+
* one report counts spans a minute at most.
|
|
3291
|
+
*/
|
|
3292
|
+
windowMs: 6e4,
|
|
3293
|
+
/**
|
|
3294
|
+
* How long the refusal that opens a window waits for more before the
|
|
3295
|
+
* report goes, so a burst is one report with its count rather than a
|
|
3296
|
+
* report of its first refusal.
|
|
3297
|
+
*/
|
|
3298
|
+
gatherMs: 15e3,
|
|
3299
|
+
/**
|
|
3300
|
+
* The longest a refused request is asked to stay open, after its 429 has
|
|
3301
|
+
* gone, for the report to be sent. Cloudflare keeps a request's
|
|
3302
|
+
* `waitUntil` work for 30 seconds after the response, shared by all of
|
|
3303
|
+
* it, and the send itself has `sendTimeoutMs` of what is left.
|
|
3304
|
+
*/
|
|
3305
|
+
maxWaitMs: 2e4,
|
|
3306
|
+
sendTimeoutMs: 5e3,
|
|
3307
|
+
/**
|
|
3308
|
+
* Reports one request carries for windows no refusal could wait for
|
|
3309
|
+
* (`flushDue`). A request that was never refused must not queue its own
|
|
3310
|
+
* reads behind posts that are not its own: Cloudflare runs six of a
|
|
3311
|
+
* request's connections at once and queues the rest.
|
|
3312
|
+
*/
|
|
3313
|
+
flushPerRequest: 2,
|
|
3314
|
+
/** Apps one gate isolate counts for at once; a refusal for another app past this is not counted. */
|
|
3315
|
+
maxApps: 1e3,
|
|
3316
|
+
/** Lines in one report: each of an app's public paths, by each ceiling. */
|
|
3317
|
+
maxEntries: 2 * PUBLIC_PATH_LIMITS.perApp,
|
|
3318
|
+
/** A path is cut to this many characters, as `public.refused`'s is. */
|
|
3319
|
+
maxPathChars: 256,
|
|
3320
|
+
/**
|
|
3321
|
+
* The most of a report's body the control plane reads. A full report is
|
|
3322
|
+
* under 8 KiB: a path a request matched is ASCII, as a URL's path is.
|
|
3323
|
+
*/
|
|
3324
|
+
bodyBytes: 16384,
|
|
3325
|
+
/**
|
|
3326
|
+
* Reports the control plane reads for one app a minute, from every isolate
|
|
3327
|
+
* and location together. Of those, the first writes the app's one
|
|
3328
|
+
* `public.throttled` row for that minute and the rest write nothing.
|
|
3329
|
+
*/
|
|
3330
|
+
perAppPerMinute: 10,
|
|
3331
|
+
/** The org's Activity page summarises this many apps' latest `public.throttled` lines, a line an app, from the last `logsPageDays` days. */
|
|
3332
|
+
activityLines: 5,
|
|
3333
|
+
/** The app's Logs page shows its latest this many `public.throttled` lines, from the last `logsPageDays` days. */
|
|
3334
|
+
logsPageLines: 20,
|
|
3335
|
+
logsPageDays: 7
|
|
3336
|
+
};
|
|
3337
|
+
EDGE_REFUSALS_NOTE = "Requests wirl refuses before your app is called, such as a public path's 429 over its ceiling, are not invocations and never show here; anyone who can deploy the app sees those 429s counted as public.throttled on its Logs page in the dashboard.";
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
|
|
3084
3341
|
// packages/core/src/supplied-bundle.ts
|
|
3085
3342
|
var SUPPLIED_BUNDLE_PATH, MAX_SUPPLIED_BUNDLE_BYTES;
|
|
3086
3343
|
var init_supplied_bundle = __esm({
|
|
@@ -3105,6 +3362,9 @@ var init_manifest_transport = __esm({
|
|
|
3105
3362
|
});
|
|
3106
3363
|
|
|
3107
3364
|
// packages/core/src/preview.ts
|
|
3365
|
+
function versionNumbersSentence(listing) {
|
|
3366
|
+
return `A preview takes a version number from the same count as a deploy, and ${listing} never lists a preview, so a deploy's number can skip: if the newest version listed is v1 and this deploy is v4, v2 and v3 were previews.`;
|
|
3367
|
+
}
|
|
3108
3368
|
var PREVIEW_TTL_DAYS, MAX_PREVIEWS_PER_APP;
|
|
3109
3369
|
var init_preview = __esm({
|
|
3110
3370
|
"packages/core/src/preview.ts"() {
|
|
@@ -3142,8 +3402,12 @@ var init_src = __esm({
|
|
|
3142
3402
|
init_broker_stream();
|
|
3143
3403
|
init_retention();
|
|
3144
3404
|
init_contract();
|
|
3405
|
+
init_version();
|
|
3145
3406
|
init_install();
|
|
3407
|
+
init_share_role();
|
|
3408
|
+
init_connect();
|
|
3146
3409
|
init_porting();
|
|
3410
|
+
init_throttle_report();
|
|
3147
3411
|
init_supplied_bundle();
|
|
3148
3412
|
init_manifest_transport();
|
|
3149
3413
|
init_preview();
|
|
@@ -4789,7 +5053,7 @@ init_src();
|
|
|
4789
5053
|
import { access, link, lstat as lstat2, mkdir as mkdir3, open, readFile as readFile3, readdir, rename as rename2, rm as rm3, stat as stat3, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
4790
5054
|
import { homedir } from "node:os";
|
|
4791
5055
|
import { basename as basename2, dirname as dirname3, isAbsolute, join as join5, relative, resolve as resolve4, sep } from "node:path";
|
|
4792
|
-
import { create as tarCreate, extract as tarExtract } from "tar";
|
|
5056
|
+
import { create as tarCreate, extract as tarExtract, Parser as TarParser } from "tar";
|
|
4793
5057
|
import { gzipSync } from "node:zlib";
|
|
4794
5058
|
|
|
4795
5059
|
// packages/core/src/node/preview-name.ts
|
|
@@ -4797,10 +5061,9 @@ init_slug();
|
|
|
4797
5061
|
import { createHash } from "node:crypto";
|
|
4798
5062
|
var HASH_SUFFIX_LENGTH = 5;
|
|
4799
5063
|
function previewNameFor(raw, orgSlug, appSlug) {
|
|
4800
|
-
const
|
|
4801
|
-
if (!
|
|
4802
|
-
|
|
4803
|
-
}
|
|
5064
|
+
const made = previewSlugFor(raw);
|
|
5065
|
+
if (!made.ok) return { ok: false, message: made.message };
|
|
5066
|
+
const slug = made.slug;
|
|
4804
5067
|
const budget = MAX_HOST_LABEL - (appLabel(orgSlug, appSlug).length + HOST_SEPARATOR.length);
|
|
4805
5068
|
if (budget < 2) {
|
|
4806
5069
|
return {
|
|
@@ -4920,6 +5183,64 @@ function shellPath(value) {
|
|
|
4920
5183
|
return shellQuote(value);
|
|
4921
5184
|
}
|
|
4922
5185
|
|
|
5186
|
+
// packages/cli/src/skill.ts
|
|
5187
|
+
init_src();
|
|
5188
|
+
var SKILL_INTRO = `A new app is visible to everyone in the org when the org came from a
|
|
5189
|
+
company email domain; in any other org, including a personal one or one
|
|
5190
|
+
made with \`wirl orgs create\`, it starts private. Deploys are one
|
|
5191
|
+
command; sharing is by email; a third-party API is declared, never pasted.`;
|
|
5192
|
+
var SKILL_DOCS_LINE = "Before you build, fetch https://docs.wirl.dev/llms-full.txt: every page of the docs as one Markdown file, so there is no need to read the site page by page.";
|
|
5193
|
+
var SKILL_RULES = `1. **Never ask for or paste an API key.** Declare the connection in \`wirl.json\` and deploy. Needs no key at all (a public API)? Declare it with "kind": "none" and its hosts. If the key is missing, the deploy prints a link to the app's keys page; show the person that link. Locally, keys live in \`.wirl/keys.json\`, which git ignores. To try a vendor call locally without the key, map its connection to a server of your own in \`.wirl/mocks.json\`, like \`{ "anthropic": "http://127.0.0.1:4010" }\`: \`wirl dev\` then sends the calls to that connection's hosts there instead (\`wirl dev --help\` has the rest).
|
|
5194
|
+
2. **Call third-party APIs by their real URL** (\`fetch("https://api.hubspot.com/...")\`). wirl attaches the key on the way out, deployed and under \`wirl dev\` alike.
|
|
5195
|
+
3. If a command names several organisations and asks which, ask the person; pass \`--org\` or put \`"org"\` in \`wirl.json\`.
|
|
5196
|
+
4. To keep a file out of the deploy, list it in a \`.wirlignore\` at the project root (gitignore-style lines; no \`!\` negation).
|
|
5197
|
+
5. **A file that can lose data waits for a person.** A deploy that would apply a destructive file (one wirl.json's \`"destructive"\` list must name) to the live database stops and names the page where a person approves each one. Show the person those pages, and deploy again once they say they approved. If \`${WIRL_BIN} deploy\` asks you to type a file's name, stop it and show the page instead, or deploy with \`--json\`, which never asks. Never type the name yourself or send the confirmation: that is the person's, and no flag skips it.`;
|
|
5198
|
+
var SKILL_MANIFEST_EXAMPLE = `\`\`\`json
|
|
5199
|
+
{ "app": "crm-sync", "server": "server.mjs", "connections": [{ "slug": "hubspot", "hosts": ["api.hubspot.com"] }] }
|
|
5200
|
+
\`\`\``;
|
|
5201
|
+
var SKILL_FILES_PARAGRAPH = "No blob storage: files go in `env.DB`, at most 2,000,000 bytes a value or row (the limit Cloudflare publishes for D1), so store a larger file in chunks, one row each, a few MB to a batch. `wirl dev` refuses a single bound value over 2,000,000 bytes, but a row's total, a value grown in SQL, or a batch bigger than one call to D1 carries can still pass locally and fail deployed.";
|
|
5202
|
+
var SKILL_STATIC_PARAGRAPH = 'A static folder maps a path to one file exactly: a path ending in `/` serves the `index.html` in that folder, `/about` never finds `about.html`, and a path that names no file goes to the module (with `"spa": true`, a page navigation gets `index.html` instead; in an app with no module, the top `index.html`, or a 404 for a missing file with an extension). Next.js runs only as a static export: `output: "export"` and `trailingSlash: true` in `next.config` make `next build` write `out/`, which goes under `"static"`; `next start` cannot run, and API routes move into the module.';
|
|
5203
|
+
function composeSkill(parts) {
|
|
5204
|
+
if (parts.description.trim() === "" || /[\r\n]/.test(parts.description)) {
|
|
5205
|
+
throw new Error("a skill description is one non-empty line");
|
|
5206
|
+
}
|
|
5207
|
+
return `---
|
|
5208
|
+
name: wirl
|
|
5209
|
+
description: ${parts.description}
|
|
5210
|
+
---
|
|
5211
|
+
|
|
5212
|
+
# wirl
|
|
5213
|
+
|
|
5214
|
+
${SKILL_INTRO}
|
|
5215
|
+
|
|
5216
|
+
${SKILL_DOCS_LINE}
|
|
5217
|
+
|
|
5218
|
+
## Commands
|
|
5219
|
+
|
|
5220
|
+
${parts.commands.trimEnd()}
|
|
5221
|
+
|
|
5222
|
+
## Rules
|
|
5223
|
+
|
|
5224
|
+
${SKILL_RULES}
|
|
5225
|
+
|
|
5226
|
+
## wirl.json
|
|
5227
|
+
|
|
5228
|
+
${SKILL_MANIFEST_EXAMPLE}
|
|
5229
|
+
|
|
5230
|
+
## What runs on wirl
|
|
5231
|
+
|
|
5232
|
+
${BUILD_CONTRACT}
|
|
5233
|
+
|
|
5234
|
+
${SKILL_FILES_PARAGRAPH}
|
|
5235
|
+
|
|
5236
|
+
${SKILL_STATIC_PARAGRAPH}
|
|
5237
|
+
|
|
5238
|
+
## Flows
|
|
5239
|
+
|
|
5240
|
+
${FLOWS_CONTRACT}
|
|
5241
|
+
`;
|
|
5242
|
+
}
|
|
5243
|
+
|
|
4923
5244
|
// packages/cli/src/core.ts
|
|
4924
5245
|
function credentialsPath(env = process.env) {
|
|
4925
5246
|
return env["WIRL_CREDENTIALS_PATH"] || join5(homedir(), ".config", "wirl", "credentials.json");
|
|
@@ -4980,7 +5301,7 @@ async function loadCredentials(env = process.env) {
|
|
|
4980
5301
|
}
|
|
4981
5302
|
} catch {
|
|
4982
5303
|
}
|
|
4983
|
-
throw new CliError("Not signed in.",
|
|
5304
|
+
throw new CliError("Not signed in.", `Run \`${WIRL_BIN} login\` first, or set WIRL_TOKEN.`, { code: "not_signed_in" });
|
|
4984
5305
|
}
|
|
4985
5306
|
async function saveCredentials(creds, env = process.env) {
|
|
4986
5307
|
const path = credentialsPath(env);
|
|
@@ -5041,11 +5362,11 @@ async function pollDeviceLogin(apiOrigin, login, options = {}) {
|
|
|
5041
5362
|
continue;
|
|
5042
5363
|
}
|
|
5043
5364
|
case "expired_token":
|
|
5044
|
-
throw new CliError("The sign-in code expired before it was approved.",
|
|
5365
|
+
throw new CliError("The sign-in code expired before it was approved.", `Run \`${WIRL_BIN} login\` again.`);
|
|
5045
5366
|
default:
|
|
5046
5367
|
throw new CliError(
|
|
5047
5368
|
typeof body["error"] === "string" ? `Sign-in failed: ${body["error"]}.` : `Sign-in failed (${response.status}).`,
|
|
5048
|
-
|
|
5369
|
+
`Run \`${WIRL_BIN} login\` again.`
|
|
5049
5370
|
);
|
|
5050
5371
|
}
|
|
5051
5372
|
}
|
|
@@ -5096,6 +5417,10 @@ var SERVER_TEMPLATE = [
|
|
|
5096
5417
|
" *",
|
|
5097
5418
|
" * Everything real happens here. The page is HTML this module returns. The",
|
|
5098
5419
|
" * data lives in env.DB, a SQLite database that belongs to this app alone.",
|
|
5420
|
+
" * Its tables are in migrations/0001_init.sql: wirl applies each new file in",
|
|
5421
|
+
" * migrations/ before a version goes live, to a preview's database when the",
|
|
5422
|
+
" * preview is put up, and to the local one when wirl dev starts. Put the",
|
|
5423
|
+
" * next change in migrations/0002_<what>.sql rather than editing that one.",
|
|
5099
5424
|
" * The person making the request is in headers wirl sets after verifying",
|
|
5100
5425
|
" * them, which the browser cannot forge. Any rule that matters, such as",
|
|
5101
5426
|
' * "you only see your own notes", is a line of code in this file: a check',
|
|
@@ -5107,18 +5432,8 @@ var SERVER_TEMPLATE = [
|
|
|
5107
5432
|
" * live in .wirl/keys.json beside this file, which git ignores.",
|
|
5108
5433
|
" */",
|
|
5109
5434
|
"",
|
|
5110
|
-
"// One line: exec runs one statement per line, as D1 does.",
|
|
5111
|
-
"const SCHEMA =",
|
|
5112
|
-
' "create table if not exists notes (" +',
|
|
5113
|
-
' " id integer primary key," +',
|
|
5114
|
-
' " user_id text not null," +',
|
|
5115
|
-
' " body text not null," +',
|
|
5116
|
-
` " created_at text not null default (datetime('now'))" +`,
|
|
5117
|
-
' ")";',
|
|
5118
|
-
"",
|
|
5119
5435
|
"export default {",
|
|
5120
5436
|
" async fetch(request, env) {",
|
|
5121
|
-
" await env.DB.exec(SCHEMA);",
|
|
5122
5437
|
" const url = new URL(request.url);",
|
|
5123
5438
|
"",
|
|
5124
5439
|
` // One line per request. Read it with ${WIRL_BIN} logs.`,
|
|
@@ -5201,56 +5516,34 @@ var SERVER_TEMPLATE = [
|
|
|
5201
5516
|
"}",
|
|
5202
5517
|
""
|
|
5203
5518
|
].join("\n");
|
|
5519
|
+
var INIT_MIGRATION = [
|
|
5520
|
+
"-- This app's tables. wirl applies each new file here, in order, before a version goes live.",
|
|
5521
|
+
"-- Make the next change in 0002_<what>.sql; never edit a file that has been deployed.",
|
|
5522
|
+
"create table notes (id integer primary key, user_id text not null, body text not null, created_at text not null default (datetime('now')))",
|
|
5523
|
+
""
|
|
5524
|
+
].join("\n");
|
|
5525
|
+
var INIT_MIGRATION_PATH = `${MIGRATIONS_DIR}/${ADOPTED_FILE}`;
|
|
5204
5526
|
var SKILL_PATH = join5(".claude", "skills", "wirl", "SKILL.md");
|
|
5205
|
-
var
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
---
|
|
5209
|
-
|
|
5210
|
-
# wirl
|
|
5211
|
-
|
|
5212
|
-
A new app is visible to everyone in the org when the org came from a
|
|
5213
|
-
company email domain; in any other org, including a personal one or one
|
|
5214
|
-
made with \`wirl orgs create\`, it starts private. Deploys are one
|
|
5215
|
-
command; sharing is by email; a third-party API is declared, never pasted.
|
|
5216
|
-
|
|
5217
|
-
Before you build, fetch https://docs.wirl.dev/llms-full.txt: every page of the docs as one Markdown file, so there is no need to read the site page by page.
|
|
5218
|
-
|
|
5219
|
-
## Commands
|
|
5527
|
+
var SKILL_ORIGIN = "https://app.wirl.dev";
|
|
5528
|
+
var SKILL_INSTALL = installFor(SKILL_ORIGIN);
|
|
5529
|
+
var SKILL_RUN = NPM_PUBLISHED ? `Run the CLI as \`${WIRL_BIN} <command>\`, which needs no install beyond Node 22.13 or later (in a terminal npm asks "Ok to proceed?" the first time: answer y); this file writes a command as \`wirl <command>\` for short anywhere but the list below.
|
|
5220
5530
|
|
|
5221
|
-
|
|
5531
|
+
` : "";
|
|
5532
|
+
var SKILL_FALLBACK = hasFallback(SKILL_INSTALL) ? `- ${fallbackWhen(namedControlPlane(SKILL_ORIGIN))} install the CLI from the control plane instead with \`npm install -g ${SKILL_INSTALL.tarball.cli}\`, run plain \`wirl\` where this file says \`${WIRL_BIN}\`, and name \`${SKILL_INSTALL.tarball.mcp}\` in place of \`${SKILL_INSTALL.mcp}\`.
|
|
5533
|
+
` : "";
|
|
5534
|
+
var SKILL_MARKDOWN = composeSkill({
|
|
5535
|
+
description: `Deploy and share the internal app in this project with wirl (${WIRL_BIN}). Use when the user wants a tool, dashboard or prototype put somewhere colleagues can open it, shared with a named person, run locally the way it will run deployed, or connected to a third-party API; or wants work that outlives one request, such as waiting for a person to approve something or trying again tomorrow.`,
|
|
5536
|
+
commands: `${SKILL_RUN}- Connect first, if the agent supports MCP: \`claude mcp add --transport http wirl https://app.wirl.dev/mcp\` needs no install. ${CLAUDE_CODE_SIGN_IN} No HTTP MCP support? \`claude mcp add wirl -- npx -y ${SKILL_INSTALL.mcp}\` runs the same tools locally. ${CLAUDE_CODE_LOCAL_SIGN_IN} Neither available: \`${WIRL_BIN} login\` prints a link and a code; the person approves in the browser. Do this once, when any command says you are not signed in.
|
|
5222
5537
|
- \`${WIRL_BIN} deploy\` \u2014 deploys this directory. Prints the URL and who can open it.
|
|
5223
5538
|
- \`${WIRL_BIN} share someone@company.com\` \u2014 that person can open it. Add \`--role editor\` so they can deploy too.
|
|
5224
5539
|
- \`${WIRL_BIN} dev\` \u2014 runs the app here as it will run deployed, signed in as a stub user.
|
|
5225
5540
|
- \`${WIRL_BIN} pull <app>\` \u2014 fetches a deployed app's source to keep building.
|
|
5226
5541
|
- \`${WIRL_BIN} logs\` \u2014 what the deployed app printed, per request, newest first; \`--errors\` for failures only, \`--follow\` to keep watching, \`--cursor <n>\` for the page before a "More before this" line. Read this before changing code when something misbehaves.
|
|
5542
|
+
- \`${WIRL_BIN} migrations\` \u2014 what this app's database has applied, read only.
|
|
5543
|
+
- \`${WIRL_BIN} migrations adopt\` \u2014 once, for an app whose tables were made in code: writes \`migrations/0001_init.sql\` from the live database's shape.
|
|
5227
5544
|
- \`${WIRL_BIN} --help\` \u2014 everything else: orgs, members, connections, move, transfer, rollback, flows and runs.
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
1. **Never ask for or paste an API key.** Declare the connection in \`wirl.json\` and deploy. Needs no key at all (a public API)? Declare it with "kind": "none" and its hosts. If the key is missing, the deploy prints a link to the app's keys page; show the person that link. Locally, keys live in \`.wirl/keys.json\`, which git ignores. To try a vendor call locally without the key, map its connection to a server of your own in \`.wirl/mocks.json\`, like \`{ "anthropic": "http://127.0.0.1:4010" }\`: \`wirl dev\` then sends the calls to that connection's hosts there instead (\`wirl dev --help\` has the rest).
|
|
5232
|
-
2. **Call third-party APIs by their real URL** (\`fetch("https://api.hubspot.com/...")\`). wirl attaches the key on the way out, deployed and under \`wirl dev\` alike.
|
|
5233
|
-
3. If a command names several organisations and asks which, ask the person; pass \`--org\` or put \`"org"\` in \`wirl.json\`.
|
|
5234
|
-
4. To keep a file out of the deploy, list it in a \`.wirlignore\` at the project root (gitignore-style lines; no \`!\` negation).
|
|
5235
|
-
|
|
5236
|
-
## wirl.json
|
|
5237
|
-
|
|
5238
|
-
\`\`\`json
|
|
5239
|
-
{ "app": "crm-sync", "server": "server.mjs", "connections": [{ "slug": "hubspot", "hosts": ["api.hubspot.com"] }] }
|
|
5240
|
-
\`\`\`
|
|
5241
|
-
|
|
5242
|
-
## What runs on wirl
|
|
5243
|
-
|
|
5244
|
-
${BUILD_CONTRACT}
|
|
5245
|
-
|
|
5246
|
-
No blob storage: files go in \`env.DB\`, at most 2,000,000 bytes a value or row (the limit Cloudflare publishes for D1), so store a larger file in chunks, one row each, a few MB to a batch. \`wirl dev\` refuses a single bound value over 2,000,000 bytes, but a row's total, a value grown in SQL, or a batch bigger than one call to D1 carries can still pass locally and fail deployed.
|
|
5247
|
-
|
|
5248
|
-
A static folder maps a path to one file exactly: a path ending in \`/\` serves the \`index.html\` in that folder, \`/about\` never finds \`about.html\`, and a path that names no file goes to the module (with \`"spa": true\`, a page navigation gets \`index.html\` instead; in an app with no module, the top \`index.html\`, or a 404 for a missing file with an extension). Next.js runs only as a static export: \`output: "export"\` and \`trailingSlash: true\` in \`next.config\` make \`next build\` write \`out/\`, which goes under \`"static"\`; \`next start\` cannot run, and API routes move into the module.
|
|
5249
|
-
|
|
5250
|
-
## Flows
|
|
5251
|
-
|
|
5252
|
-
${FLOWS_CONTRACT}
|
|
5253
|
-
`;
|
|
5545
|
+
${SKILL_FALLBACK}`
|
|
5546
|
+
});
|
|
5254
5547
|
async function pullNextStep(dir) {
|
|
5255
5548
|
return `cd ${shellPath(dir)} && ${await pulledDevCommand(dir)}`;
|
|
5256
5549
|
}
|
|
@@ -5522,7 +5815,7 @@ async function callApi(creds, path, options = {}) {
|
|
|
5522
5815
|
throw new CliError(body.message, void 0, refusal);
|
|
5523
5816
|
}
|
|
5524
5817
|
if (response.status === 401 || response.status === 403) {
|
|
5525
|
-
throw new CliError("Not signed in, or this token cannot do that.",
|
|
5818
|
+
throw new CliError("Not signed in, or this token cannot do that.", `Try \`${WIRL_BIN} login\`.`, refusal);
|
|
5526
5819
|
}
|
|
5527
5820
|
if (response.status === 404) {
|
|
5528
5821
|
if (body.error !== void 0 && body.error !== "not_found" && body.message) throw new CliError(body.message, void 0, refusal);
|
|
@@ -5554,30 +5847,58 @@ async function callApiBytes(creds, path, options = {}) {
|
|
|
5554
5847
|
body = {};
|
|
5555
5848
|
}
|
|
5556
5849
|
if (response.status === 401 || response.status === 403) {
|
|
5557
|
-
throw new CliError("Not signed in, or this token cannot do that.",
|
|
5850
|
+
throw new CliError("Not signed in, or this token cannot do that.", `Try \`${WIRL_BIN} login\`.`);
|
|
5558
5851
|
}
|
|
5559
5852
|
if (response.status === 404) throw new CliError("Not found, or you do not have access to it.");
|
|
5560
5853
|
throw new CliError(body.message ?? body.error ?? `Request failed (${response.status}).`);
|
|
5561
5854
|
}
|
|
5562
5855
|
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
5563
5856
|
if (declared > MAX_PULL_BYTES) throw new CliError(`That is ${Math.round(declared / 1048576)} MB, more than wirl will pull.`);
|
|
5564
|
-
|
|
5857
|
+
let bytes;
|
|
5858
|
+
try {
|
|
5859
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
5860
|
+
} catch {
|
|
5861
|
+
throw new CliError(PULL_CUT, void 0, { code: "pull_cut" });
|
|
5862
|
+
}
|
|
5565
5863
|
if (bytes.byteLength > MAX_PULL_BYTES) throw new CliError("The download was larger than wirl will pull.");
|
|
5566
5864
|
return bytes;
|
|
5567
5865
|
}
|
|
5866
|
+
var PULL_CUT = "The pull was cut before it finished, so nothing was written. Run it again.";
|
|
5867
|
+
var pullShort = (dir) => `The pull's archive is cut short or damaged, so nothing was written to ${dir}. Run it again.`;
|
|
5568
5868
|
var MAX_PULL_BYTES = 256 * 1048576;
|
|
5569
5869
|
var pullApp = (creds, org, app, options = {}) => callApiBytes(creds, `/api/v1/apps/${seg(org)}/${seg(app)}/pull`, options.fetchImpl ? { fetchImpl: options.fetchImpl } : {});
|
|
5870
|
+
function isWholeArchive(archive) {
|
|
5871
|
+
return new Promise((resolve6, reject) => {
|
|
5872
|
+
let sound = true;
|
|
5873
|
+
let ended2 = false;
|
|
5874
|
+
const parser = new TarParser({ onReadEntry: (entry) => void entry.resume() });
|
|
5875
|
+
parser.on("warn", (code) => {
|
|
5876
|
+
if (code === "TAR_BAD_ARCHIVE" || code === "TAR_ENTRY_INVALID") sound = false;
|
|
5877
|
+
});
|
|
5878
|
+
parser.on("eof", () => {
|
|
5879
|
+
ended2 = true;
|
|
5880
|
+
});
|
|
5881
|
+
parser.on("error", (error) => typeof error.code === "string" && error.code.startsWith("Z_") ? resolve6(false) : reject(error));
|
|
5882
|
+
parser.on("end", () => resolve6(sound && ended2));
|
|
5883
|
+
parser.end(archive);
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5570
5886
|
async function unpackPull(bytes, dir, options = {}) {
|
|
5571
|
-
await
|
|
5572
|
-
|
|
5887
|
+
const existing = await readdir(dir).catch((error) => {
|
|
5888
|
+
if (error.code === "ENOENT") return [];
|
|
5889
|
+
throw error;
|
|
5890
|
+
});
|
|
5573
5891
|
if (existing.length > 0 && !options.force) {
|
|
5574
5892
|
throw new CliError(`${dir} is not empty.`, "Pull into a new directory, or pass --force to overwrite.");
|
|
5575
5893
|
}
|
|
5894
|
+
const archive = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
5895
|
+
if (!await isWholeArchive(archive)) throw new CliError(pullShort(dir), void 0, { code: "pull_cut" });
|
|
5896
|
+
await mkdir3(dir, { recursive: true });
|
|
5576
5897
|
await new Promise((resolve6, reject) => {
|
|
5577
5898
|
const extractor = tarExtract({ cwd: dir });
|
|
5578
5899
|
extractor.on("error", reject);
|
|
5579
5900
|
extractor.on("finish", resolve6);
|
|
5580
|
-
extractor.end(
|
|
5901
|
+
extractor.end(archive);
|
|
5581
5902
|
});
|
|
5582
5903
|
const written = [];
|
|
5583
5904
|
await collect(dir, dir, written);
|
|
@@ -5935,10 +6256,13 @@ function describeVisibility(level, org) {
|
|
|
5935
6256
|
}
|
|
5936
6257
|
}
|
|
5937
6258
|
var readVisibility = (creds, org, app) => callApi(creds, `/api/v1/apps/${seg(org)}/${seg(app)}`).then((r) => r.app.visibility ?? "unknown");
|
|
5938
|
-
|
|
5939
|
-
if (!VISIBILITY_LEVELS.includes(
|
|
5940
|
-
throw new CliError(`Visibility is private, org or public, not "${String(
|
|
6259
|
+
function refuseVisibilityLevel(level) {
|
|
6260
|
+
if (!VISIBILITY_LEVELS.includes(level)) {
|
|
6261
|
+
throw new CliError(`Visibility is private, org or public, not "${String(level)}".`);
|
|
5941
6262
|
}
|
|
6263
|
+
}
|
|
6264
|
+
async function changeVisibility(creds, options) {
|
|
6265
|
+
refuseVisibilityLevel(options.level);
|
|
5942
6266
|
if (options.level === "public") {
|
|
5943
6267
|
throw new CliError(
|
|
5944
6268
|
"Public means anyone on the internet with the link can open it, without signing in, so a person decides that with their own click.",
|
|
@@ -5953,11 +6277,59 @@ async function changeVisibility(creds, options) {
|
|
|
5953
6277
|
return result2.visibility;
|
|
5954
6278
|
}
|
|
5955
6279
|
var unmailedNote = (result2) => result2.mailed === false && result2.message ? ` ${result2.message}` : "";
|
|
6280
|
+
function refuseShareRole(value) {
|
|
6281
|
+
const read = readShareRole(value);
|
|
6282
|
+
if (read.ok) return read.role;
|
|
6283
|
+
const shown2 = typeof value === "string" ? terminalSafe(value.slice(0, 40)) : typeof value;
|
|
6284
|
+
throw new CliError(`Role is viewer or editor, not "${shown2}".`);
|
|
6285
|
+
}
|
|
5956
6286
|
var shareApp = (creds, org, app, email, role) => callApi(creds, `/api/v1/apps/${seg(org)}/${seg(app)}/share`, {
|
|
5957
6287
|
method: "POST",
|
|
5958
|
-
|
|
6288
|
+
// No role is no `role`: the control plane then keeps the one a person has.
|
|
6289
|
+
body: JSON.stringify(role === void 0 ? { email } : { email, role }),
|
|
5959
6290
|
contentType: "application/json"
|
|
5960
6291
|
});
|
|
6292
|
+
function shareSentence(result2, email, asked, voice) {
|
|
6293
|
+
const roleOf = (value) => value === "viewer" || value === "editor" ? value : void 0;
|
|
6294
|
+
const outcome = ["added", "kept", "unchanged", "changed"].includes(String(result2.outcome)) ? result2.outcome : void 0;
|
|
6295
|
+
const role = roleOf(result2.role);
|
|
6296
|
+
const old = outcome === void 0 || role === void 0;
|
|
6297
|
+
const now = role ?? asked ?? "viewer";
|
|
6298
|
+
const was = roleOf(result2.previousRole);
|
|
6299
|
+
const article = (name) => name === "editor" ? "an editor" : "a viewer";
|
|
6300
|
+
const mail = unmailedNote(result2);
|
|
6301
|
+
const signIn = voice === "cli" ? "the moment they first sign in" : "when they first sign in";
|
|
6302
|
+
if (result2.pending) {
|
|
6303
|
+
const wait = `They will have access ${signIn}.`;
|
|
6304
|
+
if (!old && outcome === "kept") return `${email} was already invited as ${now}, and no role was given, so it stays ${now}. ${wait}${mail}`;
|
|
6305
|
+
const lead2 = `Invited ${email} as ${now}. ${wait}`;
|
|
6306
|
+
if (old || outcome === "added") return `${lead2}${mail}`;
|
|
6307
|
+
if (outcome === "unchanged") return `${lead2} The invitation already said that.${mail}`;
|
|
6308
|
+
if (was === void 0) return `${lead2} The invitation's role was changed.${mail}`;
|
|
6309
|
+
return `${lead2} The invitation said ${was} before.${mail}`;
|
|
6310
|
+
}
|
|
6311
|
+
if (!old && outcome === "kept") {
|
|
6312
|
+
const change = voice === "cli" ? "Pass --role viewer or --role editor" : 'Pass role "viewer" or "editor"';
|
|
6313
|
+
return `${email} already had access as ${now}, and no role was given, so it stays ${now}. ${change} to change it.`;
|
|
6314
|
+
}
|
|
6315
|
+
const lead = voice === "cli" ? `Shared with ${email} as ${now}.` : `${email} can now open the app as ${now}.`;
|
|
6316
|
+
if (old || outcome === "added") return lead;
|
|
6317
|
+
if (outcome === "unchanged") return `${lead} They already had that role.`;
|
|
6318
|
+
if (was === void 0) return `${lead} Their role was changed.`;
|
|
6319
|
+
return `${lead} They were ${article(was)} before.`;
|
|
6320
|
+
}
|
|
6321
|
+
var unshareApp = (creds, org, app, email) => callApi(creds, `/api/v1/apps/${seg(org)}/${seg(app)}/share`, {
|
|
6322
|
+
method: "DELETE",
|
|
6323
|
+
body: JSON.stringify({ email }),
|
|
6324
|
+
contentType: "application/json"
|
|
6325
|
+
});
|
|
6326
|
+
function unshareSentence(result2, email) {
|
|
6327
|
+
const share = result2.shareRevoked ?? result2.revoked;
|
|
6328
|
+
const invitation = result2.invitationWithdrawn ?? false;
|
|
6329
|
+
const found = share ? invitation ? `Revoked ${email}'s share and withdrew the invitation still waiting for them.` : `Revoked ${email}'s share.` : invitation ? `Withdrew ${email}'s invitation.` : `${email} held no share on the app and had no invitation to it, so there was nothing to take back.`;
|
|
6330
|
+
const wasDeleted = result2.appDeleted === true && (share || invitation) ? " The app was deleted when this was asked, so a restore will not bring it back." : "";
|
|
6331
|
+
return `${found}${wasDeleted} That takes back only what was shared by name: the app's level (wirl_visibility) and the rights of its owner and the organisation's owners and admins are unchanged.`;
|
|
6332
|
+
}
|
|
5961
6333
|
var listDeployments = (creds, org, app) => callApi(
|
|
5962
6334
|
creds,
|
|
5963
6335
|
`/api/v1/apps/${seg(org)}/${seg(app)}/deployments`
|
|
@@ -6085,7 +6457,7 @@ var fetchLogs = (creds, org, app, filters) => callApi(
|
|
|
6085
6457
|
);
|
|
6086
6458
|
var LINE_INDENT = " ";
|
|
6087
6459
|
function terminalSafe(text) {
|
|
6088
|
-
return escapeInvisible(text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
6460
|
+
return escapeInvisible(text.replace(/\r\n/g, "\n").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
6089
6461
|
}
|
|
6090
6462
|
function utcSecond(at2) {
|
|
6091
6463
|
const ms = Date.parse(at2);
|
|
@@ -6121,7 +6493,8 @@ function formatLogPage(page, since) {
|
|
|
6121
6493
|
// queryable. Without this an empty answer right after a request
|
|
6122
6494
|
// reads as "nothing happened".
|
|
6123
6495
|
" A request made in the last minute may not have arrived yet: ask again shortly."
|
|
6124
|
-
)}
|
|
6496
|
+
)}
|
|
6497
|
+
${EDGE_REFUSALS_NOTE}` : page.invocations.map((entry) => formatInvocation(entry)).join("\n\n");
|
|
6125
6498
|
return page.cursor === null ? body : `${body}
|
|
6126
6499
|
|
|
6127
6500
|
More before this: pass cursor=${page.cursor}.`;
|
|
@@ -6521,20 +6894,22 @@ async function previewFetch(creds, org, app, name, path, options) {
|
|
|
6521
6894
|
const headers = callerHeaders(options.headers);
|
|
6522
6895
|
headers.set(VISIT_HEADER, mint.token);
|
|
6523
6896
|
const networkFetch = options.networkFetchImpl ?? fetch;
|
|
6524
|
-
const
|
|
6897
|
+
const clock = options.now ?? (() => performance.now());
|
|
6898
|
+
const sentAt = clock();
|
|
6899
|
+
const elapsed = () => Math.round(clock() - sentAt);
|
|
6525
6900
|
const response = await networkFetch(target.toString(), {
|
|
6526
6901
|
method: options.method ?? (options.body !== void 0 ? "POST" : "GET"),
|
|
6527
6902
|
headers,
|
|
6528
6903
|
redirect: "manual",
|
|
6529
6904
|
...options.body === void 0 ? {} : { body: options.body }
|
|
6530
6905
|
});
|
|
6531
|
-
const headersMs =
|
|
6906
|
+
const headersMs = elapsed();
|
|
6532
6907
|
const contentType = response.headers.get("content-type") ?? "";
|
|
6533
6908
|
const location = response.headers.get("location");
|
|
6534
6909
|
const text = isTextBody(contentType);
|
|
6535
6910
|
const streamWaitMs = Math.min(options.streamWaitMs ?? FETCH_STREAM_WAIT_MS, FETCH_WAIT_MAX_SECONDS * 1e3);
|
|
6536
6911
|
const keep = options.keepBodyBytes === void 0 ? null : Math.min(options.keepBodyBytes, FETCH_FILE_MAX_BYTES);
|
|
6537
|
-
const read = await readBounded(response, streamWaitMs,
|
|
6912
|
+
const read = await readBounded(response, streamWaitMs, elapsed, keep ?? FETCH_READ_CEILING_BYTES);
|
|
6538
6913
|
const bytes = read.bytes;
|
|
6539
6914
|
const truncated = text && (read.cut || bytes.byteLength > FETCH_BODY_CAP_BYTES);
|
|
6540
6915
|
const media = (contentType.split(";")[0] ?? "").trim().toLowerCase();
|
|
@@ -6610,7 +6985,7 @@ var FETCH_STREAM_WAIT_MS = 1e4;
|
|
|
6610
6985
|
var FETCH_WAIT_MAX_SECONDS = 300;
|
|
6611
6986
|
var FETCH_READ_CEILING_BYTES = 8 * FETCH_BODY_CAP_BYTES;
|
|
6612
6987
|
var FETCH_FILE_MAX_BYTES = 1e8;
|
|
6613
|
-
async function readBounded(response, waitMs,
|
|
6988
|
+
async function readBounded(response, waitMs, elapsed, ceilingBytes) {
|
|
6614
6989
|
if (response.body === null) return { bytes: new Uint8Array(0), chunks: [], stillStreaming: false, cut: false };
|
|
6615
6990
|
const reader = response.body.getReader();
|
|
6616
6991
|
const chunks = [];
|
|
@@ -6629,7 +7004,7 @@ async function readBounded(response, waitMs, sentAt, ceilingBytes) {
|
|
|
6629
7004
|
break;
|
|
6630
7005
|
}
|
|
6631
7006
|
if (next.done) break;
|
|
6632
|
-
chunks.push({ atMs:
|
|
7007
|
+
chunks.push({ atMs: elapsed(), bytes: next.value });
|
|
6633
7008
|
total += next.value.byteLength;
|
|
6634
7009
|
if (total > ceilingBytes) {
|
|
6635
7010
|
cut = true;
|
|
@@ -6674,9 +7049,9 @@ function formatFetchResult(result2, saved) {
|
|
|
6674
7049
|
0,
|
|
6675
7050
|
`answered after ${seconds(result2.headersMs)} s; ${said} line${said === 1 ? "" : "s"} in ${pieces} piece${pieces === 1 ? "" : "s"} over ${seconds(last)} s (each line shown at the time it was complete)`
|
|
6676
7051
|
);
|
|
7052
|
+
const unfinished = lastLineUnfinished(result2);
|
|
6677
7053
|
timed.forEach((line, i) => {
|
|
6678
|
-
|
|
6679
|
-
lines.push(line.text === "" ? "" : `+${seconds(line.atMs)}s ${line.text}${unfinished ? " (the line had not ended)" : ""}`);
|
|
7054
|
+
lines.push(line.text === "" ? "" : `+${seconds(line.atMs)}s ${line.text}${unfinished && i === timed.length - 1 ? " (the line had not ended)" : ""}`);
|
|
6680
7055
|
});
|
|
6681
7056
|
} else {
|
|
6682
7057
|
lines.push(result2.bodyText);
|
|
@@ -6689,6 +7064,44 @@ function formatFetchResult(result2, saved) {
|
|
|
6689
7064
|
}
|
|
6690
7065
|
return lines.join("\n");
|
|
6691
7066
|
}
|
|
7067
|
+
function lastLineUnfinished(result2) {
|
|
7068
|
+
return result2.timed !== null && !result2.timed.lastComplete && (!result2.bodyComplete || result2.truncated);
|
|
7069
|
+
}
|
|
7070
|
+
var STREAMED_DATA_MAX_LINES = 2e3;
|
|
7071
|
+
var STREAMED_DATA_MAX_TEXT_BYTES = 16 * 1024;
|
|
7072
|
+
function streamedFetchData(result2) {
|
|
7073
|
+
if (result2.bodyText === null || result2.timed === null) return null;
|
|
7074
|
+
const listed2 = listedLines(result2.timed.lines);
|
|
7075
|
+
return {
|
|
7076
|
+
status: result2.status,
|
|
7077
|
+
statusText: result2.statusText,
|
|
7078
|
+
contentType: result2.contentType,
|
|
7079
|
+
location: result2.location,
|
|
7080
|
+
refused: result2.refused,
|
|
7081
|
+
answeredAfterMs: result2.headersMs,
|
|
7082
|
+
streamWaitMs: result2.streamWaitMs,
|
|
7083
|
+
stillStreaming: result2.stillStreaming,
|
|
7084
|
+
bodyComplete: result2.bodyComplete,
|
|
7085
|
+
truncated: result2.truncated,
|
|
7086
|
+
bodyBytes: result2.bodyBytes,
|
|
7087
|
+
pieces: result2.timed.pieces,
|
|
7088
|
+
lastLineEnded: !lastLineUnfinished(result2),
|
|
7089
|
+
lines: listed2,
|
|
7090
|
+
linesLeftOut: result2.timed.lines.length - listed2.length
|
|
7091
|
+
};
|
|
7092
|
+
}
|
|
7093
|
+
function listedLines(all) {
|
|
7094
|
+
const listed2 = [];
|
|
7095
|
+
let bytes = 0;
|
|
7096
|
+
for (const line of all) {
|
|
7097
|
+
if (listed2.length >= STREAMED_DATA_MAX_LINES) break;
|
|
7098
|
+
const size = Buffer.byteLength(line.text);
|
|
7099
|
+
if (listed2.length > 0 && bytes + size > STREAMED_DATA_MAX_TEXT_BYTES) break;
|
|
7100
|
+
bytes += size;
|
|
7101
|
+
listed2.push({ atMs: line.atMs, text: line.text });
|
|
7102
|
+
}
|
|
7103
|
+
return listed2;
|
|
7104
|
+
}
|
|
6692
7105
|
function seconds(ms) {
|
|
6693
7106
|
return (ms / 1e3).toFixed(2);
|
|
6694
7107
|
}
|
|
@@ -6709,7 +7122,7 @@ var ADOPT_SENTENCES2 = {
|
|
|
6709
7122
|
*/
|
|
6710
7123
|
cannotTell: (what) => `wirl did not get the adopt route's answer (${what}), so it cannot tell whether the live database recorded migrations/${ADOPTED_FILE}, and nothing was written here. Run wirl migrations adopt again: while an adopted ${ADOPTED_FILE} is all the live database records, adopting again replaces that record and writes the file.`,
|
|
6711
7124
|
/** After the call, so the live database records the file; a CLI that reads files otherwise than the control plane says this every time. */
|
|
6712
|
-
notAsRecorded: (install) => `The live database now records migrations/${ADOPTED_FILE} as adopted, but the file wirl sent does not match its own record of it, so nothing was written here. Run wirl migrations adopt again: while an adopted ${ADOPTED_FILE} is all the live database records, adopting again replaces that record and writes the file. If it says this again, this copy of wirl reads migration files differently from the control plane: update it (npm install -g ${install}).`,
|
|
7125
|
+
notAsRecorded: (install) => `The live database now records migrations/${ADOPTED_FILE} as adopted, but the file wirl sent does not match its own record of it, so nothing was written here. Run wirl migrations adopt again: while an adopted ${ADOPTED_FILE} is all the live database records, adopting again replaces that record and writes the file. If it says this again, this copy of wirl reads migration files differently from the control plane: update it (npm install -g ${install.cli}${hasFallback(install) ? `, or, to match this control plane exactly, npm install -g ${install.tarball.cli}` : ""}).`,
|
|
6713
7126
|
unwritten: (code) => `The live database now records migrations/${ADOPTED_FILE} as adopted, but wirl could not write that file here (${code}). Make migrations/ a folder you can write that holds no .sql file, then run wirl migrations adopt again: while an adopted ${ADOPTED_FILE} is all the live database records, adopting again replaces that record and writes the file.`,
|
|
6714
7127
|
nothingWritten: "Nothing was written to migrations/.",
|
|
6715
7128
|
/**
|
|
@@ -6887,7 +7300,7 @@ async function adoptIntoProject(creds, options) {
|
|
|
6887
7300
|
const read = await readMigrations([{ name: ADOPTED_FILE, text: answer.file.text }], []);
|
|
6888
7301
|
const [file, ...others] = read.ok ? read.files : [];
|
|
6889
7302
|
if (file === void 0 || others.length > 0 || file.sha256 !== answer.file.sha256 || file.statements.length !== answer.file.statements) {
|
|
6890
|
-
throw new CliError(ADOPT_SENTENCES2.notAsRecorded(installFor(creds.apiOrigin)
|
|
7303
|
+
throw new CliError(ADOPT_SENTENCES2.notAsRecorded(installFor(creds.apiOrigin)));
|
|
6891
7304
|
}
|
|
6892
7305
|
const path = await writeAdopted(projectDir, answer.file.text);
|
|
6893
7306
|
const { recordLocalAdoption: recordLocalAdoption2 } = await Promise.resolve().then(() => (init_migrations_local(), migrations_local_exports));
|
|
@@ -7039,6 +7452,8 @@ function formatMigrationsListing(listing, app) {
|
|
|
7039
7452
|
init_src();
|
|
7040
7453
|
import { hostname } from "node:os";
|
|
7041
7454
|
import { basename as basename3, resolve as resolve5 } from "node:path";
|
|
7455
|
+
var reads = (title) => ({ title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false });
|
|
7456
|
+
var readsTheirText = (title) => ({ ...reads(title), openWorldHint: true });
|
|
7042
7457
|
var APPS_DOMAIN = "wirl.run";
|
|
7043
7458
|
var pendingLogin = null;
|
|
7044
7459
|
function loginInstructions(login) {
|
|
@@ -7121,6 +7536,7 @@ var SHARED = [
|
|
|
7121
7536
|
{
|
|
7122
7537
|
name: "wirl_list_connections",
|
|
7123
7538
|
description: `List the connections in the organisation: named credentials an app may be granted, such as a HubSpot or Stripe key, with the hosts each allows, who made each (by name and email) and whether it holds its key, is waiting for one, or needs none. Use this to learn the exact name to put under "connections" in wirl.json before deploying an app that needs one; a name already listed here is that connection, not a new one, and other apps may be granted it too. The app never receives the key: it calls the vendor's own real URL (fetch("https://api.hubspot.com/...")) and wirl attaches the key on the way out. Declaring one at deploy grants it at once only when you made it or are an org admin, signed in with full access, it was never revoked from that app, and no request for it is already waiting; otherwise, unless the app already holds it, it opens or keeps a request that its maker or an org admin approves. There is no tool to approve one.`,
|
|
7539
|
+
annotations: reads("List connections"),
|
|
7124
7540
|
inputSchema: {
|
|
7125
7541
|
type: "object",
|
|
7126
7542
|
properties: {
|
|
@@ -7143,27 +7559,55 @@ var SHARED = [
|
|
|
7143
7559
|
},
|
|
7144
7560
|
{
|
|
7145
7561
|
name: "wirl_share",
|
|
7146
|
-
description: `Give a named colleague access to a deployed app, by email address. Use this when the user says someone should be able to see what they built. Access belongs to that person and can be taken back. Viewers are free \u2014 sharing never costs anything, so there is no reason to hesitate. Role "viewer" can open it; "editor" can also deploy to it. A connection with deploy-only access cannot share; if the server says so, tell the person to open the app's Access page in the dashboard instead.`,
|
|
7562
|
+
description: `Give a named colleague access to a deployed app, by email address. Use this when the user says someone should be able to see what they built. Access belongs to that person and can be taken back. Viewers are free \u2014 sharing never costs anything, so there is no reason to hesitate. Role "viewer" can open it; "editor" can also deploy to it. Sharing again with no role keeps the role a person has: pass "role" only to give or change one, and the answer says what the role is and, when it changed one, what it was. A connection with deploy-only access cannot share; if the server says so, tell the person to open the app's Access page in the dashboard instead.`,
|
|
7563
|
+
annotations: { title: "Share an app with a person", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
7147
7564
|
inputSchema: {
|
|
7148
7565
|
type: "object",
|
|
7149
7566
|
properties: {
|
|
7150
7567
|
app: { type: "string", description: 'The app slug, e.g. "quarterly-dashboard".' },
|
|
7151
7568
|
email: { type: "string", description: "Work email address of the person." },
|
|
7152
|
-
role: {
|
|
7569
|
+
role: {
|
|
7570
|
+
type: "string",
|
|
7571
|
+
enum: ["viewer", "editor"],
|
|
7572
|
+
description: "Leave it out unless the person should be given or moved to a role: a new person is a viewer, and someone who already has access, or is already invited, keeps the role they have. Naming one sets it, lowering an editor to viewer included."
|
|
7573
|
+
},
|
|
7574
|
+
org: { type: "string", description: "Organisation slug. Optional when the person belongs to only one; otherwise the tool says which ones exist and asks." }
|
|
7575
|
+
},
|
|
7576
|
+
required: ["app", "email"]
|
|
7577
|
+
},
|
|
7578
|
+
async handler(args, ctx) {
|
|
7579
|
+
const role = refuseShareRole(args["role"]);
|
|
7580
|
+
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
7581
|
+
const email = str(args, "email");
|
|
7582
|
+
const result2 = await shareApp(ctx.creds, org, str(args, "app"), email, role);
|
|
7583
|
+
return shareSentence(result2, email, role, "mcp");
|
|
7584
|
+
}
|
|
7585
|
+
},
|
|
7586
|
+
{
|
|
7587
|
+
name: "wirl_unshare",
|
|
7588
|
+
description: `Take back a named person's access to a deployed app, by the email address it was shared with. Use this when the user says someone should no longer see what they built. It takes the whole share away, one person at a time; to change a role instead, use wirl_share with "role". It also withdraws an invitation that is still waiting for an address, and works while the app is deleted and can still be restored. It says what it found; finding nothing is not an error. Needs a sign-in with full access, like wirl_share.`,
|
|
7589
|
+
annotations: { title: "Take back a share", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7590
|
+
inputSchema: {
|
|
7591
|
+
type: "object",
|
|
7592
|
+
properties: {
|
|
7593
|
+
app: { type: "string", description: 'The app slug, e.g. "quarterly-dashboard".' },
|
|
7594
|
+
email: { type: "string", description: "The email address the app was shared with." },
|
|
7153
7595
|
org: { type: "string", description: "Organisation slug. Optional when the person belongs to only one; otherwise the tool says which ones exist and asks." }
|
|
7154
7596
|
},
|
|
7155
7597
|
required: ["app", "email"]
|
|
7156
7598
|
},
|
|
7157
7599
|
async handler(args, ctx) {
|
|
7600
|
+
refuseUnshareArgs(args);
|
|
7158
7601
|
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
7159
|
-
const
|
|
7160
|
-
const result2 = await
|
|
7161
|
-
return
|
|
7602
|
+
const email = str(args, "email");
|
|
7603
|
+
const result2 = await unshareApp(ctx.creds, org, str(args, "app"), email);
|
|
7604
|
+
return unshareSentence(result2, email);
|
|
7162
7605
|
}
|
|
7163
7606
|
},
|
|
7164
7607
|
{
|
|
7165
7608
|
name: "wirl_visibility",
|
|
7166
7609
|
description: `See or change who can open an app. With no level it only reports the current one. "private": only its owner, the organisation's owners and admins, and the people it is shared with by email. "org": everyone signed in at the organisation. Use this when the person says who the app is for ("only me", "just my team and whoever I share it with", "the whole company"). It cannot make an app public, and nothing an agent can call can: anyone on the internet being able to open it is a decision a person takes with their own click, on the app's Access page in the dashboard, so if they ask for that, give them that link. To give one named person access use wirl_share instead. A connection with deploy-only access cannot change this either; if the server says so, send the person to the same Access page.`,
|
|
7610
|
+
annotations: { title: "See or set who can open an app", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7167
7611
|
inputSchema: {
|
|
7168
7612
|
type: "object",
|
|
7169
7613
|
properties: {
|
|
@@ -7187,6 +7631,7 @@ var SHARED = [
|
|
|
7187
7631
|
{
|
|
7188
7632
|
name: "wirl_list_apps",
|
|
7189
7633
|
description: "List the apps the user can open, with their URLs and who can see them. Use this to find an app before deploying to it or sharing it, or when the user asks what they have already deployed.",
|
|
7634
|
+
annotations: reads("List apps"),
|
|
7190
7635
|
inputSchema: { type: "object", properties: { org: { type: "string", description: "Organisation slug. Optional when the person belongs to only one; otherwise the tool says which ones exist and asks." } } },
|
|
7191
7636
|
async handler(args, ctx) {
|
|
7192
7637
|
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
@@ -7198,6 +7643,7 @@ var SHARED = [
|
|
|
7198
7643
|
{
|
|
7199
7644
|
name: "wirl_list_versions",
|
|
7200
7645
|
description: "List past deployments of an app, newest first, with their version numbers and ids. Use this before rolling back, to find which version to return to. A preview takes a version number from the same sequence and is never listed here, so the numbers can skip: a first deploy after one preview is v2.",
|
|
7646
|
+
annotations: reads("List app versions"),
|
|
7201
7647
|
inputSchema: {
|
|
7202
7648
|
type: "object",
|
|
7203
7649
|
properties: {
|
|
@@ -7216,6 +7662,7 @@ var SHARED = [
|
|
|
7216
7662
|
{
|
|
7217
7663
|
name: "wirl_rollback",
|
|
7218
7664
|
description: "Put an earlier deployment back to being the live one. Use this when a deploy broke something and the user wants the previous version back. Instant \u2014 nothing is rebuilt. Get the deployment id from wirl_list_versions first.",
|
|
7665
|
+
annotations: { title: "Roll an app back", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7219
7666
|
inputSchema: {
|
|
7220
7667
|
type: "object",
|
|
7221
7668
|
properties: {
|
|
@@ -7237,6 +7684,7 @@ var SHARED = [
|
|
|
7237
7684
|
{
|
|
7238
7685
|
name: "wirl_migrations",
|
|
7239
7686
|
description: "List what one of an app's databases has applied, read only: its own record of migrations (_wirl_migrations), read now, and wirl's ledger of what it sent there, one line a file, saying where the two differ. Use this before changing anything when a deploy was refused over a migration, or said it could not tell whether a file applied. It writes nothing, and needs the access a deploy needs.",
|
|
7687
|
+
annotations: reads("Show applied migrations"),
|
|
7240
7688
|
inputSchema: {
|
|
7241
7689
|
type: "object",
|
|
7242
7690
|
properties: {
|
|
@@ -7257,6 +7705,7 @@ var SHARED = [
|
|
|
7257
7705
|
{
|
|
7258
7706
|
name: "wirl_list_schedules",
|
|
7259
7707
|
description: "List an app's schedules (from its wirl.json) with the last run's outcome and the next run time. Use this to see whether a scheduled job is healthy.",
|
|
7708
|
+
annotations: reads("List schedules"),
|
|
7260
7709
|
inputSchema: {
|
|
7261
7710
|
type: "object",
|
|
7262
7711
|
properties: {
|
|
@@ -7275,6 +7724,7 @@ var SHARED = [
|
|
|
7275
7724
|
{
|
|
7276
7725
|
name: "wirl_run",
|
|
7277
7726
|
description: "Run one of an app's schedules right now and wait for the outcome (up to the schedule's timeout). Use this after deploying a schedule to check it works, instead of waiting for the clock. With `preview`, runs one of a preview's own schedules instead: it must be declared in the previewed wirl.json (a preview writes no schedule rows, so nothing of its fires on a clock), and the outcome comes back at once, with no queue to wait on.",
|
|
7727
|
+
annotations: { title: "Run a schedule now", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
7278
7728
|
inputSchema: {
|
|
7279
7729
|
type: "object",
|
|
7280
7730
|
properties: {
|
|
@@ -7308,6 +7758,7 @@ var SHARED = [
|
|
|
7308
7758
|
{
|
|
7309
7759
|
name: "wirl_list_flows",
|
|
7310
7760
|
description: `List the flows an app declares under "flows" in its wirl.json, each with its step timeout, its retries, how many of its runs are unfinished and its last run. Use this to find a flow's exact name before wirl_start_flow, or to see whether runs are piling up. With preview, lists what that preview's own build declares instead of the live app's.`,
|
|
7761
|
+
annotations: reads("List flows"),
|
|
7311
7762
|
inputSchema: {
|
|
7312
7763
|
type: "object",
|
|
7313
7764
|
properties: {
|
|
@@ -7328,6 +7779,7 @@ var SHARED = [
|
|
|
7328
7779
|
{
|
|
7329
7780
|
name: "wirl_start_flow",
|
|
7330
7781
|
description: "Start a run of one of an app's flows. Use this to try a flow you have built: put it up with wirl_preview, start it here with preview and fast (fast, for a preview's run only, makes each sleep, until and retry delay the flow asks for 5 seconds, each wait's timeout 60 seconds, and each retry of a failing step 10 seconds, 90 after a try that ran out of time; wirl_retry_run still refuses a failed run until 4 minutes after it failed), follow it with wirl_show_run, answer each wait it reaches with wirl_send_event, and read what each step printed with wirl_logs with the run's id, and with preview for a preview's run. A key names the run in the app's own terms: starting the flow again with the same key finds the run it started instead of starting another, until 30 days after that run ends, so give each new try a new key. The input is at most 64 KiB: keep rows in env.DB and pass ids. Where flows are not on for the organisation yet, the answer says so and nothing starts.",
|
|
7782
|
+
annotations: { title: "Start a flow run", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
7331
7783
|
inputSchema: {
|
|
7332
7784
|
type: "object",
|
|
7333
7785
|
properties: {
|
|
@@ -7373,6 +7825,7 @@ var SHARED = [
|
|
|
7373
7825
|
{
|
|
7374
7826
|
name: "wirl_list_runs",
|
|
7375
7827
|
description: "List an app's flow runs, newest first, each with its status, the step it is at and when it started. Use this to find a run to look at with wirl_show_run, or to see which runs failed or are waiting. Narrow it with flow and status. A preview's runs are listed only with preview, and the live app's only without it.",
|
|
7828
|
+
annotations: reads("List flow runs"),
|
|
7376
7829
|
inputSchema: {
|
|
7377
7830
|
type: "object",
|
|
7378
7831
|
properties: {
|
|
@@ -7397,6 +7850,7 @@ var SHARED = [
|
|
|
7397
7850
|
{
|
|
7398
7851
|
name: "wirl_show_run",
|
|
7399
7852
|
description: `Show one flow run: its status, who started it, what it waits for or sleeps until or, while wirl is still creating it, when wirl tries again, the step it goes on with next and since or from when (a step reports only when it ends, so one that is running says it has not reported yet), its output or error, its page on the dashboard, its newest ${SHOWN_RUN_ROWS} attempts with the version of the app that answered each and each attempt's deadline, and the earlier attempts that went wrong, and its newest ${SHOWN_RUN_ROWS} events with who sent each and whether it reached the run (the run's page lists every one). Use this after wirl_start_flow to follow a run step by step, and to learn which event a waiting run asks for before wirl_send_event. A preview's run is found only with preview, and a live run only without it.`,
|
|
7853
|
+
annotations: readsTheirText("Show a flow run"),
|
|
7400
7854
|
inputSchema: {
|
|
7401
7855
|
type: "object",
|
|
7402
7856
|
properties: {
|
|
@@ -7417,6 +7871,7 @@ var SHARED = [
|
|
|
7417
7871
|
{
|
|
7418
7872
|
name: "wirl_send_event",
|
|
7419
7873
|
description: "Answer what a preview's run waits for: send it the event its wait asks for, with an optional JSON object as the event's body. Use this to walk a flow through its waits while trying it on a preview, once wirl_show_run says which event the run waits for. The event is sent as you, so its from.email is your own address, and an app that takes a decision only from people it lists takes yours only if you are listed; to try that path, list yourself in the build you preview, and never loosen the check. It is for a preview's runs only: a live run is answered by a person, on the app's own page or the run's page on the dashboard, so this refuses a live run before sending anything and says where the person goes. Only a wait open for that event takes it, and only the first answer to a wait counts.",
|
|
7874
|
+
annotations: { title: "Answer a wait on a preview run", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
7420
7875
|
inputSchema: {
|
|
7421
7876
|
type: "object",
|
|
7422
7877
|
properties: {
|
|
@@ -7458,6 +7913,7 @@ var SHARED = [
|
|
|
7458
7913
|
{
|
|
7459
7914
|
name: "wirl_cancel_run",
|
|
7460
7915
|
description: "Cancel a flow run: a step already running is stopped where it is. Use this when a run is stuck, was started by mistake, or is doing the wrong thing. A run that had already finished is left as it is, and the answer says so. A preview's run is found only with preview, and a live run only without it.",
|
|
7916
|
+
annotations: { title: "Cancel a flow run", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
7461
7917
|
inputSchema: {
|
|
7462
7918
|
type: "object",
|
|
7463
7919
|
properties: {
|
|
@@ -7478,6 +7934,7 @@ var SHARED = [
|
|
|
7478
7934
|
{
|
|
7479
7935
|
name: "wirl_retry_run",
|
|
7480
7936
|
description: "Run a failed flow run again, from the step it failed at, or from an earlier step with from. Use this when what made the run fail has passed, such as a vendor that was down, or once a fix is deployed live, rather than starting a new run. A run can be retried from four minutes after it failed until three days after, while Cloudflare still keeps it; outside that the answer says why. A preview's failed run cannot be retried once the preview has been deployed again or removed, or has expired: start a new run. A preview's run is found only with preview, and a live run only without it. Where flows are not on for the organisation yet, the answer says so.",
|
|
7937
|
+
annotations: { title: "Retry a failed flow run", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
7481
7938
|
inputSchema: {
|
|
7482
7939
|
type: "object",
|
|
7483
7940
|
properties: {
|
|
@@ -7507,6 +7964,7 @@ var SHARED = [
|
|
|
7507
7964
|
{
|
|
7508
7965
|
name: "wirl_logs",
|
|
7509
7966
|
description: "What a deployed app printed, grouped per request (page load, webhook, schedule run, flow step), newest first, with the status and any exception. Use this first when a deploy misbehaves, a schedule or webhook failed, or someone reports an error, before changing code. Defaults to the last hour. A line usually appears within a minute of the request, not at once; if the run you expect is missing, wait and read again. With `preview`, reads that preview's own script instead of the live app's history. Logs are what the app printed, as far as Cloudflare kept them: Workers Logs can miss a request's lines, all of them at times. A run's record is wirl_show_run and the run's page, not its logs: never count a run's steps from them.",
|
|
7967
|
+
annotations: readsTheirText("Read app logs"),
|
|
7510
7968
|
inputSchema: {
|
|
7511
7969
|
type: "object",
|
|
7512
7970
|
properties: {
|
|
@@ -7559,6 +8017,7 @@ var SHARED = [
|
|
|
7559
8017
|
{
|
|
7560
8018
|
name: "wirl_egress",
|
|
7561
8019
|
description: "What a deployed app called out through wirl, call by call, newest first: when, the method and host, the connection, the status, and allowed or the code wirl refused it with; a flow step's call also names its run, step number n and attempt, and the path it called, never its query string. No header or body is recorded. Use this to check which calls a page, a schedule or a flow step actually made to a vendor, or why one was refused, before changing code: two calls of a retried step differ by attempt. Defaults to the last hour, at most seven days back. A flow step's call refused because its deadline had passed is not recorded. Only the app's own calls, for someone who can deploy it, as its logs are.",
|
|
8020
|
+
annotations: readsTheirText("Read app outgoing calls"),
|
|
7562
8021
|
inputSchema: {
|
|
7563
8022
|
type: "object",
|
|
7564
8023
|
properties: {
|
|
@@ -7590,6 +8049,7 @@ var SHARED = [
|
|
|
7590
8049
|
{
|
|
7591
8050
|
name: "wirl_list_previews",
|
|
7592
8051
|
description: `List an app's previews: folders put up at their own address, never live, each with its own database (its tables from migrations/, and at first no rows but the ones those files insert). Use this to find a preview's exact name before calling wirl_preview_fetch or wirl_delete_preview, or to see when one expires. An app holds at most ${MAX_PREVIEWS_PER_APP} previews at once, each for up to ${PREVIEW_TTL_DAYS} days unless redeployed.`,
|
|
8052
|
+
annotations: reads("List previews"),
|
|
7593
8053
|
inputSchema: {
|
|
7594
8054
|
type: "object",
|
|
7595
8055
|
properties: {
|
|
@@ -7607,7 +8067,8 @@ var SHARED = [
|
|
|
7607
8067
|
},
|
|
7608
8068
|
{
|
|
7609
8069
|
name: "wirl_preview_fetch",
|
|
7610
|
-
description: `Make one request to a preview and show what it answers: the status, the content type, and the body as text \u2014 the page source for HTML, so you can see what the app actually rendered and fix it. Use this after wirl_preview instead of asking the person to open the link and describe what they see. A binary body reports its type and size, not its bytes. A text body over 100 KB is cut, and the answer says so. A stream (server-sent events, newline-delimited JSON, or any text still arriving after 10 seconds) is shown with the time each line arrived, as far as it got, and says whether it was still going: that is how to check a stream arrives piece by piece. A preview's own database starts with the tables in migrations/ and no rows but the ones those files insert, so a page that expects rows may look blank until something writes to it, and its schedules do not run on a clock. A 401, or a redirect back to sign in, means the visit was refused or the preview has expired or been deleted, not that the app itself answered that way. The headers argument cannot set host, cookie, content-length, or any x-wirl-* header (including x-wirl-visit): those are wirl's own, and are dropped rather than sent. This tool cannot send a file or save an answer as bytes: body is sent as text, exactly as given (an @ in it is just a character), and a binary answer reports only its type and size. From a shell, wirl fetch --data @file sends a file and --output <file> saves the answer, each up to 100,000,000 bytes.`,
|
|
8070
|
+
description: `Make one request to a preview and show what it answers: the status, the content type, and the body as text \u2014 the page source for HTML, so you can see what the app actually rendered and fix it. Use this after wirl_preview instead of asking the person to open the link and describe what they see. A binary body reports its type and size, not its bytes. A text body over 100 KB is cut, and the answer says so. A stream (server-sent events, newline-delimited JSON, or any text still arriving after 10 seconds) is shown with the time each line arrived, as far as it got, and says whether it was still going: that is how to check a stream arrives piece by piece. A stream also comes back as data, in the result's structuredContent: the same lines as { atMs, text }, atMs being whole milliseconds from sending the request to the read that completed the line (lines that arrived together share one), the first ${STREAMED_DATA_MAX_LINES.toLocaleString("en-US")} lines or ${String(STREAMED_DATA_MAX_TEXT_BYTES / 1024)} KB of their text, whichever comes first (always at least the first line), with linesLeftOut counting any more (the text shows them all), and the status, the content type and the flags stillStreaming, bodyComplete, truncated and lastLineEnded; any other answer is the text alone. A preview's own database starts with the tables in migrations/ and no rows but the ones those files insert, so a page that expects rows may look blank until something writes to it, and its schedules do not run on a clock. A 401, or a redirect back to sign in, means the visit was refused or the preview has expired or been deleted, not that the app itself answered that way. The headers argument cannot set host, cookie, content-length, or any x-wirl-* header (including x-wirl-visit): those are wirl's own, and are dropped rather than sent. This tool cannot send a file or save an answer as bytes: body is sent as text, exactly as given (an @ in it is just a character), and a binary answer reports only its type and size. From a shell, wirl fetch --data @file sends a file and --output <file> saves the answer, each up to 100,000,000 bytes.`,
|
|
8071
|
+
annotations: { title: "Request a page from a preview", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
7611
8072
|
inputSchema: {
|
|
7612
8073
|
type: "object",
|
|
7613
8074
|
properties: {
|
|
@@ -7625,24 +8086,15 @@ var SHARED = [
|
|
|
7625
8086
|
},
|
|
7626
8087
|
required: ["app", "name", "path"]
|
|
7627
8088
|
},
|
|
8089
|
+
answer: previewFetchAnswer,
|
|
7628
8090
|
async handler(args, ctx) {
|
|
7629
|
-
|
|
7630
|
-
const headersArg = args["headers"];
|
|
7631
|
-
const headers = headersArg !== null && typeof headersArg === "object" && !Array.isArray(headersArg) ? Object.fromEntries(Object.entries(headersArg).map(([k, v]) => [k, String(v)])) : void 0;
|
|
7632
|
-
const appsDomain = ctx.appsDomain ?? (await whoami(ctx.creds)).appsDomain;
|
|
7633
|
-
const result2 = await previewFetch(ctx.creds, org, str(args, "app"), str(args, "name"), str(args, "path", "/"), {
|
|
7634
|
-
appsDomain,
|
|
7635
|
-
...ctx.networkFetch ? { networkFetchImpl: ctx.networkFetch } : {},
|
|
7636
|
-
...str(args, "method") ? { method: str(args, "method") } : {},
|
|
7637
|
-
...typeof args["body"] === "string" ? { body: args["body"] } : {},
|
|
7638
|
-
...headers ? { headers } : {}
|
|
7639
|
-
});
|
|
7640
|
-
return formatFetchResult(result2);
|
|
8091
|
+
return (await previewFetchAnswer(args, ctx)).text;
|
|
7641
8092
|
}
|
|
7642
8093
|
},
|
|
7643
8094
|
{
|
|
7644
8095
|
name: "wirl_delete_preview",
|
|
7645
8096
|
description: `Remove a preview: its script, its files, its database and its address all go away. Use this to free a slot when an app already has ${MAX_PREVIEWS_PER_APP} previews (the most it can hold at once) and a new one is needed, or when a preview is no longer wanted before its own expiry. Idempotent: deleting one already gone still succeeds.`,
|
|
8097
|
+
annotations: { title: "Delete a preview", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7646
8098
|
inputSchema: {
|
|
7647
8099
|
type: "object",
|
|
7648
8100
|
properties: {
|
|
@@ -7665,6 +8117,7 @@ var SHARED = [
|
|
|
7665
8117
|
{
|
|
7666
8118
|
name: "wirl_delete_app",
|
|
7667
8119
|
description: `Delete an app. Use this only when the person has asked for this app to be deleted; to try something, use wirl_preview. It is a soft delete: the app's address and its previews stop answering at once, its schedules stop and its unfinished runs are cancelled, and its data is kept for ${String(RETENTION.deletedAppGraceDays)} days. Until then its owner, while a member of the organisation, or an organisation owner or admin can bring it back with wirl apps restore <app> --org <org>, or from the Recently deleted list on the organisation's tools page in the dashboard; an app that was public comes back only from that list. Its name stays taken for good. Needs a full-scope sign-in: a deploy-scoped token is refused, with where to do it instead.`,
|
|
8120
|
+
annotations: { title: "Delete an app", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7668
8121
|
inputSchema: {
|
|
7669
8122
|
type: "object",
|
|
7670
8123
|
properties: {
|
|
@@ -7685,6 +8138,7 @@ var SHARED = [
|
|
|
7685
8138
|
{
|
|
7686
8139
|
name: "wirl_whoami",
|
|
7687
8140
|
description: "Show which account this session is signed in as and which organisations it belongs to. Use this to check the connection works, or to find an organisation slug.",
|
|
8141
|
+
annotations: reads("Show who is signed in"),
|
|
7688
8142
|
inputSchema: { type: "object", properties: {} },
|
|
7689
8143
|
async handler(_args, ctx) {
|
|
7690
8144
|
const me = await whoami(ctx.creds);
|
|
@@ -7692,9 +8146,26 @@ var SHARED = [
|
|
|
7692
8146
|
}
|
|
7693
8147
|
}
|
|
7694
8148
|
];
|
|
8149
|
+
async function previewFetchAnswer(args, ctx) {
|
|
8150
|
+
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
8151
|
+
const headersArg = args["headers"];
|
|
8152
|
+
const headers = headersArg !== null && typeof headersArg === "object" && !Array.isArray(headersArg) ? Object.fromEntries(Object.entries(headersArg).map(([k, v]) => [k, String(v)])) : void 0;
|
|
8153
|
+
const appsDomain = ctx.appsDomain ?? (await whoami(ctx.creds)).appsDomain;
|
|
8154
|
+
const result2 = await previewFetch(ctx.creds, org, str(args, "app"), str(args, "name"), str(args, "path", "/"), {
|
|
8155
|
+
appsDomain,
|
|
8156
|
+
...ctx.networkFetch ? { networkFetchImpl: ctx.networkFetch } : {},
|
|
8157
|
+
...ctx.now ? { now: ctx.now } : {},
|
|
8158
|
+
...str(args, "method") ? { method: str(args, "method") } : {},
|
|
8159
|
+
...typeof args["body"] === "string" ? { body: args["body"] } : {},
|
|
8160
|
+
...headers ? { headers } : {}
|
|
8161
|
+
});
|
|
8162
|
+
const data = streamedFetchData(result2);
|
|
8163
|
+
return { text: formatFetchResult(result2), ...data === null ? {} : { structured: { ...data } } };
|
|
8164
|
+
}
|
|
7695
8165
|
var LOGIN = {
|
|
7696
8166
|
name: "wirl_login",
|
|
7697
8167
|
description: "Sign this machine in to wirl so the other wirl tools work. Use this the first time, or whenever another wirl tool says it is not signed in. It returns a link and a short code: show both to the person, ask them to open the link and approve, then retry whatever you were doing. Nothing to paste back \u2014 the token is saved for you once they approve.",
|
|
8168
|
+
annotations: { title: "Sign in to wirl", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7698
8169
|
inputSchema: { type: "object", properties: {} },
|
|
7699
8170
|
anonymous: true,
|
|
7700
8171
|
async handler() {
|
|
@@ -7712,7 +8183,8 @@ var LOGIN = {
|
|
|
7712
8183
|
var EXISTING_CONNECTION_SENTENCE = "A declared name the org already has is that connection, not a new one: the answer names who made it, whether it holds a key and the other apps you can open that use it. Declaring a kind it does not have is refused, unless a deploy, never a preview, may change it: it has no key, you made it or are an org admin, no other app is granted it, and neither kind is webhook_secret. ";
|
|
7713
8184
|
var LOCAL_DEPLOY = {
|
|
7714
8185
|
name: "wirl_deploy",
|
|
7715
|
-
description: `Deploy a directory of files to a URL that colleagues can open. Use this when the user has built something \u2014 a dashboard, a report, an internal tool \u2014 and wants it somewhere real instead of on their laptop. A new app is visible to everyone in the org when the org came from a company email domain; in any other org, including a personal one or one made with wirl orgs create, it starts private. Either way, share it by email to add someone outside that. Returns the URL, and who can open the app as the edge has it when the deploy finishes (a visibility change made while it ran included). Creates the app on first deploy. A wirl.json beside or inside the directory declares a "server" module for a backend and any "connections" the app needs; the answer says which declared connections are usable now, which are waiting on an admin and which need a key, and names the flows the version declares. New files in migrations/ are applied to the app's database before the version goes live; one that fails fails the deploy, and the version already live, if any, keeps serving. A destructive file (one wirl.json's "destructive" list must name) waits for a person before it applies to the live database: the refusal gives the address of the page where they approve it, and nothing this tool takes skips that. ` + EXISTING_CONNECTION_SENTENCE + "Refuses, without uploading anything, if the directory contains what looks like a live API key or password.",
|
|
8186
|
+
description: `Deploy a directory of files to a URL that colleagues can open. Use this when the user has built something \u2014 a dashboard, a report, an internal tool \u2014 and wants it somewhere real instead of on their laptop. A new app is visible to everyone in the org when the org came from a company email domain; in any other org, including a personal one or one made with wirl orgs create, it starts private. Either way, share it by email to add someone outside that. Returns the URL, and who can open the app as the edge has it when the deploy finishes (a visibility change made while it ran included). Creates the app on first deploy. A wirl.json beside or inside the directory declares a "server" module for a backend and any "connections" the app needs; the answer says which declared connections are usable now, which are waiting on an admin and which need a key, and names the flows the version declares. New files in migrations/ are applied to the app's database before the version goes live; one that fails fails the deploy, and the version already live, if any, keeps serving. A destructive file (one wirl.json's "destructive" list must name) waits for a person before it applies to the live database: the refusal gives the address of the page where they approve it, and nothing this tool takes skips that. ` + EXISTING_CONNECTION_SENTENCE + "Refuses, without uploading anything, if the directory contains what looks like a live API key or password. " + versionNumbersSentence("wirl_list_versions"),
|
|
8187
|
+
annotations: { title: "Deploy an app", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
7716
8188
|
inputSchema: {
|
|
7717
8189
|
type: "object",
|
|
7718
8190
|
properties: {
|
|
@@ -7781,6 +8253,7 @@ function connectionLines(connections) {
|
|
|
7781
8253
|
var LOCAL_PULL = {
|
|
7782
8254
|
name: "wirl_pull",
|
|
7783
8255
|
description: "Fetch the source of a deployed app into a local directory, to change it and deploy again. Use this when the user wants to work on an app a colleague built and shared with them, or to recover the source of one of their own. Anyone who can open the app can pull it. Refuses a directory that already has files in it unless told to overwrite.",
|
|
8256
|
+
annotations: { title: "Pull an app source", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
7784
8257
|
inputSchema: {
|
|
7785
8258
|
type: "object",
|
|
7786
8259
|
properties: {
|
|
@@ -7809,6 +8282,7 @@ var PREVIEW_LOGS_SENTENCE = "Deploying a preview again under its name replaces i
|
|
|
7809
8282
|
var LOCAL_PREVIEW = {
|
|
7810
8283
|
name: "wirl_preview",
|
|
7811
8284
|
description: "Put a directory of files up as a preview: a version of an app that is never live, at its own address, with its own database (every file in migrations/ applied, and no rows but the ones those files insert, the first time that name is used), open to the same people who can already open the app. Use this to preview before deploying \u2014 before wirl_deploy, whenever a change has not been checked yet \u2014 then look at what it actually rendered with wirl_preview_fetch instead of asking the person to open a link and describe what they see. It uses the app's real connections (as wirl_deploy's does, the answer says which declared connections are usable now, which are waiting on an admin and which need a key, and names the flows the build declares), but its own database starts with no rows but the ones the files in migrations/ insert, and its schedules do not run on a clock, so a page that expects rows may look blank until something writes to it. " + EXISTING_CONNECTION_SENTENCE + `An app holds at most ${MAX_PREVIEWS_PER_APP} previews at once; a sixth is refused until one is deleted with wirl_delete_preview. Each one expires after ${PREVIEW_TTL_DAYS} days if not redeployed. ` + PREVIEW_LOGS_SENTENCE + "Refuses, without uploading anything, if the directory contains what looks like a live API key or password. Works for a new app too: the app is made if it is not there yet, and its own address answers 404 until wirl_deploy.",
|
|
8285
|
+
annotations: { title: "Preview an app", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
7812
8286
|
inputSchema: {
|
|
7813
8287
|
type: "object",
|
|
7814
8288
|
properties: {
|
|
@@ -7862,9 +8336,39 @@ function refuseUnlessNames(names) {
|
|
|
7862
8336
|
throw new CliError(`${JSON.stringify(value)} is not ${what} name. ${validateSlug(value).message ?? ""}`.trimEnd());
|
|
7863
8337
|
}
|
|
7864
8338
|
}
|
|
8339
|
+
var UNSHARE_TAKES = `It takes a person's whole share away, one person at a time: to change a role, use wirl_share with "role". Nothing was changed.`;
|
|
8340
|
+
function refuseUnshareArgs(args) {
|
|
8341
|
+
const allowed = ["app", "email", "org"];
|
|
8342
|
+
const extra = Object.keys(args).filter((key) => !allowed.includes(key));
|
|
8343
|
+
if (extra.length > 0) {
|
|
8344
|
+
const listed2 = extra.map((key) => JSON.stringify(key));
|
|
8345
|
+
const said = listed2.length === 1 ? listed2[0] : `${listed2.slice(0, -1).join(", ")} or ${listed2[listed2.length - 1]}`;
|
|
8346
|
+
throw new CliError(`wirl_unshare takes "app", "email" and "org", not ${said}. ${UNSHARE_TAKES}`);
|
|
8347
|
+
}
|
|
8348
|
+
const app = args["app"];
|
|
8349
|
+
if (app === void 0 || app === "") throw new CliError('Which app? Pass its slug as "app".');
|
|
8350
|
+
if (typeof app !== "string") throw new CliError(`"app" is an app's slug, as text.`);
|
|
8351
|
+
if (!isWellFormedSlug(app)) throw new CliError(`${JSON.stringify(app)} is not an app's name, so nothing was changed.`);
|
|
8352
|
+
const email = args["email"];
|
|
8353
|
+
if (email === void 0 || typeof email === "string" && email.trim() === "") {
|
|
8354
|
+
throw new CliError(`Whose access? Pass the person's email address as "email".`);
|
|
8355
|
+
}
|
|
8356
|
+
if (typeof email !== "string") throw new CliError('"email" is one address, as text: run this once for each person.');
|
|
8357
|
+
const org = args["org"];
|
|
8358
|
+
if (org !== void 0 && typeof org !== "string") throw new CliError(`"org" is an organisation's name, as text.`);
|
|
8359
|
+
if (typeof org === "string" && org !== "" && !isWellFormedSlug(org)) {
|
|
8360
|
+
throw new CliError(`${JSON.stringify(org)} is not an organisation's name, so nothing was changed.`);
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
function npmFallbackLines() {
|
|
8364
|
+
const origin = apiOriginFromEnv();
|
|
8365
|
+
const install = installFor(origin);
|
|
8366
|
+
return hasFallback(install) ? ["", `${fallbackWhen(namedControlPlane(origin))} install the wirl CLI from the control plane instead (npm install -g ${install.tarball.cli}) and run plain wirl where this says ${WIRL_BIN}.`] : [];
|
|
8367
|
+
}
|
|
7865
8368
|
var REMOTE_DEPLOY = {
|
|
7866
8369
|
name: "wirl_deploy",
|
|
7867
|
-
description: "Deploy a directory of files to a URL that colleagues can open. This server runs at app.wirl.dev and cannot read files on the machine you are on, so it returns the exact commands to run in a terminal (and makes the app first, when you name one); the upload itself happens from the machine holding the files, never through this chat. A new app is visible to everyone in the org when the org came from a company email domain; in any other org, including a personal one or one made with wirl orgs create, it starts private. Returns the URL it will get.",
|
|
8370
|
+
description: "Deploy a directory of files to a URL that colleagues can open. This server runs at app.wirl.dev and cannot read files on the machine you are on, so it returns the exact commands to run in a terminal (and makes the app first, when you name one); the upload itself happens from the machine holding the files, never through this chat. A new app is visible to everyone in the org when the org came from a company email domain; in any other org, including a personal one or one made with wirl orgs create, it starts private. Returns the URL it will get. " + versionNumbersSentence("wirl_list_versions"),
|
|
8371
|
+
annotations: { title: "Deploy an app", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
7868
8372
|
inputSchema: {
|
|
7869
8373
|
type: "object",
|
|
7870
8374
|
properties: {
|
|
@@ -7880,13 +8384,13 @@ var REMOTE_DEPLOY = {
|
|
|
7880
8384
|
const directory = str(args, "directory", ".");
|
|
7881
8385
|
const app = str(args, "app");
|
|
7882
8386
|
if (app) await ensureRemoteApp(ctx.creds, org, app);
|
|
7883
|
-
const install = installFor(apiOriginFromEnv());
|
|
7884
8387
|
return [
|
|
7885
8388
|
app ? `App ${app} exists in ${org}. Upload it from the machine that holds the files:` : "Upload it from the machine that holds the files:",
|
|
7886
8389
|
"",
|
|
7887
|
-
` npm install -g ${
|
|
7888
|
-
|
|
7889
|
-
`
|
|
8390
|
+
...NPM_PUBLISHED ? [] : [` npm install -g ${installFor(apiOriginFromEnv()).cli} # once per machine`],
|
|
8391
|
+
` ${WIRL_BIN} login # once per machine; ${CLI_LOGIN_DOES}`,
|
|
8392
|
+
` ${WIRL_BIN} deploy ${shellPath(directory)} --org ${shellQuote(org)}${app ? ` --app ${shellQuote(app)}` : ""}`,
|
|
8393
|
+
...npmFallbackLines(),
|
|
7890
8394
|
"",
|
|
7891
8395
|
app ? `It will be at https://${org}--${app}.${APPS_DOMAIN}.` : `The command takes the app from "app" in the project's wirl.json, else from the folder's name, makes it if it is new, and prints its URL.`,
|
|
7892
8396
|
"Once it is deployed, wirl_list_apps shows who can open it.",
|
|
@@ -7897,6 +8401,7 @@ var REMOTE_DEPLOY = {
|
|
|
7897
8401
|
var REMOTE_PULL = {
|
|
7898
8402
|
name: "wirl_pull",
|
|
7899
8403
|
description: "Fetch the source of a deployed app to work on it. This server cannot write to the machine you are on, so it returns the command to run in a terminal.",
|
|
8404
|
+
annotations: reads("Pull an app source"),
|
|
7900
8405
|
inputSchema: {
|
|
7901
8406
|
type: "object",
|
|
7902
8407
|
properties: {
|
|
@@ -7910,18 +8415,19 @@ var REMOTE_PULL = {
|
|
|
7910
8415
|
refuseUnlessNames({ org: str(args, "org"), app: str(args, "app") });
|
|
7911
8416
|
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
7912
8417
|
const app = str(args, "app");
|
|
7913
|
-
const install = installFor(apiOriginFromEnv());
|
|
7914
8418
|
return [
|
|
7915
8419
|
"Run in a terminal:",
|
|
7916
8420
|
"",
|
|
7917
|
-
...NPM_PUBLISHED ? [] : [` npm install -g ${
|
|
7918
|
-
` ${WIRL_BIN} pull ${shellQuote(app)} ${shellPath(str(args, "directory", app))} --org ${shellQuote(org)}
|
|
8421
|
+
...NPM_PUBLISHED ? [] : [` npm install -g ${installFor(apiOriginFromEnv()).cli} # once per machine`],
|
|
8422
|
+
` ${WIRL_BIN} pull ${shellQuote(app)} ${shellPath(str(args, "directory", app))} --org ${shellQuote(org)}`,
|
|
8423
|
+
...npmFallbackLines()
|
|
7919
8424
|
].join("\n");
|
|
7920
8425
|
}
|
|
7921
8426
|
};
|
|
7922
8427
|
var REMOTE_PREVIEW = {
|
|
7923
8428
|
name: "wirl_preview",
|
|
7924
8429
|
description: "Put a directory of files up as a preview: a version of an app that is never live, at its own address, open to the same people who can already open the app. This server runs at app.wirl.dev and cannot read files on the machine you are on, so it returns the exact command to run in a terminal; the upload itself happens from the machine holding the files, never through this chat. Prefer suggesting this over wirl_deploy for a change that has not been checked yet \u2014 preview before deploying, then look at it with wirl_preview_fetch. Works for a new app too: the command makes the app if it is not there yet. " + PREVIEW_LOGS_SENTENCE.trimEnd(),
|
|
8430
|
+
annotations: reads("Preview an app"),
|
|
7925
8431
|
inputSchema: {
|
|
7926
8432
|
type: "object",
|
|
7927
8433
|
properties: {
|
|
@@ -7938,10 +8444,9 @@ var REMOTE_PREVIEW = {
|
|
|
7938
8444
|
async handler(args, ctx) {
|
|
7939
8445
|
refuseUnlessNames({ org: str(args, "org"), app: str(args, "app") });
|
|
7940
8446
|
const given = str(args, "name");
|
|
7941
|
-
const
|
|
7942
|
-
if (
|
|
7943
|
-
|
|
7944
|
-
}
|
|
8447
|
+
const made = given === "" ? null : previewSlugFor(given);
|
|
8448
|
+
if (made !== null && !made.ok) throw new CliError(made.message);
|
|
8449
|
+
const slug = made === null ? "" : made.slug;
|
|
7945
8450
|
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
7946
8451
|
const directory = str(args, "directory", ".");
|
|
7947
8452
|
const app = str(args, "app");
|
|
@@ -7952,8 +8457,9 @@ var REMOTE_PREVIEW = {
|
|
|
7952
8457
|
"Upload it from the machine that holds the files:",
|
|
7953
8458
|
"",
|
|
7954
8459
|
...NPM_PUBLISHED ? [] : [` npm install -g ${installFor(apiOriginFromEnv()).cli} # once per machine`],
|
|
7955
|
-
` ${WIRL_BIN} login # once per machine;
|
|
8460
|
+
` ${WIRL_BIN} login # once per machine; ${CLI_LOGIN_DOES}`,
|
|
7956
8461
|
` ${WIRL_BIN} preview ${shellPath(directory)} --org ${shellQuote(org)}${app ? ` --app ${shellQuote(app)}` : ""}${name ? ` --name ${shellQuote(name)}` : ""}`,
|
|
8462
|
+
...npmFallbackLines(),
|
|
7957
8463
|
"",
|
|
7958
8464
|
...app ? [] : [`The command takes the app from "app" in the project's wirl.json, else from the folder's name.`],
|
|
7959
8465
|
"Once it is up, wirl_preview_fetch shows you what it actually renders.",
|
|
@@ -7972,6 +8478,7 @@ async function manifestFolder(directory) {
|
|
|
7972
8478
|
var LOCAL_MIGRATIONS_ADOPT = {
|
|
7973
8479
|
name: "wirl_migrations_adopt",
|
|
7974
8480
|
description: `${MIGRATIONS_ADOPT_WHAT} The project's .wirl/local.sqlite is recorded too when it has the same shape and no record of other files, so wirl dev keeps its rows. Pointed at build output, it writes beside the wirl.json next to it, and the build must copy migrations/ to the top of what it deploys. Adopting again replaces the record of an earlier adopt, and is refused once a later file has applied.`,
|
|
8481
|
+
annotations: { title: "Adopt app tables as migrations", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
7975
8482
|
inputSchema: {
|
|
7976
8483
|
type: "object",
|
|
7977
8484
|
properties: {
|
|
@@ -7995,6 +8502,7 @@ var LOCAL_MIGRATIONS_ADOPT = {
|
|
|
7995
8502
|
var REMOTE_MIGRATIONS_ADOPT = {
|
|
7996
8503
|
name: "wirl_migrations_adopt",
|
|
7997
8504
|
description: `${MIGRATIONS_ADOPT_WHAT} This server runs at app.wirl.dev and cannot write files on the machine you are on, so it returns the command to run in a terminal there.`,
|
|
8505
|
+
annotations: reads("Adopt app tables as migrations"),
|
|
7998
8506
|
inputSchema: {
|
|
7999
8507
|
type: "object",
|
|
8000
8508
|
properties: {
|
|
@@ -8013,8 +8521,9 @@ var REMOTE_MIGRATIONS_ADOPT = {
|
|
|
8013
8521
|
"Run it on the machine that holds the project, in a terminal:",
|
|
8014
8522
|
"",
|
|
8015
8523
|
...NPM_PUBLISHED ? [] : [` npm install -g ${installFor(apiOriginFromEnv()).cli} # once per machine`],
|
|
8016
|
-
` ${WIRL_BIN} login # once per machine;
|
|
8524
|
+
` ${WIRL_BIN} login # once per machine; ${CLI_LOGIN_DOES}`,
|
|
8017
8525
|
` ${WIRL_BIN} migrations adopt ${shellPath(directory)} --org ${shellQuote(org)}${app ? ` --app ${shellQuote(app)}` : ""}`,
|
|
8526
|
+
...npmFallbackLines(),
|
|
8018
8527
|
"",
|
|
8019
8528
|
...app ? [] : [`The command takes the app from "app" in the project's wirl.json, else from the folder's name.`],
|
|
8020
8529
|
"It writes migrations/0001_init.sql beside wirl.json from the live database's shape, never a row, and the live database records it as applied.",
|
|
@@ -8034,6 +8543,7 @@ function toolsFor(mode) {
|
|
|
8034
8543
|
var SERVER_INSTRUCTIONS = [
|
|
8035
8544
|
"wirl deploys a small internal tool to a URL colleagues can open, with company sign-in, a database and vaulted API keys already wired up.",
|
|
8036
8545
|
"Reach for it whenever what you just built should live somewhere shared instead of only on this machine.",
|
|
8546
|
+
...NPM_PUBLISHED ? [`The text below writes a command as \`wirl <command>\` for short; run one as \`${WIRL_BIN} <command>\`, which needs no install beyond Node 22.13 or later.`] : [],
|
|
8037
8547
|
"",
|
|
8038
8548
|
BUILD_CONTRACT,
|
|
8039
8549
|
"",
|
|
@@ -8048,6 +8558,14 @@ function negotiate(requested) {
|
|
|
8048
8558
|
function result(id, value) {
|
|
8049
8559
|
return JSON.stringify({ jsonrpc: "2.0", id, result: value });
|
|
8050
8560
|
}
|
|
8561
|
+
function escapeInvisibleDeep(value) {
|
|
8562
|
+
if (typeof value === "string") return escapeInvisible(value);
|
|
8563
|
+
if (Array.isArray(value)) return value.map(escapeInvisibleDeep);
|
|
8564
|
+
if (value !== null && typeof value === "object") {
|
|
8565
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, escapeInvisibleDeep(inner)]));
|
|
8566
|
+
}
|
|
8567
|
+
return value;
|
|
8568
|
+
}
|
|
8051
8569
|
function failure(id, code, message) {
|
|
8052
8570
|
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
8053
8571
|
}
|
|
@@ -8060,7 +8578,7 @@ async function handleRpc(request, ctx) {
|
|
|
8060
8578
|
return result(id, {
|
|
8061
8579
|
protocolVersion: negotiate(request.params?.["protocolVersion"]),
|
|
8062
8580
|
capabilities: { tools: {} },
|
|
8063
|
-
serverInfo: { name: "wirl", version:
|
|
8581
|
+
serverInfo: { name: "wirl", version: ctx?.serverVersion ?? wirlVersion() },
|
|
8064
8582
|
instructions: SERVER_INSTRUCTIONS
|
|
8065
8583
|
});
|
|
8066
8584
|
// Notifications carry no id and expect no reply.
|
|
@@ -8073,7 +8591,10 @@ async function handleRpc(request, ctx) {
|
|
|
8073
8591
|
tools: tools.map((t) => ({
|
|
8074
8592
|
name: t.name,
|
|
8075
8593
|
description: t.description,
|
|
8076
|
-
inputSchema: t.inputSchema
|
|
8594
|
+
inputSchema: t.inputSchema,
|
|
8595
|
+
// After the three fields every client has always read: a client
|
|
8596
|
+
// that knows no annotations ignores it (OIM-844).
|
|
8597
|
+
annotations: t.annotations
|
|
8077
8598
|
}))
|
|
8078
8599
|
});
|
|
8079
8600
|
case "tools/call": {
|
|
@@ -8083,8 +8604,17 @@ async function handleRpc(request, ctx) {
|
|
|
8083
8604
|
if (!tool) return failure(id, -32602, `Unknown tool: ${name}`);
|
|
8084
8605
|
const args = params["arguments"] ?? {};
|
|
8085
8606
|
try {
|
|
8086
|
-
|
|
8087
|
-
|
|
8607
|
+
let answer;
|
|
8608
|
+
if (tool.anonymous) {
|
|
8609
|
+
answer = { text: await tool.handler(args) };
|
|
8610
|
+
} else {
|
|
8611
|
+
const context = ctx ?? { creds: await credentialsOrPending(), mode: "local" };
|
|
8612
|
+
answer = tool.answer ? await tool.answer(args, context) : { text: await tool.handler(args, context) };
|
|
8613
|
+
}
|
|
8614
|
+
return result(id, {
|
|
8615
|
+
content: [{ type: "text", text: escapeInvisible(answer.text) }],
|
|
8616
|
+
...answer.structured === void 0 ? {} : { structuredContent: escapeInvisibleDeep(answer.structured) }
|
|
8617
|
+
});
|
|
8088
8618
|
} catch (error) {
|
|
8089
8619
|
const message = error instanceof CliError ? [error.message, error.hint].filter(Boolean).join("\n") : error instanceof Error ? error.message : String(error);
|
|
8090
8620
|
return result(id, { content: [{ type: "text", text: escapeInvisible(message) }], isError: true });
|