@wirl/mcp 0.2.5 → 0.2.6
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/bin.js +464 -221
- package/package.json +1 -1
package/bin.js
CHANGED
|
@@ -2040,7 +2040,238 @@ var init_migrations = __esm({
|
|
|
2040
2040
|
}
|
|
2041
2041
|
});
|
|
2042
2042
|
|
|
2043
|
+
// packages/core/src/ai-vendors.ts
|
|
2044
|
+
var AI_VENDOR_SPECS;
|
|
2045
|
+
var init_ai_vendors = __esm({
|
|
2046
|
+
"packages/core/src/ai-vendors.ts"() {
|
|
2047
|
+
"use strict";
|
|
2048
|
+
AI_VENDOR_SPECS = {
|
|
2049
|
+
openrouter: { host: "openrouter.ai", prefix: "/api", keyHeader: "authorization", label: "OpenRouter" },
|
|
2050
|
+
openai: { host: "api.openai.com", prefix: "", keyHeader: "authorization", label: "OpenAI" },
|
|
2051
|
+
anthropic: { host: "api.anthropic.com", prefix: "", keyHeader: "x-api-key", label: "Anthropic" },
|
|
2052
|
+
google: { host: "generativelanguage.googleapis.com", prefix: "", keyHeader: "x-goog-api-key", label: "Google" }
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
// packages/core/src/broker-stream.ts
|
|
2058
|
+
var BROKER_LIMITS, STREAMED_MEDIA_TYPES, MIB;
|
|
2059
|
+
var init_broker_stream = __esm({
|
|
2060
|
+
"packages/core/src/broker-stream.ts"() {
|
|
2061
|
+
"use strict";
|
|
2062
|
+
BROKER_LIMITS = {
|
|
2063
|
+
/**
|
|
2064
|
+
* From the call to the vendor's headers. For an answer read whole, the
|
|
2065
|
+
* whole answer too, as it has always been. Also under Cloudflare's
|
|
2066
|
+
* undocumented time to first byte on a Worker's fetch, about 100 seconds.
|
|
2067
|
+
*/
|
|
2068
|
+
headersMs: 6e4,
|
|
2069
|
+
/** A streamed answer's body, counted from its headers. */
|
|
2070
|
+
streamBodyMs: 13 * 6e4,
|
|
2071
|
+
/** The longest a streamed answer may go with nothing moving. A reasoning model's pause fits. */
|
|
2072
|
+
streamIdleMs: 4 * 6e4,
|
|
2073
|
+
/** Bytes in any one answer, streamed or read whole. */
|
|
2074
|
+
answerBytes: 25 * 1024 * 1024,
|
|
2075
|
+
/**
|
|
2076
|
+
* Bytes in the body of any one call an app makes. The body is read whole
|
|
2077
|
+
* before the call is sent, and before its grant is looked up, so a body
|
|
2078
|
+
* past this never opens a key and never reaches a vendor (OIM-931).
|
|
2079
|
+
* 100 MiB: Cloudflare publishes 100 MB as the request body its Free and
|
|
2080
|
+
* Pro plans take, and a deployed app's call is first held whole by the
|
|
2081
|
+
* egress Worker, in an isolate Cloudflare gives 128 MB of memory. Nothing
|
|
2082
|
+
* else bounds a caller that holds a broker token and reaches the control
|
|
2083
|
+
* plane itself: Railway sets no size on a body, only five minutes to send it.
|
|
2084
|
+
*/
|
|
2085
|
+
requestBytes: 100 * 1024 * 1024,
|
|
2086
|
+
/**
|
|
2087
|
+
* Bytes of call bodies one app's calls hold at once, its previews' with
|
|
2088
|
+
* them (OIM-951): counted as each chunk arrives, from a call's first byte
|
|
2089
|
+
* until the broker has answered it (broker-bodies.ts). At least
|
|
2090
|
+
* `requestBytes`, so one call at the cap always fits an app that holds
|
|
2091
|
+
* nothing else; a quarter of `heldBodyBytes`, so four apps of four owners
|
|
2092
|
+
* at their share fill the whole (one owner's apps cannot: `ownerHeldBodyBytes`).
|
|
2093
|
+
*/
|
|
2094
|
+
appHeldBodyBytes: 128 * 1024 * 1024,
|
|
2095
|
+
/**
|
|
2096
|
+
* Bytes of call bodies the calls of every app one owner has hold at once,
|
|
2097
|
+
* their previews' with them (OIM-967): the person `apps.owner_user_id`
|
|
2098
|
+
* names, whichever organisations their apps are in. Apps and
|
|
2099
|
+
* organisations cost nothing to create (no cap on either), so a share per
|
|
2100
|
+
* app let one person with four apps fill the whole, and one per
|
|
2101
|
+
* organisation would let them do it with four organisations; a person
|
|
2102
|
+
* costs a sign-in. Two apps at their share, so one app that runs
|
|
2103
|
+
* away still leaves its owner's other apps room, and half of
|
|
2104
|
+
* `heldBodyBytes`, so one owner can never take more than half, and a
|
|
2105
|
+
* second sign-in is what it takes to fill it. At least `requestBytes`, so
|
|
2106
|
+
* one call at the cap always fits an owner whose apps hold nothing else.
|
|
2107
|
+
*/
|
|
2108
|
+
ownerHeldBodyBytes: 256 * 1024 * 1024,
|
|
2109
|
+
/**
|
|
2110
|
+
* Bytes of call bodies every app's calls hold at once, in this process.
|
|
2111
|
+
* A call's reader starts at 16 KiB and doubles its buffer as the body
|
|
2112
|
+
* arrives, so the buffers behind this hold at most twice it, 1 GiB, and
|
|
2113
|
+
* 16 KiB a call beside (the ruling in OIM-951).
|
|
2114
|
+
*/
|
|
2115
|
+
heldBodyBytes: 512 * 1024 * 1024,
|
|
2116
|
+
/** The `Retry-After` of a call refused for either. */
|
|
2117
|
+
heldBodyRetryAfterSeconds: 10,
|
|
2118
|
+
/**
|
|
2119
|
+
* How long a call's body may send nothing before its read is stopped:
|
|
2120
|
+
* an upload's (OIM-935). A deployed app's body comes from the egress
|
|
2121
|
+
* Worker, which holds it whole before it sends it, and `wirl dev` reads it
|
|
2122
|
+
* whole first, so no ordinary sender ever pauses (fix round 1, R1-F2).
|
|
2123
|
+
*/
|
|
2124
|
+
heldBodyStallMs: 3e4,
|
|
2125
|
+
/**
|
|
2126
|
+
* How long a call's body may take to arrive whole: 100 MiB in 120 seconds
|
|
2127
|
+
* is about 7 Mbit/s, which a body between Cloudflare and the control plane
|
|
2128
|
+
* passes many times over, and well under Node's 300 seconds a request,
|
|
2129
|
+
* after which the body would be cut anyway (fix round 1, R1-F2).
|
|
2130
|
+
*/
|
|
2131
|
+
heldBodyArrivalMs: 12e4,
|
|
2132
|
+
/**
|
|
2133
|
+
* Bytes of answers read whole that one app's calls hold at once, its
|
|
2134
|
+
* previews' with them (OIM-966): counted as each chunk arrives from the
|
|
2135
|
+
* vendor, from an answer's first byte until wirl's server has taken the
|
|
2136
|
+
* last of it, or the app has gone (broker-bodies.ts). A vendor's event
|
|
2137
|
+
* stream is not counted: it is passed on as the app reads it, and holds
|
|
2138
|
+
* about 285 to 441 KiB at a time, measured end to end through the control
|
|
2139
|
+
* plane's server (`passOn` in broker.ts; OIM-966 fix round 1, R1-F3,
|
|
2140
|
+
* R2-F2). More than two `answerBytes`, so two answers at the cap fit an
|
|
2141
|
+
* app that holds nothing else; a quarter of `heldAnswerBytes`, so four apps
|
|
2142
|
+
* of four owners at their share fill the whole (one owner's apps cannot:
|
|
2143
|
+
* `ownerHeldAnswerBytes`).
|
|
2144
|
+
*/
|
|
2145
|
+
appHeldAnswerBytes: 64 * 1024 * 1024,
|
|
2146
|
+
/**
|
|
2147
|
+
* Bytes of answers read whole that the calls of every app one owner has
|
|
2148
|
+
* hold at once, their previews' with them (OIM-967), for the reasons
|
|
2149
|
+
* `ownerHeldBodyBytes` gives: two apps at their share, half of
|
|
2150
|
+
* `heldAnswerBytes`, and more than two `answerBytes` and than the room a
|
|
2151
|
+
* call needs, so an owner whose apps hold nothing else can always send.
|
|
2152
|
+
*/
|
|
2153
|
+
ownerHeldAnswerBytes: 128 * 1024 * 1024,
|
|
2154
|
+
/**
|
|
2155
|
+
* Bytes of answers read whole that every app's calls hold at once, in this
|
|
2156
|
+
* process. An answer's reader doubles its buffer as it fills, as a body's
|
|
2157
|
+
* does, and never past `answerBytes`, so while answers arrive the buffers
|
|
2158
|
+
* behind this hold at most twice it, 512 MiB, and 16 KiB an answer beside;
|
|
2159
|
+
* an answer read whole is copied to exactly its size before it is handed
|
|
2160
|
+
* on, so from then its count is its memory (the ruling in OIM-966, and its
|
|
2161
|
+
* fix round 1, R1-F2).
|
|
2162
|
+
*/
|
|
2163
|
+
heldAnswerBytes: 256 * 1024 * 1024,
|
|
2164
|
+
/**
|
|
2165
|
+
* The room an app's share of answers, and the whole, must each have left
|
|
2166
|
+
* for a call to be sent at all: one answer at the cap. With less, the call
|
|
2167
|
+
* is refused with a 503 before it leaves wirl, rather than run at its
|
|
2168
|
+
* vendor and its answer cut (OIM-966 fix round 1, R1-F1).
|
|
2169
|
+
*/
|
|
2170
|
+
heldAnswerRoomBytes: 25 * 1024 * 1024,
|
|
2171
|
+
/** The `Retry-After` of a call refused for that room, before it was sent. */
|
|
2172
|
+
heldAnswerRetryAfterSeconds: 10,
|
|
2173
|
+
/**
|
|
2174
|
+
* How long an answer read whole may wait for wirl's server to take the
|
|
2175
|
+
* last of it once it is handed back: past this it is cut, and its count
|
|
2176
|
+
* goes back, so no app holds its share, or the whole, by not reading.
|
|
2177
|
+
* Under Cloudflare's measured 125 seconds with nothing moving, which ends
|
|
2178
|
+
* a deployed app's unread answer anyway (OIM-966 fix round 1, R1-F1).
|
|
2179
|
+
*/
|
|
2180
|
+
heldAnswerHandOutMs: 12e4,
|
|
2181
|
+
/**
|
|
2182
|
+
* Streamed answers one app may have open at once. Each holds a vendor
|
|
2183
|
+
* socket and an inbound connection on one control-plane instance for up
|
|
2184
|
+
* to fourteen minutes, so one app cannot be allowed an unbounded number.
|
|
2185
|
+
* Counted in memory, per instance, like the control plane's other limits.
|
|
2186
|
+
*/
|
|
2187
|
+
openStreamsPerApp: 20,
|
|
2188
|
+
/**
|
|
2189
|
+
* How long an event stream wirl cut waits for its closing event to leave
|
|
2190
|
+
* the server before it ends the stream with its error anyway (OIM-837 fix
|
|
2191
|
+
* round 1). The event is on its way when the server's queue of what it has
|
|
2192
|
+
* to write has emptied; a reader that is slow but reading empties a full
|
|
2193
|
+
* kernel buffer, a few hundred kilobytes, well inside this. Past it the
|
|
2194
|
+
* reader is not reading, and the stream ends as it always did; the write
|
|
2195
|
+
* stall that holds a connection longer than this is OIM-968's.
|
|
2196
|
+
*/
|
|
2197
|
+
cutEventSentMs: 5e3
|
|
2198
|
+
};
|
|
2199
|
+
STREAMED_MEDIA_TYPES = Object.freeze(["text/event-stream", "application/x-ndjson"]);
|
|
2200
|
+
MIB = 1024 * 1024;
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
// packages/core/src/broker.ts
|
|
2205
|
+
function isBrokerOwnedHeader(name) {
|
|
2206
|
+
const lower = name.toLowerCase();
|
|
2207
|
+
if (lower === "authorization") return false;
|
|
2208
|
+
return STRIPPED.has(lower) || STRIPPED_PREFIXES.some((prefix) => lower.startsWith(prefix)) || isHopHeader(lower);
|
|
2209
|
+
}
|
|
2210
|
+
var STRIPPED_PREFIXES, AI_KEY_HEADERS, STRIPPED, HOP_PREFIXES, HOP_HEADERS, isHopHeader, HAND_OUT_BYTES, FIRST_HOLD_BYTES, CREDENTIAL_SHAPES;
|
|
2211
|
+
var init_broker = __esm({
|
|
2212
|
+
"packages/core/src/broker.ts"() {
|
|
2213
|
+
"use strict";
|
|
2214
|
+
init_ai_vendors();
|
|
2215
|
+
STRIPPED_PREFIXES = ["x-wirl-"];
|
|
2216
|
+
AI_KEY_HEADERS = /* @__PURE__ */ new Set([...Object.values(AI_VENDOR_SPECS).map((spec) => spec.keyHeader), "api-key"]);
|
|
2217
|
+
STRIPPED = /* @__PURE__ */ new Set([
|
|
2218
|
+
"authorization",
|
|
2219
|
+
"proxy-authorization",
|
|
2220
|
+
"cookie",
|
|
2221
|
+
"host",
|
|
2222
|
+
"connection",
|
|
2223
|
+
"keep-alive",
|
|
2224
|
+
"transfer-encoding",
|
|
2225
|
+
"upgrade",
|
|
2226
|
+
"te",
|
|
2227
|
+
"trailer",
|
|
2228
|
+
"content-length"
|
|
2229
|
+
]);
|
|
2230
|
+
HOP_PREFIXES = ["x-forwarded-", "x-railway-", "cf-cert-"];
|
|
2231
|
+
HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
2232
|
+
"forwarded",
|
|
2233
|
+
"x-real-ip",
|
|
2234
|
+
"true-client-ip",
|
|
2235
|
+
"via",
|
|
2236
|
+
"cf-connecting-ip",
|
|
2237
|
+
"cf-connecting-ipv6",
|
|
2238
|
+
"cf-pseudo-ipv4",
|
|
2239
|
+
"cf-ray",
|
|
2240
|
+
"cf-visitor",
|
|
2241
|
+
"cf-worker",
|
|
2242
|
+
"cf-ipcountry",
|
|
2243
|
+
"cf-ipcity",
|
|
2244
|
+
"cf-ipcontinent",
|
|
2245
|
+
"cf-iplatitude",
|
|
2246
|
+
"cf-iplongitude",
|
|
2247
|
+
"cf-region",
|
|
2248
|
+
"cf-region-code",
|
|
2249
|
+
"cf-metro-code",
|
|
2250
|
+
"cf-postal-code",
|
|
2251
|
+
"cf-timezone",
|
|
2252
|
+
"cf-bot-score",
|
|
2253
|
+
"cf-verified-bot",
|
|
2254
|
+
"cf-ja3-hash",
|
|
2255
|
+
"cf-ja4",
|
|
2256
|
+
"exposed-credential-check",
|
|
2257
|
+
"malicious-uploads-detection"
|
|
2258
|
+
]);
|
|
2259
|
+
isHopHeader = (lower) => HOP_HEADERS.has(lower) || HOP_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
2260
|
+
HAND_OUT_BYTES = 64 * 1024;
|
|
2261
|
+
FIRST_HOLD_BYTES = 16 * 1024;
|
|
2262
|
+
CREDENTIAL_SHAPES = Object.freeze({
|
|
2263
|
+
http_bearer: Object.freeze({ sends: "bearer", fields: Object.freeze(["token"]) }),
|
|
2264
|
+
http_header: Object.freeze({ sends: "headers", fields: Object.freeze([]) }),
|
|
2265
|
+
basic: Object.freeze({ sends: "basic", fields: Object.freeze(["username", "password"]) }),
|
|
2266
|
+
none: Object.freeze({ sends: "nothing", fields: Object.freeze([]) })
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2043
2271
|
// packages/core/src/manifest.ts
|
|
2272
|
+
function isHeaderName(name) {
|
|
2273
|
+
return typeof name === "string" && name.length <= 128 && HEADER_NAME.test(name) && !isBrokerOwnedHeader(name);
|
|
2274
|
+
}
|
|
2044
2275
|
function oneEditApart(a, b) {
|
|
2045
2276
|
if (a === b || Math.abs(a.length - b.length) > 1) return false;
|
|
2046
2277
|
if (a.length === b.length) {
|
|
@@ -2395,12 +2626,14 @@ function parseManifest(input) {
|
|
|
2395
2626
|
let slug;
|
|
2396
2627
|
let hosts;
|
|
2397
2628
|
let kind;
|
|
2629
|
+
let header;
|
|
2398
2630
|
if (typeof entry === "string") {
|
|
2399
2631
|
slug = entry;
|
|
2400
2632
|
} else if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
|
|
2401
2633
|
slug = entry["slug"];
|
|
2402
2634
|
hosts = entry["hosts"];
|
|
2403
2635
|
kind = entry["kind"];
|
|
2636
|
+
header = entry["header"];
|
|
2404
2637
|
} else {
|
|
2405
2638
|
return refuse('Each connection must be a name, or an object with a "slug".');
|
|
2406
2639
|
}
|
|
@@ -2431,13 +2664,22 @@ function parseManifest(input) {
|
|
|
2431
2664
|
}
|
|
2432
2665
|
declaredKind = kind;
|
|
2433
2666
|
}
|
|
2667
|
+
if (header !== void 0) {
|
|
2668
|
+
if (declaredKind !== "http_header") {
|
|
2669
|
+
return refuse(`"header" is used only by an http_header connection (${slug}); add "kind": "http_header".`);
|
|
2670
|
+
}
|
|
2671
|
+
if (typeof header === "string" && HEADER_NAME.test(header) && header.length <= 128 && isBrokerOwnedHeader(header)) {
|
|
2672
|
+
return refuse(`"header" for ${slug} is ${header}: ${BROKER_OWNED_HEADER_RULE}.`);
|
|
2673
|
+
}
|
|
2674
|
+
if (!isHeaderName(header)) return refuse(`"header" for ${slug} must be an HTTP header name: ${HEADER_NAME_RULE}.`);
|
|
2675
|
+
}
|
|
2434
2676
|
if (declaredKind === "webhook_secret" && list !== null) {
|
|
2435
2677
|
return refuse(`hosts are not used by a webhook_secret connection (${slug}).`);
|
|
2436
2678
|
}
|
|
2437
2679
|
if (declaredKind === "none" && (list === null || list.length === 0)) {
|
|
2438
2680
|
return refuse(`a "none" connection needs "hosts": the hosts it may reach without a key (${slug}).`);
|
|
2439
2681
|
}
|
|
2440
|
-
connections.push({ slug, hosts: list, kind: declaredKind });
|
|
2682
|
+
connections.push({ slug, hosts: list, kind: declaredKind, ...header === void 0 ? {} : { header } });
|
|
2441
2683
|
}
|
|
2442
2684
|
const publicPathsResult = parsePublicPaths(raw, connections);
|
|
2443
2685
|
if (!publicPathsResult.ok) return refuse(publicPathsResult.message);
|
|
@@ -2447,7 +2689,7 @@ function parseManifest(input) {
|
|
|
2447
2689
|
warnings
|
|
2448
2690
|
};
|
|
2449
2691
|
}
|
|
2450
|
-
var CONNECTION_KINDS, UNDELIVERED_KINDS, CREATABLE_CONNECTION_KINDS, needsNoKey, ACCENTS, SCHEDULE_LIMITS, MANIFEST_KEYS, QUIET_KEYS, UNKNOWN_KEYS_NAMED, KEY_SHOWN, squash, ONE_EDIT_MIN_LETTERS, pluralOf, SLUG, LABEL, HOST_PATTERN, refuse, asHint, SCHEDULE_NAME, TIMEOUT_PATTERN, TIMEOUT_MESSAGE, FLOW_KEYS, MIGRATIONS_KEYS, VISIBILITY_REFUSED;
|
|
2692
|
+
var CONNECTION_KINDS, UNDELIVERED_KINDS, CREATABLE_CONNECTION_KINDS, needsNoKey, ACCENTS, SCHEDULE_LIMITS, HEADER_NAME, BROKER_OWNED_HEADER_RULE, HEADER_NAME_RULE, MANIFEST_KEYS, QUIET_KEYS, UNKNOWN_KEYS_NAMED, KEY_SHOWN, squash, ONE_EDIT_MIN_LETTERS, pluralOf, SLUG, LABEL, HOST_PATTERN, refuse, asHint, SCHEDULE_NAME, TIMEOUT_PATTERN, TIMEOUT_MESSAGE, FLOW_KEYS, MIGRATIONS_KEYS, VISIBILITY_REFUSED;
|
|
2451
2693
|
var init_manifest = __esm({
|
|
2452
2694
|
"packages/core/src/manifest.ts"() {
|
|
2453
2695
|
"use strict";
|
|
@@ -2456,6 +2698,7 @@ var init_manifest = __esm({
|
|
|
2456
2698
|
init_public_path();
|
|
2457
2699
|
init_flows();
|
|
2458
2700
|
init_migrations();
|
|
2701
|
+
init_broker();
|
|
2459
2702
|
CONNECTION_KINDS = ["http_bearer", "http_header", "basic", "postgres", "env", "webhook_secret", "none"];
|
|
2460
2703
|
UNDELIVERED_KINDS = {
|
|
2461
2704
|
env: `wirl never hands a secret to the app, so an "env" connection delivers nothing. Declare the vendor's host instead ("hosts": ["api.vendor.com"]) and fetch its real URL: wirl attaches the key on the way out.`,
|
|
@@ -2471,6 +2714,9 @@ var init_manifest = __esm({
|
|
|
2471
2714
|
defaultTimeoutMs: 3e5,
|
|
2472
2715
|
maxRetries: 3
|
|
2473
2716
|
};
|
|
2717
|
+
HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
2718
|
+
BROKER_OWNED_HEADER_RULE = "wirl sets or removes that header itself on every call, so a key is never sent in it; name the header the vendor reads the key from, such as Authorization or x-api-key";
|
|
2719
|
+
HEADER_NAME_RULE = "letters, digits and !#$%&'*+-.^_`|~ only, such as Authorization or x-api-key";
|
|
2474
2720
|
MANIFEST_KEYS = Object.freeze([
|
|
2475
2721
|
"accent",
|
|
2476
2722
|
"app",
|
|
@@ -2730,157 +2976,10 @@ var init_runtime = __esm({
|
|
|
2730
2976
|
}
|
|
2731
2977
|
});
|
|
2732
2978
|
|
|
2733
|
-
// packages/core/src/broker-stream.ts
|
|
2734
|
-
var BROKER_LIMITS, STREAMED_MEDIA_TYPES, MIB;
|
|
2735
|
-
var init_broker_stream = __esm({
|
|
2736
|
-
"packages/core/src/broker-stream.ts"() {
|
|
2737
|
-
"use strict";
|
|
2738
|
-
BROKER_LIMITS = {
|
|
2739
|
-
/**
|
|
2740
|
-
* From the call to the vendor's headers. For an answer read whole, the
|
|
2741
|
-
* whole answer too, as it has always been. Also under Cloudflare's
|
|
2742
|
-
* undocumented time to first byte on a Worker's fetch, about 100 seconds.
|
|
2743
|
-
*/
|
|
2744
|
-
headersMs: 6e4,
|
|
2745
|
-
/** A streamed answer's body, counted from its headers. */
|
|
2746
|
-
streamBodyMs: 13 * 6e4,
|
|
2747
|
-
/** The longest a streamed answer may go with nothing moving. A reasoning model's pause fits. */
|
|
2748
|
-
streamIdleMs: 4 * 6e4,
|
|
2749
|
-
/** Bytes in any one answer, streamed or read whole. */
|
|
2750
|
-
answerBytes: 25 * 1024 * 1024,
|
|
2751
|
-
/**
|
|
2752
|
-
* Bytes in the body of any one call an app makes. The body is read whole
|
|
2753
|
-
* before the call is sent, and before its grant is looked up, so a body
|
|
2754
|
-
* past this never opens a key and never reaches a vendor (OIM-931).
|
|
2755
|
-
* 100 MiB: Cloudflare publishes 100 MB as the request body its Free and
|
|
2756
|
-
* Pro plans take, and a deployed app's call is first held whole by the
|
|
2757
|
-
* egress Worker, in an isolate Cloudflare gives 128 MB of memory. Nothing
|
|
2758
|
-
* else bounds a caller that holds a broker token and reaches the control
|
|
2759
|
-
* plane itself: Railway sets no size on a body, only five minutes to send it.
|
|
2760
|
-
*/
|
|
2761
|
-
requestBytes: 100 * 1024 * 1024,
|
|
2762
|
-
/**
|
|
2763
|
-
* Bytes of call bodies one app's calls hold at once, its previews' with
|
|
2764
|
-
* them (OIM-951): counted as each chunk arrives, from a call's first byte
|
|
2765
|
-
* until the broker has answered it (broker-bodies.ts). At least
|
|
2766
|
-
* `requestBytes`, so one call at the cap always fits an app that holds
|
|
2767
|
-
* nothing else; a quarter of `heldBodyBytes`, so four apps of four owners
|
|
2768
|
-
* at their share fill the whole (one owner's apps cannot: `ownerHeldBodyBytes`).
|
|
2769
|
-
*/
|
|
2770
|
-
appHeldBodyBytes: 128 * 1024 * 1024,
|
|
2771
|
-
/**
|
|
2772
|
-
* Bytes of call bodies the calls of every app one owner has hold at once,
|
|
2773
|
-
* their previews' with them (OIM-967): the person `apps.owner_user_id`
|
|
2774
|
-
* names, whichever organisations their apps are in. Apps and
|
|
2775
|
-
* organisations cost nothing to create (no cap on either), so a share per
|
|
2776
|
-
* app let one person with four apps fill the whole, and one per
|
|
2777
|
-
* organisation would let them do it with four organisations; a person
|
|
2778
|
-
* costs a sign-in. Two apps at their share, so one app that runs
|
|
2779
|
-
* away still leaves its owner's other apps room, and half of
|
|
2780
|
-
* `heldBodyBytes`, so one owner can never take more than half, and a
|
|
2781
|
-
* second sign-in is what it takes to fill it. At least `requestBytes`, so
|
|
2782
|
-
* one call at the cap always fits an owner whose apps hold nothing else.
|
|
2783
|
-
*/
|
|
2784
|
-
ownerHeldBodyBytes: 256 * 1024 * 1024,
|
|
2785
|
-
/**
|
|
2786
|
-
* Bytes of call bodies every app's calls hold at once, in this process.
|
|
2787
|
-
* A call's reader starts at 16 KiB and doubles its buffer as the body
|
|
2788
|
-
* arrives, so the buffers behind this hold at most twice it, 1 GiB, and
|
|
2789
|
-
* 16 KiB a call beside (the ruling in OIM-951).
|
|
2790
|
-
*/
|
|
2791
|
-
heldBodyBytes: 512 * 1024 * 1024,
|
|
2792
|
-
/** The `Retry-After` of a call refused for either. */
|
|
2793
|
-
heldBodyRetryAfterSeconds: 10,
|
|
2794
|
-
/**
|
|
2795
|
-
* How long a call's body may send nothing before its read is stopped:
|
|
2796
|
-
* an upload's (OIM-935). A deployed app's body comes from the egress
|
|
2797
|
-
* Worker, which holds it whole before it sends it, and `wirl dev` reads it
|
|
2798
|
-
* whole first, so no ordinary sender ever pauses (fix round 1, R1-F2).
|
|
2799
|
-
*/
|
|
2800
|
-
heldBodyStallMs: 3e4,
|
|
2801
|
-
/**
|
|
2802
|
-
* How long a call's body may take to arrive whole: 100 MiB in 120 seconds
|
|
2803
|
-
* is about 7 Mbit/s, which a body between Cloudflare and the control plane
|
|
2804
|
-
* passes many times over, and well under Node's 300 seconds a request,
|
|
2805
|
-
* after which the body would be cut anyway (fix round 1, R1-F2).
|
|
2806
|
-
*/
|
|
2807
|
-
heldBodyArrivalMs: 12e4,
|
|
2808
|
-
/**
|
|
2809
|
-
* Bytes of answers read whole that one app's calls hold at once, its
|
|
2810
|
-
* previews' with them (OIM-966): counted as each chunk arrives from the
|
|
2811
|
-
* vendor, from an answer's first byte until wirl's server has taken the
|
|
2812
|
-
* last of it, or the app has gone (broker-bodies.ts). A vendor's event
|
|
2813
|
-
* stream is not counted: it is passed on as the app reads it, and holds
|
|
2814
|
-
* about 285 to 441 KiB at a time, measured end to end through the control
|
|
2815
|
-
* plane's server (`passOn` in broker.ts; OIM-966 fix round 1, R1-F3,
|
|
2816
|
-
* R2-F2). More than two `answerBytes`, so two answers at the cap fit an
|
|
2817
|
-
* app that holds nothing else; a quarter of `heldAnswerBytes`, so four apps
|
|
2818
|
-
* of four owners at their share fill the whole (one owner's apps cannot:
|
|
2819
|
-
* `ownerHeldAnswerBytes`).
|
|
2820
|
-
*/
|
|
2821
|
-
appHeldAnswerBytes: 64 * 1024 * 1024,
|
|
2822
|
-
/**
|
|
2823
|
-
* Bytes of answers read whole that the calls of every app one owner has
|
|
2824
|
-
* hold at once, their previews' with them (OIM-967), for the reasons
|
|
2825
|
-
* `ownerHeldBodyBytes` gives: two apps at their share, half of
|
|
2826
|
-
* `heldAnswerBytes`, and more than two `answerBytes` and than the room a
|
|
2827
|
-
* call needs, so an owner whose apps hold nothing else can always send.
|
|
2828
|
-
*/
|
|
2829
|
-
ownerHeldAnswerBytes: 128 * 1024 * 1024,
|
|
2830
|
-
/**
|
|
2831
|
-
* Bytes of answers read whole that every app's calls hold at once, in this
|
|
2832
|
-
* process. An answer's reader doubles its buffer as it fills, as a body's
|
|
2833
|
-
* does, and never past `answerBytes`, so while answers arrive the buffers
|
|
2834
|
-
* behind this hold at most twice it, 512 MiB, and 16 KiB an answer beside;
|
|
2835
|
-
* an answer read whole is copied to exactly its size before it is handed
|
|
2836
|
-
* on, so from then its count is its memory (the ruling in OIM-966, and its
|
|
2837
|
-
* fix round 1, R1-F2).
|
|
2838
|
-
*/
|
|
2839
|
-
heldAnswerBytes: 256 * 1024 * 1024,
|
|
2840
|
-
/**
|
|
2841
|
-
* The room an app's share of answers, and the whole, must each have left
|
|
2842
|
-
* for a call to be sent at all: one answer at the cap. With less, the call
|
|
2843
|
-
* is refused with a 503 before it leaves wirl, rather than run at its
|
|
2844
|
-
* vendor and its answer cut (OIM-966 fix round 1, R1-F1).
|
|
2845
|
-
*/
|
|
2846
|
-
heldAnswerRoomBytes: 25 * 1024 * 1024,
|
|
2847
|
-
/** The `Retry-After` of a call refused for that room, before it was sent. */
|
|
2848
|
-
heldAnswerRetryAfterSeconds: 10,
|
|
2849
|
-
/**
|
|
2850
|
-
* How long an answer read whole may wait for wirl's server to take the
|
|
2851
|
-
* last of it once it is handed back: past this it is cut, and its count
|
|
2852
|
-
* goes back, so no app holds its share, or the whole, by not reading.
|
|
2853
|
-
* Under Cloudflare's measured 125 seconds with nothing moving, which ends
|
|
2854
|
-
* a deployed app's unread answer anyway (OIM-966 fix round 1, R1-F1).
|
|
2855
|
-
*/
|
|
2856
|
-
heldAnswerHandOutMs: 12e4,
|
|
2857
|
-
/**
|
|
2858
|
-
* Streamed answers one app may have open at once. Each holds a vendor
|
|
2859
|
-
* socket and an inbound connection on one control-plane instance for up
|
|
2860
|
-
* to fourteen minutes, so one app cannot be allowed an unbounded number.
|
|
2861
|
-
* Counted in memory, per instance, like the control plane's other limits.
|
|
2862
|
-
*/
|
|
2863
|
-
openStreamsPerApp: 20,
|
|
2864
|
-
/**
|
|
2865
|
-
* How long an event stream wirl cut waits for its closing event to leave
|
|
2866
|
-
* the server before it ends the stream with its error anyway (OIM-837 fix
|
|
2867
|
-
* round 1). The event is on its way when the server's queue of what it has
|
|
2868
|
-
* to write has emptied; a reader that is slow but reading empties a full
|
|
2869
|
-
* kernel buffer, a few hundred kilobytes, well inside this. Past it the
|
|
2870
|
-
* reader is not reading, and the stream ends as it always did; the write
|
|
2871
|
-
* stall that holds a connection longer than this is OIM-968's.
|
|
2872
|
-
*/
|
|
2873
|
-
cutEventSentMs: 5e3
|
|
2874
|
-
};
|
|
2875
|
-
STREAMED_MEDIA_TYPES = Object.freeze(["text/event-stream", "application/x-ndjson"]);
|
|
2876
|
-
MIB = 1024 * 1024;
|
|
2877
|
-
}
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
2979
|
// packages/core/src/retention.ts
|
|
2881
2980
|
function deletedAppNameMessage(held, graceDays = RETENTION.deletedAppGraceDays) {
|
|
2882
|
-
const deleted = held.deletedAt
|
|
2883
|
-
const until = held.keptUntil
|
|
2981
|
+
const deleted = heldDay(held.deletedAt);
|
|
2982
|
+
const until = heldDay(held.keptUntil);
|
|
2884
2983
|
const what = `An app named ${held.name} was deleted on ${deleted}`;
|
|
2885
2984
|
if (!held.open) {
|
|
2886
2985
|
return `${what}, and the ${String(graceDays)} days its data was kept for ended on ${until}. It cannot be restored, and the name stays taken: choose another name.`;
|
|
@@ -2904,7 +3003,7 @@ function appNameTakenMessage(name, movedToOrg = null) {
|
|
|
2904
3003
|
}
|
|
2905
3004
|
return `An app named ${name} already exists in this organisation; choose another name.`;
|
|
2906
3005
|
}
|
|
2907
|
-
var RETENTION;
|
|
3006
|
+
var RETENTION, heldDay;
|
|
2908
3007
|
var init_retention = __esm({
|
|
2909
3008
|
"packages/core/src/retention.ts"() {
|
|
2910
3009
|
"use strict";
|
|
@@ -2929,6 +3028,7 @@ var init_retention = __esm({
|
|
|
2929
3028
|
/** An AI monthly total (the rows the caps read) is kept this long, 24 months. */
|
|
2930
3029
|
aiMonthDays: 730
|
|
2931
3030
|
};
|
|
3031
|
+
heldDay = (instant) => instant.slice(0, 10);
|
|
2932
3032
|
}
|
|
2933
3033
|
});
|
|
2934
3034
|
|
|
@@ -2952,7 +3052,7 @@ npm packages work: install them in the project and the deploy bundles them in. N
|
|
|
2952
3052
|
server: native code will not run. Node built-ins work (\`node:crypto\`, \`node:path\`, ...; \`Buffer\` and
|
|
2953
3053
|
\`process\` are global), but what \`node:fs\` writes is gone by the next request: use \`env.DB\`.
|
|
2954
3054
|
|
|
2955
|
-
\`env\` holds \`DB\` and
|
|
3055
|
+
\`env\` holds \`DB\` and \`WIRL_DEPLOYMENT\`. \`env.WIRL_DEPLOYMENT\`: the version number of the deployment answering, as a string; \`dev\` under \`wirl dev\`. \`env.DB\` is Cloudflare D1: prepared statements with \`.bind()\`, at most
|
|
2956
3056
|
100 bound parameters a statement. \`exec()\` runs one statement per line, as does each file in \`migrations/\`,
|
|
2957
3057
|
which wirl applies in order to a preview and before going live; a failing one fails the deploy. Never edit
|
|
2958
3058
|
an applied file. Dropping or renaming a table or column needs \`"migrations": { "destructive": [...] }\`, its
|
|
@@ -3013,7 +3113,7 @@ var init_version = __esm({
|
|
|
3013
3113
|
"packages/core/src/version.ts"() {
|
|
3014
3114
|
"use strict";
|
|
3015
3115
|
WORKSPACE_VERSION = "0.0.0";
|
|
3016
|
-
BUILT_VERSION = true ? "0.2.
|
|
3116
|
+
BUILT_VERSION = true ? "0.2.6" : null;
|
|
3017
3117
|
}
|
|
3018
3118
|
});
|
|
3019
3119
|
|
|
@@ -3949,6 +4049,7 @@ var init_local = __esm({
|
|
|
3949
4049
|
env["WIRL_LOCAL_DB"] = databaseFile;
|
|
3950
4050
|
if (this.#databaseDir === void 0) this.#databaseFiles.set(request.databaseKey ?? request.appId, databaseFile);
|
|
3951
4051
|
}
|
|
4052
|
+
if (request.version !== void 0) env["WIRL_DEPLOYMENT"] = String(request.version);
|
|
3952
4053
|
const child = this.#spawn(command, request.dir, env);
|
|
3953
4054
|
const instance = {
|
|
3954
4055
|
id: request.deploymentId,
|
|
@@ -4241,7 +4342,7 @@ function listedNames(body) {
|
|
|
4241
4342
|
if (!Array.isArray(result2)) return [];
|
|
4242
4343
|
return result2.map((entry) => typeof entry === "object" && entry !== null ? entry.id : null).filter((id) => typeof id === "string");
|
|
4243
4344
|
}
|
|
4244
|
-
var SCRIPT_NOT_FOUND, DEFAULTS2, DATABASE_BINDING, shortHash, WorkersRunner, DATABASE_KEY, D1_UUID, BOOKMARK, ANSWER_BYTES, TOO_LARGE;
|
|
4345
|
+
var SCRIPT_NOT_FOUND, DEFAULTS2, DATABASE_BINDING, DEPLOYMENT_BINDING, shortHash, WorkersRunner, DATABASE_KEY, D1_UUID, BOOKMARK, ANSWER_BYTES, TOO_LARGE;
|
|
4245
4346
|
var init_workers = __esm({
|
|
4246
4347
|
"packages/runtime/src/workers.ts"() {
|
|
4247
4348
|
"use strict";
|
|
@@ -4258,6 +4359,7 @@ var init_workers = __esm({
|
|
|
4258
4359
|
compatibilityDate: "2026-01-01"
|
|
4259
4360
|
};
|
|
4260
4361
|
DATABASE_BINDING = "DB";
|
|
4362
|
+
DEPLOYMENT_BINDING = "WIRL_DEPLOYMENT";
|
|
4261
4363
|
shortHash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 16);
|
|
4262
4364
|
WorkersRunner = class {
|
|
4263
4365
|
#opts;
|
|
@@ -4335,7 +4437,12 @@ var init_workers = __esm({
|
|
|
4335
4437
|
// bundler leaves as imports for exactly this to satisfy. Without
|
|
4336
4438
|
// it an app that runs under `wirl dev` fails its first request.
|
|
4337
4439
|
compatibility_flags: ["nodejs_compat"],
|
|
4338
|
-
bindings: [
|
|
4440
|
+
bindings: [
|
|
4441
|
+
{ type: "d1", name: DATABASE_BINDING, id: databaseId },
|
|
4442
|
+
// Which deployment is answering, as a string, so a page can
|
|
4443
|
+
// notice a deploy and reload (OIM-1012).
|
|
4444
|
+
...request.version === void 0 ? [] : [{ type: "plain_text", name: DEPLOYMENT_BINDING, text: String(request.version) }]
|
|
4445
|
+
],
|
|
4339
4446
|
// Workers Logs: every invocation and every console line, kept by
|
|
4340
4447
|
// Cloudflare for its retention window. Sampling stays at 1 so a
|
|
4341
4448
|
// missing line means the app did not print it. Read back by
|
|
@@ -5200,12 +5307,13 @@ company email domain; in any other org, including a personal one or one
|
|
|
5200
5307
|
made with \`wirl orgs create\`, it starts private. Deploys are one
|
|
5201
5308
|
command; sharing is by email; a third-party API is declared, never pasted.`;
|
|
5202
5309
|
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.";
|
|
5203
|
-
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). Models need no key either: call https://ai.wirl/<vendor>/\u2026 (see Models).
|
|
5310
|
+
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). Models need no key either: call https://ai.wirl/<vendor>/\u2026 (see Models). A connection of kind \`http_header\` names its header in \`wirl.json\` (\`"header": "Authorization"\`), so the person pastes only the value.
|
|
5204
5311
|
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.
|
|
5205
|
-
3. **Asked to connect an app to Slack, or to receive events from any outside service:** follow the pattern on https://docs.wirl.dev/models (public path with "verify": "slack", a connection of kind http_bearer for the bot token and one of kind webhook_secret for the signing secret, the Slack app manifest you give the person to create the app with). Never ask for the token or secret in chat: the person pastes them on the app's Connections page, and you say which two values.
|
|
5312
|
+
3. **Asked to connect an app to Slack, or to receive events from any outside service:** follow the pattern on https://docs.wirl.dev/models (public path with "verify": "slack", a connection of kind http_bearer for the bot token and one of kind webhook_secret for the signing secret, the Slack app manifest you give the person to create the app with). Never ask for the token or secret in chat: the person pastes them on the app's Connections page, and you say which two values. Give the person the link that opens Slack's create-app dialog with the manifest filled in: \`https://api.slack.com/apps?new_app=1&manifest_json=<the url-encoded manifest>\`. Tell them their steps in this order: create the app from that link; copy the signing secret (Basic Information) to the app's Connections page as \`slack-signing\`; click Retry on the Request URL check in Slack if it shows unverified; Install to Workspace; then copy the Bot User OAuth Token (OAuth & Permissions) to the Connections page as \`slack-bot\`. Use wirl's Slack pattern, not Slack's own CLI, Bolt templates or socket mode, which do not fit an app hosted on wirl.
|
|
5206
5313
|
4. If a command names several organisations and asks which, ask the person; pass \`--org\` or put \`"org"\` in \`wirl.json\`.
|
|
5207
5314
|
5. To keep a file out of the deploy, list it in a \`.wirlignore\` at the project root (gitignore-style lines; no \`!\` negation).
|
|
5208
|
-
6. **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
|
|
5315
|
+
6. **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.
|
|
5316
|
+
7. **Asked for a change to an app that is deployed:** make it, try it under \`wirl dev\`, deploy it when it works, and report the address and what changed. Put it up as a preview instead only when the person asks to look first, or when the change needs a real key (see rule 1): nothing with a key runs under \`wirl dev\`, so a preview is where such a call is first tried, with \`wirl fetch <path> --preview <name>\`. Never stop after editing files without deploying or saying why.`;
|
|
5209
5317
|
var SKILL_MANIFEST_EXAMPLE = `\`\`\`json
|
|
5210
5318
|
{ "app": "crm-sync", "server": "server.mjs", "connections": [{ "slug": "hubspot", "hosts": ["api.hubspot.com"] }] }
|
|
5211
5319
|
\`\`\``;
|
|
@@ -5259,47 +5367,62 @@ var SUGGESTIONS = [
|
|
|
5259
5367
|
number: 1,
|
|
5260
5368
|
title: "Summarise the week",
|
|
5261
5369
|
capability: "A model comes with every app. Your agent just uses it; nothing to sign up for or paste.",
|
|
5262
|
-
prompt:
|
|
5263
|
-
youGet: "a summary
|
|
5370
|
+
prompt: "Replace the 'Summarise the week' card on the noticeboard with the feature itself: a Summarise button and, under it, a three-line summary of this week's notes written by a language model. Keep the latest summary so everyone who opens the page sees it, and show when it was made.",
|
|
5371
|
+
youGet: "a summary in that step's place."
|
|
5264
5372
|
},
|
|
5265
5373
|
{
|
|
5266
5374
|
number: 2,
|
|
5267
5375
|
title: "Give notes a due date",
|
|
5268
5376
|
capability: "The database is yours. Changing its shape is one migration file and one deploy; wirl applies it before the new version goes live, and the Data page on the dashboard shows the result.",
|
|
5269
|
-
prompt: "
|
|
5270
|
-
youGet: "
|
|
5377
|
+
prompt: "Replace the 'Give notes a due date' card with the feature: each note can have a due date, the form gets a date field, and notes due soonest sort to the top with the date shown. Put a short 'Due soon' list where the card was.",
|
|
5378
|
+
youGet: "due dates on notes, and the list where the card was."
|
|
5271
5379
|
},
|
|
5272
5380
|
{
|
|
5273
5381
|
number: 3,
|
|
5274
5382
|
title: "Pull in something from outside",
|
|
5275
5383
|
capability: "Every call out goes through wirl. The agent declares the host as a connection; a public API needs no key, and one that does gets its key on the dashboard, never in the code.",
|
|
5276
|
-
prompt:
|
|
5277
|
-
|
|
5384
|
+
prompt: (
|
|
5385
|
+
// The spec writes <city>; the page carries "your city" and the agent asks once.
|
|
5386
|
+
"Replace the 'Pull in something from outside' card with a seven-day weather strip for your city: one column per day, high and low as bars, a small sun, cloud or rain icon, drawn as SVG in the page's own colours, from Open-Meteo's keyless forecast API. Let the owner change the city on the page."
|
|
5387
|
+
),
|
|
5388
|
+
youGet: "a forecast you can glance at, in that step's place."
|
|
5278
5389
|
},
|
|
5279
5390
|
{
|
|
5280
5391
|
number: 4,
|
|
5281
5392
|
title: "Try a new look before anyone sees it",
|
|
5282
5393
|
// The mock says "Previews are free."; the starter carries no price words (its test forbids "free").
|
|
5283
5394
|
capability: "Previews are a safe place to try. A preview gets its own address and its own database, so you can look at a change with a colleague before it goes live.",
|
|
5284
|
-
prompt: "
|
|
5285
|
-
youGet:
|
|
5395
|
+
prompt: "Try a calmer look for the noticeboard: more space, a softer palette, the notes as cards. Put it up as a preview at its own address so I can look before anyone else sees it, and tell me the address. Don't change the live app until I say so. When I say deploy it, replace the 'Try a new look' card with a line saying which look is live.",
|
|
5396
|
+
youGet: "a preview address; the live page unchanged."
|
|
5286
5397
|
},
|
|
5287
5398
|
{
|
|
5288
5399
|
number: 5,
|
|
5289
5400
|
title: "Share it with a colleague",
|
|
5290
5401
|
capability: "Company sign-in is already on. Anyone you name opens the link with their work account; the Access page shows who can, and you can take it back any time.",
|
|
5291
|
-
prompt: "
|
|
5292
|
-
youGet: "
|
|
5402
|
+
prompt: "Share this noticeboard with <a colleague's email> so they can open it and post notes, and replace the 'Share it with a colleague' card with a short line that says who can open it now and links to the app's Access page.",
|
|
5403
|
+
youGet: "a colleague who can open it, and the line where the card was."
|
|
5293
5404
|
},
|
|
5294
5405
|
{
|
|
5295
5406
|
number: 6,
|
|
5296
5407
|
title: "Connect it to Slack",
|
|
5297
5408
|
capability: "Apps can reach the tools your company already uses. Your agent knows wirl's Slack pattern: it gives you a Slack app to create in one click, you paste two values on the app's Connections page, and the noticeboard starts a DM with you. Ask it about the notes, or tell it to add one, from Slack.",
|
|
5298
|
-
prompt: "Connect this noticeboard to Slack so it DMs me this week's summary and I can chat with it there: ask it about the notes, or tell it to add or change one. Give me
|
|
5299
|
-
youGet: "a DM from the noticeboard in Slack
|
|
5409
|
+
prompt: "Connect this noticeboard to Slack so it DMs me this week's summary and I can chat with it there: ask it about the notes, or tell it to add or change one. Give me a link that opens Slack's create-app dialog with the manifest already filled in, tell me the exact order of the steps I do by hand, and replace the 'Connect it to Slack' card with the connection's status and those steps until the bot answers.",
|
|
5410
|
+
youGet: "a DM from the noticeboard in Slack; everything above works without it.",
|
|
5300
5411
|
optional: true
|
|
5301
5412
|
}
|
|
5302
5413
|
];
|
|
5414
|
+
var SLACK_STEPS = [
|
|
5415
|
+
["Slack", "create the app from the link your agent gives you, and press Create."],
|
|
5416
|
+
["Basic Information", "show the Signing Secret and copy it."],
|
|
5417
|
+
["The app's Connections page on wirl", "paste it as slack-signing."],
|
|
5418
|
+
["Event Subscriptions", "press Retry if the Request URL shows unverified."],
|
|
5419
|
+
["Install App", "press Install to Workspace."],
|
|
5420
|
+
["OAuth & Permissions", "copy the Bot User OAuth Token, which starts with xoxb-."],
|
|
5421
|
+
["The Connections page", "paste it as slack-bot."],
|
|
5422
|
+
["Slack", "message the bot."]
|
|
5423
|
+
];
|
|
5424
|
+
var SLACK_IGNORE = "Ignore the Client Secret, the Verification Token and the app-level token.";
|
|
5425
|
+
var STEP_RULE = "Each step on the page is drawn by its own function in `server.mjs`, `step1(ctx)` to `step6(ctx)`, which returns the HTML under that step's heading; to build a step, make its function return the feature instead of the card, so the feature stands where the card was.";
|
|
5303
5426
|
var splitCapability = (capability) => {
|
|
5304
5427
|
const stop = capability.indexOf(". ");
|
|
5305
5428
|
return [capability.slice(0, stop + 1), capability.slice(stop + 2)];
|
|
@@ -5309,20 +5432,29 @@ var SUGGESTIONS_SOURCE = [
|
|
|
5309
5432
|
...SUGGESTIONS.map(
|
|
5310
5433
|
({ number, title, capability, prompt, youGet, optional }) => ` { number: ${String(number)}, title: ${JSON.stringify(title)}, capability: ${JSON.stringify(capability)}, prompt: ${JSON.stringify(prompt)}, youGet: ${JSON.stringify(youGet)}${optional ? ", optional: true" : ""} },`
|
|
5311
5434
|
),
|
|
5312
|
-
"];"
|
|
5435
|
+
"];",
|
|
5436
|
+
"",
|
|
5437
|
+
"// The Slack card's steps for you, in order: where, and what to do there.",
|
|
5438
|
+
"const SLACK_STEPS = [",
|
|
5439
|
+
...SLACK_STEPS.map(([place, what]) => ` [${JSON.stringify(place)}, ${JSON.stringify(what)}],`),
|
|
5440
|
+
"];",
|
|
5441
|
+
`const SLACK_IGNORE = ${JSON.stringify(SLACK_IGNORE)};`
|
|
5313
5442
|
].join("\n");
|
|
5314
5443
|
var SERVER_HEAD = String.raw`/**
|
|
5315
5444
|
* Team noticeboard: your first app on wirl.
|
|
5316
5445
|
*
|
|
5317
5446
|
* Your team posts short notes here, tags them, pins the ones everyone should
|
|
5318
5447
|
* see, and edits or removes their own. Every night a schedule archives old
|
|
5319
|
-
* notes nobody pinned. Below the notes, "Make it yours": six
|
|
5320
|
-
*
|
|
5321
|
-
*
|
|
5448
|
+
* notes nobody pinned. Below the notes, "Make it yours": six steps, each
|
|
5449
|
+
* drawn by its own function, step1 to step6. Until it is built a step shows
|
|
5450
|
+
* a prompt to paste to your coding agent, saying what wirl gives the app and
|
|
5451
|
+
* what you get; building it means that function returns the feature instead,
|
|
5452
|
+
* in the same place. Under them, the app's pages on app.wirl.dev.
|
|
5322
5453
|
*
|
|
5323
5454
|
* The whole app is this one file:
|
|
5324
5455
|
* - The page is HTML this module returns: plain forms, and one small script
|
|
5325
|
-
* for the Copy prompt buttons
|
|
5456
|
+
* for the Copy prompt buttons, which also reloads the page when /_version
|
|
5457
|
+
* changes, so a deploy shows up on its own.
|
|
5326
5458
|
* - Notes live in env.DB, a SQLite database that belongs to this app alone.
|
|
5327
5459
|
* Its tables are in migrations/0001_init.sql, and wirl applies each new
|
|
5328
5460
|
* file in migrations/ before a version goes live. Put the next change in
|
|
@@ -5356,6 +5488,12 @@ export default {
|
|
|
5356
5488
|
return text("No such schedule", 404);
|
|
5357
5489
|
}
|
|
5358
5490
|
|
|
5491
|
+
// Which deployment is answering: wirl sets env.WIRL_DEPLOYMENT, and the
|
|
5492
|
+
// page reloads itself when this changes.
|
|
5493
|
+
if (url.pathname === "/_version" && (request.method === "GET" || request.method === "HEAD")) {
|
|
5494
|
+
return new Response(version(env), { headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5359
5497
|
const me = visitor(request);
|
|
5360
5498
|
|
|
5361
5499
|
if (request.method === "POST") {
|
|
@@ -5485,17 +5623,19 @@ async function page(request, url, me, env) {
|
|
|
5485
5623
|
"<meta name='viewport' content='width=device-width, initial-scale=1'>",
|
|
5486
5624
|
"<title>" + TITLE + "</title>",
|
|
5487
5625
|
"<style>" + STYLE + "</style>",
|
|
5626
|
+
"<body data-version='" + escape(version(env)) + "'>",
|
|
5488
5627
|
"<main>",
|
|
5489
5628
|
"<header><h1>" + TITLE + "</h1>",
|
|
5490
5629
|
"<p class='dim'>Signed in as <b>" + escape(me.name) + "</b>" + (me.role ? " (" + escape(me.role) + ")" : "") + ".</p></header>",
|
|
5491
5630
|
me.role === "owner" ? welcome() : "",
|
|
5492
5631
|
shown,
|
|
5493
|
-
"<ul class='notes'>" + (notes.length === 0 ? "<li class='dim'>Nothing here yet.</li>" : notes.map((note) =>
|
|
5632
|
+
"<ul class='notes'>" + (notes.length === 0 ? "<li class='dim'>Nothing here yet.</li>" : notes.map((note) => noteItem(note, me, archived)).join("")) + "</ul>",
|
|
5494
5633
|
"<p class='dim'>Nightly tidy-up, which archives notes older than " + ARCHIVE_AFTER_DAYS + " days that nobody pinned: " +
|
|
5495
5634
|
(lastRun ? "last run " + escape(lastRun.ran_at.slice(0, 16)) + " UTC: " + escape(lastRun.note) : "Not run yet.") + "</p>",
|
|
5496
|
-
prompts(request),
|
|
5635
|
+
await prompts(request, me, env, notes),
|
|
5497
5636
|
"</main>",
|
|
5498
5637
|
COPY_SCRIPT,
|
|
5638
|
+
"</body>",
|
|
5499
5639
|
"</html>",
|
|
5500
5640
|
].join("\n");
|
|
5501
5641
|
}
|
|
@@ -5506,7 +5646,7 @@ function welcome() {
|
|
|
5506
5646
|
"<section class='welcome'>",
|
|
5507
5647
|
"<p class='kicker'>Your first app on wirl</p>",
|
|
5508
5648
|
"<h2>Your coding agent just built this, and it's live.</h2>",
|
|
5509
|
-
"<p>A small team noticeboard: notes, a nightly tidy-up, company sign-in, its own database. It runs at this address, it's private to you until you share it, and it's yours to change. Try out the app below and then customize it.</p>",
|
|
5649
|
+
"<p>A small team noticeboard: notes, a nightly tidy-up, company sign-in, its own database. It runs at this address, it's private to you until you share it, and it's yours to change. Try out the app below and then customize it; when your agent deploys a change, this page refreshes on its own.</p>",
|
|
5510
5650
|
"</section>",
|
|
5511
5651
|
].join("");
|
|
5512
5652
|
}
|
|
@@ -5521,7 +5661,7 @@ function form() {
|
|
|
5521
5661
|
].join("");
|
|
5522
5662
|
}
|
|
5523
5663
|
|
|
5524
|
-
function
|
|
5664
|
+
function noteItem(note, me, archived) {
|
|
5525
5665
|
const mine = note.author_id === me.id || me.role === "owner";
|
|
5526
5666
|
const action = (what, label) =>
|
|
5527
5667
|
"<form method='post' action='/notes/" + note.id + "/" + what + "'><button>" + label + "</button></form>";
|
|
@@ -5561,10 +5701,9 @@ function dashboard(request) {
|
|
|
5561
5701
|
return { app: DASHBOARD + "/apps/" + org + "/" + encodeURIComponent(parts[1]), org: DASHBOARD + "/orgs/" + org };
|
|
5562
5702
|
}
|
|
5563
5703
|
|
|
5564
|
-
//
|
|
5565
|
-
//
|
|
5566
|
-
|
|
5567
|
-
function prompts(request) {
|
|
5704
|
+
// "Make it yours": six steps, each drawn by its own function below, under
|
|
5705
|
+
// its heading. Then what the app already uses, and its pages on the dashboard.
|
|
5706
|
+
async function prompts(request, me, env, notes) {
|
|
5568
5707
|
const where = dashboard(request);
|
|
5569
5708
|
const pages = [
|
|
5570
5709
|
["Overview", "app", ""],
|
|
@@ -5578,34 +5717,104 @@ function prompts(request) {
|
|
|
5578
5717
|
];
|
|
5579
5718
|
const link = ([label, of, path]) =>
|
|
5580
5719
|
where === null ? "<span class='dim'>" + label + "</span>" : "<a href='" + escape(where[of] + path) + "'>" + label + "</a>";
|
|
5581
|
-
const
|
|
5582
|
-
|
|
5720
|
+
const ctx = { me, env, notes, where };
|
|
5721
|
+
const shown = await Promise.all(STEPS.map((step) => step(ctx)));
|
|
5722
|
+
const section = (html, index) => {
|
|
5723
|
+
const each = SUGGESTIONS[index];
|
|
5724
|
+
const isCard = String(html ?? "").startsWith('<div class="card"');
|
|
5583
5725
|
return (
|
|
5584
|
-
|
|
5726
|
+
'<section class="step" id="step-' + each.number + '">' +
|
|
5585
5727
|
"<h3><span class='number'>" + each.number + "</span> " + escape(each.title) +
|
|
5586
|
-
(each.number === 1 ? " <span class='badge start'>Start here</span>" : "") +
|
|
5587
|
-
(each.optional ? " <span class='badge'>Optional. The big one.</span>" : "") + "</h3>" +
|
|
5588
|
-
|
|
5589
|
-
"
|
|
5590
|
-
"<button type='button' class='copy' data-copy-for='prompt-" + each.number + "'>Copy prompt</button>" +
|
|
5591
|
-
"<p class='dim'>You get: " + escape(each.youGet) + "</p></div>" +
|
|
5592
|
-
"<details class='prompt'><summary>Show the prompt</summary>" +
|
|
5593
|
-
"<pre id='prompt-" + each.number + "'>" + escape(each.prompt) + "</pre></details>" +
|
|
5594
|
-
"</li>"
|
|
5728
|
+
(each.number === 1 && isCard ? " <span class='badge start'>Start here</span>" : "") +
|
|
5729
|
+
(each.optional && isCard ? " <span class='badge'>Optional. The big one.</span>" : "") + "</h3>" +
|
|
5730
|
+
(html ?? "") +
|
|
5731
|
+
"</section>"
|
|
5595
5732
|
);
|
|
5596
5733
|
};
|
|
5597
5734
|
return [
|
|
5598
5735
|
"<section id='prompts'><h2>Make it yours</h2>",
|
|
5599
|
-
"<p>
|
|
5600
|
-
"<
|
|
5601
|
-
...
|
|
5602
|
-
"</
|
|
5736
|
+
"<p>Everything below is a whole prompt. Copy one, paste it to the coding agent that made this app, and the step turns into the feature. Go in order or skip around; the last one is the big one.</p>",
|
|
5737
|
+
"<div class='steps'>",
|
|
5738
|
+
...shown.map(section),
|
|
5739
|
+
"</div>",
|
|
5603
5740
|
"<p class='dim uses'>This app already uses a database, a nightly schedule, company sign-in and logs. Each has a page on the dashboard: " + pages.map(link).join(" · ") + "</p>",
|
|
5604
5741
|
where === null ? "<p class='dim'>Running under wirl dev, so these pages are not linked here: they open on app.wirl.dev once it is deployed.</p>" : "",
|
|
5605
5742
|
"</section>",
|
|
5606
5743
|
].join("\n");
|
|
5607
5744
|
}
|
|
5608
5745
|
|
|
5746
|
+
// The steps of "Make it yours", one function each. ctx is { me, env, notes,
|
|
5747
|
+
// where }: who is asking, the app's env (env.DB and the rest), the notes on
|
|
5748
|
+
// the board, and the app's dashboard links (null under wirl dev). Each
|
|
5749
|
+
// returns the HTML under its step's heading, and may be async. Until a step
|
|
5750
|
+
// is built it returns its card, the prompt that asks for it; to build a step,
|
|
5751
|
+
// make its function return the feature instead, so the feature stands where
|
|
5752
|
+
// the card was. The heading stays.
|
|
5753
|
+
const STEPS = [step1, step2, step3, step4, step5, step6];
|
|
5754
|
+
|
|
5755
|
+
function step1(ctx) {
|
|
5756
|
+
return card(1, ctx);
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
function step2(ctx) {
|
|
5760
|
+
return card(2, ctx);
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
function step3(ctx) {
|
|
5764
|
+
return card(3, ctx);
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
function step4(ctx) {
|
|
5768
|
+
return card(4, ctx);
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
function step5(ctx) {
|
|
5772
|
+
return card(5, ctx);
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
function step6(ctx) {
|
|
5776
|
+
return card(6, ctx);
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
// A step not built yet: what wirl gives the app, Copy prompt, what you get,
|
|
5780
|
+
// and the prompt itself, whole and ready to select. The Slack card also
|
|
5781
|
+
// lists the steps you do by hand.
|
|
5782
|
+
function card(n, ctx) {
|
|
5783
|
+
const each = SUGGESTIONS[n - 1];
|
|
5784
|
+
const stop = each.capability.indexOf(". ");
|
|
5785
|
+
return (
|
|
5786
|
+
'<div class="card">' +
|
|
5787
|
+
"<p class='dim'><b>" + escape(each.capability.slice(0, stop + 1)) + "</b> " + escape(each.capability.slice(stop + 2)) + "</p>" +
|
|
5788
|
+
"<div class='getrow'>" +
|
|
5789
|
+
"<button type='button' class='copy' data-copy-for='prompt-" + each.number + "'>Copy prompt</button>" +
|
|
5790
|
+
"<p class='dim'>You get: " + escape(each.youGet) + "</p></div>" +
|
|
5791
|
+
(n === 6 ? slackSteps(ctx.where) : "") +
|
|
5792
|
+
"<details class='prompt'><summary>Show the prompt</summary>" +
|
|
5793
|
+
"<pre id='prompt-" + each.number + "'>" + escape(each.prompt) + "</pre></details>" +
|
|
5794
|
+
"</div>"
|
|
5795
|
+
);
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
// What you do by hand to connect Slack, in order, each saying where. The
|
|
5799
|
+
// Connections page is the app's own, linked once the app is deployed.
|
|
5800
|
+
function slackSteps(where) {
|
|
5801
|
+
const place = (name) =>
|
|
5802
|
+
where !== null && name.includes("Connections page")
|
|
5803
|
+
? "<a href='" + escape(where.app + "/connections") + "'>" + escape(name) + "</a>"
|
|
5804
|
+
: escape(name);
|
|
5805
|
+
return (
|
|
5806
|
+
"<p>Your steps, once your agent gives you the link:</p>" +
|
|
5807
|
+
"<ol class='slack'>" + SLACK_STEPS.map(([name, what]) => "<li><b>" + place(name) + "</b>: " + escape(what) + "</li>").join("") + "</ol>" +
|
|
5808
|
+
"<p class='dim'>" + escape(SLACK_IGNORE) + "</p>"
|
|
5809
|
+
);
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
// The deployment answering, as wirl names it: its version number, or "dev"
|
|
5813
|
+
// under wirl dev.
|
|
5814
|
+
function version(env) {
|
|
5815
|
+
return env.WIRL_DEPLOYMENT ?? "unknown";
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5609
5818
|
`;
|
|
5610
5819
|
var SERVER_TAIL = String.raw`
|
|
5611
5820
|
|
|
@@ -5627,15 +5836,15 @@ const STYLE = [
|
|
|
5627
5836
|
".meta, .actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13px; }",
|
|
5628
5837
|
".actions { margin-top: 8px; } .actions form { margin: 0; } .actions details { flex-basis: 100%; order: 3; }",
|
|
5629
5838
|
".tag { text-decoration: none; }",
|
|
5630
|
-
"
|
|
5631
|
-
".step { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; }",
|
|
5632
|
-
".step.
|
|
5839
|
+
".steps { display: grid; gap: 18px; margin: 16px 0; }",
|
|
5840
|
+
".step .card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; }",
|
|
5841
|
+
"#step-1 > .card, #step-6 > .card { border: 2px solid var(--accent); }",
|
|
5842
|
+
"ol.slack { margin: 6px 0; padding-left: 22px; } ol.slack li { margin: 2px 0; }",
|
|
5633
5843
|
".step h3 { font-size: 16px; margin: 0 0 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }",
|
|
5634
5844
|
".step p { margin: 6px 0; } .step b { color: var(--accent); }",
|
|
5635
5845
|
".number { font-size: 12px; background: var(--ink); color: var(--bg); padding: 2px 7px; border-radius: 5px; }",
|
|
5636
|
-
"
|
|
5846
|
+
"#step-6 .number { background: var(--accent); }",
|
|
5637
5847
|
".badge { font-size: 12px; font-weight: 600; color: var(--dim); border: 1px solid var(--line); padding: 1px 8px; border-radius: 999px; }",
|
|
5638
|
-
".step.first { border: 2px solid var(--accent); }",
|
|
5639
5848
|
".badge.start { color: var(--accent); border-color: var(--accent); }",
|
|
5640
5849
|
".welcome { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; margin: 16px 0 8px; }",
|
|
5641
5850
|
".welcome h2 { font-size: 19px; margin: 4px 0 8px; } .welcome p { margin: 0; }",
|
|
@@ -5667,6 +5876,21 @@ const COPY_SCRIPT = [
|
|
|
5667
5876
|
" if (!navigator.clipboard) return select();",
|
|
5668
5877
|
" navigator.clipboard.writeText(pre.textContent).then(() => flash('Copied'), select);",
|
|
5669
5878
|
"});",
|
|
5879
|
+
"// After a deploy, reload: ask /_version every 3 seconds while this tab is in",
|
|
5880
|
+
"// front, and reload when it names another deployment than the one that drew",
|
|
5881
|
+
"// this page. Stop after 30 minutes without a change.",
|
|
5882
|
+
"(() => {",
|
|
5883
|
+
" const version = document.body.getAttribute('data-version');",
|
|
5884
|
+
" const since = Date.now();",
|
|
5885
|
+
" const timer = setInterval(() => {",
|
|
5886
|
+
" if (Date.now() - since > 30 * 60 * 1000) return clearInterval(timer);",
|
|
5887
|
+
" if (document.visibilityState !== 'visible') return;",
|
|
5888
|
+
" fetch('/_version', { cache: 'no-store', redirect: 'error' })",
|
|
5889
|
+
" .then((response) => (response.ok ? response.text() : null))",
|
|
5890
|
+
" .then((now) => { if (now && now !== version) location.reload(); })",
|
|
5891
|
+
" .catch(() => {});",
|
|
5892
|
+
" }, 3000);",
|
|
5893
|
+
"})();",
|
|
5670
5894
|
"</script>",
|
|
5671
5895
|
].join("\n");
|
|
5672
5896
|
|
|
@@ -5728,7 +5952,7 @@ var README = [
|
|
|
5728
5952
|
"",
|
|
5729
5953
|
"Your first app on wirl: a board where your team posts short notes, tags them, pins the ones everyone should see, and edits or removes their own. Every night a schedule archives old notes nobody pinned.",
|
|
5730
5954
|
"",
|
|
5731
|
-
"Open it at the address `wirl deploy` prints. It starts private to you. Its page is the way in: six
|
|
5955
|
+
"Open it at the address `wirl deploy` prints. It starts private to you. Its page is the way in: six steps, each a prompt to give your coding agent until the agent builds it there, saying what wirl gives the app and what you get, and under them the app's pages on app.wirl.dev.",
|
|
5732
5956
|
"",
|
|
5733
5957
|
"## What is where",
|
|
5734
5958
|
"",
|
|
@@ -5745,11 +5969,14 @@ var README = [
|
|
|
5745
5969
|
"",
|
|
5746
5970
|
"## Make it yours",
|
|
5747
5971
|
"",
|
|
5748
|
-
"The same six prompts as the page. Each is a whole prompt: paste it to your coding agent as it is. Go in order or skip around; the last one is the big one.",
|
|
5972
|
+
"The same six prompts as the page. Each is a whole prompt: paste it to your coding agent as it is, and the step turns into the feature. Go in order or skip around; the last one is the big one.",
|
|
5973
|
+
"",
|
|
5974
|
+
STEP_RULE,
|
|
5749
5975
|
"",
|
|
5750
5976
|
...SUGGESTIONS.flatMap(({ number, title, capability, prompt, youGet, optional }) => {
|
|
5751
5977
|
const [bold, rest] = splitCapability(capability);
|
|
5752
|
-
|
|
5978
|
+
const slack = number === 6 ? ["Your steps, once your agent gives you the link:", "", ...SLACK_STEPS.map(([place, what], index) => `${String(index + 1)}. ${place}: ${what}`), "", SLACK_IGNORE, ""] : [];
|
|
5979
|
+
return [`### ${String(number)}. ${title}`, "", ...optional ? ["Optional. The big one.", ""] : [], `**${bold}** ${rest}`, "", `> ${prompt}`, "", `You get: ${youGet}`, "", ...slack];
|
|
5753
5980
|
})
|
|
5754
5981
|
].join("\n");
|
|
5755
5982
|
var STARTER_FILES = {
|
|
@@ -6444,9 +6671,15 @@ function formatResolvedConnections(resolved) {
|
|
|
6444
6671
|
if (each.existing === null) continue;
|
|
6445
6672
|
const { kind, keyed, alsoGrantedTo } = each.existing;
|
|
6446
6673
|
const others = alsoGrantedTo.length > 0 ? `; also granted to ${alsoGrantedTo.join(", ")}` : "";
|
|
6674
|
+
const sentAs = each.headerChanged ? `; now sent as ${each.headerChanged.to}` : "";
|
|
6447
6675
|
lines.push(
|
|
6448
|
-
`${each.slug}: the org's existing connection (${kind}, ${keyState(kind, keyed)}), made by ${connectionCreatorLabel(each.existing)}${others}.`
|
|
6676
|
+
`${each.slug}: the org's existing connection (${kind}, ${keyState(kind, keyed)}), made by ${connectionCreatorLabel(each.existing)}${others}${sentAs}.`
|
|
6449
6677
|
);
|
|
6678
|
+
if (each.headerHeld) {
|
|
6679
|
+
lines.push(
|
|
6680
|
+
`${each.slug}: wirl.json names the header ${each.headerHeld.declared}, but its key is still sent as ${each.headerHeld.sentAs}: only its maker or an org admin can change that.`
|
|
6681
|
+
);
|
|
6682
|
+
}
|
|
6450
6683
|
if (each.kindChanged) {
|
|
6451
6684
|
lines.push(`Changed: this deploy changed ${each.slug}'s kind from ${each.kindChanged.from} to ${each.kindChanged.to}, as wirl.json declares.`);
|
|
6452
6685
|
}
|
|
@@ -7106,6 +7339,7 @@ var deletePreview = (creds, org, app, name, options = {}) => callApi(creds, `/ap
|
|
|
7106
7339
|
() => void 0
|
|
7107
7340
|
);
|
|
7108
7341
|
var mintVisit = (creds, org, app, name, options = {}) => callApi(creds, `/api/v1/apps/${seg(org)}/${seg(app)}/previews/${seg(name)}/visit`, pass(options, "POST"));
|
|
7342
|
+
var mintLiveVisit = (creds, org, app, options = {}) => callApi(creds, `/api/v1/apps/${seg(org)}/${seg(app)}/visit`, pass(options, "POST"));
|
|
7109
7343
|
var runPreviewSchedule = (creds, org, app, name, schedule) => callApi(
|
|
7110
7344
|
creds,
|
|
7111
7345
|
`/api/v1/apps/${seg(org)}/${seg(app)}/previews/${seg(name)}/schedules/${seg(schedule)}/run`,
|
|
@@ -7388,9 +7622,9 @@ function isLocalApiOrigin(apiOrigin) {
|
|
|
7388
7622
|
return false;
|
|
7389
7623
|
}
|
|
7390
7624
|
}
|
|
7391
|
-
function
|
|
7625
|
+
function isOwnAppHost(target, org, app, preview, appsDomain, apiOrigin) {
|
|
7392
7626
|
const parsed = parseAppHost(target.host, appsDomain);
|
|
7393
|
-
if (parsed === null || parsed.preview !==
|
|
7627
|
+
if (parsed === null || parsed.preview !== preview || parsed.orgSlug !== org || parsed.appSlug !== app) return false;
|
|
7394
7628
|
return target.protocol === "https:" || target.protocol === "http:" && isLocalApiOrigin(apiOrigin);
|
|
7395
7629
|
}
|
|
7396
7630
|
function isVisitRefusal(status, location, apiOrigin) {
|
|
@@ -7403,12 +7637,13 @@ function isVisitRefusal(status, location, apiOrigin) {
|
|
|
7403
7637
|
return false;
|
|
7404
7638
|
}
|
|
7405
7639
|
}
|
|
7406
|
-
async function
|
|
7407
|
-
const
|
|
7640
|
+
async function fetchPage(creds, org, app, preview, path, options) {
|
|
7641
|
+
const client = options.fetchImpl ? { fetchImpl: options.fetchImpl } : {};
|
|
7642
|
+
const mint = preview === null ? await mintLiveVisit(creds, org, app, client) : await mintVisit(creds, org, app, preview, client);
|
|
7408
7643
|
const target = new URL(mint.url);
|
|
7409
|
-
if (!
|
|
7644
|
+
if (!isOwnAppHost(target, org, app, preview, options.appsDomain, creds.apiOrigin)) {
|
|
7410
7645
|
throw new CliError(
|
|
7411
|
-
`Refusing to send the visit token to ${target.host}: that is not ${org}--${app}--${
|
|
7646
|
+
`Refusing to send the visit token to ${target.host}: that is not ${preview === null ? `${org}--${app}` : `${org}--${app}--${preview}`}'s own address.`
|
|
7412
7647
|
);
|
|
7413
7648
|
}
|
|
7414
7649
|
const withoutHash = path.split("#", 1)[0] ?? "";
|
|
@@ -7450,6 +7685,7 @@ async function previewFetch(creds, org, app, name, path, options) {
|
|
|
7450
7685
|
const media = (contentType.split(";")[0] ?? "").trim().toLowerCase();
|
|
7451
7686
|
const stream = text && (STREAMED_MEDIA_TYPES.includes(media) || read.stillStreaming);
|
|
7452
7687
|
return {
|
|
7688
|
+
...preview === null ? { live: true } : {},
|
|
7453
7689
|
status: response.status,
|
|
7454
7690
|
statusText: response.statusText,
|
|
7455
7691
|
contentType,
|
|
@@ -7566,7 +7802,9 @@ function formatFetchResult(result2, saved) {
|
|
|
7566
7802
|
];
|
|
7567
7803
|
if (result2.location) lines.push(`location: ${result2.location}`);
|
|
7568
7804
|
if (result2.refused) {
|
|
7569
|
-
lines.push(
|
|
7805
|
+
lines.push(
|
|
7806
|
+
result2.live ? "The visit to the live app was refused: check that you may still deploy it, and that it has been deployed." : "The visit was refused, or the preview has expired or been deleted. Check `wirl previews`."
|
|
7807
|
+
);
|
|
7570
7808
|
}
|
|
7571
7809
|
lines.push("");
|
|
7572
7810
|
if (saved !== void 0) {
|
|
@@ -8609,13 +8847,14 @@ var SHARED = [
|
|
|
8609
8847
|
},
|
|
8610
8848
|
{
|
|
8611
8849
|
name: "wirl_preview_fetch",
|
|
8612
|
-
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.`,
|
|
8613
|
-
annotations: { title: "Request a page
|
|
8850
|
+
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. With live: true and no name it makes the request to the live app instead, as the person, after a deploy: only for someone who may deploy the app, the same people wirl_logs answers. 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.`,
|
|
8851
|
+
annotations: { title: "Request a page: preview or live app", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
8614
8852
|
inputSchema: {
|
|
8615
8853
|
type: "object",
|
|
8616
8854
|
properties: {
|
|
8617
8855
|
app: { type: "string", description: "The app slug the preview belongs to." },
|
|
8618
|
-
name: { type: "string", description: "The preview's name, from wirl_preview or wirl_list_previews." },
|
|
8856
|
+
name: { type: "string", description: "The preview's name, from wirl_preview or wirl_list_previews. Leave it out with live." },
|
|
8857
|
+
live: { type: "boolean", description: "Request the live app instead of a preview, as the person. Only for someone who may deploy it; never with name." },
|
|
8619
8858
|
path: { type: "string", description: 'The path to request, e.g. "/" or "/api/notes".' },
|
|
8620
8859
|
method: { type: "string", description: "HTTP method. Defaults to GET, or POST when a body is given." },
|
|
8621
8860
|
body: { type: "string", description: "Request body, sent as-is." },
|
|
@@ -8626,7 +8865,7 @@ var SHARED = [
|
|
|
8626
8865
|
},
|
|
8627
8866
|
org: ORG_ARG
|
|
8628
8867
|
},
|
|
8629
|
-
required: ["app", "
|
|
8868
|
+
required: ["app", "path"]
|
|
8630
8869
|
},
|
|
8631
8870
|
answer: previewFetchAnswer,
|
|
8632
8871
|
async handler(args, ctx) {
|
|
@@ -8692,8 +8931,12 @@ async function previewFetchAnswer(args, ctx) {
|
|
|
8692
8931
|
const org = await defaultOrg(ctx.creds, str(args, "org"));
|
|
8693
8932
|
const headersArg = args["headers"];
|
|
8694
8933
|
const headers = headersArg !== null && typeof headersArg === "object" && !Array.isArray(headersArg) ? Object.fromEntries(Object.entries(headersArg).map(([k, v]) => [k, String(v)])) : void 0;
|
|
8934
|
+
const live = args["live"] === true;
|
|
8935
|
+
const name = str(args, "name");
|
|
8936
|
+
if (live && name !== "") throw new CliError("live reads the live app and name a preview: give one of them, not both.");
|
|
8937
|
+
if (!live && name === "") throw new CliError("Which preview? Give its name, or live: true for the live app.");
|
|
8695
8938
|
const appsDomain = ctx.appsDomain ?? (await whoami(ctx.creds)).appsDomain;
|
|
8696
|
-
const result2 = await
|
|
8939
|
+
const result2 = await fetchPage(ctx.creds, org, str(args, "app"), live ? null : name, str(args, "path", "/"), {
|
|
8697
8940
|
appsDomain,
|
|
8698
8941
|
...ctx.networkFetch ? { networkFetchImpl: ctx.networkFetch } : {},
|
|
8699
8942
|
...ctx.now ? { now: ctx.now } : {},
|