@wenathlan/saddle 2.0.163 → 2.0.165
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/Dockerfile +44 -17
- package/README.md +1 -1
- package/cli.ts +99 -2
- package/communication.ts +455 -0
- package/docker.config +55 -0
- package/docs/distributionlanes.md +19 -0
- package/docs/doc.go +25 -0
- package/docs/ninetymodalities.md +8 -0
- package/docs/osdistributions.md +149 -0
- package/integration.ts +221 -0
- package/modalities.ts +4665 -0
- package/package.json +11 -5
- package/scriptlanes.ts +774 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# os.distributions — the seven system seats the saddle container carries
|
|
2
|
+
|
|
3
|
+
> the owner order (pr15, verbatim): "iniciar distribuição do container
|
|
4
|
+
> saddle com por exemplo, grapheneos, omarchy e outros sistemas dentro do
|
|
5
|
+
> saddle" — the saddle container starts its distribution catalog with
|
|
6
|
+
> grapheneos, omarchy and the other systems inside the saddle. and:
|
|
7
|
+
> "saddle também vira com outras distribuições de sistema operacional
|
|
8
|
+
> além de linux como por exemplo, maru os is firmware not an application
|
|
9
|
+
> / vanilla os is based on ubuntu / v / omarchy is arch linux-based
|
|
10
|
+
> distribution created by dhh / grapheneos is a privacy and security
|
|
11
|
+
> hardened version of android / linux normal / max / lite / balanced /
|
|
12
|
+
> android / arm64 / amd64 / vgpu / vcpu / por exemplo, 50gb por plano
|
|
13
|
+
> via config."
|
|
14
|
+
|
|
15
|
+
## the registry
|
|
16
|
+
|
|
17
|
+
`saddle/containerarch.ts` — the file that already owned the
|
|
18
|
+
eleven-architecture standard and the build architecture table — now
|
|
19
|
+
carries the os distribution catalog beside them (section 1c of the
|
|
20
|
+
file): one honest row per system seat, pure data and pure functions,
|
|
21
|
+
frozen at module load, zero storage, zero node-only imports (the one
|
|
22
|
+
import it takes — `engineplans.ts` — is itself pure data).
|
|
23
|
+
|
|
24
|
+
| seat | kind | base (the true fact) | package | arch pairing | legs | tiers |
|
|
25
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
26
|
+
| grapheneos | mobile | android | apk | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
|
|
27
|
+
| omarchy | desktop | arch linux | pacman | linux-x64, linux-x64-musl, linux-arm64 | amd64+arm64 | max/balanced/lite |
|
|
28
|
+
| vanillaos | distribution | ubuntu | apt | linux-x64, linux-x64-musl, linux-arm64 | amd64+arm64 | max/balanced/lite |
|
|
29
|
+
| maruos | firmware | android firmware with a debian desktop | abuild | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
|
|
30
|
+
| v | languageos | the v language platform (vlang, the 0.5.2 catalog seat) | vpm | linux-x64, linux-arm64, darwin-arm64, darwin-x64, windows-x64, wasm32 | amd64+arm64 | max/balanced/lite |
|
|
31
|
+
| linux | distribution | the plain linux baseline the engine container rides | apt | linux-x64, linux-x64-musl, linux-arm64, linux-riscv64 | amd64+arm64+riscv64 | max/balanced/lite |
|
|
32
|
+
| android | mobile | the android application platform | apk | linux-arm64, darwin-arm64-v8 | arm64 | max/balanced/lite |
|
|
33
|
+
|
|
34
|
+
the true facts the owner's law pins, verbatim in the rows' notes:
|
|
35
|
+
|
|
36
|
+
- **grapheneos** is a privacy and security hardened version of android
|
|
37
|
+
(the hardened mobile seat, the apk grammar, the arm64 legs).
|
|
38
|
+
- **omarchy** is the arch linux-based distribution created by dhh (the
|
|
39
|
+
desktop seat, the pacman grammar, the amd64 and arm64 legs).
|
|
40
|
+
- **vanilla os** is based on ubuntu (the atomic desktop seat beside the
|
|
41
|
+
plain linux baseline).
|
|
42
|
+
- **maru os** is firmware, not an application (the catalog carries the
|
|
43
|
+
seat honestly, the engine never installs it as a guest application —
|
|
44
|
+
it rides no vgpu path).
|
|
45
|
+
- **v** names the v language platform the dependency catalog pins at
|
|
46
|
+
0.5.2 (`package.json` carries `"v": "^0.5.2"`), never a guest
|
|
47
|
+
distribution — the languagev compiler seat of the ninety modalities
|
|
48
|
+
answers it (the owner's nickname law, the same law
|
|
49
|
+
`docs/ninetymodalities.md` records).
|
|
50
|
+
- **linux** is the normal baseline: the default distribution the saddle
|
|
51
|
+
container itself rides (the resolver's default), every linux seat the
|
|
52
|
+
standard carries answers it.
|
|
53
|
+
- **android** is the mobile platform the grapheneos hardening builds on.
|
|
54
|
+
|
|
55
|
+
## the pairings (real joins, never duplicated lists)
|
|
56
|
+
|
|
57
|
+
- the **arch pairing**: every `archs` entry of every row is a real seat
|
|
58
|
+
id of the eleven-architecture standard (`CONTAINERARCHIDS` — the
|
|
59
|
+
tests assert it, never an invented seat id). the legs column is not
|
|
60
|
+
stored data: `osdistributiondockerarchs(id)` derives the amd64/arm64
|
|
61
|
+
docker legs by joining the row's pairings against the carried seats
|
|
62
|
+
of `CONTAINERARCHES` (the target seats the v row pairs contribute no
|
|
63
|
+
leg — the binary and npm lanes own them).
|
|
64
|
+
- the **tier binding**: every seat binds the full max/balanced/lite
|
|
65
|
+
engine plan trio, and the trio itself is derived from
|
|
66
|
+
`ENGINEPLANS.map((row) => row.id)` — the ids stay the engineplans
|
|
67
|
+
table's own, never a duplicated tier list. the tier axis scales the
|
|
68
|
+
virtual hardware plan, never the catalog: the tier and the seat stay
|
|
69
|
+
orthogonal, exactly like the sandbox bases (`SANDBOXBASES`) stay
|
|
70
|
+
orthogonal to the guest os ids.
|
|
71
|
+
- the **virtual device law** (vgpu/vcpu): `vgpu` and `vcpu` are law
|
|
72
|
+
fields, not caps — vgpu marks the seats that present a virtual gpu
|
|
73
|
+
path (false for the firmware seat and the language seat), vcpu pins
|
|
74
|
+
that every seat rides the engine's virtual cpu bank. the counts
|
|
75
|
+
arrive from the engine plan profile, never from the catalog.
|
|
76
|
+
|
|
77
|
+
## the plan-resource law (config, never a cap)
|
|
78
|
+
|
|
79
|
+
the owner's law: "por exemplo, 50gb por plano via config". the
|
|
80
|
+
`OSDISTRIBUTIONPLANLAW` row carries it as data:
|
|
81
|
+
|
|
82
|
+
- `examplestoragegb: 50` — the example per-plan storage quota, plan
|
|
83
|
+
config data a plan row stamps;
|
|
84
|
+
- `quotaowner: "devthink"` — the devthink system plan logic owns every
|
|
85
|
+
quota;
|
|
86
|
+
- `enginelimits: ENGINELIMITS` — the engineplans limits law rides the
|
|
87
|
+
row verbatim (the reuse law): unlimited workloads, unlimited
|
|
88
|
+
sessions, unlimited modalities, unlimited artifacts.
|
|
89
|
+
|
|
90
|
+
no row of the catalog carries a numeric resource field — the tests scan
|
|
91
|
+
every field of every row and fail on any number. the 50 gb is plan
|
|
92
|
+
config, the engine is unlimited, and the catalog never bakes a ceiling
|
|
93
|
+
(an engine limit would be a product decision wearing an engine costume
|
|
94
|
+
— the same law `docs/planningengine.md` and `engineplans.ts` record).
|
|
95
|
+
|
|
96
|
+
## the consumable surface (what the container and the modality system read)
|
|
97
|
+
|
|
98
|
+
`containerarch.ts` exports the catalog the same way it exports the
|
|
99
|
+
arch standard:
|
|
100
|
+
|
|
101
|
+
- `OSDISTRIBUTIONIDS`, `OSDISTRIBUTIONS`, `OSDISTRIBUTIONCOUNT` — the
|
|
102
|
+
catalog rows (the publish matrix and the modality seats read the ids);
|
|
103
|
+
- `OSDISTRIBUTIONPROFILEIDS` — the derived engine plan trio (the tier
|
|
104
|
+
binding);
|
|
105
|
+
- `OSDISTRIBUTIONPLANLAW` — the plan-resource law of record;
|
|
106
|
+
- `getosdistribution`, `resolveosdistribution`, `osdistributionerror`
|
|
107
|
+
— the resolver (undefined/null answers the linux row, a known id
|
|
108
|
+
answers its row, anything else throws the coded
|
|
109
|
+
`OS_DISTRIBUTION_UNKNOWN` refusal naming the seven seats);
|
|
110
|
+
- `osdistributionsforarch`, `osdistributionsforprofile` — the joins the
|
|
111
|
+
modality system reads (which seats ride one arch seat; which seats
|
|
112
|
+
ride one tier — an unknown tier throws
|
|
113
|
+
`OS_DISTRIBUTION_PROFILE_UNKNOWN`);
|
|
114
|
+
- `osdistributiondockerarchs` — the derived docker legs (amd64, arm64,
|
|
115
|
+
riscv64) the container matrix reads;
|
|
116
|
+
- `osdistributioncoverage`, `osdistributionlines` — the honest coverage
|
|
117
|
+
report and the rendered table lines the container lanes stamp on
|
|
118
|
+
every profile run.
|
|
119
|
+
|
|
120
|
+
## the build wiring (why no new build legs)
|
|
121
|
+
|
|
122
|
+
the distribution seats ride the arch axis the pinned base already
|
|
123
|
+
resolves: the mobile and firmware seats ride the arm64 legs, the
|
|
124
|
+
desktop and baseline seats ride the amd64 and arm64 legs — the same
|
|
125
|
+
legs the eleven-architecture standard carries. the dockerfile documents
|
|
126
|
+
the law (the `THE OS DISTRIBUTION SEATS` paragraph) and the publish
|
|
127
|
+
lanes stay untouched: no per-os build matrix exists or is needed, the
|
|
128
|
+
container itself keeps riding the normal linux baseline (the container
|
|
129
|
+
lane law) and the seven seats are the catalog the modality system hosts
|
|
130
|
+
inside the engine, never host distributions the container installs.
|
|
131
|
+
|
|
132
|
+
## the visitor law (the critical law)
|
|
133
|
+
|
|
134
|
+
every system seat of the catalog is a hosted GUEST the engine drives on
|
|
135
|
+
its own twenty-four hour internet infrastructure — the catalog never
|
|
136
|
+
describes the visitor machine (no visitor gpu, no visitor cpu, no
|
|
137
|
+
visitor browser, no visitor local storage, no localforage). the engine
|
|
138
|
+
owns every cycle it spends through saddle and vault edge processing.
|
|
139
|
+
|
|
140
|
+
## the pinning battery
|
|
141
|
+
|
|
142
|
+
`saddle/tests/osdistrib.test.ts` (eight tests, green): the seven seats
|
|
143
|
+
in law order, the true facts verbatim, the arch pairing honesty (every
|
|
144
|
+
pairing a real seat of the standard, the derived legs per seat), the
|
|
145
|
+
tier binding reused from engineplans (the deep-equality against
|
|
146
|
+
`ENGINEPLANS.map`), the plan-resource law (50 as plan config, the
|
|
147
|
+
engine unlimited, zero numeric fields in the rows), the resolver (the
|
|
148
|
+
linux default, the coded refusals), the arch and profile joins, and the
|
|
149
|
+
coverage report plus the rendered lines.
|
package/integration.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* integration.ts — replaceable forge, application, MCP and transport adapters.
|
|
3
|
+
*
|
|
4
|
+
* This file is the v2.0.0 grand-merge consolidation of the former nested
|
|
5
|
+
* folders of the repository (adapters) into the single
|
|
6
|
+
* root-level domain file of the saddle family standard: one optimized
|
|
7
|
+
* TypeScript file per correlated domain, ordinal sections, JSDoc on every
|
|
8
|
+
* block, intra-domain imports dissolved, cross-domain imports rewritten to
|
|
9
|
+
* ./<domain>.js. Every capability of every source file is preserved here;
|
|
10
|
+
* the pure re-export barrels (none in this domain) folded their surface into this file directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { jsondecode, jsonencode } from "./communication.ts";
|
|
14
|
+
|
|
15
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
16
|
+
/* Section 1: adapters/transport.ts — transport centralizes timeout retry and jitter without choosing a host or vendor. */
|
|
17
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
18
|
+
/**
|
|
19
|
+
* transport centralizes timeout retry and jitter without choosing a host or vendor.
|
|
20
|
+
*/
|
|
21
|
+
export function transport(options = {}) {
|
|
22
|
+
const fetcher = options.fetcher ?? fetch;
|
|
23
|
+
const attempts = options.attempts ?? 3;
|
|
24
|
+
const timeout = options.timeout ?? 30000;
|
|
25
|
+
const retrycodes = new Set(options.retrycodes ?? [408, 409, 429, 500, 502, 503, 504]);
|
|
26
|
+
async function request(url, init = {}) {
|
|
27
|
+
let last;
|
|
28
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetcher(url, { ...init, signal: init.signal ?? controller.signal });
|
|
33
|
+
if (response.ok || !retrycodes.has(response.status) || attempt === attempts - 1) return response;
|
|
34
|
+
last = new Error(`request failed with ${response.status}`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
last = error;
|
|
37
|
+
if (attempt === attempts - 1) throw error;
|
|
38
|
+
} finally { clearTimeout(timer); }
|
|
39
|
+
await delay(backoff(options, attempt));
|
|
40
|
+
}
|
|
41
|
+
throw last ?? new Error("request failed");
|
|
42
|
+
}
|
|
43
|
+
return { request };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function backoff(options, attempt) { const base = options.backoff ?? 250; const jitter = options.jitter ?? 0; return base * 2 ** attempt + Math.floor(Math.random() * jitter); }
|
|
47
|
+
function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }
|
|
48
|
+
|
|
49
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
50
|
+
/* Section 2: adapters/socket.ts — socket adapter keeps realtime optional and accepts a caller supplied websocket constructor. */
|
|
51
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
52
|
+
/**
|
|
53
|
+
* socket adapter keeps realtime optional and accepts a caller supplied websocket constructor.
|
|
54
|
+
*/
|
|
55
|
+
export function socketadapter(options = {}) {
|
|
56
|
+
const websocket = options.websocket ?? globalThis.WebSocket;
|
|
57
|
+
if (!websocket) throw new Error("websocket implementation is required");
|
|
58
|
+
return {
|
|
59
|
+
connect(url, protocols) {
|
|
60
|
+
if (!url) throw new TypeError("socket url is required");
|
|
61
|
+
const socket = new websocket(url, protocols);
|
|
62
|
+
return { socket, send(value) { socket.send(typeof value === "string" ? value : JSON.stringify(value)); }, close(code, reason) { socket.close(code, reason); } };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
68
|
+
/* Section 3: adapters/github.ts — github adapter uses caller supplied credentials and base url configuration. */
|
|
69
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
70
|
+
/**
|
|
71
|
+
* github adapter uses caller supplied credentials and base url configuration.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
export function githubadapter(options = {}) {
|
|
75
|
+
if (!options.baseurl || typeof options.token !== "function") throw new TypeError("github adapter requires baseurl and token function");
|
|
76
|
+
const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
|
|
77
|
+
async function call(path, init = {}) {
|
|
78
|
+
const token = await options.token();
|
|
79
|
+
const headers = { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "x-github-api-version": options.apiversion ?? "2022-11-28", ...(init.headers ?? {}) };
|
|
80
|
+
return client.request(new URL(path, options.baseurl), { ...init, headers });
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
async health() { const response = await call("/rate_limit"); return { ok: response.ok, status: response.status }; },
|
|
84
|
+
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 }; },
|
|
85
|
+
async run(owner, repository, runid) { const response = await call(`/repos/${owner}/${repository}/actions/runs/${runid}`); return response.json(); }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
90
|
+
/* Section 4: adapters/gitlab.ts — gitlab adapter keeps project addressing and token ownership outside the package. */
|
|
91
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
92
|
+
/**
|
|
93
|
+
* gitlab adapter keeps project addressing and token ownership outside the package.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
export function gitlabadapter(options = {}) {
|
|
97
|
+
const project = encodeURIComponent(options.project ?? "");
|
|
98
|
+
const base = forgeadapter({ ...options, kind: "gitlab" });
|
|
99
|
+
return { ...base, async dispatch(spec) { return base.dispatch({ ...spec, path: spec.path ?? `/api/v4/projects/${project}/pipeline` }); } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
103
|
+
/* Section 5: adapters/forge.ts — forge adapter defines the common dispatch and artifact surface for compatible forges. */
|
|
104
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
105
|
+
/**
|
|
106
|
+
* forge adapter defines the common dispatch and artifact surface for compatible forges.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
export function forgeadapter(options = {}) {
|
|
110
|
+
if (!options.baseurl || typeof options.token !== "function") throw new TypeError("forge adapter requires baseurl and token function");
|
|
111
|
+
const client = transport({ fetcher: options.fetcher, attempts: options.attempts, timeout: options.timeout });
|
|
112
|
+
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 ?? {}) } }); }
|
|
113
|
+
return {
|
|
114
|
+
kind: options.kind ?? "forge",
|
|
115
|
+
async health(path = "/") { const response = await call(path); return { ok: response.ok, status: response.status }; },
|
|
116
|
+
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 }; },
|
|
117
|
+
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 }; }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
122
|
+
/* Section 6: adapters/forgejo.ts — forgejo and gitea can reuse the open forge contract with a caller supplied base url. */
|
|
123
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
124
|
+
/**
|
|
125
|
+
* forgejo and gitea can reuse the open forge contract with a caller supplied base url.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
export function forgejoadapter(options = {}) { return forgeadapter({ ...options, kind: "forgejo" }); }
|
|
129
|
+
export function giteaadapter(options = {}) { return forgeadapter({ ...options, kind: "gitea" }); }
|
|
130
|
+
export function codebergadapter(options = {}) { return forgeadapter({ ...options, kind: "codeberg" }); }
|
|
131
|
+
|
|
132
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
133
|
+
/* Section 7: adapters/huggingface.ts — hugging face storage remains an explicit adapter with caller supplied repository path. */
|
|
134
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
135
|
+
/**
|
|
136
|
+
* hugging face storage remains an explicit adapter with caller supplied repository path.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
export function huggingfaceadapter(options = {}) { return forgeadapter({ ...options, kind: "huggingface" }); }
|
|
140
|
+
|
|
141
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
142
|
+
/* Section 8: adapters/apps.ts — app registry keeps installation, scope and revocation state outside platform credentials. */
|
|
143
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
144
|
+
/**
|
|
145
|
+
* app registry keeps installation, scope and revocation state outside platform credentials.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
export const appstatuses = Object.freeze(["installed", "suspended", "revoked"]);
|
|
149
|
+
|
|
150
|
+
/** Creates a caller-owned app installation registry with explicit scopes. */
|
|
151
|
+
export function appregistry(options = {}) {
|
|
152
|
+
const values = new Map(options.apps ? options.apps.map((app) => [app.id, normalize(app)]) : []);
|
|
153
|
+
function install(input = {}) { if (!input.id || !input.name) throw new TypeError("app installation requires id and name"); const app = normalize({ ...input, status: "installed", installedat: input.installedat ?? Date.now() }); values.set(app.id, app); return { ...app, scopes: [...app.scopes] }; }
|
|
154
|
+
function suspend(id) { return transition(id, "suspended"); }
|
|
155
|
+
function revoke(id) { return transition(id, "revoked"); }
|
|
156
|
+
function restore(id) { return transition(id, "installed"); }
|
|
157
|
+
function authorize(id, required = []) { const app = requireapp(id); if (app.status !== "installed") return { allowed: false, reason: `app-${app.status}` }; const missing = required.filter((scope) => !app.scopes.includes(scope)); return { allowed: missing.length === 0, missing, appid: app.id }; }
|
|
158
|
+
function get(id) { const app = values.get(String(id)); return app ? { ...app, scopes: [...app.scopes] } : null; }
|
|
159
|
+
function list() { return [...values.values()].map((app) => ({ ...app, scopes: [...app.scopes] })); }
|
|
160
|
+
return { install, suspend, revoke, restore, authorize, get, list };
|
|
161
|
+
function requireapp(id) { const app = values.get(String(id)); if (!app) throw new Error(`app not found: ${id}`); return app; }
|
|
162
|
+
function transition(id, status) { const app = requireapp(id); if (!appstatuses.includes(status)) throw new TypeError(`unsupported app status: ${status}`); app.status = status; app.updatedat = Date.now(); return get(id); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalize(input) { return { id: String(input.id), name: String(input.name), status: input.status ?? "installed", scopes: [...new Set((input.scopes ?? []).map(String))], installedat: Number(input.installedat ?? Date.now()), updatedat: Number(input.updatedat ?? Date.now()), metadata: { ...(input.metadata ?? {}) } }; }
|
|
166
|
+
|
|
167
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
168
|
+
/* Section 9: adapters/mcptransport.ts — mcp transport keeps JSON RPC framing separate from the tool server. */
|
|
169
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
170
|
+
/**
|
|
171
|
+
* mcp transport keeps JSON RPC framing separate from the tool server.
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
/** Creates JSONL and HTTP handlers for the transport agnostic MCP server. */
|
|
175
|
+
export function mcptransport(server) {
|
|
176
|
+
if (typeof server?.handle !== "function") throw new TypeError("mcp transport requires server");
|
|
177
|
+
return {
|
|
178
|
+
async handleline(line) { const request = jsondecode(line); return jsonencode(await server.handle(request)); },
|
|
179
|
+
async *stream(lines) { for await (const line of lines) if (String(line).trim()) yield await this.handleline(line); },
|
|
180
|
+
async handlehttp(request) { const body = await request.text(); const response = await server.handle(jsondecode(body)); return new Response(jsonencode(response), { status: 200, headers: { "content-type": "application/json" } }); }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
185
|
+
/* Section 10: adapters/mcpserver.ts — mcp server implements a small JSON RPC tool surface and remains transport agnostic. */
|
|
186
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
187
|
+
/**
|
|
188
|
+
* mcp server implements a small JSON RPC tool surface and remains transport agnostic.
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
export function mcpserver(options = {}) {
|
|
192
|
+
const tools = {
|
|
193
|
+
serialize: async (input) => jsonencode(input.value),
|
|
194
|
+
...(options.browser ? browsertools(options.browser) : {}),
|
|
195
|
+
...(options.tools ?? {})
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
listtools() { return Object.keys(tools).map((name) => ({ name, description: `saddle ${name} tool` })); },
|
|
199
|
+
async handle(request) {
|
|
200
|
+
if (request.method === "tools/list") return { jsonrpc: "2.0", id: request.id, result: { tools: this.listtools() } };
|
|
201
|
+
if (request.method === "tools/call") { const handler = tools[request.params?.name]; if (!handler) return { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "tool not found" } }; try { return { jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: JSON.stringify(await handler(request.params.arguments ?? {})) }] } }; } catch (error) { return { jsonrpc: "2.0", id: request.id, error: { code: -32000, message: error.message } }; } }
|
|
202
|
+
return { jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "method not found" } };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
208
|
+
/* Section 11: adapters/mcpbrowser.ts — browser MCP tools expose snapshots and actions only through an injected browser adapter. */
|
|
209
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
210
|
+
/**
|
|
211
|
+
* browser MCP tools expose snapshots and actions only through an injected browser adapter.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
/** Creates optional browser tools for an MCP server without selecting Playwright or another vendor. */
|
|
215
|
+
export function browsertools(browser) {
|
|
216
|
+
if (typeof browser?.snapshot !== "function" || typeof browser?.action !== "function") throw new TypeError("browser MCP tools require snapshot and action");
|
|
217
|
+
return {
|
|
218
|
+
browser_snapshot: async (input = {}) => browser.snapshot(input),
|
|
219
|
+
browser_action: async (input = {}) => browser.action(input)
|
|
220
|
+
};
|
|
221
|
+
}
|