@obversa/engine-anthropic-api 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonny Neill
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.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @obversa/engine-anthropic-api
2
+
3
+ `@obversa/engine-anthropic-api` runs Obversa engine requests through the
4
+ Anthropic Messages API.
5
+
6
+ ## Requirements
7
+
8
+ - Node.js 22.12 or later
9
+ - An Anthropic API key
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @obversa/engine-anthropic-api
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```ts
20
+ import { AnthropicApiEngine } from '@obversa/engine-anthropic-api';
21
+
22
+ const engine = new AnthropicApiEngine({
23
+ defaultModel: 'claude-haiku-4-5-20251001',
24
+ apiKey: process.env.ANTHROPIC_API_KEY,
25
+ });
26
+ ```
27
+
28
+ Pass the engine instance to an Obversa runtime.
29
+
30
+ ## License
31
+
32
+ [MIT](LICENSE)
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Engine plugin: the raw Anthropic Messages API (`@anthropic-ai/sdk`). Lowest
3
+ * level, token-level streaming, and the cheapest path for validator models.
4
+ * Ordinary calls retry transient 5xx and connection failures with backoff.
5
+ * Preflight-purpose calls disable both retry layers. Provider limits are
6
+ * returned to the caller without the adapter's retry loop.
7
+ *
8
+ * Needs `ANTHROPIC_API_KEY` (or `EngineOptions.apiKey`). Constructed lazily by
9
+ * the registry, so other engines work without a key present.
10
+ *
11
+ * Ignores `AgentRequest.env`: no subprocess is spawned and the API call takes
12
+ * no environment.
13
+ */
14
+ import { type AgentRequest, type AgentResult, type Engine, type EngineEventSink, type EngineSelectionRecord } from '@obversa/api';
15
+ export interface AnthropicApiEngineOptions {
16
+ readonly defaultModel?: string;
17
+ readonly apiKey?: string;
18
+ }
19
+ export declare class AnthropicApiEngine implements Engine {
20
+ private readonly opts;
21
+ readonly name = "anthropic-api";
22
+ private clientPromise?;
23
+ constructor(opts?: AnthropicApiEngineOptions);
24
+ private apiKey;
25
+ private selection;
26
+ admit(request: Omit<AgentRequest, 'prompt'>, signal: AbortSignal, expectedSelection?: EngineSelectionRecord): Promise<EngineSelectionRecord>;
27
+ private client;
28
+ run(req: AgentRequest, onEvent: EngineEventSink, signal: AbortSignal): Promise<AgentResult>;
29
+ }
@@ -0,0 +1 @@
1
+ export { AnthropicApiEngine, type AnthropicApiEngineOptions, } from './anthropic-api.js';
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ import pRetry, { AbortError } from 'p-retry';
2
+ import pTimeout, { TimeoutError } from 'p-timeout';
3
+ import { isDeepStrictEqual } from 'util';
4
+ import { EngineError, engineSelection, classifyEngineFailure, reportedUsage, assistantResult } from '@obversa/api';
5
+ import { retryAfterHeaderToMs } from '@obversa/core/command';
6
+
7
+ // src/anthropic-api.ts
8
+ function isTransient(error) {
9
+ const status = error?.status;
10
+ if (typeof status === "number") return status >= 500;
11
+ const name = error?.name ?? "";
12
+ return /connection|timeout|socket|network/i.test(name);
13
+ }
14
+ function headerValue(error, name) {
15
+ const headers = error.headers;
16
+ if (headers && typeof headers.get === "function") {
17
+ return headers.get(name) ?? void 0;
18
+ }
19
+ if (headers && typeof headers === "object") {
20
+ const v = headers[name];
21
+ if (typeof v === "string") return v;
22
+ }
23
+ return void 0;
24
+ }
25
+ function classifyLimit(error) {
26
+ const status = error?.status;
27
+ const type = error?.type;
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ if (status === 429 || type === "rate_limit_error") {
30
+ return new EngineError({
31
+ kind: "rate-limit",
32
+ message: `anthropic-api rate limited: ${message}`,
33
+ cause: error,
34
+ retryAfterMs: retryAfterHeaderToMs(headerValue(error, "retry-after"))
35
+ });
36
+ }
37
+ if (type === "billing_error") {
38
+ return new EngineError({
39
+ kind: "quota",
40
+ message: `anthropic-api usage/billing limit: ${message}`,
41
+ cause: error
42
+ });
43
+ }
44
+ return void 0;
45
+ }
46
+ function assertTextOnly(request) {
47
+ if (request.workspaceMode === "read" || request.workspaceMode === "write" || request.tools !== void 0 && (!Array.isArray(request.tools) || request.tools.length > 0) || request.allowedTools !== void 0 && (!Array.isArray(request.allowedTools) || request.allowedTools.length > 0)) {
48
+ throw new EngineError({
49
+ kind: "invalid-config",
50
+ message: "anthropic-api supports text-only requests without tools or workspace access"
51
+ });
52
+ }
53
+ }
54
+ var AnthropicApiEngine = class {
55
+ constructor(opts = {}) {
56
+ this.opts = opts;
57
+ }
58
+ opts;
59
+ name = "anthropic-api";
60
+ clientPromise;
61
+ apiKey() {
62
+ const apiKey = this.opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
63
+ if (!apiKey) {
64
+ throw new EngineError({
65
+ kind: "invalid-config",
66
+ message: "the anthropic-api engine needs an API key \u2014 set ANTHROPIC_API_KEY or pass --api-key (or use the agent-sdk / claude-cli engine, which use host Claude auth)"
67
+ });
68
+ }
69
+ return apiKey;
70
+ }
71
+ selection(request) {
72
+ return engineSelection({
73
+ adapter: "anthropic-api",
74
+ provider: "anthropic",
75
+ model: request.model ?? this.opts.defaultModel ?? "claude-haiku-4-5-20251001",
76
+ executable: null,
77
+ capabilities: []
78
+ });
79
+ }
80
+ async admit(request, signal, expectedSelection) {
81
+ if (signal.aborted) {
82
+ throw new EngineError({ kind: "aborted", message: "anthropic-api admission aborted" });
83
+ }
84
+ assertTextOnly(request);
85
+ this.apiKey();
86
+ let selected;
87
+ try {
88
+ selected = this.selection(request);
89
+ } catch (cause) {
90
+ throw new EngineError({ kind: "invalid-config", message: "anthropic-api admission selection is invalid", cause });
91
+ }
92
+ if (expectedSelection !== void 0 && !isDeepStrictEqual(selected, expectedSelection)) {
93
+ throw new EngineError({ kind: "invalid-config", message: "anthropic-api admission does not match the saved selection" });
94
+ }
95
+ return selected;
96
+ }
97
+ async client() {
98
+ if (!this.clientPromise) {
99
+ const apiKey = this.apiKey();
100
+ this.clientPromise = import('@anthropic-ai/sdk').then(
101
+ // One cast at the boundary to the structural shape we consume.
102
+ (m) => new m.default({ apiKey })
103
+ );
104
+ }
105
+ return this.clientPromise;
106
+ }
107
+ async run(req, onEvent, signal) {
108
+ assertTextOnly(req);
109
+ const preflight = req.purpose === "preflight";
110
+ const client = await this.client();
111
+ const selection = this.selection(req);
112
+ const model = selection.model;
113
+ const maxTokens = req.maxTokens ?? 1024;
114
+ const startedAt = Date.now();
115
+ const controller = new AbortController();
116
+ const onAbort = () => controller.abort();
117
+ if (signal.aborted) controller.abort();
118
+ else signal.addEventListener("abort", onAbort, { once: true });
119
+ const attempt = async () => {
120
+ try {
121
+ const stream = client.messages.stream(
122
+ {
123
+ model,
124
+ max_tokens: maxTokens,
125
+ system: req.system,
126
+ messages: [{ role: "user", content: req.prompt }]
127
+ },
128
+ { signal: controller.signal, ...preflight ? { maxRetries: 0 } : {} }
129
+ );
130
+ stream.on("text", (delta) => onEvent({ type: "text", delta }));
131
+ return await stream.finalMessage();
132
+ } catch (e) {
133
+ const limit = classifyLimit(e);
134
+ if (limit) throw new AbortError(limit);
135
+ if (signal.aborted || !isTransient(e)) {
136
+ throw new AbortError(e instanceof Error ? e : new Error(String(e)));
137
+ }
138
+ throw e;
139
+ }
140
+ };
141
+ let message;
142
+ const hardTimeout = req.timeoutMs && req.timeoutGraceMs ? req.timeoutMs + req.timeoutGraceMs : req.timeoutMs;
143
+ let timedOut = false;
144
+ try {
145
+ const pending = pRetry(attempt, {
146
+ retries: preflight ? 0 : 2,
147
+ minTimeout: 500,
148
+ factor: 2
149
+ });
150
+ message = await (hardTimeout ? pTimeout(pending, { milliseconds: hardTimeout }).catch((e) => {
151
+ if (e instanceof TimeoutError) {
152
+ timedOut = true;
153
+ controller.abort();
154
+ }
155
+ throw e;
156
+ }) : pending);
157
+ } catch (e) {
158
+ if (signal.aborted)
159
+ throw new EngineError({
160
+ kind: "aborted",
161
+ message: "anthropic-api run aborted"
162
+ });
163
+ if (timedOut)
164
+ throw new EngineError({
165
+ kind: "timeout",
166
+ message: "anthropic-api run timed out",
167
+ cause: e
168
+ });
169
+ if (e instanceof EngineError) throw e;
170
+ throw new EngineError({
171
+ kind: classifyEngineFailure(e),
172
+ message: e instanceof Error ? e.message : String(e),
173
+ cause: e
174
+ });
175
+ } finally {
176
+ signal.removeEventListener("abort", onAbort);
177
+ }
178
+ const text = message.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
179
+ const usage = reportedUsage({
180
+ inputTokens: message.usage.input_tokens,
181
+ outputTokens: message.usage.output_tokens
182
+ });
183
+ onEvent({ type: "usage", usage, model });
184
+ const late = typeof req.timeoutMs === "number" && Date.now() - startedAt > req.timeoutMs;
185
+ return assistantResult({
186
+ text,
187
+ usage,
188
+ requested: selection,
189
+ stopReason: message.stop_reason ?? void 0,
190
+ ...late ? {
191
+ transportFailure: {
192
+ kind: "timeout",
193
+ message: "anthropic-api result arrived after the soft timeout",
194
+ exitCode: null
195
+ }
196
+ } : {},
197
+ raw: message
198
+ });
199
+ }
200
+ };
201
+
202
+ export { AnthropicApiEngine };
203
+ //# sourceMappingURL=index.js.map
204
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/anthropic-api.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Engine plugin: the raw Anthropic Messages API (`@anthropic-ai/sdk`). Lowest\n * level, token-level streaming, and the cheapest path for validator models.\n * Ordinary calls retry transient 5xx and connection failures with backoff.\n * Preflight-purpose calls disable both retry layers. Provider limits are\n * returned to the caller without the adapter's retry loop.\n *\n * Needs `ANTHROPIC_API_KEY` (or `EngineOptions.apiKey`). Constructed lazily by\n * the registry, so other engines work without a key present.\n *\n * Ignores `AgentRequest.env`: no subprocess is spawned and the API call takes\n * no environment.\n */\n\nimport pRetry, { AbortError } from 'p-retry';\nimport pTimeout, { TimeoutError } from 'p-timeout';\nimport { isDeepStrictEqual } from 'node:util';\nimport {\n EngineError,\n assistantResult,\n classifyEngineFailure,\n engineSelection,\n reportedUsage,\n type AgentRequest,\n type AgentResult,\n type Engine,\n type EngineEventSink,\n type EngineSelectionRecord,\n} from '@obversa/api';\nimport { retryAfterHeaderToMs } from '@obversa/core/command';\n\nexport interface AnthropicApiEngineOptions {\n readonly defaultModel?: string;\n readonly apiKey?: string;\n}\n\n/**\n * Transient backend errors that warrant p-retry's blind backoff: 5xx (incl.\n * 529 overloaded) and connection/timeout. A 429 is not here: a rate limit is\n * classified to a typed `RATE_LIMIT` and handed to the loop's `onLimit` policy,\n * which waits the provider's actual reset rather than a generic backoff.\n */\nfunction isTransient(error: unknown): boolean {\n const status = (error as { status?: number })?.status;\n if (typeof status === 'number') return status >= 500;\n const name = (error as { name?: string })?.name ?? '';\n return /connection|timeout|socket|network/i.test(name);\n}\n\n/**\n * Best-effort `.get()` off the SDK error's `headers` (a web `Headers`). The SDK\n * attaches the response headers on `APIError`; read defensively in case a\n * version exposes them as a plain object instead.\n */\nfunction headerValue(error: unknown, name: string): string | undefined {\n const headers = (error as { headers?: unknown }).headers;\n if (headers && typeof (headers as Headers).get === 'function') {\n return (headers as Headers).get(name) ?? undefined;\n }\n if (headers && typeof headers === 'object') {\n const v = (headers as Record<string, unknown>)[name];\n if (typeof v === 'string') return v;\n }\n return undefined;\n}\n\n/**\n * Classify an Anthropic SDK error into a typed provider limit, or return\n * `undefined` to let the generic handling take over. The SDK throws an\n * `APIError` with `.status` (HTTP), `.headers` (web `Headers`), and `.type`\n * (the body `error.type`, e.g. `rate_limit_error` / `billing_error`).\n * - 429 → RATE_LIMIT, reading `retry-after` (seconds) into `retryAfterMs`.\n * - a billing/quota error → QUOTA with no reset (not auto-waitable).\n * 529 / overloaded is deliberately not a limit: it is a transient ENGINE error\n * and stays on p-retry's backoff path.\n */\nfunction classifyLimit(error: unknown): EngineError | undefined {\n const status = (error as { status?: number })?.status;\n const type = (error as { type?: string })?.type;\n const message = error instanceof Error ? error.message : String(error);\n\n if (status === 429 || type === 'rate_limit_error') {\n return new EngineError({\n kind: 'rate-limit',\n message: `anthropic-api rate limited: ${message}`,\n cause: error,\n retryAfterMs: retryAfterHeaderToMs(headerValue(error, 'retry-after')),\n });\n }\n if (type === 'billing_error') {\n return new EngineError({\n kind: 'quota',\n message: `anthropic-api usage/billing limit: ${message}`,\n cause: error,\n });\n }\n return undefined;\n}\n\n/** The slice of the Anthropic SDK this adapter actually consumes. */\ninterface FinalMessage {\n content: { type: string; text?: string }[];\n usage: { input_tokens: number; output_tokens: number };\n stop_reason: string | null;\n}\ninterface MessageStreamLike {\n on(event: 'text', cb: (delta: string) => void): void;\n finalMessage(): Promise<FinalMessage>;\n}\ninterface MessagesClientLike {\n messages: {\n stream(body: unknown, opts?: { signal?: AbortSignal; maxRetries?: number }): MessageStreamLike;\n };\n}\n\nfunction assertTextOnly(request: Omit<AgentRequest, 'prompt'>): void {\n if (request.workspaceMode === 'read' || request.workspaceMode === 'write'\n || (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.length > 0))\n || (request.allowedTools !== undefined\n && (!Array.isArray(request.allowedTools) || request.allowedTools.length > 0))) {\n throw new EngineError({\n kind: 'invalid-config',\n message: 'anthropic-api supports text-only requests without tools or workspace access',\n });\n }\n}\n\nexport class AnthropicApiEngine implements Engine {\n readonly name = 'anthropic-api';\n private clientPromise?: Promise<MessagesClientLike>;\n\n constructor(private readonly opts: AnthropicApiEngineOptions = {}) {}\n\n private apiKey(): string {\n const apiKey = this.opts.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n throw new EngineError({\n kind: 'invalid-config',\n message:\n 'the anthropic-api engine needs an API key — set ANTHROPIC_API_KEY or pass --api-key (or use the agent-sdk / claude-cli engine, which use host Claude auth)',\n });\n }\n return apiKey;\n }\n\n private selection(request: Pick<AgentRequest, 'model'>): EngineSelectionRecord {\n return engineSelection({\n adapter: 'anthropic-api',\n provider: 'anthropic',\n model: request.model ?? this.opts.defaultModel ?? 'claude-haiku-4-5-20251001',\n executable: null,\n capabilities: [],\n });\n }\n\n async admit(\n request: Omit<AgentRequest, 'prompt'>,\n signal: AbortSignal,\n expectedSelection?: EngineSelectionRecord,\n ): Promise<EngineSelectionRecord> {\n if (signal.aborted) {\n throw new EngineError({ kind: 'aborted', message: 'anthropic-api admission aborted' });\n }\n assertTextOnly(request);\n this.apiKey();\n let selected: EngineSelectionRecord;\n try {\n selected = this.selection(request);\n } catch (cause) {\n throw new EngineError({ kind: 'invalid-config', message: 'anthropic-api admission selection is invalid', cause });\n }\n if (expectedSelection !== undefined && !isDeepStrictEqual(selected, expectedSelection)) {\n throw new EngineError({ kind: 'invalid-config', message: 'anthropic-api admission does not match the saved selection' });\n }\n return selected;\n }\n\n private async client(): Promise<MessagesClientLike> {\n if (!this.clientPromise) {\n const apiKey = this.apiKey();\n this.clientPromise = import('@anthropic-ai/sdk').then(\n // One cast at the boundary to the structural shape we consume.\n (m) => new m.default({ apiKey }) as unknown as MessagesClientLike,\n );\n }\n return this.clientPromise;\n }\n\n async run(\n req: AgentRequest,\n onEvent: EngineEventSink,\n signal: AbortSignal,\n ): Promise<AgentResult> {\n assertTextOnly(req);\n const preflight = req.purpose === 'preflight';\n const client = await this.client();\n const selection = this.selection(req);\n const model = selection.model!;\n const maxTokens = req.maxTokens ?? 1024;\n const startedAt = Date.now();\n const controller = new AbortController();\n const onAbort = () => controller.abort();\n if (signal.aborted) controller.abort();\n else signal.addEventListener('abort', onAbort, { once: true });\n\n const attempt = async (): Promise<FinalMessage> => {\n try {\n const stream = client.messages.stream(\n {\n model,\n max_tokens: maxTokens,\n system: req.system,\n messages: [{ role: 'user', content: req.prompt }],\n },\n { signal: controller.signal, ...(preflight ? { maxRetries: 0 } : {}) },\n );\n stream.on('text', (delta) => onEvent({ type: 'text', delta }));\n return await stream.finalMessage();\n } catch (e) {\n // A provider limit is not for p-retry's blind backoff: stop retrying\n // and let the loop's onLimit policy wait the actual reset. Wrap the\n // typed engine error as the AbortError cause so it survives to the catch.\n const limit = classifyLimit(e);\n if (limit) throw new AbortError(limit);\n if (signal.aborted || !isTransient(e)) {\n throw new AbortError(e instanceof Error ? e : new Error(String(e)));\n }\n throw e;\n }\n };\n\n let message;\n const hardTimeout =\n req.timeoutMs && req.timeoutGraceMs\n ? req.timeoutMs + req.timeoutGraceMs\n : req.timeoutMs;\n let timedOut = false;\n try {\n const pending = pRetry(attempt, {\n retries: preflight ? 0 : 2,\n minTimeout: 500,\n factor: 2,\n });\n message = await (hardTimeout\n ? pTimeout(pending, { milliseconds: hardTimeout }).catch((e) => {\n if (e instanceof TimeoutError) {\n timedOut = true;\n controller.abort();\n }\n throw e;\n })\n : pending);\n } catch (e) {\n if (signal.aborted)\n throw new EngineError({\n kind: 'aborted',\n message: 'anthropic-api run aborted',\n });\n if (timedOut)\n throw new EngineError({\n kind: 'timeout',\n message: 'anthropic-api run timed out',\n cause: e,\n });\n // p-retry unwraps AbortError to its cause; surface a typed limit as-is.\n if (e instanceof EngineError) throw e;\n throw new EngineError({\n kind: classifyEngineFailure(e),\n message: e instanceof Error ? e.message : String(e),\n cause: e,\n });\n } finally {\n signal.removeEventListener('abort', onAbort);\n }\n\n const text = message.content\n .filter((b) => b.type === 'text')\n .map((b) => b.text ?? '')\n .join('');\n const usage = reportedUsage({\n inputTokens: message.usage.input_tokens,\n outputTokens: message.usage.output_tokens,\n });\n onEvent({ type: 'usage', usage, model });\n const late =\n typeof req.timeoutMs === 'number' && Date.now() - startedAt > req.timeoutMs;\n return assistantResult({\n text,\n usage,\n requested: selection,\n stopReason: message.stop_reason ?? undefined,\n ...(late\n ? {\n transportFailure: {\n kind: 'timeout' as const,\n message: 'anthropic-api result arrived after the soft timeout',\n exitCode: null,\n },\n }\n : {}),\n raw: message,\n });\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@obversa/engine-anthropic-api",
3
+ "version": "0.1.0",
4
+ "description": "Run engine requests through the Anthropic Messages API.",
5
+ "author": "Jonny Neill <jonnyneill@hotmail.com>",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "sideEffects": false,
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.12.0"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "dependencies": {
29
+ "@anthropic-ai/sdk": "0.105.0",
30
+ "p-retry": "^8.0.0",
31
+ "p-timeout": "^7.0.1",
32
+ "@obversa/api": "^0.1.0",
33
+ "@obversa/core": "^0.1.0"
34
+ },
35
+ "keywords": [
36
+ "agent",
37
+ "graph",
38
+ "workflow",
39
+ "runtime",
40
+ "obversa"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/jonny981/obversa.git"
45
+ },
46
+ "homepage": "https://docs.obversa.ai",
47
+ "bugs": {
48
+ "url": "https://github.com/jonny981/obversa/issues"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup && tsc -p tsconfig.build.json",
52
+ "test": "vitest run",
53
+ "typecheck": "tsc --noEmit -p tsconfig.json",
54
+ "typecheck:ts6": "tsc6 --noEmit -p tsconfig.json"
55
+ }
56
+ }