@wenathlan/saddle 1.8.1

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.
Files changed (167) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +192 -0
  3. package/adapters/forge.js +16 -0
  4. package/adapters/forgejo.js +8 -0
  5. package/adapters/github.js +19 -0
  6. package/adapters/gitlab.js +10 -0
  7. package/adapters/huggingface.js +6 -0
  8. package/adapters/socket.js +14 -0
  9. package/adapters/transport.js +30 -0
  10. package/ai/chunk.js +22 -0
  11. package/ai/llmstxt.js +12 -0
  12. package/ai/provenance.js +18 -0
  13. package/ai/rag.js +14 -0
  14. package/ai/tokens.js +9 -0
  15. package/api/auth.js +13 -0
  16. package/api/contracts.js +17 -0
  17. package/api/control.js +33 -0
  18. package/api/http.js +12 -0
  19. package/api/rate.js +31 -0
  20. package/api/security.js +42 -0
  21. package/api/service.js +36 -0
  22. package/binary/build.js +17 -0
  23. package/bot/adapter.js +8 -0
  24. package/bot/bot.js +39 -0
  25. package/bot/commands.js +18 -0
  26. package/bot/permissions.js +16 -0
  27. package/browser/actions.js +33 -0
  28. package/browser/agent.js +9 -0
  29. package/browser/context.js +52 -0
  30. package/browser/fingerprint.js +12 -0
  31. package/browser/index.js +10 -0
  32. package/browser/recorder.js +15 -0
  33. package/browser/session.js +19 -0
  34. package/browser/snapshot.js +57 -0
  35. package/captcha/contract.js +15 -0
  36. package/captcha/evidence.js +9 -0
  37. package/captcha/guard.js +10 -0
  38. package/cli/main.js +36 -0
  39. package/core/errors.js +37 -0
  40. package/core/events.js +21 -0
  41. package/core/hash.js +73 -0
  42. package/core/ids.js +15 -0
  43. package/crawl/crawler.js +29 -0
  44. package/crawl/frontier.js +34 -0
  45. package/crawl/normalize.js +14 -0
  46. package/crawl/persistent.js +13 -0
  47. package/dispatch/resumable.js +31 -0
  48. package/dispatch/workflow.js +33 -0
  49. package/docs/assets/architecture.svg +45 -0
  50. package/docs/assets/saddlemark.svg +13 -0
  51. package/docs/comparativeaudit.md +63 -0
  52. package/docs/ecosystemplan.md +59 -0
  53. package/docs/enginearchitecture.md +83 -0
  54. package/docs/featureaudit.md +63 -0
  55. package/docs/gapmatrix.md +80 -0
  56. package/docs/libraryapi.md +63 -0
  57. package/docs/modes.md +27 -0
  58. package/docs/productindex.md +28 -0
  59. package/docs/registryresearch.md +56 -0
  60. package/docs/release.md +28 -0
  61. package/docs/release17notes.md +24 -0
  62. package/docs/release181notes.md +15 -0
  63. package/docs/release18notes.md +15 -0
  64. package/docs/roadmapp2p3.md +33 -0
  65. package/docs/toolchains.md +28 -0
  66. package/docs/usage.md +107 -0
  67. package/domain/artifacts.js +13 -0
  68. package/domain/jobs.js +20 -0
  69. package/domain/providers.js +8 -0
  70. package/domain/runtime.js +10 -0
  71. package/domain/sessions.js +34 -0
  72. package/errors/taxonomy.js +18 -0
  73. package/examples/localjob.js +15 -0
  74. package/examples/publicapi.js +7 -0
  75. package/extension/README.md +23 -0
  76. package/extension/content.js +85 -0
  77. package/extension/index.js +5 -0
  78. package/extension/manifest.json +10 -0
  79. package/extension/popup.css +13 -0
  80. package/extension/popup.html +24 -0
  81. package/extension/popup.js +25 -0
  82. package/extension/protocol.js +76 -0
  83. package/extension/serviceworker.js +43 -0
  84. package/extension/worker.js +20 -0
  85. package/format/check.js +21 -0
  86. package/index.js +120 -0
  87. package/library/public.js +83 -0
  88. package/license.md +203 -0
  89. package/license.txt +203 -0
  90. package/mcp/browser.js +12 -0
  91. package/mcp/server.js +28 -0
  92. package/mcp/transport.js +14 -0
  93. package/memory/bridge.js +16 -0
  94. package/memory/engine.js +45 -0
  95. package/memory/modes.js +55 -0
  96. package/memory/objects.js +18 -0
  97. package/memory/targets.js +21 -0
  98. package/memory/transforms.js +15 -0
  99. package/modes/matrix.js +20 -0
  100. package/modes/modes.js +16 -0
  101. package/modes/resolve.js +39 -0
  102. package/package.json +47 -0
  103. package/packager/manifest.js +28 -0
  104. package/packager/publish.js +15 -0
  105. package/persistence/adapter.js +8 -0
  106. package/persistence/drizzle.js +10 -0
  107. package/persistence/memory.js +26 -0
  108. package/persistence/migrations.js +14 -0
  109. package/persistence/prisma.js +23 -0
  110. package/persistence/schema.js +29 -0
  111. package/persistence/sql.js +30 -0
  112. package/protocol/blocks.js +18 -0
  113. package/protocol/json.js +5 -0
  114. package/protocol/ndjson.js +17 -0
  115. package/protocol/sse.js +22 -0
  116. package/proxy/pool.js +12 -0
  117. package/queue/idempotency.js +12 -0
  118. package/queue/persistent.js +44 -0
  119. package/queue/queue.js +50 -0
  120. package/queue/saga.js +13 -0
  121. package/readme.txt +163 -0
  122. package/retry/circuit.js +15 -0
  123. package/retry/policy.js +12 -0
  124. package/runners/health.js +23 -0
  125. package/runners/heartbeat.js +26 -0
  126. package/runners/inprocess.js +19 -0
  127. package/runners/scheduler.js +16 -0
  128. package/runtime/abort.js +10 -0
  129. package/runtime/compatibility.js +13 -0
  130. package/runtime/detect.js +14 -0
  131. package/runtime/engine.js +56 -0
  132. package/runtime/worker.js +18 -0
  133. package/scrape/cache.js +14 -0
  134. package/scrape/extract.js +14 -0
  135. package/scrape/robots.js +32 -0
  136. package/scrape/schema.js +21 -0
  137. package/scrape/scraper.js +40 -0
  138. package/scrape/semantic.js +22 -0
  139. package/server/node.js +34 -0
  140. package/sessions/file.js +13 -0
  141. package/sessions/replay.js +21 -0
  142. package/sessions/store.js +13 -0
  143. package/storage/adapter.js +8 -0
  144. package/storage/cache.js +54 -0
  145. package/storage/checksum.js +17 -0
  146. package/storage/chunked.js +58 -0
  147. package/storage/content.js +42 -0
  148. package/storage/filehosting.js +17 -0
  149. package/storage/githubcontents.js +18 -0
  150. package/storage/index.js +10 -0
  151. package/storage/local.js +35 -0
  152. package/storage/memory.js +28 -0
  153. package/storage/s3compatible.js +23 -0
  154. package/storage/sync.js +55 -0
  155. package/surfaces/adapters.js +48 -0
  156. package/surfaces/controls.js +37 -0
  157. package/surfaces/manifest.js +25 -0
  158. package/surfaces/n8n.js +24 -0
  159. package/surfaces/operations.js +43 -0
  160. package/surfaces/targets.js +16 -0
  161. package/webhook/delivery.js +26 -0
  162. package/webhook/receiver.js +20 -0
  163. package/webhook/signature.js +7 -0
  164. package/workflow/manifest.js +20 -0
  165. package/workflow/registry.js +16 -0
  166. package/workflow/templates.js +18 -0
  167. package/workflow/triggers.js +31 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * session logs are versioned and validated before any replay adapter sees them.
