@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.12
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/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
- package/dist/serve/docs-ui/docs/index.json +200 -25
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +131 -66
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/llms.txt +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/package.json +5 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +493 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +46 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +540 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +75 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-plugin.ts +1102 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +202 -0
- package/src/serve/worker/client/core.ts +350 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +179 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1112 -0
- package/src/serve/worker/serve-init.ts +606 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +17 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process session store for `firestore_discover_paths` continuations.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3.3 locked the session API: the agent receives an opaque
|
|
5
|
+
* `continuation` token between batches and the server holds the
|
|
6
|
+
* partial-schema state. This module owns the storage layer; Item 4.2
|
|
7
|
+
* wires it into the crawler.
|
|
8
|
+
*
|
|
9
|
+
* Hard caps (per prerequisite 0.G — bounded session memory):
|
|
10
|
+
* - `maxSessions` = 8 — total live sessions
|
|
11
|
+
* - `maxSessionBytes` = 32 MB — per-session payload (well above
|
|
12
|
+
* Phase 0.4's 11.64 MB worst case)
|
|
13
|
+
* - `ttlMs` = 30 min — idle TTL (Phase 3.3 lock)
|
|
14
|
+
*
|
|
15
|
+
* Token format: `disc_` + base64url(16-byte ULID). ULID gives natural
|
|
16
|
+
* monotonic ordering (timestamp-prefixed) and avoids a runtime
|
|
17
|
+
* dependency on the `ulid` package.
|
|
18
|
+
*
|
|
19
|
+
* Error model — never throws on expected paths (per Item 4 acceptance
|
|
20
|
+
* criteria). Returns `{ ok: false, error: { code, message, recoveryHint } }`:
|
|
21
|
+
* - `SESSION_EXPIRED` — token unknown OR past TTL (per 0.C)
|
|
22
|
+
* - `SESSION_EVICTED` — token was capacity-evicted by an LRU
|
|
23
|
+
* bump from a concurrent agent (per 0.G)
|
|
24
|
+
* - `SESSION_PAYLOAD_TOO_LARGE` — state would exceed `maxSessionBytes`
|
|
25
|
+
* - `SESSION_MALFORMED_TOKEN` — wrong prefix or undecodable b64
|
|
26
|
+
*/
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default RNG — Web Crypto's `getRandomValues`. Works in modern
|
|
31
|
+
* browsers AND Node 19+ (webcrypto on globalThis) AND Bun, so this
|
|
32
|
+
* module evaluates cleanly in any environment without a top-level
|
|
33
|
+
* `node:crypto` import.
|
|
34
|
+
*
|
|
35
|
+
* Tests + Node consumers that want a deterministic seam can still
|
|
36
|
+
* override via `SessionStoreOptions.randomBytes`.
|
|
37
|
+
*/
|
|
38
|
+
function defaultRandomBytes(n: number): Uint8Array {
|
|
39
|
+
const buf = new Uint8Array(n);
|
|
40
|
+
globalThis.crypto.getRandomValues(buf);
|
|
41
|
+
return buf;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Defaults (per prerequisite 0.G + Phase 3.3) ──────────────────────────
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_MAX_SESSIONS = 8;
|
|
47
|
+
export const DEFAULT_MAX_SESSION_BYTES = 32 * 1024 * 1024; // 32 MB
|
|
48
|
+
export const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 min idle
|
|
49
|
+
/**
|
|
50
|
+
* How many recently-evicted tokens to remember so a returning agent can
|
|
51
|
+
* be told *why* their session is gone (`SESSION_EVICTED`) rather than
|
|
52
|
+
* the generic `SESSION_EXPIRED`. Bounded ring buffer; older entries get
|
|
53
|
+
* overwritten and degrade silently to `SESSION_EXPIRED`.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_EVICTION_LOG_SIZE = 64;
|
|
56
|
+
|
|
57
|
+
const TOKEN_PREFIX = 'disc_';
|
|
58
|
+
|
|
59
|
+
// ─── Types ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export type SessionErrorCode =
|
|
62
|
+
| 'SESSION_EXPIRED'
|
|
63
|
+
| 'SESSION_EVICTED'
|
|
64
|
+
| 'SESSION_PAYLOAD_TOO_LARGE'
|
|
65
|
+
| 'SESSION_MALFORMED_TOKEN';
|
|
66
|
+
|
|
67
|
+
export interface SessionError {
|
|
68
|
+
code: SessionErrorCode;
|
|
69
|
+
message: string;
|
|
70
|
+
recoveryHint: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A live session record. `state` is opaque to the store — Item 4.2 will
|
|
75
|
+
* instantiate `SessionStore` with the concrete crawler-state type.
|
|
76
|
+
*/
|
|
77
|
+
export interface SessionRecord<TState> {
|
|
78
|
+
/** Raw ULID — internal id; not the token surfaced to agents. */
|
|
79
|
+
readonly id: string;
|
|
80
|
+
/** Opaque continuation handle: `disc_<base64url-ulid>`. */
|
|
81
|
+
readonly token: string;
|
|
82
|
+
readonly createdAt: number;
|
|
83
|
+
/** Last get/update; drives both LRU eviction and TTL. */
|
|
84
|
+
lastAccessedAt: number;
|
|
85
|
+
/** Caller-reported payload size for byte-cap enforcement. */
|
|
86
|
+
bytes: number;
|
|
87
|
+
state: TState;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SessionStoreOptions {
|
|
91
|
+
maxSessions?: number;
|
|
92
|
+
maxSessionBytes?: number;
|
|
93
|
+
ttlMs?: number;
|
|
94
|
+
/** Test seam — defaults to `Date.now`. */
|
|
95
|
+
now?: () => number;
|
|
96
|
+
/** Test seam — defaults to `crypto.randomBytes`. */
|
|
97
|
+
randomBytes?: (n: number) => Uint8Array;
|
|
98
|
+
evictionLogSize?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Discriminated-union result so callers don't have to try/catch. */
|
|
102
|
+
export type SessionResult<T> =
|
|
103
|
+
| { ok: true; value: T }
|
|
104
|
+
| { ok: false; error: SessionError };
|
|
105
|
+
|
|
106
|
+
// ─── ULID + token codec ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 16-byte ULID: 6 bytes big-endian ms timestamp, 10 bytes randomness.
|
|
110
|
+
* Sortable by creation time, collision-free at 80 bits randomness.
|
|
111
|
+
*/
|
|
112
|
+
function generateUlidBytes(now: number, rand: (n: number) => Uint8Array): Uint8Array {
|
|
113
|
+
const bytes = new Uint8Array(16);
|
|
114
|
+
let ts = Math.max(0, Math.floor(now));
|
|
115
|
+
for (let i = 5; i >= 0; i--) {
|
|
116
|
+
bytes[i] = ts & 0xff;
|
|
117
|
+
ts = Math.floor(ts / 256);
|
|
118
|
+
}
|
|
119
|
+
const r = rand(10);
|
|
120
|
+
bytes.set(r, 6);
|
|
121
|
+
return bytes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
125
|
+
let bin = '';
|
|
126
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);
|
|
127
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function base64UrlToBytes(s: string): Uint8Array | null {
|
|
131
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) return null;
|
|
132
|
+
let b64 = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
133
|
+
while (b64.length % 4) b64 += '=';
|
|
134
|
+
try {
|
|
135
|
+
const bin = atob(b64);
|
|
136
|
+
const out = new Uint8Array(bin.length);
|
|
137
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
138
|
+
return out;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
145
|
+
let s = '';
|
|
146
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
147
|
+
s += bytes[i]!.toString(16).padStart(2, '0');
|
|
148
|
+
}
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Encode a 16-byte ULID into a `disc_<base64url>` token. Internal — used
|
|
154
|
+
* by `SessionStore.create`.
|
|
155
|
+
*/
|
|
156
|
+
export function encodeToken(ulidBytes: Uint8Array): string {
|
|
157
|
+
if (ulidBytes.length !== 16) {
|
|
158
|
+
throw new Error(`encodeToken: expected 16-byte ULID, got ${ulidBytes.length}`);
|
|
159
|
+
}
|
|
160
|
+
return TOKEN_PREFIX + bytesToBase64Url(ulidBytes);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Decode a `disc_<base64url>` token. Returns `null` on any malformation
|
|
165
|
+
* — caller maps null to `SESSION_MALFORMED_TOKEN`.
|
|
166
|
+
*/
|
|
167
|
+
export function decodeToken(token: string): { id: string } | null {
|
|
168
|
+
if (typeof token !== 'string' || !token.startsWith(TOKEN_PREFIX)) return null;
|
|
169
|
+
const tail = token.slice(TOKEN_PREFIX.length);
|
|
170
|
+
if (tail.length === 0) return null;
|
|
171
|
+
const bytes = base64UrlToBytes(tail);
|
|
172
|
+
if (bytes === null || bytes.length !== 16) return null;
|
|
173
|
+
return { id: bytesToHex(bytes) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Session store ────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* In-process LRU session store with TTL sweep and per-session byte cap.
|
|
180
|
+
*
|
|
181
|
+
* Eviction policy:
|
|
182
|
+
* 1. On every `create`/`get`/`update`, sweep TTL-expired sessions first.
|
|
183
|
+
* Their tokens land in the eviction log as `SESSION_EXPIRED`.
|
|
184
|
+
* 2. If `create` would exceed `maxSessions`, evict the LRU
|
|
185
|
+
* (oldest-by-`lastAccessedAt`). Its token lands in the eviction log
|
|
186
|
+
* as `SESSION_EVICTED` so the displaced agent gets a meaningful
|
|
187
|
+
* error on its next call.
|
|
188
|
+
* 3. `update` rejects with `SESSION_PAYLOAD_TOO_LARGE` if the new
|
|
189
|
+
* `bytes` exceeds `maxSessionBytes` (per-session cap, not aggregate).
|
|
190
|
+
*
|
|
191
|
+
* The eviction log is a bounded ring buffer; once it overflows, evicted
|
|
192
|
+
* tokens degrade silently to `SESSION_EXPIRED` (still actionable — the
|
|
193
|
+
* recoveryHint is the same: re-issue without continuation).
|
|
194
|
+
*/
|
|
195
|
+
export class SessionStore<TState> {
|
|
196
|
+
private readonly maxSessions: number;
|
|
197
|
+
private readonly maxSessionBytes: number;
|
|
198
|
+
private readonly ttlMs: number;
|
|
199
|
+
private readonly evictionLogSize: number;
|
|
200
|
+
private readonly now: () => number;
|
|
201
|
+
private readonly rand: (n: number) => Uint8Array;
|
|
202
|
+
|
|
203
|
+
private readonly sessions = new Map<string, SessionRecord<TState>>();
|
|
204
|
+
/** id → eviction reason. Bounded — see `recordEviction`. */
|
|
205
|
+
private readonly evictionLog = new Map<string, SessionErrorCode>();
|
|
206
|
+
|
|
207
|
+
constructor(opts: SessionStoreOptions = {}) {
|
|
208
|
+
this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
209
|
+
this.maxSessionBytes = opts.maxSessionBytes ?? DEFAULT_MAX_SESSION_BYTES;
|
|
210
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
211
|
+
this.evictionLogSize = opts.evictionLogSize ?? DEFAULT_EVICTION_LOG_SIZE;
|
|
212
|
+
this.now = opts.now ?? Date.now;
|
|
213
|
+
this.rand = opts.randomBytes ?? defaultRandomBytes;
|
|
214
|
+
|
|
215
|
+
if (!Number.isInteger(this.maxSessions) || this.maxSessions < 1) {
|
|
216
|
+
throw new RangeError(
|
|
217
|
+
`SessionStore: maxSessions must be a positive integer, got ${this.maxSessions}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!Number.isInteger(this.maxSessionBytes) || this.maxSessionBytes < 1) {
|
|
221
|
+
throw new RangeError(
|
|
222
|
+
`SessionStore: maxSessionBytes must be a positive integer, got ${this.maxSessionBytes}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (!Number.isInteger(this.ttlMs) || this.ttlMs < 1) {
|
|
226
|
+
throw new RangeError(
|
|
227
|
+
`SessionStore: ttlMs must be a positive integer, got ${this.ttlMs}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Live session count. */
|
|
233
|
+
get size(): number {
|
|
234
|
+
return this.sessions.size;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a new session. Always succeeds unless `bytes` exceeds the
|
|
239
|
+
* per-session byte cap. On cap-hit, evicts the LRU session — the
|
|
240
|
+
* displaced token will report `SESSION_EVICTED` on its next access.
|
|
241
|
+
*/
|
|
242
|
+
create(state: TState, bytes: number): SessionResult<SessionRecord<TState>> {
|
|
243
|
+
if (bytes > this.maxSessionBytes) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: {
|
|
247
|
+
code: 'SESSION_PAYLOAD_TOO_LARGE',
|
|
248
|
+
message: `Session payload (${bytes} bytes) exceeds cap (${this.maxSessionBytes} bytes)`,
|
|
249
|
+
recoveryHint:
|
|
250
|
+
'Reduce per-batch result size (lower maxSamples or rootFilter to a smaller subtree)',
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const now = this.now();
|
|
255
|
+
this.sweepExpired(now);
|
|
256
|
+
if (this.sessions.size >= this.maxSessions) {
|
|
257
|
+
this.evictLru();
|
|
258
|
+
}
|
|
259
|
+
const ulidBytes = generateUlidBytes(now, this.rand);
|
|
260
|
+
const id = bytesToHex(ulidBytes);
|
|
261
|
+
const token = encodeToken(ulidBytes);
|
|
262
|
+
const record: SessionRecord<TState> = {
|
|
263
|
+
id,
|
|
264
|
+
token,
|
|
265
|
+
createdAt: now,
|
|
266
|
+
lastAccessedAt: now,
|
|
267
|
+
bytes,
|
|
268
|
+
state,
|
|
269
|
+
};
|
|
270
|
+
this.sessions.set(id, record);
|
|
271
|
+
return { ok: true, value: record };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Look up a session by token. Touches `lastAccessedAt` on success so
|
|
276
|
+
* subsequent reads keep the session warm. On expired/malformed/evicted
|
|
277
|
+
* tokens returns the appropriate structured error.
|
|
278
|
+
*/
|
|
279
|
+
get(token: string): SessionResult<SessionRecord<TState>> {
|
|
280
|
+
const decoded = decodeToken(token);
|
|
281
|
+
if (decoded === null) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: {
|
|
285
|
+
code: 'SESSION_MALFORMED_TOKEN',
|
|
286
|
+
message: `Continuation token is malformed (expected ${TOKEN_PREFIX}<base64url>)`,
|
|
287
|
+
recoveryHint: 'Re-issue without continuation to start fresh',
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const now = this.now();
|
|
292
|
+
this.sweepExpired(now);
|
|
293
|
+
const record = this.sessions.get(decoded.id);
|
|
294
|
+
if (!record) return this.lookupEvicted(decoded.id);
|
|
295
|
+
record.lastAccessedAt = now;
|
|
296
|
+
return { ok: true, value: record };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Replace the state of an existing session. Same lookup/error model
|
|
301
|
+
* as `get`, plus per-session-bytes enforcement on the new payload.
|
|
302
|
+
*/
|
|
303
|
+
update(token: string, state: TState, bytes: number): SessionResult<SessionRecord<TState>> {
|
|
304
|
+
const lookup = this.get(token);
|
|
305
|
+
if (!lookup.ok) return lookup;
|
|
306
|
+
if (bytes > this.maxSessionBytes) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: {
|
|
310
|
+
code: 'SESSION_PAYLOAD_TOO_LARGE',
|
|
311
|
+
message: `Updated session payload (${bytes} bytes) exceeds cap (${this.maxSessionBytes} bytes)`,
|
|
312
|
+
recoveryHint:
|
|
313
|
+
'Crawl produced more state than the per-session cap allows; surface partial result and stop',
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const record = lookup.value;
|
|
318
|
+
record.state = state;
|
|
319
|
+
record.bytes = bytes;
|
|
320
|
+
// `get` already touched lastAccessedAt; no need to re-stamp.
|
|
321
|
+
return { ok: true, value: record };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Best-effort delete; returns true if a session was removed. */
|
|
325
|
+
delete(token: string): boolean {
|
|
326
|
+
const decoded = decodeToken(token);
|
|
327
|
+
if (decoded === null) return false;
|
|
328
|
+
return this.sessions.delete(decoded.id);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Drop sessions whose `lastAccessedAt + ttlMs` is in the past.
|
|
333
|
+
* Returns the number of sessions evicted. Public for tests +
|
|
334
|
+
* future scheduled-sweep usage; `create`/`get`/`update` all call it
|
|
335
|
+
* lazily so callers don't normally need to.
|
|
336
|
+
*/
|
|
337
|
+
sweepExpired(now: number = this.now()): number {
|
|
338
|
+
let count = 0;
|
|
339
|
+
const cutoff = now - this.ttlMs;
|
|
340
|
+
for (const [id, record] of this.sessions) {
|
|
341
|
+
if (record.lastAccessedAt < cutoff) {
|
|
342
|
+
this.sessions.delete(id);
|
|
343
|
+
this.recordEviction(id, 'SESSION_EXPIRED');
|
|
344
|
+
count++;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return count;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── internals ───────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
private evictLru(): void {
|
|
353
|
+
let oldestId: string | null = null;
|
|
354
|
+
let oldestTs = Infinity;
|
|
355
|
+
for (const [id, record] of this.sessions) {
|
|
356
|
+
if (record.lastAccessedAt < oldestTs) {
|
|
357
|
+
oldestTs = record.lastAccessedAt;
|
|
358
|
+
oldestId = id;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (oldestId !== null) {
|
|
362
|
+
this.sessions.delete(oldestId);
|
|
363
|
+
this.recordEviction(oldestId, 'SESSION_EVICTED');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private recordEviction(id: string, reason: SessionErrorCode): void {
|
|
368
|
+
if (this.evictionLog.size >= this.evictionLogSize) {
|
|
369
|
+
// Drop the oldest entry — Map iteration is insertion-ordered.
|
|
370
|
+
const firstKey = this.evictionLog.keys().next().value;
|
|
371
|
+
if (firstKey !== undefined) this.evictionLog.delete(firstKey);
|
|
372
|
+
}
|
|
373
|
+
this.evictionLog.set(id, reason);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private lookupEvicted(id: string): SessionResult<SessionRecord<TState>> {
|
|
377
|
+
const reason = this.evictionLog.get(id);
|
|
378
|
+
if (reason === 'SESSION_EVICTED') {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: {
|
|
382
|
+
code: 'SESSION_EVICTED',
|
|
383
|
+
message:
|
|
384
|
+
'Continuation was evicted because the session-store capacity is full (a concurrent agent bumped you).',
|
|
385
|
+
recoveryHint:
|
|
386
|
+
'Retry the discover_paths call without the continuation; consider serializing concurrent crawls',
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// SESSION_EXPIRED covers both TTL-expired and never-existed tokens.
|
|
391
|
+
// Both surface the same recovery action: re-issue without continuation.
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: {
|
|
395
|
+
code: 'SESSION_EXPIRED',
|
|
396
|
+
message:
|
|
397
|
+
'Continuation is unknown or past the 30-minute TTL. The server has discarded the partial state.',
|
|
398
|
+
recoveryHint: 'Re-issue without continuation to start fresh',
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createFirestoreDiscoverTools({ resolveDb })` — `ToolHandler[]` for
|
|
3
|
+
* the discover-paths feature, browser-safe (no firebase-admin import).
|
|
4
|
+
*
|
|
5
|
+
* Two handlers:
|
|
6
|
+
* - `firestore_discover_paths` — BFS sample every collection
|
|
7
|
+
* reachable from the root, return per-templatePath schema +
|
|
8
|
+
* cost counters. Supports `dryRun` cost preview and
|
|
9
|
+
* `continuation` resume.
|
|
10
|
+
* - `firestore_find_collection_group` — bounded
|
|
11
|
+
* `collectionGroup(id).select().limit(N).get()` to answer "where
|
|
12
|
+
* does collection ID X appear?".
|
|
13
|
+
*
|
|
14
|
+
* Session lifetime: per-factory-call. Each `createFirestoreDiscoverTools`
|
|
15
|
+
* invocation creates its own `SessionStore` for continuation tokens.
|
|
16
|
+
* Per the in-progress agents work, a single agent process should
|
|
17
|
+
* therefore reuse one factory instance for the duration of a session.
|
|
18
|
+
*
|
|
19
|
+
* Per F2 / F4: identity is a value (the resolver), lifecycle is
|
|
20
|
+
* per-dispatch. `resolveDb()` runs inside each tool execute so hosts
|
|
21
|
+
* can swap the underlying CrawlerFirestore (e.g. admin → sandbox)
|
|
22
|
+
* between calls without re-wiring.
|
|
23
|
+
*
|
|
24
|
+
* Output shape: `crawl()`'s native `finalizedSchemas: Map<...>` flattens
|
|
25
|
+
* to a `schemas: Record<templatePath, ...>` shape for JSON
|
|
26
|
+
* serialization across the tool boundary. `discovered` (raw refs) is
|
|
27
|
+
* dropped — agents consume the finalized schemas.
|
|
28
|
+
*/
|
|
29
|
+
import type { ToolHandler } from '@inbrowser/agent';
|
|
30
|
+
import { crawl, type FullCrawlResult } from './crawler.js';
|
|
31
|
+
import { findCollectionGroup } from './findCollectionGroup.js';
|
|
32
|
+
import { SessionStore } from './session.js';
|
|
33
|
+
import type { PersistedCrawlState } from './crawler.js';
|
|
34
|
+
import type { CollectionSchema, DiscoverEvent } from './types.js';
|
|
35
|
+
import type {
|
|
36
|
+
CollectionGroupCapableFirestore,
|
|
37
|
+
CrawlerFirestore,
|
|
38
|
+
} from './firestore-source.js';
|
|
39
|
+
|
|
40
|
+
export interface FirestoreDiscoverToolDeps {
|
|
41
|
+
/**
|
|
42
|
+
* Resolver returning the CrawlerFirestore to scan. Called per
|
|
43
|
+
* dispatch (F4). For `firestore_find_collection_group` the returned
|
|
44
|
+
* Firestore must also satisfy {@link CollectionGroupCapableFirestore}.
|
|
45
|
+
*/
|
|
46
|
+
resolveDb(): CrawlerFirestore & CollectionGroupCapableFirestore;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** JSON-serializable shape returned by `firestore_discover_paths`. */
|
|
50
|
+
export interface DiscoverPathsToolResult {
|
|
51
|
+
/** Per-templatePath finalized schemas, keyed by templatePath. */
|
|
52
|
+
schemas: Record<string, CollectionSchema>;
|
|
53
|
+
events: DiscoverEvent[];
|
|
54
|
+
/** `listCollections` + `listDocuments` calls — cumulative across batches. */
|
|
55
|
+
listOps: number;
|
|
56
|
+
/** `.get()` calls during sampling — cumulative across batches. */
|
|
57
|
+
readOps: number;
|
|
58
|
+
/** Opaque resume handle iff the crawl paused at a payload boundary. */
|
|
59
|
+
continuation?: string;
|
|
60
|
+
/** True iff the crawl finished. Equivalent to `continuation === undefined`. */
|
|
61
|
+
complete: boolean;
|
|
62
|
+
/** Present iff this was a `dryRun: true` preview. */
|
|
63
|
+
dryRunCostEstimate?: FullCrawlResult['dryRunCostEstimate'];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DiscoverPathsArgs {
|
|
67
|
+
maxDepth?: number;
|
|
68
|
+
maxConcurrency?: number;
|
|
69
|
+
maxSamples?: number;
|
|
70
|
+
stopOnStable?: number;
|
|
71
|
+
maxBatchBytes?: number;
|
|
72
|
+
rootPrefix?: string;
|
|
73
|
+
dryRun?: boolean;
|
|
74
|
+
dryRunSubtreeMultiplier?: number;
|
|
75
|
+
continuation?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface FindCollectionGroupArgs {
|
|
79
|
+
collectionId: string;
|
|
80
|
+
limit?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createFirestoreDiscoverTools(
|
|
84
|
+
deps: FirestoreDiscoverToolDeps,
|
|
85
|
+
): ToolHandler[] {
|
|
86
|
+
const { resolveDb } = deps;
|
|
87
|
+
// Per-factory session store — see header note on lifetime.
|
|
88
|
+
const sessions = new SessionStore<PersistedCrawlState>();
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
name: 'firestore_discover_paths',
|
|
93
|
+
description:
|
|
94
|
+
'Sample every collection reachable from the database root and return per-templatePath schemas (field types, presence, enums, examples) plus discovered subcollection paths. Use this to understand a Firestore database\'s shape before generating client code, security rules, or analytics queries. ' +
|
|
95
|
+
'Cost is bounded: structure walk is one listCollections/listDocuments per collection; sampling is min(maxSamples, available) per templatePath with adaptive early-exit. Cumulative cost is reported as listOps + readOps. ' +
|
|
96
|
+
'Set dryRun: true for an informational cost preview that issues exactly one root listCollections RPC and returns a heuristic projection — no documents are read. Large databases pause at payload boundaries and return a continuation token; pass it back as the next call\'s continuation to resume.',
|
|
97
|
+
parameters: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
maxDepth: { type: 'integer', minimum: 1, description: 'Max BFS depth from root collections. Default 10 — runaway guard.' },
|
|
101
|
+
maxConcurrency: { type: 'integer', minimum: 1, description: 'In-flight RPC cap. Default 32.' },
|
|
102
|
+
maxSamples: { type: 'integer', minimum: 1, description: 'Hard cap on docs sampled per templatePath. Default 50.' },
|
|
103
|
+
stopOnStable: { type: 'integer', minimum: 1, description: 'Adaptive early-exit: stop sampling a templatePath after this many consecutive no-change merges. Default 8. Set higher than maxSamples to disable.' },
|
|
104
|
+
maxBatchBytes: { type: 'integer', minimum: 1024, description: 'Pause when persisted state exceeds this many bytes. Default 1 MB. Only effective when continuations are in play.' },
|
|
105
|
+
rootPrefix: { type: 'string', description: 'Restrict the crawl to root collections whose ID starts with this prefix.' },
|
|
106
|
+
dryRun: { type: 'boolean', description: 'Cost-preview mode. Issues exactly one root listCollections RPC and returns a heuristic projection. No documents are read. Cannot be combined with a continuation token.' },
|
|
107
|
+
dryRunSubtreeMultiplier: { type: 'integer', minimum: 1, description: 'Multiplier in the dryRun projection: roots × this = projected total collections. Default 3 (conservative).' },
|
|
108
|
+
continuation: { type: 'string', description: 'Resume a paused crawl. Use the continuation token from a prior result. Pass nothing to start fresh.' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
async execute(args) {
|
|
112
|
+
const params = (args ?? {}) as DiscoverPathsArgs;
|
|
113
|
+
try {
|
|
114
|
+
const db = resolveDb();
|
|
115
|
+
const rootFilter = params.rootPrefix
|
|
116
|
+
? (id: string) => id.startsWith(params.rootPrefix!)
|
|
117
|
+
: undefined;
|
|
118
|
+
const result = await crawl(
|
|
119
|
+
db,
|
|
120
|
+
{
|
|
121
|
+
maxDepth: params.maxDepth,
|
|
122
|
+
maxConcurrency: params.maxConcurrency,
|
|
123
|
+
maxSamples: params.maxSamples,
|
|
124
|
+
stopOnStable: params.stopOnStable,
|
|
125
|
+
maxBatchBytes: params.maxBatchBytes,
|
|
126
|
+
rootFilter,
|
|
127
|
+
dryRun: params.dryRun,
|
|
128
|
+
dryRunSubtreeMultiplier: params.dryRunSubtreeMultiplier,
|
|
129
|
+
continuation: params.continuation,
|
|
130
|
+
},
|
|
131
|
+
sessions,
|
|
132
|
+
);
|
|
133
|
+
const schemas: Record<string, CollectionSchema> = {};
|
|
134
|
+
for (const [templatePath, schema] of result.finalizedSchemas) {
|
|
135
|
+
schemas[templatePath] = schema;
|
|
136
|
+
}
|
|
137
|
+
const data: DiscoverPathsToolResult = {
|
|
138
|
+
schemas,
|
|
139
|
+
events: result.events,
|
|
140
|
+
listOps: result.listOps,
|
|
141
|
+
readOps: result.readOps,
|
|
142
|
+
complete: result.complete,
|
|
143
|
+
...(result.continuation !== undefined && { continuation: result.continuation }),
|
|
144
|
+
...(result.dryRunCostEstimate !== undefined && {
|
|
145
|
+
dryRunCostEstimate: result.dryRunCostEstimate,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
summary: `Discovered ${Object.keys(schemas).length} template path(s); ${result.readOps} read(s), ${result.listOps} list(s)${result.complete ? '' : ' — paused, resume with continuation'}`,
|
|
151
|
+
data,
|
|
152
|
+
};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
summary: `firestore_discover_paths failed: ${message}`,
|
|
158
|
+
data: { success: false, error: { code: 'DISCOVER_FAILED', message, recoverable: false } },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
name: 'firestore_find_collection_group',
|
|
166
|
+
description:
|
|
167
|
+
'Find every collection-group host of a given collection ID. Answers "where does collection ID X appear in the database?" by issuing one collectionGroup(id).select().limit(N).get() — cost is exactly N reads (default 100). ' +
|
|
168
|
+
'Returns hosts in template-path form (e.g. users/{userId}/posts) with the per-host sampleDocCount from the N-doc draw. Coverage of all hosts is statistical: with K hosts, N=100 covers ~22 distinct hosts with high confidence (coupon-collector). The result\'s limitWasReached flag signals whether to raise the cap. ' +
|
|
169
|
+
'Use this as a cold-start alternative to firestore_discover_paths when you already know the collection ID and just need the parent paths.',
|
|
170
|
+
parameters: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
collectionId: { type: 'string', minLength: 1, description: 'The leaf collection ID to find (e.g. "posts", "moves").' },
|
|
174
|
+
limit: { type: 'integer', minimum: 1, description: 'Max docs to fetch from the collection group. Default 100. Each doc costs one read; raise this if limitWasReached is true and you suspect more hosts.' },
|
|
175
|
+
},
|
|
176
|
+
required: ['collectionId'],
|
|
177
|
+
},
|
|
178
|
+
async execute(args) {
|
|
179
|
+
const params = args as FindCollectionGroupArgs;
|
|
180
|
+
try {
|
|
181
|
+
const db = resolveDb();
|
|
182
|
+
const result = await findCollectionGroup(db, params.collectionId, {
|
|
183
|
+
limit: params.limit,
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
summary: `Found ${result.hosts.length} host(s) for '${params.collectionId}'${result.limitWasReached ? ' (limit reached — raise to find more)' : ''}`,
|
|
188
|
+
data: result,
|
|
189
|
+
};
|
|
190
|
+
} catch (e) {
|
|
191
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
summary: `firestore_find_collection_group failed: ${message}`,
|
|
195
|
+
data: { success: false, error: { code: 'FIND_COLLECTION_GROUP_FAILED', message, recoverable: false } },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
}
|