forked from paperclipai/paperclip
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.ts
More file actions
138 lines (122 loc) · 4.09 KB
/
Copy pathclient.ts
File metadata and controls
138 lines (122 loc) · 4.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import type { PaperclipMcpConfig } from "./config.js";
export class PaperclipApiError extends Error {
readonly status: number;
readonly method: string;
readonly path: string;
readonly body: unknown;
constructor(input: {
status: number;
method: string;
path: string;
body: unknown;
message: string;
}) {
super(input.message);
this.name = "PaperclipApiError";
this.status = input.status;
this.method = input.method;
this.path = input.path;
this.body = input.body;
}
}
/**
* Thrown for a `401` response from the Paperclip API. Distinct from the
* generic `PaperclipApiError` so a rejected/expired agent credential is
* classified as a terminal auth failure immediately, never mistaken for a
* timeout/5xx/network hiccup that is safe to retry (RBR-1036). MCP tool
* callers should surface this error class as-is rather than retrying —
* retrying a dead credential only burns wall clock a live run needs
* elsewhere.
*/
export class PaperclipApiAuthError extends PaperclipApiError {
constructor(input: { method: string; path: string; body: unknown; message: string }) {
super({ ...input, status: 401 });
this.name = "PaperclipApiAuthError";
}
}
export interface JsonRequestOptions {
body?: unknown;
includeRunId?: boolean;
}
function isWriteMethod(method: string): boolean {
return !["GET", "HEAD"].includes(method.toUpperCase());
}
function buildErrorMessage(method: string, path: string, status: number, body: unknown): string {
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
return `${method} ${path} failed with ${status}: ${body.error}`;
}
return `${method} ${path} failed with ${status}`;
}
async function parseResponseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text) as unknown;
} catch {
return text;
}
}
export class PaperclipApiClient {
constructor(private readonly config: PaperclipMcpConfig) {}
get defaults() {
return {
companyId: this.config.companyId,
agentId: this.config.agentId,
runId: this.config.runId,
};
}
resolveCompanyId(companyId?: string | null): string {
const resolved = companyId?.trim() || this.config.companyId;
if (!resolved) {
throw new Error("companyId is required because PAPERCLIP_COMPANY_ID is not set");
}
return resolved;
}
resolveAgentId(agentId?: string | null): string {
const resolved = agentId?.trim() || this.config.agentId;
if (!resolved) {
throw new Error("agentId is required because PAPERCLIP_AGENT_ID is not set");
}
return resolved;
}
async requestJson<T>(method: string, path: string, options: JsonRequestOptions = {}): Promise<T> {
if (!path.startsWith("/")) {
throw new Error(`API path must start with "/": ${path}`);
}
const url = new URL(path.slice(1), `${this.config.apiUrl}/`);
const headers: Record<string, string> = {
Authorization: `Bearer ${this.config.apiKey}`,
Accept: "application/json",
};
if (options.body !== undefined) {
headers["Content-Type"] = "application/json";
}
if ((options.includeRunId ?? isWriteMethod(method)) && this.config.runId) {
headers["X-Paperclip-Run-Id"] = this.config.runId;
}
const response = await fetch(url, {
method,
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const parsedBody = await parseResponseBody(response);
if (!response.ok) {
if (response.status === 401) {
throw new PaperclipApiAuthError({
method: method.toUpperCase(),
path,
body: parsedBody,
message: buildErrorMessage(method.toUpperCase(), path, response.status, parsedBody),
});
}
throw new PaperclipApiError({
status: response.status,
method: method.toUpperCase(),
path,
body: parsedBody,
message: buildErrorMessage(method.toUpperCase(), path, response.status, parsedBody),
});
}
return parsedBody as T;
}
}