3
+ */
4
+ import { validationerror } from "../core/errors.js";
5
+
6
+ const eventtypes = new Set(["move", "click", "drag", "scroll", "key"]);
7
+
8
+ export function validatesession(value) {
9
+ if (!value || typeof value !== "object") throw validationerror("session must be an object");
10
+ if (value.version !== 1 || typeof value.id !== "string" || typeof value.agentname !== "string" || typeof value.originurl !== "string" || typeof value.seed !== "string") throw validationerror("session header is invalid");
11
+ if (!Array.isArray(value.events)) throw validationerror("session events must be an array");
12
+ if (!["created", "recording", "closed"].includes(value.status)) throw validationerror("session status is invalid");
13
+ if (!Number.isFinite(value.startedat) || value.startedat < 0) throw validationerror("session startedat is invalid");
14
+ return {
15
+ version: 1,
16
+ id: value.id,
17
+ agentname: value.agentname,
18
+ originurl: value.originurl,
19
+ seed: value.seed,
20
+ status: value.status,
21
+ startedat: value.startedat,
22
+ finishedat: Number.isFinite(value.finishedat) ? value.finishedat : undefined,
23
+ events: value.events.map((event, index) => validateevent(event, index))
24
+ };
25
+ }
26
+
27
+ function validateevent(value, index) {
28
+ if (!value || typeof value !== "object" || !Number.isFinite(value.t) || value.t < 0 || !eventtypes.has(value.type)) throw validationerror(`session event ${index} is invalid`);
29
+ for (const name of ["x", "y", "tx", "ty", "dx", "dy"]) if (value[name] !== undefined && !Number.isFinite(value[name])) throw validationerror(`session event ${index} ${name} is invalid`);
30
+ if (value.key !== undefined && typeof value.key !== "string") throw validationerror(`session event ${index} key is invalid`);
31
+ if (value.target !== undefined && typeof value.target !== "string") throw validationerror(`session event ${index} target is invalid`);
32
+ if (value.button !== undefined && !["left", "right"].includes(value.button)) throw validationerror(`session event ${index} button is invalid`);
33
+ return { ...value };
34
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * scrape errors carry a stable code status retry flag severity and recovery hint.
3
+ */
4
+ export const errorcatalog = Object.freeze({
5
+ timeout: { code: "E1001", statuscode: 504, retryable: true, recovery: "WAIT_AND_RETRY" },
6
+ connectionrefused: { code: "E1002", statuscode: 503, retryable: true, recovery: "WAIT_AND_RETRY" },
7
+ dns: { code: "E1003", statuscode: 503, retryable: true, recovery: "ROTATE_PROXY" },
8
+ ratelimited: { code: "E2001", statuscode: 429, retryable: true, recovery: "WAIT_AND_RETRY" },
9
+ forbidden: { code: "E2002", statuscode: 403, retryable: false, recovery: "REVIEW_ROBOTS_TXT" },
10
+ notfound: { code: "E2003", statuscode: 404, retryable: false, recovery: "STOP_CRAWLING" },
11
+ parse: { code: "E4002", statuscode: 422, retryable: false, recovery: "STOP_CRAWLING" },
12
+ captcha: { code: "E4003", statuscode: 403, retryable: false, recovery: "REVIEW_ROBOTS_TXT" },
13
+ session: { code: "E5001", statuscode: 401, retryable: true, recovery: "ROTATE_USER_AGENT" },
14
+ config: { code: "E6001", statuscode: 400, retryable: false, recovery: "STOP_CRAWLING" }
15
+ });
16
+
17
+ export function webscrapeerror(kind, message, options = {}) { const preset = errorcatalog[kind] ?? errorcatalog.config; const error = new Error(message, { cause: options.cause }); error.name = "webscrapeerror"; error.code = options.code ?? preset.code; error.statuscode = options.statuscode ?? preset.statuscode; error.retryable = options.retryable ?? preset.retryable; error.recovery = options.recovery ?? preset.recovery; error.severity = options.severity ?? (error.statuscode >= 500 ? "high" : "medium"); error.details = options.details ?? {}; return error; }
18
+ export function classifyerror(error) { if (error?.name === "webscrapeerror") return error; const message = String(error?.message ?? error); if (/timeout|aborted/i.test(message)) return webscrapeerror("timeout", message, { cause: error }); if (/dns|enotfound/i.test(message)) return webscrapeerror("dns", message, { cause: error }); return webscrapeerror("config", message, { cause: error }); }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * the local example shows that the library can run without a remote service.
3
+ */
4
+ import { mkdtemp } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { eventbus, engine, inprocess, scheduler } from "../index.js";
8
+ import { localmemory } from "../memory/bridge.js";
9
+ import { localstorage } from "../storage/local.js";
10
+
11
+ const root = await mkdtemp(join(tmpdir(), "saddleexample"));
12
+ const events = eventbus();
13
+ const run = engine({ storage: localstorage(root), memory: localmemory(), scheduler: scheduler([inprocess()]), events });
14
+ const result = await run.run({ name: "localexample", input: { source: "example" }, outputkey: "results/example.json" }, ({ job }) => ({ jobid: job.id, ok: true }));
15
+ console.log({ jobid: result.job.id, artifact: result.artifact.key, eventcount: events.all().length });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * public api example with an injected fetcher; no network is required to run it.
3
+ */
4
+ import { scrapeurl, formatforagent } from "../library/public.js";
5
+
6
+ const result = await scrapeurl("https://example.com", { fetcher: async () => ({ ok: true, status: 200, text: async () => "<title>Example</title><p>Injected content.</p>" }) });
7
+ console.log(JSON.stringify(formatforagent(result), null, 2));
@@ -0,0 +1,23 @@
1
+ # saddle browser bridge
2
+
3
+ This directory contains the first Manifest V3 surface for Saddle. It is a user initiated bridge, not a hidden automation service.
4
+
5
+ ## load unpacked
6
+
7
+ 1. Open `chrome://extensions`.
8
+ 2. Enable developer mode.
9
+ 3. Choose **Load unpacked**.
10
+ 4. Select this `extension/` directory.
11
+ 5. Open a page, invoke Saddle from the extension action and choose `Snapshot` or `Read page`.
12
+
13
+ The manifest requests only `activeTab`, `scripting` and `storage`. It does not request broad host permissions, cookies, tabs, webRequest or debugger access. `activeTab` gives temporary access after the user invokes the action for the current tab.
14
+
15
+ ## boundaries
16
+
17
+ The content bridge runs in Chrome's isolated world. It exposes bounded page metadata, visible text, stable references and user initiated click or fill commands. The service worker forwards versioned messages and stores only the latest snapshot metadata in session storage. No endpoint, credential, remote script or browser profile is embedded.
18
+
19
+ `protocol.js` and `serviceworker.js` are reusable ESM contracts. `content.js` is intentionally a classic injected file because programmatic Chrome content scripts are loaded as files; it exposes a small global bridge and avoids arbitrary page JavaScript evaluation.
20
+
21
+ ## next slices
22
+
23
+ The next extension slices should add snapshot diffing, tab and frame identity, resumable command records, optional host permission escalation, browser action results and a deterministic zip workflow. Browser providers, login profiles, captcha solvers and remote runners remain caller owned adapters.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * content bridge runs in the isolated world and exposes bounded page facts and user initiated actions.
3
+ * It stays classic JavaScript because Chrome injects programmatic content scripts as files.
4
+ */
5
+
6
+ (function installglobalbridge(global) {
7
+ const installedkey = "__saddlecontentbridge";
8
+ const protocolversion = 1;
9
+ const commands = ["snapshot", "readpage", "clickref", "fillref"];
10
+
11
+ function createbridge(documentref, now = () => Date.now()) {
12
+ let snapshotid = null;
13
+ let sequence = 0;
14
+ const references = new Map();
15
+
16
+ function snapshotpage() {
17
+ const nextid = `snap${++sequence}${now()}`;
18
+ const elements = [];
19
+ references.clear();
20
+ const candidates = documentref.querySelectorAll?.("a,button,input,textarea,select,[role]") ?? [];
21
+ for (const element of Array.from(candidates).slice(0, 100)) {
22
+ if (!visible(element)) continue;
23
+ const ref = `e${elements.length + 1}`;
24
+ references.set(ref, { element, snapshotid: nextid });
25
+ elements.push({ ref, role: roleof(element), name: nameof(element) });
26
+ }
27
+ snapshotid = nextid;
28
+ return { version: protocolversion, snapshotid, createdat: now(), url: String(documentref.location?.href ?? ""), title: String(documentref.title ?? ""), text: String(documentref.body?.innerText ?? "").slice(0, 100000), elements };
29
+ }
30
+
31
+ function readpage() {
32
+ const page = snapshotpage();
33
+ return { version: page.version, snapshotid: page.snapshotid, url: page.url, title: page.title, text: page.text };
34
+ }
35
+
36
+ function resolve(ref, requestedid) {
37
+ if (requestedid !== snapshotid) throw failure("stale_snapshot", "page snapshot is stale");
38
+ const entry = references.get(String(ref));
39
+ if (!entry || entry.snapshotid !== snapshotid) throw failure("unknown_reference", `unknown page reference: ${ref}`);
40
+ return entry.element;
41
+ }
42
+
43
+ function handle(request) {
44
+ if (request?.version !== protocolversion || request.type !== "command" || !commands.includes(request.command)) throw failure("invalid_message", "invalid content command");
45
+ const payload = request.payload ?? {};
46
+ if (request.command === "snapshot") return snapshotpage();
47
+ if (request.command === "readpage") return readpage();
48
+ const element = resolve(payload.ref, payload.snapshotid);
49
+ if (request.command === "clickref") { element.click?.(); return { ref: payload.ref, clicked: true, snapshotid }; }
50
+ if (!isfillable(element)) throw failure("not_fillable", `element is not fillable: ${payload.ref}`);
51
+ setvalue(element, payload.value);
52
+ return { ref: payload.ref, filled: true, snapshotid };
53
+ }
54
+
55
+ return { handle, snapshotpage, readpage };
56
+ }
57
+
58
+ function install(runtime = global.chrome?.runtime, documentref = global.document) {
59
+ if (!runtime?.onMessage || !documentref) throw new TypeError("content bridge requires runtime and document");
60
+ if (global[installedkey]) return global[installedkey];
61
+ const bridge = createbridge(documentref);
62
+ const listener = (message, sender, sendresponse) => {
63
+ Promise.resolve().then(() => bridge.handle(message)).then((payload) => sendresponse({ version: protocolversion, type: "response", id: `resp${Date.now()}`, requestid: message?.id, payload })).catch((error) => sendresponse({ version: protocolversion, type: "error", id: `err${Date.now()}`, requestid: message?.id, error: { code: error.code ?? "content_error", message: error.message } }));
64
+ return true;
65
+ };
66
+ runtime.onMessage.addListener(listener);
67
+ global[installedkey] = { bridge, listener, dispose() { runtime.onMessage.removeListener?.(listener); delete global[installedkey]; } };
68
+ return global[installedkey];
69
+ }
70
+
71
+ function visible(element) {
72
+ const style = global.getComputedStyle?.(element);
73
+ if (style && (style.display === "none" || style.visibility === "hidden")) return false;
74
+ return typeof element.getClientRects !== "function" || element.getClientRects().length > 0;
75
+ }
76
+
77
+ function roleof(element) { return String(element.getAttribute?.("role") || element.tagName || "generic").toLowerCase(); }
78
+ function nameof(element) { return String(element.getAttribute?.("aria-label") || element.innerText || element.textContent || element.getAttribute?.("placeholder") || "").trim().replace(/\s+/g, " ").slice(0, 200); }
79
+ function isfillable(element) { return ["INPUT", "TEXTAREA"].includes(String(element.tagName).toUpperCase()); }
80
+ function setvalue(element, value) { element.value = String(value ?? ""); element.dispatchEvent?.(new Event("input", { bubbles: true })); element.dispatchEvent?.(new Event("change", { bubbles: true })); }
81
+ function failure(code, message) { const error = new Error(message); error.code = code; return error; }
82
+
83
+ global.saddlecontent = { createbridge, install };
84
+ if (global.chrome?.runtime?.onMessage && global.document) install();
85
+ })(globalThis);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * extension public contracts expose browser neutral message and worker routing primitives.
3
+ */
4
+ export * from "./protocol.js";
5
+ export * from "./serviceworker.js";
@@ -0,0 +1,10 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Saddle browser bridge",
4
+ "version": "1.1.0",
5
+ "description": "User initiated page snapshots through the Saddle browser contract.",
6
+ "minimum_chrome_version": "110",
7
+ "permissions": ["activeTab", "scripting", "storage"],
8
+ "background": { "service_worker": "worker.js", "type": "module" },
9
+ "action": { "default_title": "Saddle", "default_popup": "popup.html" }
10
+ }
@@ -0,0 +1,13 @@
1
+ /* popup keeps the extension surface compact, readable and free of remote assets. */
2
+ :root { color-scheme: light; font-family: system-ui, sans-serif; background: #f7f1e8; color: #202a2f; }
3
+ body { width: 320px; margin: 0; }
4
+ main { padding: 16px; }
5
+ header { display: flex; align-items: center; gap: 10px; }
6
+ .mark { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 9px; background: #d35d3d; color: #fff; font-weight: 800; }
7
+ header strong, header small { display: block; }
8
+ header small { color: #6d7777; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
9
+ #status { color: #596663; font-size: 12px; line-height: 1.4; }
10
+ .actions { display: flex; gap: 8px; }
11
+ button { border: 0; border-radius: 8px; padding: 8px 10px; background: #202a2f; color: #fff; cursor: pointer; font: inherit; font-size: 12px; }
12
+ button:hover { background: #d35d3d; }
13
+ pre { max-height: 260px; overflow: auto; margin: 14px 0 0; padding: 10px; border-radius: 8px; background: #fff; color: #394446; font: 11px/1.45 ui-monospace, monospace; white-space: pre-wrap; }
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Saddle</title>
7
+ <link rel="stylesheet" href="popup.css" />
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <header>
12
+ <span class="mark">S</span>
13
+ <div><strong>Saddle</strong><small>page bridge</small></div>
14
+ </header>
15
+ <p id="status" role="status">Choose a read action for the active tab.</p>
16
+ <div class="actions">
17
+ <button data-command="snapshot">Snapshot</button>
18
+ <button data-command="readpage">Read page</button>
19
+ </div>
20
+ <pre id="output">No request yet.</pre>
21
+ </main>
22
+ <script type="module" src="popup.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,25 @@
1
+ /**
2
+ * popup sends user initiated read commands through the service worker.
3
+ */
4
+
5
+ import { createcommand } from "./protocol.js";
6
+
7
+ const status = document.querySelector("#status");
8
+ const output = document.querySelector("#output");
9
+
10
+ for (const button of document.querySelectorAll("[data-command]")) button.addEventListener("click", () => request(button.dataset.command));
11
+
12
+ async function request(command) {
13
+ try {
14
+ status.textContent = "Reading active tab…";
15
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
16
+ if (!tab?.id) throw new Error("no active tab");
17
+ const response = await chrome.runtime.sendMessage(createcommand(command, { tabid: tab.id }));
18
+ if (response?.type === "error") throw new Error(response.error?.message ?? "extension request failed");
19
+ output.textContent = JSON.stringify(response?.payload ?? response, null, 2);
20
+ status.textContent = "Complete.";
21
+ } catch (error) {
22
+ status.textContent = "Request failed.";
23
+ output.textContent = String(error.message ?? error);
24
+ }
25
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * extension protocol defines serializable commands, responses, errors and page snapshots.
3
+ */
4
+
5
+ export const protocolversion = 1;
6
+ export const extensioncommands = Object.freeze(["snapshot", "readpage", "clickref", "fillref"]);
7
+
8
+ /** Creates a compact identifier without embedding a host, port or credential. */
9
+ export function createid(prefix = "msg", source) {
10
+ const generator = source ?? (() => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}${Math.random().toString(16).slice(2)}`);
11
+ const value = generator().replaceAll("-", "");
12
+ return `${prefix}${value}`;
13
+ }
14
+
15
+ /** Creates a versioned command for the extension message bus. */
16
+ export function createcommand(command, payload = {}, options = {}) {
17
+ if (!extensioncommands.includes(command)) throw new TypeError(`unsupported extension command: ${command}`);
18
+ return createmessage("command", { ...options, command, payload });
19
+ }
20
+
21
+ /** Creates a serializable message envelope. */
22
+ export function createmessage(type, options = {}) {
23
+ if (!["command", "response", "error", "event"].includes(type)) throw new TypeError(`unsupported extension message type: ${type}`);
24
+ const message = { version: protocolversion, type, id: options.id ?? createid(type) };
25
+ if (options.requestid) message.requestid = options.requestid;
26
+ if (options.command) message.command = options.command;
27
+ if (options.payload !== undefined) message.payload = options.payload;
28
+ if (options.error) message.error = options.error;
29
+ assertserializable(message);
30
+ return message;
31
+ }
32
+
33
+ /** Creates a correlated successful response. */
34
+ export function createresponse(request, payload = {}) {
35
+ assertmessage(request);
36
+ return createmessage("response", { requestid: request.id, payload });
37
+ }
38
+
39
+ /** Creates a correlated error response with stable error fields. */
40
+ export function createerror(request, error, options = {}) {
41
+ const requestid = request?.id ?? options.requestid;
42
+ const failure = { code: options.code ?? error?.code ?? "extension_error", message: String(error?.message ?? error ?? "extension request failed"), retryable: Boolean(options.retryable ?? error?.retryable) };
43
+ return createmessage("error", { requestid, error: failure });
44
+ }
45
+
46
+ /** Validates a message before a privileged context handles it. */
47
+ export function assertmessage(message) {
48
+ if (!message || typeof message !== "object") throw new TypeError("extension message must be an object");
49
+ if (message.version !== protocolversion) throw new TypeError(`unsupported extension protocol version: ${message.version}`);
50
+ if (typeof message.type !== "string" || typeof message.id !== "string") throw new TypeError("extension message requires type and id");
51
+ if (message.type === "command" && !extensioncommands.includes(message.command)) throw new TypeError(`unsupported extension command: ${message.command}`);
52
+ if (message.payload !== undefined && (!message.payload || typeof message.payload !== "object" || Array.isArray(message.payload))) throw new TypeError("extension payload must be an object");
53
+ return message;
54
+ }
55
+
56
+ /** Creates a structured page snapshot with stable element references. */
57
+ export function createsnapshot(input = {}) {
58
+ const snapshot = {
59
+ version: protocolversion,
60
+ snapshotid: String(input.snapshotid ?? createid("snap")),
61
+ createdat: Number(input.createdat ?? Date.now()),
62
+ url: String(input.url ?? ""),
63
+ title: String(input.title ?? ""),
64
+ text: String(input.text ?? ""),
65
+ elements: Array.isArray(input.elements) ? input.elements.map((element) => ({ ref: String(element.ref), role: String(element.role ?? "generic"), name: String(element.name ?? "") })) : []
66
+ };
67
+ assertserializable(snapshot);
68
+ return snapshot;
69
+ }
70
+
71
+ /** Returns whether a reference still belongs to the current page snapshot. */
72
+ export function isfreshsnapshot(snapshotid, currentid) { return Boolean(snapshotid && currentid && snapshotid === currentid); }
73
+
74
+ function assertserializable(value) {
75
+ try { JSON.stringify(value); } catch (error) { throw new TypeError(`extension message is not serializable: ${error.message}`); }
76
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * service worker router forwards user initiated commands to the active tab and persists resumable state.
3
+ */
4
+
5
+ import { assertmessage } from "./protocol.js";
6
+
7
+ /** Creates a browser independent router around Chrome tabs, scripting and storage APIs. */
8
+ export function createworkerrouter(options = {}) {
9
+ const tabs = options.tabs;
10
+ const scripting = options.scripting;
11
+ const storage = options.storage;
12
+ const contentfile = options.contentfile ?? "content.js";
13
+ const statekey = options.statekey ?? "saddleextensionstate";
14
+ if (typeof tabs?.sendMessage !== "function") throw new TypeError("extension router requires tabs.sendMessage");
15
+
16
+ async function ensurecontent(tabid) {
17
+ if (!Number.isInteger(tabid)) throw new TypeError("extension command requires a tab id");
18
+ if (typeof scripting?.executeScript !== "function") throw new TypeError("extension router requires scripting.executeScript");
19
+ await scripting.executeScript({ target: { tabId: tabid }, files: [contentfile] });
20
+ }
21
+
22
+ async function readstate() {
23
+ if (typeof storage?.get !== "function") return {};
24
+ const result = await storage.get(statekey);
25
+ return result?.[statekey] ?? {};
26
+ }
27
+
28
+ async function savestate(value) {
29
+ if (typeof storage?.set === "function") await storage.set({ [statekey]: value });
30
+ }
31
+
32
+ async function handle(message, sender = {}) {
33
+ const request = assertmessage(message);
34
+ if (request.type !== "command") throw new TypeError("extension router accepts commands only");
35
+ const tabid = request.payload?.tabid ?? sender.tab?.id;
36
+ await ensurecontent(tabid);
37
+ const response = await tabs.sendMessage(tabid, request);
38
+ if (response?.type === "response" && response.payload?.snapshotid) await savestate({ tabid, snapshotid: response.payload.snapshotid, updatedat: Date.now() });
39
+ return response;
40
+ }
41
+
42
+ return { ensurecontent, readstate, savestate, handle };
43
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * worker binds the generic extension router to the Manifest V3 runtime APIs.
3
+ */
4
+
5
+ import { createerror } from "./protocol.js";
6
+ import { createworkerrouter } from "./serviceworker.js";
7
+
8
+ /** Installs the service worker listeners against a caller supplied Chrome API object. */
9
+ export function startworker(chromeapi = globalThis.chrome) {
10
+ if (!chromeapi?.runtime?.onMessage || !chromeapi?.tabs || !chromeapi?.scripting) throw new TypeError("extension worker requires runtime tabs and scripting APIs");
11
+ const router = createworkerrouter({ tabs: chromeapi.tabs, scripting: chromeapi.scripting, storage: chromeapi.storage?.session });
12
+ const listener = (message, sender, sendresponse) => {
13
+ router.handle(message, sender).then(sendresponse).catch((error) => sendresponse(createerror(message, error)));
14
+ return true;
15
+ };
16
+ chromeapi.runtime.onMessage.addListener(listener);
17
+ return { router, dispose() { chromeapi.runtime.onMessage.removeListener?.(listener); } };
18
+ }
19
+
20
+ if (globalThis.chrome?.runtime?.onMessage) startworker();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * format check validates the public root based JavaScript layout before release.
3
+ */
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+
7
+ const roots = ["core", "domain", "memory", "runners", "runtime", "storage", "sessions", "modes", "adapters", "persistence", "queue", "dispatch", "scrape", "crawl", "api", "mcp", "browser", "proxy", "captcha", "ai", "webhook", "surfaces", "library", "errors", "retry", "server", "binary", "deploy", "workflow", "packager", "bot", "protocol", "cli"];
8
+
9
+ /** Finds JavaScript files that do not follow the skill formatting contract. */
10
+ export async function formatissues(root = process.cwd()) {
11
+ const issues = [];
12
+ for (const directory of roots) for (const file of await javascriptfiles(join(root, directory))) { const relative = file.slice(root.length + 1); const source = await readFile(file, "utf8"); if (/[A-Z_-]/.test(relative)) issues.push(`${relative}: invalid path format`); if (!source.includes("/**")) issues.push(`${relative}: missing jsdoc`); }
13
+ return issues;
14
+ }
15
+
16
+ /** Runs the check as a CLI and throws a short diagnostic on failure. */
17
+ export async function runformatcheck() { const issues = await formatissues(); if (issues.length) throw new Error(issues.join("\n")); return { ok: true, checked: roots.length }; }
18
+
19
+ async function javascriptfiles(directory) { let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch { return []; } const files = []; for (const entry of entries) { const file = join(directory, entry.name); if (entry.isDirectory()) files.push(...await javascriptfiles(file)); else if (entry.name.endsWith(".js")) files.push(file); } return files; }
20
+
21
+ if (import.meta.url === `file://${process.argv[1]}`) runformatcheck().then((result) => console.log(JSON.stringify(result))).catch((error) => { console.error(error.message); process.exitCode = 1; });
package/index.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * public entry point exports stable factories and leaves adapters replaceable.
3
+ */
4
+ export * from "./core/errors.js";
5
+ export * from "./core/events.js";
6
+ export * from "./core/ids.js";
7
+ export * from "./core/hash.js";
8
+ export * from "./domain/artifacts.js";
9
+ export * from "./domain/jobs.js";
10
+ export * from "./domain/providers.js";
11
+ export * from "./domain/runtime.js";
12
+ export * from "./domain/sessions.js";
13
+ export * from "./memory/modes.js";
14
+ export * from "./memory/objects.js";
15
+ export * from "./memory/targets.js";
16
+ export * from "./memory/transforms.js";
17
+ export * from "./memory/engine.js";
18
+ export * from "./runners/inprocess.js";
19
+ export * from "./runners/scheduler.js";
20
+ export * from "./runtime/engine.js";
21
+ export * from "./storage/adapter.js";
22
+ export * from "./storage/cache.js";
23
+ export * from "./storage/content.js";
24
+ export * from "./storage/memory.js";
25
+ export * from "./storage/sync.js";
26
+ export * from "./runners/health.js";
27
+ export * from "./runners/heartbeat.js";
28
+ export * from "./workflow/triggers.js";
29
+ export * from "./dispatch/resumable.js";
30
+ export * from "./scrape/semantic.js";
31
+ export * from "./crawl/frontier.js";
32
+ export * from "./ai/provenance.js";
33
+ export * from "./observability/metrics.js";
34
+ export * from "./api/auth.js";
35
+ export * from "./api/contracts.js";
36
+ export * from "./mcp/browser.js";
37
+ export * from "./apps/registry.js";
38
+ export * from "./bot/permissions.js";
39
+ export * from "./webhook/delivery.js";
40
+ export * from "./storage/chunked.js";
41
+ export * from "./storage/s3compatible.js";
42
+ export * from "./modes/modes.js";
43
+ export * from "./adapters/transport.js";
44
+ export * from "./adapters/github.js";
45
+ export * from "./adapters/socket.js";
46
+ export * from "./adapters/forge.js";
47
+ export * from "./adapters/gitlab.js";
48
+ export * from "./adapters/forgejo.js";
49
+ export * from "./adapters/huggingface.js";
50
+ export * from "./persistence/schema.js";
51
+ export * from "./persistence/adapter.js";
52
+ export * from "./persistence/memory.js";
53
+ export * from "./persistence/sql.js";
54
+ export * from "./persistence/drizzle.js";
55
+ export * from "./persistence/prisma.js";
56
+ export * from "./queue/idempotency.js";
57
+ export * from "./queue/queue.js";
58
+ export * from "./queue/saga.js";
59
+ export * from "./dispatch/workflow.js";
60
+ export * from "./sessions/replay.js";
61
+ export * from "./scrape/robots.js";
62
+ export * from "./scrape/cache.js";
63
+ export * from "./scrape/extract.js";
64
+ export * from "./scrape/scraper.js";
65
+ export * from "./packager/manifest.js";
66
+ export * from "./bot/commands.js";
67
+ export * from "./bot/adapter.js";
68
+ export * from "./bot/bot.js";
69
+ export * from "./protocol/json.js";
70
+ export * from "./protocol/ndjson.js";
71
+ export * from "./protocol/sse.js";
72
+ export * from "./protocol/blocks.js";
73
+ export * from "./workflow/manifest.js";
74
+ export * from "./workflow/templates.js";
75
+ export * from "./workflow/registry.js";
76
+ export * from "./crawl/normalize.js";
77
+ export * from "./crawl/crawler.js";
78
+ export * from "./crawl/persistent.js";
79
+ export * from "./scrape/schema.js";
80
+ export * from "./mcp/server.js";
81
+ export * from "./runtime/detect.js";
82
+ export * from "./runtime/abort.js";
83
+ export * from "./runtime/compatibility.js";
84
+ export * from "./runtime/worker.js";
85
+ export * from "./packager/publish.js";
86
+ export * from "./browser/fingerprint.js";
87
+ export * from "./browser/session.js";
88
+ export * from "./browser/index.js";
89
+ export * from "./proxy/pool.js";
90
+ export * from "./captcha/contract.js";
91
+ export * from "./captcha/guard.js";
92
+ export * from "./ai/tokens.js";
93
+ export * from "./ai/chunk.js";
94
+ export * from "./ai/rag.js";
95
+ export * from "./ai/llmstxt.js";
96
+ export * from "./webhook/signature.js";
97
+ export * from "./webhook/receiver.js";
98
+ export * from "./surfaces/manifest.js";
99
+ export * from "./surfaces/n8n.js";
100
+ export * from "./surfaces/adapters.js";
101
+ export * from "./surfaces/controls.js";
102
+ export * from "./surfaces/operations.js";
103
+ export * from "./library/public.js";
104
+ export * from "./errors/taxonomy.js";
105
+ export * from "./retry/policy.js";
106
+ export * from "./retry/circuit.js";
107
+ export * from "./storage/githubcontents.js";
108
+ export * from "./storage/filehosting.js";
109
+ export * from "./persistence/migrations.js";
110
+ export * from "./mcp/transport.js";
111
+ export * from "./api/security.js";
112
+ export * from "./modes/matrix.js";
113
+ export * from "./modes/resolve.js";
114
+ export * from "./binary/build.js";
115
+ export * from "./surfaces/targets.js";
116
+ export * from "./api/rate.js";
117
+ export * from "./api/http.js";
118
+ export * from "./api/service.js";
119
+ export * from "./api/control.js";
120
+ export * from "./extension/index.js";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * public library helpers compose fetch extraction serialization chunking and crawl contracts.
3
+ */
4
+ import { crawl } from "../crawl/crawler.js";
5
+ import { chunkmarkdown } from "../ai/chunk.js";
6
+ import { estimatetokens } from "../ai/tokens.js";
7
+ import { extracthtml } from "../scrape/extract.js";
8
+ import { browseragent } from "../browser/agent.js";
9
+
10
+ /** Selects the fetch or browser execution path. */
11
+ export async function saddleurl(url, options = {}) {
12
+ const mode = options.mode ?? "fetch";
13
+ if (mode === "browser") return scrapewithbrowser(url, options);
14
+ return scrapeurl(url, options);
15
+ }
16
+
17
+ /** Fetches one URL through the caller supplied transport. */
18
+ export async function scrapeurl(url, options = {}) {
19
+ const target = safeurl(url);
20
+ const response = await (options.fetcher ?? fetch)(target, { signal: options.signal, headers: options.headers });
21
+ if (!response.ok) throw new Error(`scrape request failed with ${response.status}`);
22
+ const html = await response.text();
23
+ return formatresult(scrapehtml(html, target, options), options);
24
+ }
25
+
26
+ /** Extracts a serializable result from HTML without network access. */
27
+ export function scrapehtml(html, url, options = {}) {
28
+ const result = extracthtml(html, url);
29
+ return { content: result.text, metadata: { url: result.url, title: result.title, description: result.description, links: result.links }, html: options.includehtml ? html : undefined };
30
+ }
31
+
32
+ /** Exposes structured extraction for callers that do not need formatting. */
33
+ export function extractcontent(html, options = {}) { return extracthtml(html, options.url); }
34
+
35
+ /** Runs a scrape through the injected browser agent contract. */
36
+ export async function scrapewithbrowser(url, options = {}) {
37
+ const agent = browseragent(options.browser);
38
+ await agent.navigate({ url, waituntil: options.waituntil ?? "networkidle" });
39
+ return formatresult({ content: await agent.text(), metadata: { url, title: await agent.title(), html: options.includehtml ? await agent.html() : undefined } }, options);
40
+ }
41
+
42
+ /** Serializes a result into a supported output format. */
43
+ export function serializeresult(result, options = {}) {
44
+ const format = options.format ?? "json";
45
+ if (format === "json") return JSON.stringify(result, null, options.pretty ? 2 : 0);
46
+ if (format === "text") return result.content ?? "";
47
+ if (format === "markdown") return `# ${result.metadata?.title ?? "result"}\n\n${result.content ?? ""}`;
48
+ if (format === "xml") return `<result><title>${escape(result.metadata?.title ?? "")}</title><content>${escape(result.content ?? "")}</content></result>`;
49
+ if (format === "redis") return JSON.stringify({ content: result.content, metadata: result.metadata });
50
+ throw new TypeError(`unsupported format: ${format}`);
51
+ }
52
+
53
+ /** Converts local HTML into Markdown. */
54
+ export function serializehtml(html, options = {}) { return serializeresult(scrapehtml(html, options.url), { format: "markdown" }); }
55
+
56
+ /** Builds compact context for an agent or a vector pipeline. */
57
+ export function formatforagent(result, options = {}) {
58
+ const content = result.content ?? "";
59
+ const chunks = chunkmarkdown(content, { maxtokens: options.maxchunksize ?? 4000 });
60
+ const lines = content.split(/[.!?]\s+/).filter(Boolean);
61
+ return { summary: lines.slice(0, 2).join(". "), keypoints: lines.slice(0, options.keypoints ?? 5), relevanturls: result.metadata?.links ?? [], chunks, tokencount: estimatetokens(content, options.model) };
62
+ }
63
+
64
+ /** Processes URLs in bounded groups and emits progress events. */
65
+ export async function batchscrape(options = {}) {
66
+ const urls = options.urls ?? [];
67
+ const concurrency = options.concurrency ?? 10;
68
+ const results = [];
69
+ for (let index = 0; index < urls.length; index += concurrency) {
70
+ const group = urls.slice(index, index + concurrency);
71
+ const completed = await Promise.all(group.map((url) => scrapeurl(url, options)));
72
+ results.push(...completed);
73
+ options.onprogress?.({ completed: results.length, total: urls.length });
74
+ }
75
+ return results;
76
+ }
77
+
78
+ /** Runs the crawler through the public scrape contract. */
79
+ export async function crawlurl(url, options = {}) { return crawl(url, { ...options, scrape: (target) => scrapeurl(target, options) }); }
80
+
81
+ function safeurl(value) { const url = new URL(value); if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("url must use http or https"); return url.href; }
82
+ function formatresult(result, options) { return options.format ? { ...result, serialized: serializeresult(result, options) } : result; }
83
+ function escape(value) { return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;"); }