@devosurf/tesser-connectors 0.1.0-alpha.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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@devosurf/tesser-connectors",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Tesser connector library — typed integrations consumed as ctx.connections.<name>.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./catalog": "./catalog/index.ts",
10
+ "./*": "./*/index.ts"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "dependencies": {
16
+ "zod": "^4.4.3",
17
+ "@devosurf/tesser-sdk": "0.1.0-alpha.0"
18
+ },
19
+ "devDependencies": {
20
+ "@devosurf/tesser-testing": "^0.1.0-alpha.0"
21
+ },
22
+ "files": [
23
+ "index.ts",
24
+ "catalog/index.ts",
25
+ "providers/*.ts",
26
+ "*/index.ts",
27
+ "manifest.json",
28
+ "README.md",
29
+ "LICENSE"
30
+ ]
31
+ }
package/pi/index.ts ADDED
@@ -0,0 +1,230 @@
1
+ // Pi Harness adapter (ADR-0019): one-shot, non-interactive, broker-authed, and run
2
+ // with isolated Pi config/session dirs so host Pi login state is never used.
3
+
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { spawn } from "node:child_process";
8
+ import { apiKey, customAuth, defineConnector } from "@devosurf/tesser-sdk/connector";
9
+ import type { HarnessDef, HarnessRunRequest, HarnessRunResult, Serializable } from "@devosurf/tesser-sdk";
10
+ import { decodeJournal, encodeJournal, parseDuration } from "@devosurf/tesser-sdk/internal";
11
+
12
+ export default defineConnector<Record<string, never>>({
13
+ id: "pi",
14
+ describe: "Pi one-shot Harness runner",
15
+ auth: {
16
+ anthropicApiKey: apiKey({ name: "ANTHROPIC_API_KEY", describe: "Anthropic API key for Pi" }),
17
+ anthropicOAuth: customAuth({
18
+ describe: "Anthropic OAuth token for Pi",
19
+ fields: ["oauth_token"],
20
+ sign: () => {},
21
+ }),
22
+ },
23
+ actions: {},
24
+ harnessProvider: {
25
+ adapter: "pi-print-json",
26
+ run: async (ctx, request, def) => runPi(ctx.auth.mode ?? "anthropicApiKey", ctx.auth.fields, request, def),
27
+ },
28
+ });
29
+
30
+ async function runPi(
31
+ authMode: string,
32
+ fields: Readonly<Record<string, string>>,
33
+ request: HarnessRunRequest<unknown>,
34
+ def: HarnessDef,
35
+ ): Promise<HarnessRunResult<unknown>> {
36
+ const timeoutMs = parseDuration(request.timeout ?? def.timeout ?? "10m", "pi harness timeout");
37
+ const maxOutputBytes = request.maxOutputBytes ?? def.maxOutputBytes ?? 2_000_000;
38
+ const home = await mkdtemp(join(tmpdir(), "tesser-pi-home-"));
39
+ const agentDir = await mkdtemp(join(tmpdir(), "tesser-pi-agent-"));
40
+ const sessionDir = await mkdtemp(join(tmpdir(), "tesser-pi-sessions-"));
41
+ const cwd = await mkdtemp(join(tmpdir(), "tesser-pi-cwd-"));
42
+ try {
43
+ await writeFile(join(cwd, "input.json"), JSON.stringify(request.input ?? {}, null, 2));
44
+ const model = process.env["TESSER_PI_MODEL"] ?? "anthropic/haiku";
45
+ const args = [
46
+ "--print",
47
+ "--mode",
48
+ "json",
49
+ "--model",
50
+ model,
51
+ "--no-session",
52
+ "--no-extensions",
53
+ "--no-skills",
54
+ "--no-prompt-templates",
55
+ "--no-themes",
56
+ "--no-context-files",
57
+ ...(def.permissions === "read-only" ? ["--tools", "read,grep,find,ls"] : []),
58
+ request.prompt + "\n\nReturn JSON only matching the requested schema. Input JSON is in ./input.json and also below:\n" + JSON.stringify(request.input ?? {}),
59
+ ];
60
+ const token = authMode === "anthropicOAuth" ? fields["oauth_token"] : fields["api_key"];
61
+ if (!token) throw new Error(`pi ${authMode} credential is empty`);
62
+ const proc = await runProcess(process.env["TESSER_PI_BIN"] ?? "pi", args, {
63
+ cwd,
64
+ timeoutMs,
65
+ maxOutputBytes,
66
+ env: {
67
+ PATH: process.env["PATH"] ?? "/usr/bin:/bin:/opt/homebrew/bin:/usr/local/bin",
68
+ HOME: home,
69
+ PI_CODING_AGENT_DIR: agentDir,
70
+ PI_CODING_AGENT_SESSION_DIR: sessionDir,
71
+ PI_OFFLINE: "1",
72
+ NO_COLOR: "1",
73
+ ...(authMode === "anthropicOAuth" ? { ANTHROPIC_OAUTH_TOKEN: token } : { ANTHROPIC_API_KEY: token }),
74
+ },
75
+ });
76
+ const output = parsePiOutput(proc.stdout);
77
+ return {
78
+ output: toSerializable(output),
79
+ status: proc.exitCode === 0 ? "completed" : "failed",
80
+ exitCode: proc.exitCode,
81
+ artifacts: [
82
+ { name: "stdout.json", kind: "transcript", bytes: Buffer.byteLength(proc.stdout), content: clip(proc.stdout, 120_000) },
83
+ ...(proc.stderr.length > 0
84
+ ? [{ name: "stderr.log", kind: "log" as const, bytes: Buffer.byteLength(proc.stderr), content: clip(proc.stderr, 80_000) }]
85
+ : []),
86
+ ],
87
+ transcript: clip(proc.stdout, 120_000),
88
+ adapter: "pi-print-json",
89
+ raw: toSerializable({ exitCode: proc.exitCode }),
90
+ };
91
+ } finally {
92
+ await rm(home, { recursive: true, force: true });
93
+ await rm(agentDir, { recursive: true, force: true });
94
+ await rm(sessionDir, { recursive: true, force: true });
95
+ await rm(cwd, { recursive: true, force: true });
96
+ }
97
+ }
98
+
99
+ function runProcess(
100
+ command: string,
101
+ args: string[],
102
+ opts: { cwd: string; env: Record<string, string>; timeoutMs: number; maxOutputBytes: number },
103
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
104
+ return new Promise((resolve, reject) => {
105
+ const child = spawn(command, args, { cwd: opts.cwd, env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
106
+ const chunks: Buffer[] = [];
107
+ const errChunks: Buffer[] = [];
108
+ let size = 0;
109
+ let settled = false;
110
+ const fail = (err: Error) => {
111
+ if (settled) return;
112
+ settled = true;
113
+ child.kill("SIGTERM");
114
+ reject(err);
115
+ };
116
+ const timer = setTimeout(() => fail(new Error(`pi harness timed out after ${opts.timeoutMs}ms`)), opts.timeoutMs);
117
+ timer.unref?.();
118
+ const collect = (buf: Buffer, target: Buffer[]) => {
119
+ size += buf.length;
120
+ if (size > opts.maxOutputBytes) {
121
+ fail(new Error(`pi harness exceeded maxOutputBytes (${opts.maxOutputBytes})`));
122
+ return;
123
+ }
124
+ target.push(buf);
125
+ };
126
+ child.stdout.on("data", (b: Buffer) => collect(b, chunks));
127
+ child.stderr.on("data", (b: Buffer) => collect(b, errChunks));
128
+ child.on("error", (err) => {
129
+ if (settled) return;
130
+ settled = true;
131
+ clearTimeout(timer);
132
+ reject(err);
133
+ });
134
+ child.on("close", (code) => {
135
+ if (settled) return;
136
+ settled = true;
137
+ clearTimeout(timer);
138
+ resolve({ exitCode: code ?? 1, stdout: Buffer.concat(chunks).toString("utf8"), stderr: Buffer.concat(errChunks).toString("utf8") });
139
+ });
140
+ });
141
+ }
142
+
143
+ function parsePiOutput(stdout: string): unknown {
144
+ const text = stdout.trim();
145
+ if (!text) return {};
146
+ const parsed = tryJson(text);
147
+ if (parsed !== undefined) return normalizeOutputValue(unwrapKnownOutput(parsed));
148
+ const streamed = parsePiJsonLines(text);
149
+ if (streamed !== undefined) return streamed;
150
+ return parseJsonishText(text) ?? { text };
151
+ }
152
+
153
+ function parsePiJsonLines(text: string): unknown | undefined {
154
+ const events = text
155
+ .split(/\r?\n/)
156
+ .map((l) => tryJson(l.trim()))
157
+ .filter((v): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v));
158
+ for (let i = events.length - 1; i >= 0; i -= 1) {
159
+ const event = events[i]!;
160
+ const direct = unwrapKnownOutput(event);
161
+ if (direct !== event) return normalizeOutputValue(direct);
162
+ if (event["type"] === "agent_end" && Array.isArray(event["messages"])) {
163
+ const textValue = lastAssistantText(event["messages"]);
164
+ if (textValue !== undefined) return normalizeOutputValue(textValue);
165
+ }
166
+ if (typeof event["message"] === "object" && event["message"] !== null) {
167
+ const textValue = messageText(event["message"] as Record<string, unknown>);
168
+ if (textValue !== undefined) return normalizeOutputValue(textValue);
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ function lastAssistantText(messages: unknown[]): string | undefined {
175
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
176
+ const msg = messages[i];
177
+ if (typeof msg !== "object" || msg === null || Array.isArray(msg)) continue;
178
+ const obj = msg as Record<string, unknown>;
179
+ if (obj["role"] !== "assistant") continue;
180
+ const text = messageText(obj);
181
+ if (text !== undefined) return text;
182
+ }
183
+ return undefined;
184
+ }
185
+
186
+ function messageText(message: Record<string, unknown>): string | undefined {
187
+ const content = message["content"];
188
+ if (typeof content === "string") return content;
189
+ if (!Array.isArray(content)) return undefined;
190
+ const texts = content
191
+ .filter((part): part is Record<string, unknown> => typeof part === "object" && part !== null && !Array.isArray(part))
192
+ .filter((part) => part["type"] === "text" && typeof part["text"] === "string")
193
+ .map((part) => part["text"] as string);
194
+ return texts.at(-1);
195
+ }
196
+
197
+ function normalizeOutputValue(value: unknown): unknown {
198
+ if (typeof value === "string") return parseJsonishText(value) ?? { text: value };
199
+ return value;
200
+ }
201
+
202
+ function unwrapKnownOutput(value: unknown): unknown {
203
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
204
+ const obj = value as Record<string, unknown>;
205
+ for (const key of ["structured_output", "output", "result", "message", "text", "content"]) {
206
+ if (obj[key] !== undefined) return obj[key];
207
+ }
208
+ return value;
209
+ }
210
+
211
+ function parseJsonishText(text: string): unknown | undefined {
212
+ const trimmed = text.trim();
213
+ return tryJson(trimmed) ?? tryJson(trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
214
+ }
215
+
216
+ function tryJson(text: string): unknown | undefined {
217
+ try {
218
+ return JSON.parse(text);
219
+ } catch {
220
+ return undefined;
221
+ }
222
+ }
223
+
224
+ function toSerializable(value: unknown): Serializable {
225
+ return decodeJournal(encodeJournal(value)) as Serializable;
226
+ }
227
+
228
+ function clip(text: string, max: number): string {
229
+ return text.length <= max ? text : text.slice(0, max - 3) + "...";
230
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
2
+
3
+ export const anthropicProvider: ProviderFacts = {
4
+ id: "anthropic",
5
+ displayName: "Anthropic",
6
+ baseUrl: "https://api.anthropic.com",
7
+ docsUrl: "https://docs.anthropic.com",
8
+ };
@@ -0,0 +1,20 @@
1
+ // Provider facts: public OAuth/API knowledge we own (ADR-0004/0012). Declared here,
2
+ // referenced by connectors, assembled into the Catalog by codegen — never hand-edit
3
+ // the generated catalog.
4
+
5
+ import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
6
+
7
+ export const githubProvider: ProviderFacts = {
8
+ id: "github",
9
+ displayName: "GitHub",
10
+ baseUrl: "https://api.github.com",
11
+ docsUrl: "https://docs.github.com/rest",
12
+ oauth2: {
13
+ authorizeUrl: "https://github.com/login/oauth/authorize",
14
+ tokenUrl: "https://github.com/login/oauth/access_token",
15
+ clientAuth: "body",
16
+ scopeSeparator: " ",
17
+ // GitHub OAuth apps accept but do not enforce PKCE; sending it is harmless.
18
+ pkce: true,
19
+ },
20
+ };
@@ -0,0 +1,18 @@
1
+ import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
2
+
3
+ // One Provider backs several Connectors (gmail, google-calendar) — the unit an OAuth
4
+ // app is registered against (CONTEXT.md "Provider").
5
+ export const googleProvider: ProviderFacts = {
6
+ id: "google",
7
+ displayName: "Google",
8
+ docsUrl: "https://developers.google.com/identity/protocols/oauth2",
9
+ oauth2: {
10
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
11
+ tokenUrl: "https://oauth2.googleapis.com/token",
12
+ clientAuth: "body",
13
+ scopeSeparator: " ",
14
+ pkce: true,
15
+ // Without these, Google never issues a refresh token (the classic quirk).
16
+ extraAuthorizeParams: { access_type: "offline", prompt: "consent" },
17
+ },
18
+ };
@@ -0,0 +1,8 @@
1
+ import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
2
+
3
+ export const resendProvider: ProviderFacts = {
4
+ id: "resend",
5
+ displayName: "Resend",
6
+ baseUrl: "https://api.resend.com",
7
+ docsUrl: "https://resend.com/docs",
8
+ };
@@ -0,0 +1,16 @@
1
+ import type { ProviderFacts } from "@devosurf/tesser-sdk/connector";
2
+
3
+ export const slackProvider: ProviderFacts = {
4
+ id: "slack",
5
+ displayName: "Slack",
6
+ baseUrl: "https://slack.com/api",
7
+ docsUrl: "https://docs.slack.dev",
8
+ oauth2: {
9
+ authorizeUrl: "https://slack.com/oauth/v2/authorize",
10
+ tokenUrl: "https://slack.com/api/oauth.v2.access",
11
+ clientAuth: "body",
12
+ // Slack separates scopes with commas, not spaces.
13
+ scopeSeparator: ",",
14
+ pkce: false,
15
+ },
16
+ };
@@ -0,0 +1,51 @@
1
+ // Resend connector — the P0 email path, and the exemplar of declared provider
2
+ // idempotency: emails.send is a WRITE that derives retry-safety from the
3
+ // Idempotency-Key header (ADR-0012).
4
+
5
+ import { z } from "zod";
6
+ import { action, apiKey, defineConnector } from "@devosurf/tesser-sdk/connector";
7
+ import { resendProvider } from "../providers/resend.js";
8
+
9
+ export default defineConnector({
10
+ id: "resend",
11
+ describe: "Transactional email via Resend",
12
+ provider: resendProvider,
13
+ auth: apiKey({ prefix: "Bearer ", describe: "Resend API key (re_…)" }),
14
+ idempotencyHeader: "Idempotency-Key",
15
+ samples: {
16
+ "emails.send": { id: "9f3c1a2b-0000-4000-8000-000000000000" },
17
+ },
18
+ actions: {
19
+ emails: {
20
+ send: action({
21
+ describe: "Send an email",
22
+ input: z.object({
23
+ from: z.string().min(3),
24
+ to: z.union([z.string(), z.array(z.string()).min(1)]),
25
+ subject: z.string().min(1),
26
+ html: z.string().optional(),
27
+ text: z.string().optional(),
28
+ replyTo: z.string().optional(),
29
+ }),
30
+ output: z.object({ id: z.string() }),
31
+ run: async (ctx, i) => {
32
+ const res = (await ctx.http.post(
33
+ "/emails",
34
+ {
35
+ from: i.from,
36
+ to: Array.isArray(i.to) ? i.to : [i.to],
37
+ subject: i.subject,
38
+ ...(i.html !== undefined ? { html: i.html } : {}),
39
+ ...(i.text !== undefined ? { text: i.text } : {}),
40
+ ...(i.replyTo !== undefined ? { reply_to: i.replyTo } : {}),
41
+ },
42
+ ctx.idempotencyKey !== undefined
43
+ ? { headers: { "Idempotency-Key": ctx.idempotencyKey } }
44
+ : {},
45
+ )) as { id: string };
46
+ return { id: res.id };
47
+ },
48
+ }),
49
+ },
50
+ },
51
+ });