semrush-cli 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +163 -2
- package/THIRD-PARTY-NOTICES.md +19 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +1 -0
- package/dist/auth-B26NjmuZ.js +82 -0
- package/dist/auth-B26NjmuZ.js.map +1 -0
- package/dist/browser-layer-DijkeDly.js +269 -0
- package/dist/browser-layer-DijkeDly.js.map +1 -0
- package/dist/browser-service-BYYE-vSm.js +9 -0
- package/dist/browser-service-BYYE-vSm.js.map +1 -0
- package/dist/browser-service-CL8YPaIT.js +3 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon/galleton-darwin-arm64 +0 -0
- package/dist/daemon/galleton-darwin-x64 +0 -0
- package/dist/daemon/galleton-linux-arm64 +0 -0
- package/dist/daemon/galleton-linux-x64 +0 -0
- package/dist/daemon/galleton-win32-x64.exe +0 -0
- package/dist/errors-aN5dmXgv.js +2119 -0
- package/dist/errors-aN5dmXgv.js.map +1 -0
- package/dist/mcp-DUyKjMzQ.js +95 -0
- package/dist/mcp-DUyKjMzQ.js.map +1 -0
- package/dist/session-layer-u9NMfM8W.js +239 -0
- package/dist/session-layer-u9NMfM8W.js.map +1 -0
- package/dist/validation-BIbDUKRg.js +30 -0
- package/dist/validation-BIbDUKRg.js.map +1 -0
- package/dist/validation-D4jV0Q8q.js +4 -0
- package/docs/architecture.md +21 -0
- package/package.json +56 -4
- package/src/vendor/GALLETON-LICENSE +21 -0
- package/src/vendor/README.md +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-layer-u9NMfM8W.js","names":["code: string","retryAt?: string","status: number","headers: Record<string, string[]>","body: Uint8Array","revision: number","response: Response","text: string","decoded: unknown"],"sources":["../src/session-service.ts","../src/vendor/galleton.ts","../src/session-layer.ts"],"sourcesContent":["import { Context, Effect } from 'effect';\nimport type { Credentials, SessionStatus } from './vendor/galleton.js';\nimport type { SessionError } from './errors.js';\n\n/** Galleton owns all durable credentials. Consumers only receive scoped headers. */\nexport class Sessions extends Context.Service<Sessions, {\n readonly headers: () => Effect.Effect<Record<string, string>, SessionError>;\n readonly status: () => Effect.Effect<SessionStatus, SessionError>;\n readonly save: (credentials: Credentials) => Effect.Effect<SessionStatus, SessionError>;\n readonly capture: (cookies: string[]) => Effect.Effect<void, SessionError>;\n readonly forget: () => Effect.Effect<void, SessionError>;\n}>()('semrush/Sessions') {}\n","/** Server-side client for an authenticated, loopback-only Galleton daemon. */\nexport interface Credentials {\n provider: string;\n cookie_origin?: string;\n cookie_header?: string;\n set_cookies?: string[];\n access_token?: string;\n refresh_token?: string;\n access_expires_at?: string;\n secrets?: Record<string, string>;\n replace?: boolean;\n expected_revision?: number;\n}\nexport interface Problem { code: string; message: string; retry_at?: string }\nexport interface SessionStatus {\n id: string; provider: string; status: string; revision: number;\n created_at: string; last_refresh?: string; next_refresh?: string;\n access_expires_at?: string; cookie_count: number; error?: Problem;\n}\nexport class GalletonError extends Error {\n constructor(public readonly code: string, message: string,\n public readonly status = 0, public readonly retryAt?: string) {\n super(message); this.name = \"GalletonError\";\n }\n}\nexport interface RequestOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: Uint8Array | string;\n json?: unknown;\n}\nexport class ManagedResponse {\n constructor(public readonly status: number,\n public readonly headers: Record<string, string[]>,\n public readonly body: Uint8Array,\n public readonly revision: number) {}\n get ok(): boolean { return this.status >= 200 && this.status < 300; }\n text(): string { return new TextDecoder().decode(this.body); }\n json<T = unknown>(): T { return JSON.parse(this.text()) as T; }\n}\nfunction encode64(bytes: Uint8Array): string {\n let binary = \"\";\n for (let start = 0; start < bytes.length; start += 8192) {\n binary += String.fromCharCode(...bytes.subarray(start, start + 8192));\n }\n return btoa(binary);\n}\nfunction decode64(value: string): Uint8Array {\n return Uint8Array.from(atob(value), char => char.charCodeAt(0));\n}\nfunction sessionPath(id: string): string {\n if (!/^[A-Za-z0-9_-]{1,64}$/.test(id)) throw new TypeError(\"Invalid session ID\");\n return `/v1/sessions/${id}`;\n}\nasync function responseText(response: Response): Promise<string> {\n const reader = response.body?.getReader();\n if (!reader) return \"\";\n const decoder = new TextDecoder();\n let size = 0;\n let text = \"\";\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > 8 * 1024 * 1024) {\n await reader.cancel().catch(() => {});\n throw new GalletonError(\"invalid_response\", \"Oversized daemon response.\", response.status);\n }\n text += decoder.decode(value, { stream: true });\n }\n return text + decoder.decode();\n } finally {\n reader.releaseLock();\n }\n}\nexport class Galleton {\n private readonly base: string;\n private readonly token: string;\n private readonly timeoutMs: number;\n constructor(options: { token: string; baseURL?: string; timeoutMs?: number }) {\n const url = new URL(options.baseURL ?? \"http://127.0.0.1:8766\");\n const host = url.hostname;\n const loopback = host === \"[::1]\" ||\n /^127(?:\\.[0-9]{1,3}){3}$/.test(host) && host.split(\".\").every(n => +n <= 255);\n if (!loopback || url.protocol !== \"http:\" || url.username || url.password ||\n url.search || url.hash || url.pathname !== \"/\") {\n throw new TypeError(\"Galleton requires a literal loopback HTTP origin\");\n }\n this.base = url.origin;\n this.token = options.token.trim();\n if (!this.token || /[\\r\\n]/.test(this.token)) throw new TypeError(\"Invalid local API token\");\n this.timeoutMs = options.timeoutMs ?? 300_000;\n if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new TypeError(\"Invalid timeout\");\n }\n private async call<T>(method: string, path: string, body?: unknown): Promise<T> {\n let response: Response;\n try {\n response = await fetch(this.base + path, {\n method,\n headers: { Authorization: `Bearer ${this.token}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n redirect: \"manual\", signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch {\n throw new GalletonError(\"daemon_unavailable\", \"Galleton is unavailable or the local request timed out.\");\n }\n let text: string;\n try { text = await responseText(response); }\n catch (error) {\n if (error instanceof GalletonError) throw error;\n throw new GalletonError(\"daemon_unavailable\", \"Galleton response was interrupted; the request may already have completed.\", response.status);\n }\n let decoded: unknown;\n try { decoded = JSON.parse(text); }\n catch {\n if (!response.ok) throw new GalletonError(\"daemon_error\", \"Galleton request failed.\", response.status);\n throw new GalletonError(\"invalid_response\", \"Expected a JSON daemon response.\", response.status);\n }\n if (!response.ok) {\n const error = (decoded as { error?: Problem }).error;\n throw new GalletonError(error?.code ?? \"daemon_error\",\n error?.message ?? \"Galleton request failed.\", response.status, error?.retry_at);\n }\n return decoded as T;\n }\n connect(id: string, credentials: Credentials): Promise<SessionStatus> {\n return this.call(\"PUT\", sessionPath(id), credentials);\n }\n status(id: string): Promise<SessionStatus> { return this.call(\"GET\", sessionPath(id)); }\n async list(): Promise<SessionStatus[]> {\n return (await this.call<{ sessions: SessionStatus[] }>(\"GET\", \"/v1/sessions\")).sessions;\n }\n refresh(id: string): Promise<SessionStatus> { return this.call(\"POST\", sessionPath(id) + \"/refresh\", {}); }\n async headers(id: string, url: string): Promise<Record<string, string>> {\n return (await this.call<{ headers: Record<string, string> }>(\n \"POST\", sessionPath(id) + \"/headers\", { url })).headers;\n }\n capture(id: string, url: string, setCookie: string[]): Promise<SessionStatus> {\n return this.call(\"POST\", sessionPath(id) + \"/cookies\", { url, set_cookie: setCookie });\n }\n async forget(id: string): Promise<void> { await this.call(\"DELETE\", sessionPath(id)); }\n /** Buffered request with automatic Cookie/Authorization injection and Set-Cookie capture.\n * Resource calls are never automatically retried. Non-2xx upstream statuses are returned. */\n async request(id: string, url: string, options: RequestOptions = {}): Promise<ManagedResponse> {\n if (options.body !== undefined && options.json !== undefined) throw new TypeError(\"Use body or json, not both\");\n const headers = { ...options.headers };\n let body = options.body;\n if (options.json !== undefined) {\n body = JSON.stringify(options.json);\n for (const name of Object.keys(headers)) {\n if (name.toLowerCase() === \"content-type\") delete headers[name];\n }\n headers[\"Content-Type\"] = \"application/json\";\n }\n const bytes = typeof body === \"string\" ? new TextEncoder().encode(body) : body ?? new Uint8Array();\n if (bytes.length > 1024 * 1024) throw new TypeError(\"Request body exceeds 1 MiB\");\n const wire = await this.call<{\n status: number; headers: Record<string, string[]>; body_base64: string; revision: number;\n }>(\"POST\", sessionPath(id) + \"/request\", {\n url, method: options.method ?? \"GET\", headers, body_base64: encode64(bytes),\n });\n return new ManagedResponse(wire.status, wire.headers, decode64(wire.body_base64), wire.revision);\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { Config, Effect, Layer } from 'effect';\nimport { Galleton, GalletonError, type Credentials } from './vendor/galleton.js';\nimport { SessionError } from './errors.js';\nimport { Sessions } from './session-service.js';\n\nexport const origin = 'https://www.semrush.com';\nexport const defaultStateDirectory = join(homedir(), '.local', 'share', 'semrush-cli', 'galleton');\n\n/** Raw SDK errors stop here; daemon and upstream payloads never reach diagnostics. */\nconst sdkError = (cause: unknown): SessionError => {\n if (cause instanceof GalletonError) {\n const recovery = cause.code === 'not_found' ? 'Run semrush auth login or semrush auth import --cdp-url URL.' : 'Run semrush daemon start and semrush auth status. Reimport if renewal or authentication failed.';\n return new SessionError({ message: `Galleton ${cause.code}. ${recovery}`, code: cause.code });\n }\n return new SessionError({ message: 'Galleton operation failed. Run semrush doctor and check local configuration.' });\n};\n\nexport const makeSessionsLayer = (credentialOrigin: string) => Layer.effect(Sessions, Effect.gen(function* () {\n const baseURL = yield* Config.String('SEMRUSH_GALLETON_URL').pipe(Config.withDefault('http://127.0.0.1:8768'));\n const tokenFile = yield* Config.String('SEMRUSH_GALLETON_TOKEN_FILE').pipe(Config.withDefault(join(defaultStateDirectory, 'api.token')));\n const id = yield* Config.String('SEMRUSH_SESSION').pipe(Config.withDefault('semrush-cli'));\n const token = yield* Effect.tryPromise({ try: () => readFile(tokenFile, 'utf8'), catch: () => new SessionError({ message: 'Galleton token unavailable. Run semrush setup, or set SEMRUSH_GALLETON_TOKEN_FILE.' }) });\n const sdk = yield* Effect.try({ try: () => new Galleton({ baseURL, token, timeoutMs: 60000 }), catch: sdkError });\n const status = Effect.fn('Sessions.status')(() => Effect.tryPromise({ try: () => sdk.status(id), catch: sdkError }));\n return Sessions.of({\n status,\n headers: Effect.fn('Sessions.headers')(() => Effect.tryPromise({ try: () => sdk.headers(id, credentialOrigin + '/'), catch: sdkError })),\n save: Effect.fn('Sessions.save')(function* (credentials: Credentials) {\n // Replacements are explicit and revision-checked; missing profiles are created.\n const previous = yield* status().pipe(Effect.map(value => value.revision), Effect.catchTag('SessionError', error => error.code === 'not_found' ? Effect.succeed(undefined) : Effect.fail(error)));\n const input = { ...credentials };\n if (previous !== undefined) { input.replace = true; input.expected_revision = previous; }\n return yield* Effect.tryPromise({ try: () => sdk.connect(id, input), catch: sdkError });\n }),\n capture: Effect.fn('Sessions.capture')((cookies: string[]) => Effect.tryPromise({ try: async () => { await sdk.capture(id, credentialOrigin + '/', cookies); }, catch: sdkError })),\n forget: Effect.fn('Sessions.forget')(() => Effect.tryPromise({ try: () => sdk.forget(id), catch: sdkError })),\n });\n}));\n\nexport const sessionsLive = makeSessionsLayer(origin);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { a as reports, r as UsageError } from "./errors-aN5dmXgv.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Ajv } from "ajv";
|
|
4
|
+
|
|
5
|
+
//#region src/validation.ts
|
|
6
|
+
const ajv = new Ajv({
|
|
7
|
+
useDefaults: true,
|
|
8
|
+
allErrors: true,
|
|
9
|
+
strict: false
|
|
10
|
+
});
|
|
11
|
+
const validators = new Map(Object.entries(reports).map(([name, report]) => [name, ajv.compile(report.schema)]));
|
|
12
|
+
function isReportName(name) {
|
|
13
|
+
return Object.hasOwn(reports, name);
|
|
14
|
+
}
|
|
15
|
+
/** Validate once at the public boundary, populate schema defaults, reject unknown keys. */
|
|
16
|
+
const parseReport = Effect.fn("Reports.parse")(function* (name, input) {
|
|
17
|
+
if (!isReportName(name)) return yield* Effect.fail(new UsageError({ message: `Unknown report: ${name}. Run semrush reports.` }));
|
|
18
|
+
const validate = validators.get(name);
|
|
19
|
+
const query = structuredClone(input);
|
|
20
|
+
if (!validate || !validate(query)) return yield* Effect.fail(new UsageError({ message: `Invalid ${name} input: ${ajv.errorsText(validate?.errors)}. Run semrush ${name.replaceAll("_", "-")} --help.` }));
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
definition: reports[name],
|
|
24
|
+
query
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { parseReport as n, isReportName as t };
|
|
30
|
+
//# sourceMappingURL=validation-BIbDUKRg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-BIbDUKRg.js","names":[],"sources":["../src/validation.ts"],"sourcesContent":["import { Ajv } from 'ajv';\nimport { Effect } from 'effect';\nimport { reports, type Query, type ReportName } from './report-parameters.js';\nimport { UsageError } from './errors.js';\n\nconst ajv = new Ajv({ useDefaults: true, allErrors: true, strict: false });\nconst validators = new Map(Object.entries(reports).map(([name, report]) => [name, ajv.compile<Query>(report.schema)]));\nexport function isReportName(name: string): name is ReportName { return Object.hasOwn(reports, name); }\n\n/** Validate once at the public boundary, populate schema defaults, reject unknown keys. */\nexport const parseReport = Effect.fn('Reports.parse')(function* (name: string, input: Query) {\n if (!isReportName(name)) return yield* Effect.fail(new UsageError({ message: `Unknown report: ${name}. Run semrush reports.` }));\n const validate = validators.get(name);\n const query = structuredClone(input);\n if (!validate || !validate(query)) return yield* Effect.fail(new UsageError({ message: `Invalid ${name} input: ${ajv.errorsText(validate?.errors)}. Run semrush ${name.replaceAll('_', '-')} --help.` }));\n return { name, definition: reports[name], query };\n});\n\nexport type ReportRequest = Effect.Success<ReturnType<typeof parseReport>>;\n"],"mappings":";;;;;AAKA,MAAM,MAAM,IAAI,IAAI;CAAE,aAAa;CAAM,WAAW;CAAM,QAAQ;CAAO,CAAC;AAC1E,MAAM,aAAa,IAAI,IAAI,OAAO,QAAQ,QAAQ,CAAC,KAAK,CAAC,MAAM,YAAY,CAAC,MAAM,IAAI,QAAe,OAAO,OAAO,CAAC,CAAC,CAAC;AACtH,SAAgB,aAAa,MAAkC;AAAE,QAAO,OAAO,OAAO,SAAS,KAAK;;;AAGpG,MAAa,cAAc,OAAO,GAAG,gBAAgB,CAAC,WAAW,MAAc,OAAc;AAC3F,KAAI,CAAC,aAAa,KAAK,CAAE,QAAO,OAAO,OAAO,KAAK,IAAI,WAAW,EAAE,SAAS,mBAAmB,KAAK,yBAAyB,CAAC,CAAC;CAChI,MAAM,WAAW,WAAW,IAAI,KAAK;CACrC,MAAM,QAAQ,gBAAgB,MAAM;AACpC,KAAI,CAAC,YAAY,CAAC,SAAS,MAAM,CAAE,QAAO,OAAO,OAAO,KAAK,IAAI,WAAW,EAAE,SAAS,WAAW,KAAK,UAAU,IAAI,WAAW,UAAU,OAAO,CAAC,gBAAgB,KAAK,WAAW,KAAK,IAAI,CAAC,WAAW,CAAC,CAAC;AACzM,QAAO;EAAE;EAAM,YAAY,QAAQ;EAAO;EAAO;EACjD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
The CLI and stdio MCP share a catalog of 48 read reports and one Semrush Effect service. Input validation populates defaults before credential-bearing resources are acquired. The browser layer owns a transient Chromium context, reads page bootstrap metadata without evaluating page-owned JavaScript, serializes report requests, and closes its browser with the Effect scope. JSON parsing outside Chromium preserves large identifiers as decimal strings.
|
|
4
|
+
|
|
5
|
+
Galleton owns durable encrypted cookies. Its unchanged TypeScript SDK is pinned and vendored with attribution because there is no npm SDK release. The npm package bundles platform builds of the pinned Galleton daemon plus the storage-only provider extension in `patches/galleton-static-provider.patch`.
|
|
6
|
+
|
|
7
|
+
## Storage-only Galleton provider
|
|
8
|
+
|
|
9
|
+
Upstream Galleton `efe7b742536d6c118c37a269bf626c5927e2a1b9` requires a renewal adapter and schedules renewal immediately after import. Semrush's observed browser login has no proven HTTP renewal contract. A `/home/` request does not return replacement cookies and correctly fails Galleton's renewal-evidence check. Treating that request as renewal would misrepresent session validity.
|
|
10
|
+
|
|
11
|
+
The bundled daemon therefore adds one canonical `static` provider kind. It permits encrypted credential import, scoped header retrieval, cookie capture and explicit forgetting, with no automatic renewal. Static providers reject renewal configuration. Explicit refresh fails with `reauth_required`. The scheduler skips unscheduled sessions. Existing Galleton HTTP/OAuth semantics remain intact. Semrush validates account access before every report; expired sessions require a new login or import.
|
|
12
|
+
|
|
13
|
+
The patch includes a real Go integration test for import, origin restrictions, persistence, cookie rotation, explicit refresh refusal, and absence of network requests. The build runs the upstream core suite including this test before compiling platform binaries. No upstream repository is modified or published by this project.
|
|
14
|
+
|
|
15
|
+
This is a current feature extension, not a compatibility shim or an alternate credential path. Remove the patch when an upstream Galleton revision supplies equivalent storage-only semantics and its tests pass here; update the SDK and daemon pin together. Do not revert to a fabricated renewal adapter.
|
|
16
|
+
|
|
17
|
+
## Trust boundaries
|
|
18
|
+
|
|
19
|
+
Only fixed Semrush endpoints and observed read RPC methods are exposed. Users cannot supply a URL, auth field, JavaScript snippet or arbitrary RPC method. The local Galleton API is authenticated and loopback-only. CDP imports accept literal loopback HTTP origins. Durable state lives outside the repository. The CLI never exports browser cookies. MCP exposes only report tools.
|
|
20
|
+
|
|
21
|
+
Provider report data can contain nested objects and provider errors. Secret fields and known secret strings are redacted before output. Raw browser and SDK failures stop at their adapters. Account limits and permission errors remain failures, never empty datasets.
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semrush-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Semrush research from your terminal and MCP clients. TypeScript, Effect, and Galleton.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"semrush": "dist/cli.js",
|
|
8
|
+
"semrush-cli": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src/vendor/GALLETON-LICENSE",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"assets",
|
|
16
|
+
"docs/architecture.md",
|
|
17
|
+
"src/vendor/README.md",
|
|
18
|
+
"THIRD-PARTY-NOTICES.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsdown && node scripts/build-daemon.mjs",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"lint": "oxlint src tests --deny-warnings",
|
|
28
|
+
"check": "npm run typecheck && npm run lint && npm run build && npm test",
|
|
29
|
+
"prepublishOnly": "npm run check"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Microck/semrush-cli.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://semrush.micr.dev",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Microck/semrush-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Microck <contact@micr.dev>",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
43
|
+
"ajv": "^8.17.1",
|
|
44
|
+
"commander": "^14.0.0",
|
|
45
|
+
"effect": "4.0.4",
|
|
46
|
+
"json-bigint": "^1.0.0",
|
|
47
|
+
"patchright": "1.64.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@oxlint/plugins": "^1.88.0",
|
|
51
|
+
"@types/json-bigint": "^1.0.4",
|
|
52
|
+
"@types/node": "^24.0.0",
|
|
53
|
+
"oxlint": "^1.88.0",
|
|
54
|
+
"tsdown": "^0.16.0",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"vitest": "^4.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Galleton contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Galleton SDK provenance
|
|
2
|
+
|
|
3
|
+
`galleton.ts` is an unchanged copy of `sdk/typescript/index.ts` from
|
|
4
|
+
https://github.com/Microck/galleton/tree/efe7b742536d6c118c37a269bf626c5927e2a1b9.
|
|
5
|
+
The SDK is not published to npm. It is bundled into the executable with its MIT
|
|
6
|
+
license. `scripts/build-daemon.mjs` builds the daemon with the static-provider extension documented in `docs/architecture.md` from the same
|
|
7
|
+
pinned commit for Linux arm64/x64, macOS arm64/x64, and Windows x64. No Go
|
|
8
|
+
installation is required to use the published npm package.
|
|
9
|
+
|
|
10
|
+
To update, inspect the upstream changes, replace the SDK and license from one
|
|
11
|
+
commit, update the build script revision and this note, and rerun integration tests.
|