@uncoil.dev/sdk 0.0.0-stage → 0.6.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.
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uncoilMiddleware = uncoilMiddleware;
4
+ const observation_1 = require("./observation");
5
+ function usageOf(usage = {}) {
6
+ return {
7
+ input_tokens: usage.inputTokens ?? usage.promptTokens ?? 0,
8
+ output_tokens: usage.outputTokens ?? usage.completionTokens ?? 0,
9
+ };
10
+ }
11
+ function textOf(result) {
12
+ if (typeof result.text === "string")
13
+ return result.text;
14
+ if (Array.isArray(result.content))
15
+ return result.content.filter((part) => part?.type === "text").map((part) => part.text ?? "").join("");
16
+ return "";
17
+ }
18
+ /**
19
+ * One-line tracing for the Vercel AI SDK. Pass it to `wrapLanguageModel`:
20
+ *
21
+ * const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: uncoilMiddleware() });
22
+ *
23
+ * Every `generateText`, `streamText` or `generateObject` call made with that model becomes an Uncoil step.
24
+ */
25
+ function uncoilMiddleware(options = {}) {
26
+ const start = (model, params) => (0, observation_1.observation)(model?.provider?.split?.(".")[0] ?? "vercel-ai", model?.modelId ?? "unknown", params, options);
27
+ return {
28
+ async wrapGenerate({ doGenerate, params, model }) {
29
+ const obs = start(model, params);
30
+ try {
31
+ const result = await doGenerate();
32
+ if (result.usage)
33
+ obs.usage(usageOf(result.usage));
34
+ obs.data.metadata = { ...obs.data.metadata, finishReason: result.finishReason };
35
+ obs.finish(textOf(result));
36
+ return result;
37
+ }
38
+ catch (error) {
39
+ obs.finish(undefined, error);
40
+ throw error;
41
+ }
42
+ },
43
+ async wrapStream({ doStream, params, model }) {
44
+ const obs = start(model, params);
45
+ let result;
46
+ try {
47
+ result = await doStream();
48
+ }
49
+ catch (error) {
50
+ obs.finish(undefined, error);
51
+ throw error;
52
+ }
53
+ let text = "";
54
+ const watcher = new TransformStream({
55
+ transform(part, controller) {
56
+ if (part?.type === "text-delta") {
57
+ obs.token();
58
+ text = (text + (part.delta ?? part.textDelta ?? "")).slice(0, 10000);
59
+ }
60
+ else if (part?.type === "finish" && part.usage)
61
+ obs.usage(usageOf(part.usage));
62
+ else if (part?.type === "error")
63
+ obs.finish(text, part.error ?? new Error("Stream failed"));
64
+ controller.enqueue(part);
65
+ },
66
+ flush() { obs.finish(text); },
67
+ cancel() { obs.finish(text, undefined, true); },
68
+ });
69
+ return { ...result, stream: result.stream.pipeThrough(watcher) };
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,37 @@
1
+ import { type KeyObject } from "crypto";
2
+ import { type UncoilRegion } from "./index";
3
+ export interface LoginOptions {
4
+ region?: UncoilRegion;
5
+ host?: string;
6
+ /** Create a project with this name instead of picking one in the browser. */
7
+ newProject?: string;
8
+ /** Do not try to open a browser; only print the address and code. */
9
+ browser?: boolean;
10
+ envFile?: string;
11
+ cwd?: string;
12
+ /** Print machine-readable lines instead of text. */
13
+ json?: boolean;
14
+ /** Test hooks. */
15
+ out?: (line: string) => void;
16
+ sleep?: (ms: number) => Promise<void>;
17
+ open?: (url: string) => void;
18
+ now?: () => number;
19
+ }
20
+ export interface LoginResult {
21
+ ok: boolean;
22
+ message: string;
23
+ projectId?: string;
24
+ projectName?: string;
25
+ envFile?: string;
26
+ }
27
+ /** Sets or replaces variables in an env file's text and keeps every other line as it was. */
28
+ export declare function upsertEnv(content: string, vars: Record<string, string | undefined>): string;
29
+ /** True when .gitignore text already keeps this file out of git. */
30
+ export declare function gitignoreCovers(content: string, fileName: string): boolean;
31
+ /** Reads UNCOIL_* variables from env files into process.env without overriding values already set. Used by the CLI only. */
32
+ export declare function loadUncoilEnv(cwd: string, files?: string[]): void;
33
+ /** Adds the env file to .gitignore when this is a git repo that would otherwise commit the key. Returns true if it changed the file. */
34
+ export declare function ensureGitignored(dir: string, envFile: string): boolean;
35
+ export declare function decryptKey(privateKey: KeyObject, encrypted: string): string;
36
+ /** Browser sign-in for the terminal. Never prints or logs the key; it is written only to the env file. */
37
+ export declare function runLogin(options?: LoginOptions): Promise<LoginResult>;
package/dist/login.js ADDED
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.upsertEnv = upsertEnv;
37
+ exports.gitignoreCovers = gitignoreCovers;
38
+ exports.loadUncoilEnv = loadUncoilEnv;
39
+ exports.ensureGitignored = ensureGitignored;
40
+ exports.decryptKey = decryptKey;
41
+ exports.runLogin = runLogin;
42
+ const crypto_1 = require("crypto");
43
+ const child_process_1 = require("child_process");
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const index_1 = require("./index");
47
+ const KEY_NAME = "UNCOIL_API_KEY";
48
+ const PROJECT_ID_NAME = "UNCOIL_PROJECT_ID";
49
+ const REGION_NAME = "UNCOIL_REGION";
50
+ /** Sets or replaces variables in an env file's text and keeps every other line as it was. */
51
+ function upsertEnv(content, vars) {
52
+ const lines = content === "" ? [] : content.replace(/\r\n/g, "\n").split("\n");
53
+ if (lines.length && lines[lines.length - 1] === "")
54
+ lines.pop();
55
+ for (const [name, value] of Object.entries(vars)) {
56
+ const pattern = new RegExp(`^\\s*(export\\s+)?${name}\\s*=`);
57
+ const index = lines.findIndex((line) => pattern.test(line));
58
+ if (value === undefined) {
59
+ if (index >= 0)
60
+ lines.splice(index, 1);
61
+ continue;
62
+ }
63
+ const next = `${name}=${value}`;
64
+ if (index >= 0)
65
+ lines[index] = next;
66
+ else
67
+ lines.push(next);
68
+ }
69
+ return `${lines.join("\n")}\n`;
70
+ }
71
+ /** True when .gitignore text already keeps this file out of git. */
72
+ function gitignoreCovers(content, fileName) {
73
+ const base = path.basename(fileName);
74
+ const broad = base.startsWith(".env") ? [".env*", ".env.*", ".env*.local", ".env.*.local", "*.local"] : [];
75
+ return content.split(/\r?\n/).map((line) => line.trim().replace(/^\//, "")).some((line) => Boolean(line) && !line.startsWith("#") && !line.startsWith("!") && (line === base || broad.includes(line)));
76
+ }
77
+ function insideGitRepo(dir) {
78
+ for (let current = path.resolve(dir);; current = path.dirname(current)) {
79
+ if (fs.existsSync(path.join(current, ".git")))
80
+ return true;
81
+ if (path.dirname(current) === current)
82
+ return false;
83
+ }
84
+ }
85
+ /** Reads UNCOIL_* variables from env files into process.env without overriding values already set. Used by the CLI only. */
86
+ function loadUncoilEnv(cwd, files = [".env.local", ".env"]) {
87
+ for (const file of files) {
88
+ let text;
89
+ try {
90
+ text = fs.readFileSync(path.resolve(cwd, file), "utf8");
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ for (const line of text.split(/\r?\n/)) {
96
+ const match = /^\s*(?:export\s+)?(UNCOIL_[A-Z0-9_]+)\s*=\s*(.*?)\s*$/.exec(line);
97
+ if (!match || process.env[match[1]] !== undefined)
98
+ continue;
99
+ process.env[match[1]] = match[2].replace(/^(["'])(.*)\1$/, "$2");
100
+ }
101
+ }
102
+ }
103
+ /** Adds the env file to .gitignore when this is a git repo that would otherwise commit the key. Returns true if it changed the file. */
104
+ function ensureGitignored(dir, envFile) {
105
+ if (!insideGitRepo(dir))
106
+ return false;
107
+ const target = path.join(dir, ".gitignore");
108
+ const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
109
+ if (gitignoreCovers(existing, envFile))
110
+ return false;
111
+ const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
112
+ fs.writeFileSync(target, `${existing}${prefix}${path.basename(envFile)}\n`);
113
+ return true;
114
+ }
115
+ /** Opens the address in the person's browser. Failure is fine: the address is always printed too. */
116
+ function openBrowser(url) {
117
+ try {
118
+ const command = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
119
+ const child = (0, child_process_1.spawn)(command[0], [...command[1]], { stdio: "ignore", detached: true });
120
+ child.on("error", () => undefined);
121
+ child.unref();
122
+ }
123
+ catch { /* the printed address still works */ }
124
+ }
125
+ function decryptKey(privateKey, encrypted) {
126
+ return (0, crypto_1.privateDecrypt)({ key: privateKey, padding: crypto_1.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, Buffer.from(encrypted, "base64")).toString("utf8");
127
+ }
128
+ function loginHost(options) {
129
+ const envRegion = (process.env.UNCOIL_REGION ?? process.env.TRACIFY_REGION)?.trim().toLowerCase();
130
+ const region = options.region ?? (envRegion === "us" || envRegion === "eu" ? envRegion : "eu");
131
+ return { region, host: (options.host || (process.env.UNCOIL_HOST ?? process.env.TRACIFY_HOST) || index_1.UNCOIL_REGION_HOSTS[region]).replace(/\/$/, "") };
132
+ }
133
+ async function post(url, body) {
134
+ const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) });
135
+ let data = null;
136
+ try {
137
+ data = await response.json();
138
+ }
139
+ catch { /* not JSON */ }
140
+ return { status: response.status, data };
141
+ }
142
+ /** Browser sign-in for the terminal. Never prints or logs the key; it is written only to the env file. */
143
+ async function runLogin(options = {}) {
144
+ const out = options.out ?? ((line) => console.log(line));
145
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
146
+ const now = options.now ?? Date.now;
147
+ const cwd = options.cwd ?? process.cwd();
148
+ const envFile = options.envFile ?? ".env.local";
149
+ const { region, host } = loginHost(options);
150
+ const say = (event, text, extra = {}) => out(options.json ? JSON.stringify({ event, ...extra, message: text }) : text);
151
+ const nodeMajor = Number(process.versions?.node?.split(".")[0]);
152
+ if (typeof fetch !== "function" || nodeMajor < 18)
153
+ return { ok: false, message: "Uncoil login needs Node 18 or newer." };
154
+ const { publicKey, privateKey } = (0, crypto_1.generateKeyPairSync)("rsa", { modulusLength: 2048 });
155
+ const spki = publicKey.export({ type: "spki", format: "der" }).toString("base64");
156
+ let started;
157
+ try {
158
+ started = await post(`${host}/api/cli/login/start`, { publicKey: spki, newProjectName: options.newProject });
159
+ }
160
+ catch (error) {
161
+ return { ok: false, message: `Could not reach ${host} (${error?.message ?? error}). Check your internet connection.` };
162
+ }
163
+ if (started.status === 429)
164
+ return { ok: false, message: started.data?.error || "Too many sign-in attempts. Try again in a few minutes." };
165
+ if (started.status !== 200 || !started.data?.deviceCode)
166
+ return { ok: false, message: started.data?.error || `Uncoil could not start the sign-in (${started.status}).` };
167
+ const { deviceCode, userCode, verificationUrl, expiresAt } = started.data;
168
+ const interval = Math.max(2, Number(started.data.interval) || 3) * 1000;
169
+ say("waiting", `Open this page to sign in: ${verificationUrl}\nCode: ${userCode}\nWaiting for you to confirm in the browser…`, { url: verificationUrl, code: userCode, expiresAt });
170
+ if (options.browser !== false)
171
+ (options.open ?? openBrowser)(verificationUrl);
172
+ let waitMs = interval;
173
+ while (now() < expiresAt + 120000) {
174
+ await sleep(waitMs);
175
+ let polled;
176
+ try {
177
+ polled = await post(`${host}/api/cli/login/poll`, { deviceCode });
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ if (polled.status === 429) {
183
+ waitMs = Math.min(waitMs * 2, 15000);
184
+ continue;
185
+ }
186
+ waitMs = interval;
187
+ const status = polled.data?.status;
188
+ if (status === "pending")
189
+ continue;
190
+ if (status === "denied")
191
+ return { ok: false, message: "Sign-in was cancelled in the browser." };
192
+ if (status === "expired" || polled.status >= 400)
193
+ return { ok: false, message: "The sign-in code expired. Run the command again." };
194
+ if (status === "approved") {
195
+ let apiKey;
196
+ try {
197
+ apiKey = decryptKey(privateKey, polled.data.encryptedKey);
198
+ }
199
+ catch {
200
+ return { ok: false, message: "Could not read the key Uncoil sent. Run the command again." };
201
+ }
202
+ const target = path.resolve(cwd, envFile);
203
+ const before = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
204
+ fs.mkdirSync(path.dirname(target), { recursive: true });
205
+ fs.writeFileSync(target, upsertEnv(before, { [KEY_NAME]: apiKey, [PROJECT_ID_NAME]: String(polled.data.projectId), [REGION_NAME]: region === "us" ? "us" : undefined }), { mode: 0o600 });
206
+ const ignored = ensureGitignored(path.dirname(target), envFile);
207
+ const projectName = String(polled.data.projectName ?? "");
208
+ const message = `Connected to project "${projectName}" (${region.toUpperCase()}). Key saved to ${envFile}.${ignored ? ` Added ${path.basename(envFile)} to .gitignore.` : ""}`;
209
+ say("connected", message, { projectId: String(polled.data.projectId), projectName, envFile });
210
+ return { ok: true, message, projectId: String(polled.data.projectId), projectName, envFile };
211
+ }
212
+ }
213
+ return { ok: false, message: "Timed out waiting for the browser. Run the command again." };
214
+ }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@uncoil.dev/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "description": "Observability, runtime enforcement, and tracing for AI agents.",
5
+ "homepage": "https://www.uncoil.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Uncoil-dev/Uncoil.git",
9
+ "directory": "packages/ts-sdk"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "bin": {
14
+ "uncoil": "dist/cli.js",
15
+ "uncoil-doctor": "dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "vitest run",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/Uncoil-dev/Uncoil/issues"
27
+ },
28
+ "keywords": [
29
+ "ai",
30
+ "agents",
31
+ "observability",
32
+ "tracing",
33
+ "llm",
34
+ "llm-observability",
35
+ "agent-tracing",
36
+ "opentelemetry",
37
+ "cost-tracking",
38
+ "langchain",
39
+ "openai",
40
+ "anthropic"
41
+ ],
42
+ "author": "Uncoil Team",
43
+ "license": "MIT",
44
+ "sideEffects": false,
45
+ "devDependencies": {
46
+ "@anthropic-ai/sdk": "^0.134.0",
47
+ "@langchain/core": "^1.2.17",
48
+ "@langchain/langgraph": "^1.5.0",
49
+ "@types/node": "^20.0.0",
50
+ "openai": "^7.34.0",
51
+ "typescript": "^5.0.0",
52
+ "vitest": "^4.1.10"
53
+ }
54
+ }