@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
package/LICENSE ADDED
@@ -0,0 +1,203 @@
1
+ PROPRIETARY SOURCE-AVAILABLE LICENSE - VIEW ONLY
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (C) August 2026 devthink, nathlan, akadion, nathu filho, allan neris, andraneris
5
+ Everyone is permitted to view this license document, but changing it
6
+ is not allowed. This license is NOT an Open Source license.
7
+
8
+ Preamble
9
+
10
+ This License governs the source code made available by the Licensor.
11
+ Unlike free software licenses such as the GNU GPL, this is a
12
+ proprietary, source-available license.
13
+
14
+ The Software is made visible for transparency and verification
15
+ purposes only. It is NOT free software. It is NOT open source.
16
+
17
+ When we speak of proprietary, we are referring to full ownership and
18
+ restriction, not freedom. Our intention is to protect the Licensor's
19
+ exclusive rights to copy, modify, distribute and use the work. By
20
+ contrast to the GPL which guarantees your freedom to share and change,
21
+ this License is intended to guarantee that the Software remains the
22
+ exclusive property of its authors and that no rights beyond viewing
23
+ are granted.
24
+
25
+ To protect our rights, we must prevent others from copying, modifying,
26
+ distributing, or using the Software without permission. Therefore,
27
+ you have no rights to convey, modify, or use the Software beyond
28
+ viewing it in its public repository form.
29
+
30
+ For the authors' protection, this License clearly explains that there
31
+ is no warranty for this proprietary software and that any unauthorized
32
+ use will be treated as copyright infringement.
33
+
34
+ The precise terms and conditions for viewing and restriction follow.
35
+
36
+ TERMS AND CONDITIONS
37
+
38
+ 0. Definitions.
39
+
40
+ "This License" refers to version 1.0 of the Proprietary Source-Available
41
+ License - View Only.
42
+
43
+ "Copyright" also means copyright-like laws that apply to other kinds of
44
+ works.
45
+
46
+ "The Program" or "The Software" refers to any copyrightable work licensed
47
+ under this License, including source code, object code, documentation,
48
+ and associated files. Each licensee is addressed as "you".
49
+
50
+ "Licensor" refers to the copyright holders: devthink, nathlan, akadion,
51
+ nathu filho, alllan neris, andraneris.
52
+
53
+ To "view" means to read and examine the Software in its public repository
54
+ form through a web browser, without creating a local copy beyond the
55
+ temporary cache of your browser.
56
+
57
+ To "use" means to execute, run, compile, deploy, host, or otherwise
58
+ benefit from the functionality of the Software, which is NOT permitted.
59
+
60
+ 1. Ownership and Intellectual Property.
61
+
62
+ The Software is and shall remain the exclusive property of the Licensor.
63
+ All right, title, and interest in and to the Software, including all
64
+ copyrights, trademarks, trade secrets, and other intellectual property
65
+ rights, are owned solely by the Licensor.
66
+
67
+ No ownership rights, implied or otherwise, are transferred to you under
68
+ this License. All rights not expressly granted are reserved.
69
+
70
+ 2. Grant of Rights - VIEW ONLY.
71
+
72
+ Subject to the terms of this License, Licensor grants you a limited,
73
+ non-exclusive, non-transferable, revocable license solely to VIEW the
74
+ Software in its public repository form for personal evaluation.
75
+
76
+ This grant DOES NOT include any right to:
77
+
78
+ a) use, execute, or run the Software;
79
+ b) copy, reproduce, or duplicate;
80
+ c) modify, adapt, or create derivative works;
81
+ d) distribute, publish, sublicense, or sell;
82
+ e) reverse engineer or decompile.
83
+
84
+ 3. Prohibitions and Restrictions.
85
+
86
+ You are STRICTLY PROHIBITED from undertaking any of the following
87
+ without prior written permission from the Licensor:
88
+
89
+ a) Copying, reproducing, duplicating, or storing the Software, in
90
+ whole or in part, in any medium;
91
+
92
+ b) Modifying, adapting, translating, transforming, or creating
93
+ derivative works based on the Software;
94
+
95
+ c) Distributing, publishing, sublicensing, disclosing, or otherwise
96
+ making the Software available to third parties, whether gratis or
97
+ for a fee;
98
+
99
+ d) Using the Software for any purpose, including but not limited to
100
+ development, production, commercial use, internal business operations,
101
+ offering as a service (SaaS), or training artificial intelligence
102
+ models;
103
+
104
+ e) Removing, altering, or obscuring any copyright, proprietary, or
105
+ license notices contained in the Software;
106
+
107
+ f) Reverse engineering, decompiling, disassembling, or attempting to
108
+ derive the source code of the Software.
109
+
110
+ Any use beyond viewing constitutes copyright infringement and will be
111
+ prosecuted to the fullest extent of the law.
112
+
113
+ 4. Confidentiality.
114
+
115
+ The Software contains confidential information and proprietary trade
116
+ secrets of the Licensor. Unauthorized access, inspection, or disclosure
117
+ is strictly prohibited and may result in civil and criminal liability.
118
+
119
+ 5. No Warranty.
120
+
121
+ The Software is provided for viewing only and may contain errors or
122
+ omissions. No support, maintenance, or updates are implied.
123
+
124
+ 6. Acceptance Not Required for Viewing.
125
+
126
+ You are not required to accept this License to view the Software in a
127
+ web browser. However, any action beyond viewing, such as copying,
128
+ modifying, or using the Software, requires explicit written permission
129
+ and a separate license agreement. By doing so without permission, you
130
+ infringe copyright.
131
+
132
+ 7. Termination.
133
+
134
+ This License and all rights granted herein automatically terminate upon
135
+ any breach of Section 3 by you. Upon termination, you must cease all
136
+ viewing and destroy all copies, including cached or local copies, of
137
+ the Software in your possession.
138
+
139
+ Moreover, termination does not terminate the rights of the Licensor
140
+ to seek damages or injunctive relief.
141
+
142
+ 8. Disclaimer of Warranty.
143
+
144
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
145
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
146
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
147
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
148
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
149
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
150
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
151
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
152
+
153
+ 9. Limitation of Liability.
154
+
155
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
156
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO HAS VIEWED THE PROGRAM
157
+ AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
158
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
159
+ VIEWING OR INABILITY TO VIEW THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS
160
+ OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
161
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE), EVEN IF SUCH HOLDER OR
162
+ OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
163
+
164
+ 10. Interpretation and Governing Law.
165
+
166
+ If the disclaimer of warranty and limitation of liability provided above
167
+ cannot be given local legal effect according to their terms, reviewing
168
+ courts shall apply local law that most closely approximates an absolute
169
+ waiver of all civil liability in connection with the Program, unless a
170
+ warranty or assumption of liability accompanies a copy of the Program
171
+ in return for a fee.
172
+
173
+ This License shall be governed by the laws of Brazil.
174
+
175
+ END OF TERMS AND CONDITIONS
176
+
177
+ How to Apply These Terms to Your New Programs
178
+
179
+ If you develop a new program, and you want it to remain proprietary and
180
+ fully owned by you, with source visible but with no rights granted, the
181
+ best way to achieve this is to attach the following notices to the program.
182
+ It is safest to attach them to the start of each source file.
183
+
184
+ <one line to give the program's name and a brief idea of what it does.>
185
+ Copyright (C) August 2026 devthink, nathlan, akadion, nathu filho, alllan neris, andraneris
186
+ Project ID: <saddle>
187
+
188
+ This program is proprietary and confidential: you can view its source
189
+ code for evaluation purposes only under the terms of the Proprietary
190
+ Source-Available License - View Only as published by the Licensor.
191
+ You are not permitted to copy, modify, distribute, or use this program
192
+ without prior written permission.
193
+
194
+ This program is distributed in the hope that it will be transparent,
195
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
196
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
197
+ Proprietary Source-Available License for more details.
198
+
199
+ You should have received a copy of the Proprietary Source-Available
200
+ License along with this program. If not, see
201
+ <https://github.com/iakadion/saddle/LICENSE>
202
+
203
+ Also add information on how to contact you by electronic and paper mail.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Saddle
2
+
3
+ <p align="center">
4
+ <img src="docs/assets/saddlemark.svg" alt="Saddle" width="720" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ <strong>Storage-backed jobs, scraping contracts and portable runners for Node.js.</strong><br/>
9
+ <strong>Binary computing agent, agent browser, computer-use, scraper and packager.</strong><br/>
10
+ <a href="https://github.com/iakadion/saddle/actions/workflows/ci.yml"><img src="https://github.com/iakadion/saddle/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
11
+ <a href="https://github.com/iakadion/saddle/releases/tag/v1.8.0"><img src="https://img.shields.io/badge/release-v1.8.0-d35d3d" alt="Release 1.8.0" /></a>
12
+ <a href="https://github.com/iakadion/saddle/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-202a2f" alt="GPL 3.0 license" /></a>
13
+ </p>
14
+
15
+ > **Core idea:** storage is the durable side of the working set; the runner is replaceable; the artifact is the boundary. **Storage == Compute** — RAM and disk are the same construct, differing only by usage flag.
16
+
17
+ Saddle is a **JavaScript ESM engine** for jobs that move data between storage, a working set, an injected runner and durable artifacts. It is also a **virtual machine you publish as a package** that runs on other people's computers (GitHub Actions, Forgejo, Gitea, GitLab, Codeberg, free Docker containers) and turns unlimited third-party storage buckets into virtual RAM/GPU/CPU. Nothing runs on the operator's local machine.
18
+
19
+ Ships as a library, CLI, binary, n8n node, CRX extension, Android/iOS and Tauri desktop app. Package `@wenathlan/saddle` — published to npm, GitHub Packages, Maven, NuGet, RubyGems and GHCR (auto-mirrored to jsDelivr).
20
+
21
+ ## Start here
22
+
23
+ Saddle requires **Node.js 22 or newer**.
24
+
25
+ ```bash
26
+ npm install @wenathlan/saddle
27
+ ```
28
+
29
+ ```js
30
+ import { scrapeurl, formatforagent } from "@wenathlan/saddle";
31
+
32
+ const result = await scrapeurl("https://example.com", { format: "markdown" });
33
+ const context = formatforagent(result, { maxchunksize: 2000, keypoints: 4 });
34
+
35
+ console.log(context.summary);
36
+ ```
37
+
38
+ Deterministic example with no network:
39
+
40
+ ```bash
41
+ node examples/publicapi.js
42
+ ```
43
+
44
+ ## What is included
45
+
46
+ | Area | Contract | Result |
47
+ | --- | --- | --- |
48
+ | Jobs | `engine`, `scheduler`, `inprocess` | `prepare → process → sync → cleanup` |
49
+ | Storage | local, chunked, content-addressed, S3-compatible, GitHub Contents, file hosting | durable objects, ranges, dedupe and sync |
50
+ | Working set | memory bridge, modes, objects, transforms | storage-to-compute and compute-to-storage |
51
+ | Scraping | robots, cache, extraction, semantic facts, schema, scraper | text, metadata, links, controls and structured output |
52
+ | Crawl | normalization, priority frontier, BFS crawler, per-domain budgets and persistent frontier | bounded domain-aware crawling |
53
+ | Browser | snapshots, tabs, frames, actions, fingerprint, session, replay and injected agent | browser actions without vendor lock-in |
54
+ | Operations | queues, idempotency, saga, retry, circuit breaker, health and heartbeat | controlled execution and recovery |
55
+ | Protocols | JSON, NDJSON, SSE, blocks, API envelopes and MCP | transport-neutral messages |
56
+ | Delivery | manifests, workflow registry, binary/container plans | package and runner surfaces |
57
+ | Integrations | GitHub, GitLab, Forgejo, app lifecycle, command scopes and delivery adapters | caller-owned provider connectivity |
58
+ | Agent Browser | capture & replay, stealth, fingerprint | Brave capture, movement replay, session recording |
59
+ | Compute Backends | github-actions, huggingface, gitlab-ci, kaggle, oracle-cloud | free runners chain |
60
+ | Storage Backends | HF, Kaggle, Terabox, R2, Telegram, Discord via rclone | unlimited disk as RAM |
61
+ | Extension | Manifest V3 bridge, snapshot protocol, popup and service worker | user initiated browser control |
62
+
63
+ ## Public API
64
+
65
+ | Export | Purpose |
66
+ | --- | --- |
67
+ | `saddleurl` | choose fetch or injected browser path |
68
+ | `scrapeurl` | fetch one URL and extract |
69
+ | `scrapehtml` | extract from HTML without network |
70
+ | `extractcontent` | structured extraction |
71
+ | `serializeresult` | serialize as JSON, Markdown, XML |
72
+ | `formatforagent` | summary, chunks, token count |
73
+ | `batchscrape` | bounded URL groups |
74
+ | `crawlurl` | crawl contract |
75
+ | `browseragent` | navigation, click, type, screenshot |
76
+ | `mcpserver` / `mcptransport` | MCP tools over JSONL/HTTP |
77
+ | `nodeserver` | Web Request/Response handler |
78
+
79
+ Complete API: `docs/libraryapi.md`. Surface overview: [`docs/productindex.md`](docs/productindex.md). Usage examples: [`docs/usage.md`](docs/usage.md).
80
+
81
+ ## The execution model
82
+
83
+ Saddle coordinates contracts instead of hiding providers. A repo + CI runner is a virtual processor:
84
+
85
+ - Repo = Disk (persistent state)
86
+ - CI = CPU (workflow_dispatch = function call)
87
+ - Pages = Bus + CDN
88
+ - Static site = BIOS
89
+ - repository_dispatch = IPC
90
+
91
+ ```js
92
+ import { engine, eventbus, inprocess, scheduler } from "@wenathlan/saddle";
93
+ import { localmemory } from "@wenathlan/saddle/memory-node";
94
+ import { localstorage } from "@wenathlan/saddle/storage-node";
95
+ const events = eventbus();
96
+ const run = engine({
97
+ storage: localstorage("./.saddle-data"),
98
+ memory: localmemory(),
99
+ scheduler: scheduler([inprocess()]),
100
+ events
101
+ });
102
+ const result = await run.run(
103
+ { name: "example", input: { value: 42 } },
104
+ ({ job }) => ({ jobid: job.id, ok: true })
105
+ );
106
+ ```
107
+
108
+ The caller still chooses how to provide `fetcher`, browser transport, persistence, proxy pool, captcha solver, webhook secret and remote credentials. The root entry is transport-neutral; Node filesystem and HTTP adapters are explicit subpaths such as `@wenathlan/saddle/storage-node`, `@wenathlan/saddle/memory-node`, `@wenathlan/saddle/server-node`, `@wenathlan/saddle/sessions-file` and `@wenathlan/saddle/queue-persistent`. Saddle does not embed secrets, fixed hosts or a mandatory cloud vendor.
109
+
110
+ ## Browser extension
111
+
112
+ Version 1.1 includes a pure JavaScript Manifest V3 reference surface in [`extension/`](extension/). It is deliberately narrow: the user invokes the action, the popup sends a versioned command, the service worker routes it, and an isolated content bridge returns bounded page metadata, visible text or a user initiated action result.
113
+
114
+ ```bash
115
+ # load the unpacked extension from chrome://extensions
116
+ ls extension/manifest.json extension/worker.js extension/content.js extension/popup.html
117
+ ```
118
+
119
+ The extension requests `activeTab`, `scripting` and `storage`; it does not request broad host permissions, cookies, `webRequest`, debugger access or arbitrary page code execution. Its public contracts are available from `@wenathlan/saddle/extension`. See [`extension/README.md`](extension/README.md) for the unpacked development flow.
120
+
121
+ ## CLI
122
+
123
+ ```bash
124
+ saddle help
125
+ saddle modes
126
+ saddle runexample
127
+ saddle mcp
128
+ ```
129
+
130
+ ## Security boundaries
131
+
132
+ | Boundary | Policy |
133
+ | --- | --- |
134
+ | Credentials | injected at runtime; never committed |
135
+ | Network | http/https validated; private targets blocked |
136
+ | Crawling | robots rules and crawl delay explicit |
137
+ | Storage | adapters replaceable |
138
+ | Runtime | Node HTTP isolated |
139
+ | Failure | retry, circuit breaker, idempotency configurable |
140
+
141
+ ## Package surfaces
142
+
143
+ | Registry | Artifact | Workflow | Status |
144
+ | --- | --- | --- | --- |
145
+ | GitHub npm | `@iakadion/saddle@1.8.0` | publishgithubnpm.yml | published |
146
+ | GHCR | `ghcr.io/iakadion/saddle:1.8.0` and `latest` | publishghcr.yml | published |
147
+ | Maven | `io.devthink:saddle:1.8.0` | publishmaven.yml | published after JDK 26 retry |
148
+ | NuGet | `Saddle.1.8.0.nupkg` | publishnuget.yml | published |
149
+ | RubyGems | `saddle 1.8.0` | publishrubygems.yml | published |
150
+ | npmjs | `@wenathlan/saddle@1.8.1` | publishnpmjs.yml | pending follow-up release workflow |
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ npm ci
156
+ npm test
157
+ npm run check
158
+ npm run formatcheck
159
+ npm run pack:check
160
+ ```
161
+
162
+ Test suite deterministic, no network or real credentials required.
163
+
164
+ ## Repository map
165
+
166
+ ```
167
+ core/ errors, events and identifiers
168
+ domain/ jobs, artifacts, sessions and providers
169
+ memory/ working-set bridge, modes, objects and transforms
170
+ storage/ local, chunked, remote and file-hosting adapters
171
+ scrape/ robots, cache, extraction, schema and scraper
172
+ crawl/ URL normalization, crawler and persistent frontier
173
+ queue/ queue, idempotency, saga and recovery
174
+ browser/ fingerprint, session and agent contracts
175
+ browser/ snapshots, tabs, frames, actions and recorder contracts
176
+ mcp/ optional server and JSONL/HTTP transport
177
+ protocol/ JSON, NDJSON, SSE and block serializers
178
+ workflow/ manifests, templates and registry
179
+ tests/ deterministic engine coverage
180
+ docs/ architecture, API, release and registry notes
181
+ surfaces/ browser, extension, desktop, mobile and n8n contracts
182
+ ```
183
+
184
+ Root-based JavaScript ESM layout, no src/ directory, no TypeScript build required.
185
+
186
+ ## Current scope
187
+
188
+ Version 1.8 establishes the engine contracts, browser snapshot foundation, storage sync primitives, runner recovery contracts, scraping context provenance, API/MCP security contracts, bot integration lifecycle, the first tested extension bridge, desktop/mobile/n8n surface contracts, a framework-neutral operator control boundary and the first cross-runtime import boundary. Native runtimes, n8n host registration, provider credentials, persistent databases and production deployment remain caller-selected adapters. The next improvements should extend these contracts without coupling the core to one forge, registry, browser or storage vendor.
189
+
190
+ ## License
191
+
192
+ Saddle is distributed under the [GNU General Public License v3.0](LICENSE).
@@ -0,0 +1,16 @@
1
+ /**
2
+ * forge adapter defines the common dispatch and artifact surface for compatible forges.
3
+ */
4
+ import { transport } from "./transport.js";
5
+
6
+ export function forgeadapter(options = {}) {
7
+ if (!options.baseurl || typeof options.token !== "function") throw new TypeError("forge adapter requires baseurl and token function");
8
+ const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
9
+ async function call(path, init = {}) { const token = await options.token(); return client.request(new URL(path, options.baseurl), { ...init, headers: { authorization: `Bearer ${token}`, accept: "application/json", ...(init.headers ?? {}) } }); }
10
+ return {
11
+ kind: options.kind ?? "forge",
12
+ async health(path = "/") { const response = await call(path); return { ok: response.ok, status: response.status }; },
13
+ async dispatch(spec) { if (!spec?.path || !spec.ref) throw new TypeError("forge dispatch requires path and ref"); const response = await call(spec.path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: spec.ref, inputs: spec.inputs ?? {} }) }); return { accepted: response.ok, status: response.status, body: response.json ? await response.json() : undefined }; },
14
+ async upload(spec) { if (!spec?.path || !spec.data) throw new TypeError("forge upload requires path and data"); const response = await call(spec.path, { method: "PUT", headers: { "content-type": spec.contenttype ?? "application/octet-stream" }, body: spec.data }); return { accepted: response.ok, status: response.status }; }
15
+ };
16
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * forgejo and gitea can reuse the open forge contract with a caller supplied base url.
3
+ */
4
+ import { forgeadapter } from "./forge.js";
5
+
6
+ export function forgejoadapter(options = {}) { return forgeadapter({ ...options, kind: "forgejo" }); }
7
+ export function giteaadapter(options = {}) { return forgeadapter({ ...options, kind: "gitea" }); }
8
+ export function codebergadapter(options = {}) { return forgeadapter({ ...options, kind: "codeberg" }); }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * github adapter uses caller supplied credentials and base url configuration.
3
+ */
4
+ import { transport } from "./transport.js";
5
+
6
+ export function githubadapter(options = {}) {
7
+ if (!options.baseurl || typeof options.token !== "function") throw new TypeError("github adapter requires baseurl and token function");
8
+ const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
9
+ async function call(path, init = {}) {
10
+ const token = await options.token();
11
+ const headers = { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "x-github-api-version": options.apiversion ?? "2022-11-28", ...(init.headers ?? {}) };
12
+ return client.request(new URL(path, options.baseurl), { ...init, headers });
13
+ }
14
+ return {
15
+ async health() { const response = await call("/rate_limit"); return { ok: response.ok, status: response.status }; },
16
+ async dispatch(owner, repository, workflow, input = {}) { const response = await call(`/repos/${owner}/${repository}/actions/workflows/${workflow}/dispatches`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref: input.ref ?? "main", inputs: input.inputs ?? {} }) }); return { accepted: response.status === 204, status: response.status }; },
17
+ async run(owner, repository, runid) { const response = await call(`/repos/${owner}/${repository}/actions/runs/${runid}`); return response.json(); }
18
+ };
19
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * gitlab adapter keeps project addressing and token ownership outside the package.
3
+ */
4
+ import { forgeadapter } from "./forge.js";
5
+
6
+ export function gitlabadapter(options = {}) {
7
+ const project = encodeURIComponent(options.project ?? "");
8
+ const base = forgeadapter({ ...options, kind: "gitlab" });
9
+ return { ...base, async dispatch(spec) { return base.dispatch({ ...spec, path: spec.path ?? `/api/v4/projects/${project}/pipeline` }); } };
10
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * hugging face storage remains an explicit adapter with caller supplied repository path.
3
+ */
4
+ import { forgeadapter } from "./forge.js";
5
+
6
+ export function huggingfaceadapter(options = {}) { return forgeadapter({ ...options, kind: "huggingface" }); }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * socket adapter keeps realtime optional and accepts a caller supplied websocket constructor.
3
+ */
4
+ export function socketadapter(options = {}) {
5
+ const websocket = options.websocket ?? globalThis.WebSocket;
6
+ if (!websocket) throw new Error("websocket implementation is required");
7
+ return {
8
+ connect(url, protocols) {
9
+ if (!url) throw new TypeError("socket url is required");
10
+ const socket = new websocket(url, protocols);
11
+ return { socket, send(value) { socket.send(typeof value === "string" ? value : JSON.stringify(value)); }, close(code, reason) { socket.close(code, reason); } };
12
+ }
13
+ };
14
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * transport centralizes timeout retry and jitter without choosing a host or vendor.
3
+ */
4
+ export function transport(options = {}) {
5
+ const fetcher = options.fetcher ?? fetch;
6
+ const attempts = options.attempts ?? 3;
7
+ const timeout = options.timeout ?? 30000;
8
+ const retrycodes = new Set(options.retrycodes ?? [408, 409, 429, 500, 502, 503, 504]);
9
+ async function request(url, init = {}) {
10
+ let last;
11
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), timeout);
14
+ try {
15
+ const response = await fetcher(url, { ...init, signal: init.signal ?? controller.signal });
16
+ if (response.ok || !retrycodes.has(response.status) || attempt === attempts - 1) return response;
17
+ last = new Error(`request failed with ${response.status}`);
18
+ } catch (error) {
19
+ last = error;
20
+ if (attempt === attempts - 1) throw error;
21
+ } finally { clearTimeout(timer); }
22
+ await delay(backoff(options, attempt));
23
+ }
24
+ throw last ?? new Error("request failed");
25
+ }
26
+ return { request };
27
+ }
28
+
29
+ function backoff(options, attempt) { const base = options.backoff ?? 250; const jitter = options.jitter ?? 0; return base * 2 ** attempt + Math.floor(Math.random() * jitter); }
30
+ function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }
package/ai/chunk.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * markdown chunking preserves heading paths and uses paragraph boundaries before hard cuts.
3
+ */
4
+ import { estimatetokens } from "./tokens.js";
5
+
6
+ export function chunkmarkdown(markdown, options = {}) {
7
+ const maxtokens = options.maxtokens ?? 512;
8
+ const overlap = options.overlaptokens ?? 50;
9
+ const lines = String(markdown ?? "").split(/\r?\n/);
10
+ const chunks = [];
11
+ let headingpath = [];
12
+ let buffer = [];
13
+ function flush() { if (!buffer.length) return; const content = buffer.join("\n").trim(); if (content) chunks.push({ content, headingpath: [...headingpath], tokencount: estimatetokens(content, options.model) }); buffer = []; }
14
+ for (const line of lines) {
15
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
16
+ if (heading) { flush(); const level = heading[1].length; headingpath = headingpath.slice(0, level - 1); headingpath[level - 1] = heading[2].trim(); buffer.push(line); continue; }
17
+ buffer.push(line);
18
+ if (estimatetokens(buffer.join("\n"), options.model) > maxtokens) { const last = buffer.pop(); flush(); const overlaptext = buffer.slice(-overlap).join("\n"); buffer = overlaptext ? [overlaptext, last] : [last]; }
19
+ }
20
+ flush();
21
+ return chunks.map((chunk, index) => ({ ...chunk, id: `chunk${index}` }));
22
+ }
package/ai/llmstxt.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * llms text generation creates compact absolute links for agent consumption.
3
+ */
4
+ export function llmstxt(options = {}) {
5
+ const title = options.title ?? "saddle";
6
+ const description = options.description ?? "binary computing engine and browser automation library";
7
+ const pages = (options.pages ?? []).filter((page) => page?.url && /^https:\/\//.test(page.url)).slice(0, options.limit ?? 100);
8
+ const lines = [`# ${title}`, `> ${description}`, "", "## pages", "", ...pages.map((page) => `- [${page.title ?? page.url}](${page.url}): ${(page.description ?? "").slice(0, 100)}`)];
9
+ return `${lines.join("\n")}\n`;
10
+ }
11
+
12
+ export function llmsfull(options = {}) { return (options.pages ?? []).filter((page) => page?.url && /^https:\/\//.test(page.url)).map((page) => `# ${page.title ?? page.url}\n\nsource: ${page.url}\n\n${page.content ?? ""}`).join("\n\n---\n\n"); }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * context provenance links retrieved chunks to source, query and transformation metadata.
3
+ */
4
+
5
+ /** Creates a serializable retrieval record for an agent context result. */
6
+ export function provenance(input = {}) {
7
+ if (!input.source && !input.sourceurl) throw new TypeError("provenance requires a source");
8
+ return { version: 1, source: input.source ?? input.sourceurl, sourceurl: input.sourceurl, documentid: input.documentid, query: input.query, retrievedat: Number(input.retrievedat ?? Date.now()), chunks: Array.isArray(input.chunks) ? input.chunks.map((chunk, index) => ({ id: String(chunk.id ?? index), contenthash: chunk.contenthash, score: chunk.score === undefined ? undefined : Number(chunk.score), headingpath: chunk.headingpath, tokencount: chunk.tokencount, citation: chunk.citation ?? input.sourceurl })) : [], metadata: { ...(input.metadata ?? {}) } };
9
+ }
10
+
11
+ /** Merges provenance records while deduplicating chunk identifiers. */
12
+ export function mergeprovenance(records = []) {
13
+ const valid = records.filter(Boolean);
14
+ const chunks = [];
15
+ const seen = new Set();
16
+ for (const record of valid) for (const chunk of record.chunks ?? []) { const key = `${record.documentid ?? record.source}:${chunk.id}`; if (seen.has(key)) continue; seen.add(key); chunks.push({ ...chunk, source: record.source, documentid: record.documentid }); }
17
+ return { version: 1, sources: [...new Set(valid.map((record) => record.source))], chunks, mergedat: Date.now() };
18
+ }
package/ai/rag.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * rag manifests connect chunks to embedding stores without forcing a vendor client.
3
+ */
4
+ export async function ragmanifest(input = {}) {
5
+ const chunks = input.chunks ?? [];
6
+ const unique = [];
7
+ const hashes = new Set();
8
+ for (const chunk of chunks) { const hash = await hashtext(chunk.content); if (hashes.has(hash)) continue; hashes.add(hash); unique.push({ ...chunk, contenthash: hash, documentid: hash.slice(0, 16), metadata: { ...(input.metadata ?? {}), ...(chunk.metadata ?? {}) } }); }
9
+ return { documentid: (await hashtext(input.source ?? unique.map((chunk) => chunk.content).join("\n"))).slice(0, 16), source: input.source, chunks: unique, embeddingmodel: input.embeddingmodel, embeddingdimensions: input.embeddingdimensions, createdat: Date.now() };
10
+ }
11
+
12
+ export function vectorrecord(chunk, vector, options = {}) { return { id: `${chunk.documentid}-${chunk.id}`, vector, metadata: { headingpath: chunk.headingpath, contenthash: chunk.contenthash, sourceurl: options.sourceurl, tokencount: chunk.tokencount, contenttype: options.contenttype ?? "text", language: options.language ?? "en", embeddingmodel: options.embeddingmodel, embeddingdimensions: vector?.length } }; }
13
+
14
+ async function hashtext(text) { const bytes = new TextEncoder().encode(String(text)); if (globalThis.crypto?.subtle) { const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join(""); } return Array.from(bytes).map((value) => value.toString(16).padStart(2, "0")).join("").slice(0, 64).padEnd(64, "0"); }
package/ai/tokens.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * token helpers use configurable model ratios and never require a provider tokenizer.
3
+ */
4
+ const ratios = Object.freeze({ default: 4, gpt: 3.5, claude: 3.2, gemini: 3.5 });
5
+
6
+ export function estimatetokens(text, model = "default") { const ratio = ratios[model] ?? ratios.default; return Math.ceil(String(text ?? "").length / ratio); }
7
+ export function fitscontext(text, context, model = "default") { return estimatetokens(text, model) <= context; }
8
+ export function tokenbudget(text, options = {}) { const tokens = estimatetokens(text, options.model); return { tokens, context: options.context ?? null, fits: options.context == null ? true : tokens <= options.context, remaining: options.context == null ? null : Math.max(0, options.context - tokens) }; }
9
+ export function settokenratios(values = {}) { Object.assign(ratios, values); return { ...ratios }; }
package/api/auth.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * api authorization delegates token verification to the caller and never stores credentials.
3
+ */
4
+
5
+ /** Authorizes a request through an injected verifier or returns an anonymous principal. */
6
+ export async function authorize(request, options = {}) {
7
+ const token = request?.headers?.get?.("authorization")?.replace(/^Bearer\s+/i, "") ?? request?.headers?.get?.("x-api-key");
8
+ if (typeof options.verify !== "function") return { authenticated: false, subject: "anonymous", tokenpresent: Boolean(token) };
9
+ if (!token) return { authenticated: false, subject: "anonymous", tokenpresent: false };
10
+ const principal = await options.verify(token, request);
11
+ if (!principal) { const error = new Error("request is not authorized"); error.code = "UNAUTHORIZED"; throw error; }
12
+ return { authenticated: true, subject: String(principal.subject ?? principal.id ?? "caller"), claims: { ...(principal.claims ?? {}) } };
13
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * api contracts keep request identity and success envelopes stable across HTTP and MCP adapters.
3
+ */
4
+
5
+ export const apiversion = 1;
6
+
7
+ /** Extracts a caller supplied request id or creates a local id without exposing secrets. */
8
+ export function requestcontext(request, options = {}) {
9
+ const requestid = request?.headers?.get?.("x-request-id") ?? options.requestid ?? `request${Date.now().toString(36)}`;
10
+ return { version: apiversion, requestid: String(requestid), method: request?.method ?? options.method, path: options.path };
11
+ }
12
+
13
+ /** Creates a versioned success envelope for APIs that opt into envelopes. */
14
+ export function successpayload(data, context = {}) { return { version: apiversion, requestid: String(context.requestid ?? `request${Date.now().toString(36)}`), data }; }
15
+
16
+ /** Creates a versioned error payload with a stable retry hint. */
17
+ export function errorpayload(code, message, context = {}) { return { version: apiversion, requestid: String(context.requestid ?? `request${Date.now().toString(36)}`), error: { code: String(code), message: String(message), retryafter: Number(context.retryafter ?? 0) } }; }