@harny/sdk 0.0.1-alpha.1 → 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.
Files changed (84) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -42
  3. package/dist/cjs/agent/adapters.d.ts +167 -0
  4. package/dist/cjs/agent/adapters.js +557 -0
  5. package/dist/cjs/agent/adapters.js.map +1 -0
  6. package/dist/cjs/agent/hooks.d.ts +52 -0
  7. package/dist/cjs/agent/hooks.js +41 -0
  8. package/dist/cjs/agent/hooks.js.map +1 -0
  9. package/dist/cjs/agent/index.d.ts +34 -0
  10. package/dist/cjs/agent/index.js +62 -0
  11. package/dist/cjs/agent/index.js.map +1 -0
  12. package/dist/cjs/agent/loop.d.ts +401 -0
  13. package/dist/cjs/agent/loop.js +1252 -0
  14. package/dist/cjs/agent/loop.js.map +1 -0
  15. package/dist/cjs/agent/roster.d.ts +62 -0
  16. package/dist/cjs/agent/roster.js +149 -0
  17. package/dist/cjs/agent/roster.js.map +1 -0
  18. package/dist/cjs/agent/session.d.ts +106 -0
  19. package/dist/cjs/agent/session.js +261 -0
  20. package/dist/cjs/agent/session.js.map +1 -0
  21. package/dist/cjs/agent/spend.d.ts +39 -0
  22. package/dist/cjs/agent/spend.js +70 -0
  23. package/dist/cjs/agent/spend.js.map +1 -0
  24. package/dist/cjs/agent/tools.d.ts +83 -0
  25. package/dist/cjs/agent/tools.js +212 -0
  26. package/dist/cjs/agent/tools.js.map +1 -0
  27. package/dist/cjs/agent/tracing.d.ts +72 -0
  28. package/dist/cjs/agent/tracing.js +128 -0
  29. package/dist/cjs/agent/tracing.js.map +1 -0
  30. package/dist/cjs/agent/util.d.ts +60 -0
  31. package/dist/cjs/agent/util.js +188 -0
  32. package/dist/cjs/agent/util.js.map +1 -0
  33. package/dist/cjs/conflictQuestion.d.ts +137 -0
  34. package/dist/cjs/conflictQuestion.js +356 -0
  35. package/dist/cjs/conflictQuestion.js.map +1 -0
  36. package/dist/cjs/index.d.ts +950 -0
  37. package/dist/cjs/index.js +1118 -0
  38. package/dist/cjs/index.js.map +1 -0
  39. package/dist/cjs/memoryV1.d.ts +121 -0
  40. package/dist/cjs/memoryV1.js +132 -0
  41. package/dist/cjs/memoryV1.js.map +1 -0
  42. package/dist/cjs/package.json +1 -0
  43. package/dist/esm/agent/adapters.d.ts +167 -0
  44. package/dist/esm/agent/adapters.js +543 -0
  45. package/dist/esm/agent/adapters.js.map +1 -0
  46. package/dist/esm/agent/hooks.d.ts +52 -0
  47. package/dist/esm/agent/hooks.js +36 -0
  48. package/dist/esm/agent/hooks.js.map +1 -0
  49. package/dist/esm/agent/index.d.ts +34 -0
  50. package/dist/esm/agent/index.js +28 -0
  51. package/dist/esm/agent/index.js.map +1 -0
  52. package/dist/esm/agent/loop.d.ts +401 -0
  53. package/dist/esm/agent/loop.js +1243 -0
  54. package/dist/esm/agent/loop.js.map +1 -0
  55. package/dist/esm/agent/roster.d.ts +62 -0
  56. package/dist/esm/agent/roster.js +143 -0
  57. package/dist/esm/agent/roster.js.map +1 -0
  58. package/dist/esm/agent/session.d.ts +106 -0
  59. package/dist/esm/agent/session.js +221 -0
  60. package/dist/esm/agent/session.js.map +1 -0
  61. package/dist/esm/agent/spend.d.ts +39 -0
  62. package/dist/esm/agent/spend.js +65 -0
  63. package/dist/esm/agent/spend.js.map +1 -0
  64. package/dist/esm/agent/tools.d.ts +83 -0
  65. package/dist/esm/agent/tools.js +205 -0
  66. package/dist/esm/agent/tools.js.map +1 -0
  67. package/dist/esm/agent/tracing.d.ts +72 -0
  68. package/dist/esm/agent/tracing.js +119 -0
  69. package/dist/esm/agent/tracing.js.map +1 -0
  70. package/dist/esm/agent/util.d.ts +60 -0
  71. package/dist/esm/agent/util.js +168 -0
  72. package/dist/esm/agent/util.js.map +1 -0
  73. package/dist/esm/conflictQuestion.d.ts +137 -0
  74. package/dist/esm/conflictQuestion.js +346 -0
  75. package/dist/esm/conflictQuestion.js.map +1 -0
  76. package/dist/esm/index.d.ts +950 -0
  77. package/dist/esm/index.js +1066 -0
  78. package/dist/esm/index.js.map +1 -0
  79. package/dist/esm/memoryV1.d.ts +121 -0
  80. package/dist/esm/memoryV1.js +124 -0
  81. package/dist/esm/memoryV1.js.map +1 -0
  82. package/dist/esm/package.json +1 -0
  83. package/package.json +41 -15
  84. package/index.js +0 -270
@@ -0,0 +1,1118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RECALL_LEVELS = exports.questionText = exports.normaliseMode = exports.formatDay = exports.decideConflictQuestion = exports.conflictKey = exports.QuestionPhrases = exports.InMemoryConflictAskLedger = exports.DEFAULT_CONFLICT_QUESTION_MODE = exports.CONFLICT_QUESTION_MODES = exports.ASKING_KINDS = exports.resolvePreset = exports.parseTraceparent = exports.buildAdapter = exports.Usage = exports.Turn = exports.Tracer = exports.SpendMeter = exports.SpendCap = exports.RunResult = exports.ResolvedModel = exports.PresetResolutionError = exports.PRESET_NAMES = exports.ModelTurn = exports.ModelProviderError = exports.MessagesAdapter = exports.Message = exports.MemorySessionStore = exports.MODEL_REACHABLE_CALLS = exports.HookBlocked = exports.HarnyTools = exports.HARNY_CALLS = exports.GenerateContentAdapter = exports.FileSessionStore = exports.ChatCompletionsAdapter = exports.AgentPendingConfirmation = exports.AgentConfig = exports.Agent = exports.ActionTrace = exports.ActionNote = exports.ActDecision = exports.HarnyClient = exports.END_USER_MESSAGE_MAX_CHARS = exports.ACTION_KINDS = exports.HarnyMissingKeyError = exports.HarnyInvalidKeyError = exports.HarnyStateError = exports.HarnyError = void 0;
4
+ exports.computeWebhookSignature = computeWebhookSignature;
5
+ exports.verifyWebhookSignature = verifyWebhookSignature;
6
+ exports.parseSourceArrivalEvent = parseSourceArrivalEvent;
7
+ const util_js_1 = require("./agent/util.js");
8
+ const memoryV1_js_1 = require("./memoryV1.js");
9
+ const SDK_VERSION = "0.1.0";
10
+ class HarnyError extends Error {
11
+ constructor(statusCode, errorCode, message, recoveryHint, docsUrl, retryAfterS,
12
+ /** Field-level reasons when the API rejected the request's fields: where, why, and the kind. */
13
+ detail) {
14
+ super(message);
15
+ this.statusCode = statusCode;
16
+ this.errorCode = errorCode;
17
+ this.recoveryHint = recoveryHint;
18
+ this.docsUrl = docsUrl;
19
+ this.retryAfterS = retryAfterS;
20
+ this.detail = detail;
21
+ this.name = "HarnyError";
22
+ }
23
+ }
24
+ exports.HarnyError = HarnyError;
25
+ /**
26
+ * Thrown for local state problems that never reach the network — calling a
27
+ * user-scoped method before `provisionUser`, or provisioning twice on one
28
+ * client. Mirrors the Python SDK's `HarnyStateError`.
29
+ */
30
+ class HarnyStateError extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "HarnyStateError";
34
+ }
35
+ }
36
+ exports.HarnyStateError = HarnyStateError;
37
+ /** A key was given that is not text (a number, an object, ...). */
38
+ class HarnyInvalidKeyError extends HarnyStateError {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "HarnyInvalidKeyError";
42
+ }
43
+ }
44
+ exports.HarnyInvalidKeyError = HarnyInvalidKeyError;
45
+ /** No API key was given and none was found in `HARNY_API_KEY` (or its alias `HARNY_APP_KEY`). */
46
+ class HarnyMissingKeyError extends HarnyStateError {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = "HarnyMissingKeyError";
50
+ }
51
+ }
52
+ exports.HarnyMissingKeyError = HarnyMissingKeyError;
53
+ const USER_ROLES = ["staff", "test"];
54
+ const ANSWERED_BY = ["tap", "timeout", "dismissed", "server"];
55
+ const CONFIRM_EDIT_FIELDS = [
56
+ "title",
57
+ "start",
58
+ "end",
59
+ "duration_minutes",
60
+ "time_zone",
61
+ "description",
62
+ "location",
63
+ "attendees",
64
+ ];
65
+ const TRACEPARENT = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
66
+ function conflictRef(conflict, asked = false) {
67
+ if (asked && conflict?.ask === false) {
68
+ throw new Error("this decision says not to ask, so there is no question to report");
69
+ }
70
+ const inner = conflict?.conflict;
71
+ const source = (inner && typeof inner === "object" ? inner : conflict);
72
+ const ids = [];
73
+ for (const name of ["record_id_1", "record_id_2"]) {
74
+ const value = source?.[name];
75
+ if (typeof value !== "string" || !value.trim())
76
+ throw new Error(`${name} must be a non-blank string`);
77
+ ids.push(value.trim());
78
+ }
79
+ return { record_id_1: ids[0], record_id_2: ids[1] };
80
+ }
81
+ const BRIDGE_FINISHED = ["done", "failed", "expired"];
82
+ const BRIDGE_SOURCES = ["gmail", "calendar", "drive"];
83
+ function bridgeText(name, value) {
84
+ if (typeof value !== "string")
85
+ throw new Error(`${name} must be a string`);
86
+ const trimmed = value.trim();
87
+ if (!trimmed)
88
+ throw new Error(`${name} must not be blank`);
89
+ return trimmed;
90
+ }
91
+ function withTrace(headers, traceparent) {
92
+ if (traceparent === undefined)
93
+ return headers;
94
+ const value = traceparent.trim().toLowerCase();
95
+ // All-zero trace or span ids are invalid (W3C), and Harny would read the
96
+ // call as outside any run.
97
+ if (!TRACEPARENT.test(value) || value.split("-")[1] === "0".repeat(32) || value.split("-")[2] === "0".repeat(16)) {
98
+ throw new Error("traceparent must be a W3C trace context header: 00-<32 hex>-<16 hex>-<2 hex>");
99
+ }
100
+ return { ...(headers ?? {}), traceparent: value };
101
+ }
102
+ /** The four kinds of action, in the order Harny lists them everywhere. */
103
+ exports.ACTION_KINDS = ["nobody_else", "others_affected", "money", "irreversible"];
104
+ function toActionOutcome(payload) {
105
+ const block = (payload.action ?? {});
106
+ const confirmation = (block.confirmation ?? {});
107
+ const view = (block.model_view ?? {});
108
+ const outcome = {
109
+ executionId: String(payload.execution_id),
110
+ action: String(block.name ?? ""),
111
+ outcome: block.outcome ?? "failed",
112
+ kinds: Array.isArray(block.kinds) ? block.kinds : [],
113
+ kindReasons: Array.isArray(block.kind_reasons) ? block.kind_reasons : [],
114
+ decidedBy: block.decided_by ?? null,
115
+ recordId: block.record_id ?? null,
116
+ refusal: block.refusal ?? null,
117
+ result: block.result ?? null,
118
+ alreadyDecided: Boolean(block.already_decided),
119
+ replayed: Boolean(block.replayed),
120
+ confirmExpiresAt: confirmation.expires_at ?? null,
121
+ forModel: () => ({
122
+ status: String(view.status ?? block.outcome ?? "failed"),
123
+ message: String(view.message ?? block.outcome ?? "failed"),
124
+ }),
125
+ };
126
+ Object.defineProperty(outcome, "forModel", { enumerable: false });
127
+ if (typeof confirmation.confirm_ref === "string") {
128
+ Object.defineProperty(outcome, "confirmRef", {
129
+ value: confirmation.confirm_ref,
130
+ enumerable: false,
131
+ });
132
+ }
133
+ return outcome;
134
+ }
135
+ /**
136
+ * The longest end-user message Harny reads for the addresses it names; a
137
+ * longer one is sent cut to this length (addresses past it are not read).
138
+ */
139
+ exports.END_USER_MESSAGE_MAX_CHARS = 100000;
140
+ /** The end user's own words as sent, or undefined when there are none. */
141
+ function endUserWords(value) {
142
+ if (value === undefined || value === null)
143
+ return undefined;
144
+ if (typeof value !== "string")
145
+ throw new Error("endUserMessage must be a string");
146
+ if (!value.trim())
147
+ return undefined;
148
+ return value.slice(0, exports.END_USER_MESSAGE_MAX_CHARS);
149
+ }
150
+ /** The `Idempotency-Key` header next to the acting-user headers; the same value as the body's `idempotencyKey`. */
151
+ function withIdempotencyHeader(headers, key) {
152
+ if (!key)
153
+ return headers;
154
+ return { ...(headers ?? {}), "Idempotency-Key": key };
155
+ }
156
+ function newIdempotencyKey() {
157
+ const random = typeof globalThis.crypto?.randomUUID === "function"
158
+ ? globalThis.crypto.randomUUID()
159
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
160
+ return `sdk-${random}`;
161
+ }
162
+ const MAX_FALLBACK_MESSAGE_CHARS = 200;
163
+ /**
164
+ * The error for an answer that is not Harny's JSON envelope: a proxy, load balancer or parked host answered with a page.
165
+ * A short plain-text body is kept (it is usually the real reason); markup and long bodies give way to the status.
166
+ * 502, 503 and 504 say that nothing reached the API, and the hint names the address to check.
167
+ */
168
+ function unreadableErrorBody(status, text) {
169
+ const body = text.trim();
170
+ const looksLikeMarkup = body.startsWith("<") || body.slice(0, 512).toLowerCase().includes("<html");
171
+ const gateway = status === 502 || status === 503 || status === 504;
172
+ const hint = gateway
173
+ ? "Check baseUrl (or HARNY_API_URL) and that the host is serving, then try again. Nothing was read or written."
174
+ : "";
175
+ if (body && !looksLikeMarkup && body.length <= MAX_FALLBACK_MESSAGE_CHARS) {
176
+ return new HarnyError(status, "UNKNOWN", body, hint, "", null);
177
+ }
178
+ if (gateway) {
179
+ return new HarnyError(status, "UNKNOWN", `The request did not reach the Harny API (HTTP ${status} from an upstream gateway)`, hint, "", null);
180
+ }
181
+ return new HarnyError(status, "UNKNOWN", `Request failed with status ${status}`, hint, "", null);
182
+ }
183
+ /** The API's field-level reasons for a rejected request: where, why and the kind. The submitted values are dropped (they can hold a secret). */
184
+ function fieldDetail(body) {
185
+ if (!Array.isArray(body.detail))
186
+ return undefined;
187
+ const rows = [];
188
+ for (const entry of body.detail) {
189
+ if (typeof entry === "object" && entry !== null && typeof entry.msg === "string") {
190
+ const e = entry;
191
+ rows.push({
192
+ loc: Array.isArray(e.loc) ? e.loc.map((part) => String(part)) : [],
193
+ msg: e.msg,
194
+ ...(typeof e.type === "string" ? { type: e.type } : {}),
195
+ });
196
+ }
197
+ }
198
+ return rows.length > 0 ? rows : undefined;
199
+ }
200
+ function describeFields(rows) {
201
+ return rows.map((row) => (row.loc.length > 0 ? `${row.loc.join(".")}: ${row.msg}` : row.msg)).join("; ");
202
+ }
203
+ function parseError(status, body) {
204
+ if (typeof body === "object" && body !== null) {
205
+ const obj = body;
206
+ const rows = fieldDetail(obj);
207
+ // The memory routes' error body: { code, status, debugMessage, data }.
208
+ if (typeof obj.code === "string" && typeof obj.debugMessage === "string" && obj.error_code === undefined) {
209
+ return new HarnyError(status, obj.code, obj.debugMessage, "", "", obj.retry_after_s ?? null);
210
+ }
211
+ if (typeof obj.error_code === "string") {
212
+ const message = obj.message ?? "Request failed";
213
+ return new HarnyError(status, obj.error_code, rows ? `${message} (${describeFields(rows)})` : message, obj.recovery_hint ?? "", obj.docs_url ?? "", obj.retry_after_s ?? null, rows);
214
+ }
215
+ if (typeof obj.detail === "string") {
216
+ return new HarnyError(status, "UNKNOWN", obj.detail, "", "", null);
217
+ }
218
+ if (Array.isArray(obj.detail)) {
219
+ return new HarnyError(status, "VALIDATION_ERROR", rows
220
+ ? describeFields(rows)
221
+ : obj.detail.map((d) => String(d)).join("; "), "", "", null, rows);
222
+ }
223
+ }
224
+ return new HarnyError(status, "UNKNOWN", "Request failed", "", "", null);
225
+ }
226
+ /**
227
+ * A person's context reads are limited per minute (and per day by plan): the
228
+ * API answers `assembly_rate_limited`. What happened and what to do, in plain
229
+ * words, with the wait filled in. The Python client
230
+ * writes the same text, so an agent's model reads the same words from either
231
+ * SDK.
232
+ */
233
+ const CONTEXT_READ_LIMIT_CODE = "assembly_rate_limited";
234
+ const CONTEXT_READ_LIMIT_HINT = "This person has reached Harny's limit on context reads for now; no new key is needed.";
235
+ /** A response's `Retry-After` header, if the response carries headers at all. */
236
+ function retryAfterHeader(response) {
237
+ const headers = response.headers;
238
+ return typeof headers?.get === "function" ? headers.get("retry-after") : null;
239
+ }
240
+ /** Seconds from a `Retry-After` header, read as the Python client reads it (at least 1). */
241
+ function retryAfterFromHeader(value) {
242
+ const text = value.trim();
243
+ return /^[+-]?\d+$/.test(text) ? Math.max(1, Number.parseInt(text, 10)) : 1;
244
+ }
245
+ /**
246
+ * A 429 says what happened and what to do: `retryAfterS` comes from the
247
+ * `Retry-After` header when the body has none, and a person's context-read
248
+ * limit says so in plain words, with the wait.
249
+ */
250
+ function withRateLimitDetail(error, headerValue) {
251
+ if (error.statusCode !== 429)
252
+ return error;
253
+ let retryAfterS = error.retryAfterS;
254
+ if (retryAfterS === null && headerValue !== null)
255
+ retryAfterS = retryAfterFromHeader(headerValue);
256
+ let hint = error.recoveryHint;
257
+ if (error.errorCode === CONTEXT_READ_LIMIT_CODE) {
258
+ hint =
259
+ retryAfterS !== null
260
+ ? `${CONTEXT_READ_LIMIT_HINT} Wait ${retryAfterS} s, then read again.`
261
+ : `${CONTEXT_READ_LIMIT_HINT} ${hint.trim()}`.trim();
262
+ }
263
+ if (retryAfterS === error.retryAfterS && hint === error.recoveryHint)
264
+ return error;
265
+ return new HarnyError(error.statusCode, error.errorCode, error.message, hint, error.docsUrl, retryAfterS, error.detail);
266
+ }
267
+ /** `HARNY_API_KEY`, else its alias `HARNY_APP_KEY`, from the process environment, trimmed; undefined when unset, blank, or there is no process (browser). */
268
+ function environmentApiKey() {
269
+ const env = (0, util_js_1.processEnv)();
270
+ return (env.HARNY_API_KEY ?? "").trim() || (env.HARNY_APP_KEY ?? "").trim() || undefined;
271
+ }
272
+ /** `HARNY_API_URL` from the process environment, trimmed; empty when unset, blank, or there is no process (browser). */
273
+ function environmentApiUrl() {
274
+ return ((0, util_js_1.processEnv)().HARNY_API_URL ?? "").trim();
275
+ }
276
+ class HarnyClient {
277
+ constructor({ apiKey, userApiKey, baseUrl, timeoutMs } = {}) {
278
+ for (const [name, given] of [["apiKey", apiKey], ["userApiKey", userApiKey]]) {
279
+ if (given !== undefined && given !== null && typeof given !== "string") {
280
+ throw new HarnyInvalidKeyError(`${name} must be a string (the key itself, starting with fsa_ or fsk_), got ${typeof given}. ` +
281
+ "Read it from the environment: process.env.HARNY_API_KEY");
282
+ }
283
+ }
284
+ const trimmedUser = userApiKey?.trim();
285
+ // With no key given, Node reads HARNY_API_KEY (HARNY_APP_KEY is the older name some setup files still write).
286
+ // Nothing is read in a browser: there is no process, and a key in a browser bundle is public.
287
+ const trimmedApp = apiKey?.trim() || (trimmedUser ? undefined : environmentApiKey());
288
+ if (!trimmedApp && !trimmedUser) {
289
+ throw new HarnyMissingKeyError("Provide apiKey (the app key, starts with fsa_) or userApiKey (one person's key, starts with fsk_), " +
290
+ "or set the HARNY_API_KEY environment variable.");
291
+ }
292
+ if (trimmedApp && trimmedApp.startsWith("fsk_")) {
293
+ throw new Error("apiKey expects an app key (prefix 'fsa_'). You passed a user key (prefix 'fsk_'). Use userApiKey for user keys.");
294
+ }
295
+ if (trimmedUser && trimmedUser.startsWith("fsa_")) {
296
+ throw new Error("userApiKey expects a user key (prefix 'fsk_'). You passed an app key (prefix 'fsa_'). Use apiKey for app keys.");
297
+ }
298
+ this.apiKey = trimmedApp || undefined;
299
+ this.userApiKeyValue = trimmedUser || undefined;
300
+ this.userKeyGiven = Boolean(trimmedUser);
301
+ // An explicit baseUrl wins; without one the client reads HARNY_API_URL (the Python SDK's rule), else the public API.
302
+ this.baseUrl = this.resolveBaseUrl(baseUrl ?? (environmentApiUrl() || "https://api.harny.ai"));
303
+ this.timeoutMs = timeoutMs ?? 20000;
304
+ // The keys are never listed, logged or serialized with the client.
305
+ for (const field of ["apiKey", "userApiKeyValue"]) {
306
+ Object.defineProperty(this, field, { enumerable: false, writable: true, configurable: true });
307
+ }
308
+ }
309
+ /** A key-free view: `JSON.stringify(client)` never carries a key. */
310
+ toJSON() {
311
+ return { baseUrl: this.baseUrl, appKey: this.apiKey ? "set" : "not set", userKey: this.userApiKeyValue ? "set" : "not set" };
312
+ }
313
+ /**
314
+ * The current user-scoped key, or `undefined` before `provisionUser`.
315
+ * Persist it to restore this user's session on a later run instead of
316
+ * provisioning again (provisioning is rate-limited).
317
+ */
318
+ get userApiKey() {
319
+ return this.userApiKeyValue;
320
+ }
321
+ resolveBaseUrl(url) {
322
+ const trimmed = url.trim().replace(/\/+$/, "");
323
+ let parsed;
324
+ try {
325
+ parsed = new URL(trimmed);
326
+ }
327
+ catch {
328
+ throw new Error("baseUrl must be a valid absolute http/https URL");
329
+ }
330
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
331
+ throw new Error("baseUrl must be a valid absolute http/https URL");
332
+ }
333
+ // Every call carries a key: plain http only to this machine.
334
+ if (parsed.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname.toLowerCase())) {
335
+ throw new Error("baseUrl must use https unless the host is localhost or a loopback address");
336
+ }
337
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
338
+ }
339
+ bearer(mode, operation) {
340
+ if (mode === "user") {
341
+ if (!this.userApiKeyValue) {
342
+ throw new HarnyStateError(`${operation} is user-scoped. Call provisionUser({ identifier, email }) first, ` +
343
+ "or construct the client with userApiKey: 'fsk_...' to restore an existing session. " +
344
+ "If that person already existed, Harny does not show their key twice: use the key you saved, " +
345
+ "or use the app key and name the person with user.");
346
+ }
347
+ return `Bearer ${this.userApiKeyValue}`;
348
+ }
349
+ if (mode === "app") {
350
+ if (!this.apiKey) {
351
+ throw new HarnyStateError(`${operation} needs an app key. Construct the client with apiKey: 'fsa_...'.`);
352
+ }
353
+ return `Bearer ${this.apiKey}`;
354
+ }
355
+ const key = this.userApiKeyValue ?? this.apiKey;
356
+ return `Bearer ${key}`;
357
+ }
358
+ async request(method, path, mode, operation, body, extraHeaders) {
359
+ const url = `${this.baseUrl}${path}`;
360
+ const headers = {
361
+ "Authorization": this.bearer(mode, operation),
362
+ "User-Agent": `harny-ts-sdk/${SDK_VERSION}`,
363
+ ...(extraHeaders ?? {}),
364
+ };
365
+ const init = {
366
+ method,
367
+ headers,
368
+ signal: AbortSignal.timeout(this.timeoutMs),
369
+ };
370
+ if (body !== undefined && body instanceof FormData) {
371
+ init.body = body;
372
+ }
373
+ else if (body !== undefined) {
374
+ headers["Content-Type"] = "application/json";
375
+ init.body = JSON.stringify(body);
376
+ }
377
+ let response;
378
+ try {
379
+ response = await fetch(url, init);
380
+ }
381
+ catch (err) {
382
+ if (err instanceof DOMException && err.name === "TimeoutError") {
383
+ throw new HarnyError(0, "TIMEOUT", `Request timed out after ${this.timeoutMs}ms`, "", "", null);
384
+ }
385
+ if (err instanceof DOMException && err.name === "AbortError") {
386
+ throw new HarnyError(0, "ABORTED", "Request was aborted", "", "", null);
387
+ }
388
+ if (err instanceof TypeError) {
389
+ throw new HarnyError(0, "NETWORK_ERROR", err.message || "Network request failed", "Nothing reached Harny. Check baseUrl (or HARNY_API_URL) and your network connection, then try again.", "", null);
390
+ }
391
+ throw err;
392
+ }
393
+ if (response.ok) {
394
+ const text = await response.text();
395
+ if (!text)
396
+ return undefined;
397
+ return JSON.parse(text);
398
+ }
399
+ let errorBody;
400
+ let errorText = "";
401
+ try {
402
+ errorText = await response.text();
403
+ errorBody = JSON.parse(errorText);
404
+ }
405
+ catch {
406
+ throw withRateLimitDetail(unreadableErrorBody(response.status, errorText), retryAfterHeader(response));
407
+ }
408
+ throw withRateLimitDetail(parseError(response.status, errorBody), retryAfterHeader(response));
409
+ }
410
+ /**
411
+ * Per-subsystem health snapshot. Needs no user session; with one, the
412
+ * response also carries per-source freshness.
413
+ */
414
+ async diagnose() {
415
+ return this.request("GET", "/harness/diagnose", "any", "diagnose");
416
+ }
417
+ /**
418
+ * Create or retrieve one app-scoped end user and switch this client to
419
+ * that user's key. Call it again for the next person: the client then
420
+ * acts for that person.
421
+ *
422
+ * Requires an `fsa_` app key. Provisioning is rate-limited: persist
423
+ * `client.userApiKey` and reuse it instead of re-provisioning per run.
424
+ * For a person who already exists, the call returns that user and no new
425
+ * key: nothing new is cached (name the person with `user`).
426
+ */
427
+ async provisionUser(identifier, email, options) {
428
+ if (this.userKeyGiven) {
429
+ throw new HarnyStateError("This client was built with one person's key (userApiKey), so it acts only for that person. " +
430
+ "To add people, construct a HarnyClient with the app key (apiKey).");
431
+ }
432
+ const trimmedIdentifier = identifier?.trim();
433
+ if (!trimmedIdentifier) {
434
+ throw new Error("identifier must be a non-empty string");
435
+ }
436
+ const trimmedEmail = email?.trim();
437
+ if (!trimmedEmail) {
438
+ throw new Error("email must be a non-empty string");
439
+ }
440
+ const role = options?.role;
441
+ if (role !== undefined && role !== null && !USER_ROLES.includes(role)) {
442
+ throw new Error(`role must be one of ${USER_ROLES.join(", ")}, or null to remove the mark`);
443
+ }
444
+ const payload = await this.request("POST", "/users", "app", "provisionUser", {
445
+ identifier: trimmedIdentifier,
446
+ identifier_type: options?.identifier_type ?? "app_id",
447
+ email: trimmedEmail,
448
+ email_trust_level: options?.email_trust_level ?? "app_asserted",
449
+ ...(role !== undefined ? { role } : {}),
450
+ });
451
+ const issued = payload?.api_key;
452
+ if (typeof issued === "string" && issued.trim()) {
453
+ this.userApiKeyValue = issued.trim();
454
+ this.provisionedUserId = payload.user?.id;
455
+ }
456
+ else if (this.provisionedUserId !== undefined && this.provisionedUserId !== payload.user?.id) {
457
+ // No key in the answer means the person already existed: Harny shows a user key once, when the person is
458
+ // first made. The call still succeeded and the user is returned. The client was acting as an earlier
459
+ // person, so stop: a later call without `user` must not write into that other person's vault.
460
+ // Use the app key with `user`, or construct the client with the `userApiKey` you saved.
461
+ this.userApiKeyValue = undefined;
462
+ this.provisionedUserId = undefined;
463
+ }
464
+ return payload.user;
465
+ }
466
+ /**
467
+ * Persist a plain-text document to the user's vault (the provisioned
468
+ * user's, or `options.user` with an app key). `options.writtenByPerson`:
469
+ * set only when the text is the person's own typed words, never pasted,
470
+ * received or model-written content, and only with `options.user` (your
471
+ * app key for that person); it is ignored with the person's own key. An
472
+ * email address in such text then counts as one the person gave: after
473
+ * your agent read someone else's content, an invitation or message to it
474
+ * follows your settings instead of waiting for their yes.
475
+ */
476
+ async ingestText(content, title, options) {
477
+ const body = { content };
478
+ if (title !== undefined)
479
+ body.title = title;
480
+ if (options?.writtenByPerson)
481
+ body.written_by_person = true;
482
+ // The server accepts a long note, answers `queued` after about 15 seconds and finishes in the background; the key makes a
483
+ // repeat of THIS call (a timeout, a dropped connection) the same ingest, never a second one or a second charge. One key per
484
+ // call, kept across the single automatic retry below. Pass `idempotencyKey` to reuse a key across your own retries.
485
+ const key = options?.idempotencyKey ?? newIdempotencyKey();
486
+ const send = () => {
487
+ if (options?.user === undefined) {
488
+ return this.request("POST", "/vault/ingest/text", "user", "ingestText", body, { "Idempotency-Key": key });
489
+ }
490
+ const [mode, headers] = this.actingAuth(options.user, "ingestText");
491
+ return this.request("POST", "/vault/ingest/text", mode, "ingestText", body, { ...headers, "Idempotency-Key": key });
492
+ };
493
+ return this.retryOnceWhenLost(send);
494
+ }
495
+ /** Run an idempotent request; if the answer never arrived (timeout or dropped connection), send it once more with the same key. */
496
+ async retryOnceWhenLost(send) {
497
+ try {
498
+ return await send();
499
+ }
500
+ catch (err) {
501
+ if (err instanceof HarnyError && (err.errorCode === "TIMEOUT" || err.errorCode === "NETWORK_ERROR")) {
502
+ return send();
503
+ }
504
+ throw err;
505
+ }
506
+ }
507
+ /** Upload a binary file to the provisioned user's vault. */
508
+ async ingestFile(file, filename) {
509
+ const form = new FormData();
510
+ // A Node Buffer is a Uint8Array; FormData takes bytes only as a Blob, so wrap them. The cast quiets the type
511
+ // difference between Node and DOM Blob declarations.
512
+ const part = file instanceof Uint8Array ? new Blob([file]) : file;
513
+ form.append("file", part, filename ?? "upload");
514
+ return this.request("POST", "/vault/ingest/file", "user", "ingestFile", form);
515
+ }
516
+ /**
517
+ * Retrieve assembled context for a query.
518
+ *
519
+ * Check `result.degraded_sources` before treating an empty `blocks` array
520
+ * as "this user has no data" — a non-empty array means retrieval FAILED
521
+ * for those sources.
522
+ */
523
+ async assembleContext(query, options, target) {
524
+ if (options?.token_budget !== undefined) {
525
+ if (!Number.isInteger(options.token_budget) ||
526
+ options.token_budget < 256 ||
527
+ options.token_budget > 128000) {
528
+ throw new Error("token_budget must be an integer between 256 and 128000");
529
+ }
530
+ }
531
+ if (options?.min_relevance_score !== undefined &&
532
+ options.min_relevance_score !== null) {
533
+ const score = options.min_relevance_score;
534
+ if (typeof score !== "number" || Number.isNaN(score) || score < 0 || score > 1) {
535
+ throw new Error("min_relevance_score must be a number between 0 and 1");
536
+ }
537
+ }
538
+ if (options?.conversation_context !== undefined &&
539
+ options.conversation_context !== null &&
540
+ options.conversation_context.length > 5) {
541
+ throw new Error("conversation_context accepts at most 5 turns");
542
+ }
543
+ // Built field by field: the API's ContextAssembleRequest is
544
+ // `extra="forbid"`, so an unknown key here is a 422, not an ignored
545
+ // hint. Never spread caller-supplied options into this body.
546
+ const body = { query };
547
+ if (options?.token_budget !== undefined)
548
+ body.token_budget = options.token_budget;
549
+ if (options?.assembly_profile !== undefined) {
550
+ body.assembly_profile = options.assembly_profile;
551
+ }
552
+ if (options?.thread_id !== undefined)
553
+ body.thread_id = options.thread_id;
554
+ if (options?.include_tools !== undefined)
555
+ body.include_tools = options.include_tools;
556
+ if (options?.source_filter !== undefined)
557
+ body.source_filter = options.source_filter;
558
+ if (options?.min_relevance_score !== undefined) {
559
+ body.min_relevance_score = options.min_relevance_score;
560
+ }
561
+ if (options?.conversation_context !== undefined) {
562
+ body.conversation_context = options.conversation_context;
563
+ }
564
+ const [mode, headers] = this.actingAuth(target?.user, "assembleContext");
565
+ return this.request("POST", "/context/assemble", mode, "assembleContext", body, withTrace(headers, target?.traceparent));
566
+ }
567
+ /**
568
+ * Tell Harny you asked the user about a conflict, so the question is not put twice. Call it BEFORE you show the
569
+ * question `decideConflictQuestion` returned, and show it only when the result's `first_time` is true: the first
570
+ * report wins, so a second action in the same turn, a retry or a second process cannot put the question twice. The
571
+ * next context response marks that conflict `asked`. `conflict` is a `ContextConflict`, an object with its
572
+ * `record_id_1` and `record_id_2`, or the decision itself (a decision that says not to ask is refused). Safe to repeat. `options.user`: the end user (needs an app key); omit it
573
+ * on a client that provisioned a user. Never expose this to a model as a tool: asking is your confirm step's.
574
+ * Throws `HarnyError` (404, `conflict_not_found`) for a conflict that was not in this user's context response.
575
+ */
576
+ async markConflictAsked(conflict, options) {
577
+ const body = conflictRef(conflict, true);
578
+ const [mode, headers] = this.actingAuth(options?.user, "markConflictAsked");
579
+ return this.request("POST", "/conflicts/asked", mode, "markConflictAsked", body, withTrace(headers, options?.traceparent));
580
+ }
581
+ /**
582
+ * Send the user's answer to a conflict. It is saved as something the user said, and the conflict is no
583
+ * longer returned. `answer` is the user's answer as a statement that stands on its own, in their words (for
584
+ * example "Maya prefers email"); it is saved the way `ingestText` saves text and goes through the
585
+ * same limits. Throws `HarnyError` as `ingestText` does (for example when the memory service is unavailable;
586
+ * nothing is marked answered then), and 404 `conflict_not_found` for a conflict that was not in this user's
587
+ * context response (nothing is saved then). `conflict` and `options` are as for `markConflictAsked`.
588
+ */
589
+ async answerConflict(conflict, answer, options) {
590
+ const text = typeof answer === "string" ? answer.trim() : "";
591
+ if (!text)
592
+ throw new Error("answer must not be blank");
593
+ const body = { ...conflictRef(conflict), answer: text };
594
+ const [mode, headers] = this.actingAuth(options?.user, "answerConflict");
595
+ return this.request("POST", "/conflicts/answer", mode, "answerConflict", body, withTrace(headers, options?.traceparent));
596
+ }
597
+ /**
598
+ * Read one arrived email (an arrival's `item_ref`) for its end user. The
599
+ * text comes back hardened — secrets redacted, instruction-like lines
600
+ * removed — and is third-party content: pass it to a model as data only.
601
+ */
602
+ async getSourceItem(itemRef, options) {
603
+ const [mode, headers] = this.actingAuth(options?.user, "getSourceItem");
604
+ return this.request("GET", `/context-sources/items/${encodeURIComponent(itemRef)}`, mode, "getSourceItem", undefined, withTrace(headers, options?.traceparent));
605
+ }
606
+ /**
607
+ * Poll new items that arrived on your end users' email sources (app key),
608
+ * oldest first — the same arrivals the `source.sync_complete` webhook
609
+ * delivers. Pass the last `next_cursor` as `after`; process each
610
+ * `dedupe_key` once. `user` narrows to one end user.
611
+ */
612
+ async listArrivals(options) {
613
+ const after = options?.after ?? 0;
614
+ const limit = options?.limit ?? 50;
615
+ if (!Number.isInteger(after) || after < 0) {
616
+ throw new Error("after must be a non-negative integer cursor");
617
+ }
618
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
619
+ throw new Error("limit must be an integer from 1 to 200");
620
+ }
621
+ const headers = options?.user !== undefined ? { "X-Harny-User": options.user } : undefined;
622
+ return this.request("GET", `/context-sources/arrivals?after=${after}&limit=${limit}`, "app", "listArrivals", undefined, withTrace(headers, options?.traceparent));
623
+ }
624
+ /**
625
+ * Start an OAuth flow for one external source. Send the end user to the
626
+ * returned `authorization_url`.
627
+ *
628
+ * Which providers a deployment can connect varies, and Gmail / Calendar /
629
+ * Drive sit behind Early Access approval — do not hardcode a provider
630
+ * name without checking the account's access first.
631
+ *
632
+ * `sharedFactsOptIn` passes the person's own answer to Harny's sharing
633
+ * question, the one Harny's connect widget shows (unticked). Send `true`
634
+ * only when the person themselves ticked it: Harny keeps the answer with
635
+ * this connect and saves it, as the person's own choice, when the
636
+ * connection completes. `false` and leaving it out record nothing and never
637
+ * withdraw an earlier yes. A person flagged as a child is never opted in.
638
+ * Anything but `true` or `false` throws.
639
+ */
640
+ async connectSource(provider, returnUrl, options) {
641
+ const body = {};
642
+ if (returnUrl !== undefined)
643
+ body.return_url = returnUrl;
644
+ if (options?.capabilities?.length)
645
+ body.capabilities = options.capabilities;
646
+ if (options?.sharedFactsOptIn !== undefined) {
647
+ if (typeof options.sharedFactsOptIn !== "boolean") {
648
+ throw new Error("sharedFactsOptIn must be true or false");
649
+ }
650
+ body.shared_facts_opt_in = options.sharedFactsOptIn;
651
+ }
652
+ const [mode, headers] = this.actingAuth(options?.user, "connectSource");
653
+ return this.request("POST", `/context-sources/${encodeURIComponent(provider)}/connect`, mode, "connectSource", body, headers);
654
+ }
655
+ /** The end user an action is for: app key + named user, or the user's own key. */
656
+ actingAuth(user, operation) {
657
+ if (user !== undefined) {
658
+ if (!this.apiKey) {
659
+ throw new HarnyStateError(`${operation} with user needs an app key. Construct the client with apiKey: 'fsa_...'.`);
660
+ }
661
+ return ["app", { "X-Harny-User": user }];
662
+ }
663
+ return ["user", undefined];
664
+ }
665
+ /**
666
+ * Perform one catalog action (for example `calendar.create_event`) for one
667
+ * end user. Harny works out which kinds of action it is from what it does,
668
+ * follows the most careful of your business's settings for those kinds,
669
+ * checks the user's write consent, performs the action and records the
670
+ * outcome. No model is called. Tell a model only `outcome.forModel()`.
671
+ * After your agent read someone else's content (an email, a calendar
672
+ * entry, a shared document), where your setting is act, an invitation or
673
+ * message to an address the person never gave waits for their yes; with
674
+ * `initiatedBy: "system_event"` it always does. `endUserMessage`: the
675
+ * person's own words for this request (their chat message), from your
676
+ * backend, never a model; an email address in them counts as given. Only
677
+ * the addresses are kept, never the words; with `initiatedBy:
678
+ * "system_event"` they are not counted.
679
+ */
680
+ async act(action, args, options) {
681
+ const body = {
682
+ action,
683
+ arguments: args,
684
+ request_hash: options?.idempotencyKey ?? newIdempotencyKey(),
685
+ };
686
+ if (options?.initiatedBy)
687
+ body.initiated_by = options.initiatedBy;
688
+ if (options?.correlationId)
689
+ body.correlation_id = options.correlationId;
690
+ const words = endUserWords(options?.endUserMessage);
691
+ if (words !== undefined)
692
+ body.end_user_message = words;
693
+ const [mode, headers] = this.actingAuth(options?.user, "act");
694
+ const payload = await this.request("POST", "/tools/execute", mode, "act", body, withTrace(headers, options?.traceparent));
695
+ return toActionOutcome(payload);
696
+ }
697
+ /**
698
+ * Send the end user's decision on an ask-first action, from your UI
699
+ * handler. Never expose this to a model as a tool.
700
+ *
701
+ * `answeredBy` says how the card was answered, as your app knows it: `tap` (the person pressed the button),
702
+ * `timeout` (the card ran out of time), `dismissed` (the person closed it without choosing) or `server` (your backend
703
+ * answered without showing a card). Leave it out when you do not know; an answer marked `timeout`, `dismissed` or
704
+ * `server` is not counted as a person's choice when Harny reads how your users answer.
705
+ *
706
+ * `edit` is what the person corrected on the card before approving it (only with `approve: true`, for a calendar
707
+ * event): `title`, `start`, `end`, `duration_minutes`, `time_zone`, `description`, `location`, or a shorter guest list in
708
+ * `attendees` (the same guests or fewer). The corrected action is the one that runs, and the record shows what changed.
709
+ * An edit never adds a guest: one who was not on the card is refused (`confirm_edit_not_allowed`) and the card stays
710
+ * open; the agent proposes that as a new request. Any other field throws before a call is made.
711
+ */
712
+ async confirm(executionId, confirmRef, approve, options) {
713
+ const [mode, headers] = this.actingAuth(options?.user, "confirm");
714
+ const answeredBy = options?.answeredBy;
715
+ if (answeredBy !== undefined && !ANSWERED_BY.includes(answeredBy)) {
716
+ throw new Error(`answeredBy must be one of ${ANSWERED_BY.join(", ")}`);
717
+ }
718
+ const edit = options?.edit;
719
+ if (edit !== undefined) {
720
+ if (!approve) {
721
+ throw new Error("edit goes with approve: true: a declined card changes nothing");
722
+ }
723
+ if (edit === null || typeof edit !== "object" || Array.isArray(edit) || Object.keys(edit).length === 0) {
724
+ throw new Error("edit must be an object of the fields the person changed");
725
+ }
726
+ const unknown = Object.keys(edit).filter((key) => !CONFIRM_EDIT_FIELDS.includes(key));
727
+ if (unknown.length > 0) {
728
+ throw new Error(`edit can change only ${CONFIRM_EDIT_FIELDS.join(", ")}; not ${unknown.join(", ")}. ` +
729
+ "A guest who was not on the card is a new request, not an edit.");
730
+ }
731
+ }
732
+ const payload = await this.request("POST", `/tools/executions/${encodeURIComponent(executionId)}/confirm`, mode, "confirm", {
733
+ confirm_ref: confirmRef,
734
+ approve,
735
+ ...(answeredBy !== undefined ? { answered_by: answeredBy } : {}),
736
+ ...(edit !== undefined ? { edit: { ...edit } } : {}),
737
+ }, withTrace(headers, options?.traceparent));
738
+ return toActionOutcome(payload);
739
+ }
740
+ /**
741
+ * Mint a short-lived token for ONE end user (app key), for a hosted agent
742
+ * runtime's MCP connection. The runtime reads that person's context and
743
+ * performs catalog actions for that person only, through your acting
744
+ * policy, until the token expires. It also reads that person's action
745
+ * history by default (`actionHistory: false` leaves it out).
746
+ */
747
+ async mintRuntimeToken(user, options) {
748
+ if (!user || !user.trim()) {
749
+ throw new Error("user is required: a runtime token acts for exactly one end user");
750
+ }
751
+ const body = { read_only: Boolean(options?.readOnly) };
752
+ if (options?.ttlSeconds !== undefined)
753
+ body.ttl_seconds = options.ttlSeconds;
754
+ if (options?.actions !== undefined)
755
+ body.actions = options.actions;
756
+ if (options?.initiatedBy !== undefined)
757
+ body.initiated_by = options.initiatedBy;
758
+ if (options?.label !== undefined)
759
+ body.label = options.label;
760
+ if (options?.connectUrl !== undefined)
761
+ body.connect_url = options.connectUrl;
762
+ if (options?.userDrivenOnly)
763
+ body.user_driven_only = true;
764
+ if (options?.actionHistory === false)
765
+ body.action_history = false;
766
+ const words = endUserWords(options?.endUserMessage);
767
+ if (words !== undefined)
768
+ body.end_user_message = words;
769
+ return this.request("POST", "/runtime-tokens", "app", "mintRuntimeToken", body, {
770
+ "X-Harny-User": user.trim(),
771
+ });
772
+ }
773
+ /** Revoke one runtime token now (app key). Rotating the app key revokes them all. */
774
+ async revokeRuntimeToken(tokenId) {
775
+ return this.request("DELETE", `/runtime-tokens/${encodeURIComponent(tokenId)}`, "app", "revokeRuntimeToken");
776
+ }
777
+ /**
778
+ * The confirm reference of an ask-first action a hosted runtime asked for
779
+ * over MCP (app key). Call it when the approval-needed webhook names the
780
+ * execution, show your confirm card, then `confirm(...)`.
781
+ */
782
+ async getPendingConfirmation(executionId, options) {
783
+ if (!options?.user || !options.user.trim()) {
784
+ throw new Error("user is required: name the end user the action was for");
785
+ }
786
+ return this.request("GET", `/tools/executions/${encodeURIComponent(executionId)}/confirmation`, "app", "getPendingConfirmation", undefined, { "X-Harny-User": options.user.trim() });
787
+ }
788
+ /**
789
+ * Set up the person's Google bridge: a small script they copy into their own Google account.
790
+ *
791
+ * Call it first without `acceptedTextSha256`: it returns `{ registered: false, consent, next_step }`. Show the
792
+ * person `consent.text` and ask which of `gmail`, `calendar` and `drive` to include. After their yes, call it
793
+ * again with `acceptedTextSha256: consent.text_sha256` (and `sources`): it registers and returns the setup
794
+ * answer once (`setup_code`, `script_properties`, `agent_instructions`). Needs the bridge to be switched on for
795
+ * your deployment (otherwise a 404 `google_bridge_off`). `user`: as in `act`.
796
+ */
797
+ async googleBridgeSetup(sources, options) {
798
+ const [mode, headers] = this.actingAuth(options?.user, "googleBridgeSetup");
799
+ if (options?.acceptedTextSha256 === undefined) {
800
+ const consent = await this.request("GET", "/google-bridge/consent", mode, "googleBridgeSetup", undefined, headers);
801
+ return {
802
+ registered: false,
803
+ consent,
804
+ next_step: "Show the person consent.text and ask which of gmail, calendar and drive to include. After their yes, call " +
805
+ "googleBridgeSetup again with acceptedTextSha256: consent.text_sha256 (and sources: [...]).",
806
+ };
807
+ }
808
+ const body = {
809
+ accepted_text_sha256: bridgeText("acceptedTextSha256", options.acceptedTextSha256),
810
+ };
811
+ if (options.label !== undefined)
812
+ body.label = options.label;
813
+ if (sources !== undefined) {
814
+ const chosen = sources.map((item) => bridgeText("sources", item).toLowerCase());
815
+ const unknown = chosen.filter((item) => !BRIDGE_SOURCES.includes(item));
816
+ if (unknown.length) {
817
+ throw new Error(`sources must be some of ${BRIDGE_SOURCES.join(", ")}; got ${unknown.join(", ")}`);
818
+ }
819
+ body.sources = chosen;
820
+ }
821
+ const answer = await this.request("POST", "/google-bridge/register", mode, "googleBridgeSetup", body, headers);
822
+ return { ...answer, registered: true };
823
+ }
824
+ /**
825
+ * Ask the person's Google bridge to do one thing in their own Google account.
826
+ *
827
+ * `kind`: `gmail.send`, `gmail.draft`, `gmail.search`, `gmail.read`, `calendar.list`, `calendar.create`,
828
+ * `calendar.delete`, `drive.search`, `drive.read` or `drive.create`; `params` holds that kind's fields. With
829
+ * `wait` (the default) it polls until the action is `done`, `failed` or `expired`, or `timeoutMs` pass, and
830
+ * returns the last view: check `status`. A send, draft or create whose delivery is unconfirmed is not repeated:
831
+ * read `status` before asking again. `user`: as in `act`.
832
+ */
833
+ async googleBridgeAct(kind, params, options) {
834
+ const [mode, headers] = this.actingAuth(options?.user, "googleBridgeAct");
835
+ let view = await this.request("POST", "/google-bridge/actions", mode, "googleBridgeAct", { kind: bridgeText("kind", kind), params: { ...(params ?? {}) } }, headers);
836
+ const wait = options?.wait ?? true;
837
+ const deadline = Date.now() + Math.max(0, options?.timeoutMs ?? 30000);
838
+ const pause = Math.max(0, options?.pollIntervalMs ?? 1000);
839
+ while (wait && !BRIDGE_FINISHED.includes(view.status) && Date.now() < deadline) {
840
+ await new Promise((resolve) => setTimeout(resolve, pause));
841
+ view = await this.request("GET", `/google-bridge/actions/${encodeURIComponent(bridgeText("action_id", String(view.action_id)))}`, mode, "googleBridgeAct", undefined, headers);
842
+ }
843
+ return view;
844
+ }
845
+ /** Undo an action your agent performed, by its action-record id. */
846
+ async undo(recordId, options) {
847
+ return this.act("calendar.delete_event", { reverses: recordId }, options);
848
+ }
849
+ /**
850
+ * Read the platform model roster (app key): presets over provider/model
851
+ * entries with capability flags, region hints and list prices. Your agent
852
+ * runs on your own model key.
853
+ */
854
+ async getModelRoster() {
855
+ return this.request("GET", "/models/roster", "app", "getModelRoster");
856
+ }
857
+ /**
858
+ * The setup bundle a coding assistant stands your agent up from (app key).
859
+ * It names key variables and never carries a key.
860
+ */
861
+ async getSetupBundle(appId, options) {
862
+ const body = {
863
+ shape: options.shape,
864
+ deploy_target: options.deployTarget ?? "unspecified",
865
+ };
866
+ if (options.preset !== undefined)
867
+ body.preset = options.preset;
868
+ if (options.surface !== undefined)
869
+ body.surface = options.surface;
870
+ if (options.sources?.length)
871
+ body.sources = options.sources;
872
+ if (options.actions?.length)
873
+ body.actions = options.actions;
874
+ if (options.emailArrivalTrigger !== undefined)
875
+ body.email_arrival_trigger = options.emailArrivalTrigger;
876
+ if (options.description !== undefined)
877
+ body.description = options.description;
878
+ return this.request("POST", `/apps/${encodeURIComponent(appId)}/setup-bundle`, "app", "getSetupBundle", body);
879
+ }
880
+ /** Read your app's acting policy (app key). */
881
+ async getActingPolicy(appId) {
882
+ return this.request("GET", `/apps/${encodeURIComponent(appId)}/acting-policy`, "app", "getActingPolicy");
883
+ }
884
+ /**
885
+ * Set what your agent may do for each kind of action: "act", "ask_first",
886
+ * "off", or null (unset — the agent does not act on that kind). Kinds you
887
+ * omit are unchanged. The kinds: `nobody_else` (only the customer's own
888
+ * account), `others_affected` (reaches other people),
889
+ * `money` (any charge, payment, refund, discount or credit) and
890
+ * `irreversible` (can't be undone once done). An action can be more than
891
+ * one kind; your agent follows the most careful of your settings for the
892
+ * kinds it is. Where that is act and your agent has read someone else's
893
+ * content, an invitation or message to an address the person never gave
894
+ * waits for their yes.
895
+ *
896
+ * Needs the APP OWNER's user key (`userApiKey: 'fsk_...'`), never the
897
+ * `fsa_` app key your agent runs with: Harny refuses that key here (403
898
+ * owner_credential_required) so an agent can never turn acting on for
899
+ * itself. Keep the owner key out of the agent's deployment, or set the
900
+ * policy in the console.
901
+ */
902
+ async setActingPolicy(appId, policy) {
903
+ const body = { set_via: policy.setVia ?? "api" };
904
+ for (const kind of exports.ACTION_KINDS) {
905
+ if (!(kind in policy))
906
+ continue;
907
+ const mode = policy[kind] ?? null;
908
+ if (mode !== null && mode !== "act" && mode !== "ask_first" && mode !== "off") {
909
+ throw new Error(`${kind} must be "act", "ask_first", "off" or null (unset)`);
910
+ }
911
+ body[kind] = mode;
912
+ }
913
+ if (Object.keys(body).length === 1) {
914
+ throw new Error("name at least one kind of action: nobody_else, others_affected, money or irreversible");
915
+ }
916
+ if (policy.confirm === true)
917
+ body.confirm = true;
918
+ if (!this.userKeyGiven) {
919
+ throw new HarnyStateError("setActingPolicy needs the app owner's user key: construct a separate client with " +
920
+ "userApiKey: 'fsk_...' (the owner's key, kept out of the agent's deployment), or set the " +
921
+ "policy in the console. The fsa_ app key your agent runs with is refused.");
922
+ }
923
+ return this.request("PUT", `/apps/${encodeURIComponent(appId)}/acting-policy`, "user", "setActingPolicy", body);
924
+ }
925
+ /** List the provisioned user's connected sources and their freshness. */
926
+ async listSources() {
927
+ return this.request("GET", "/context-sources", "user", "listSources");
928
+ }
929
+ /** Trigger a sync job for one connected source. */
930
+ async syncSource(provider, options) {
931
+ return this.request("POST", `/context-sources/${encodeURIComponent(provider)}/sync`, "user", "syncSource", { full_resync: options?.fullResync ?? false });
932
+ }
933
+ // ── Memory API v1: data points, status, delete, recall, usage ───────────
934
+ /**
935
+ * Send one data point: text plus the time it happened. Answers 202 with an `ingestionId` to poll with
936
+ * `getIngestion`. Send the same `id` again with changed content and the new text replaces the old; the same
937
+ * content again answers `unchanged: true` and costs nothing. `options.user` names the person (needs an app key).
938
+ */
939
+ async ingestDataPoint(point, options) {
940
+ const body = (0, memoryV1_js_1.ingestBody)(point, options?.idempotencyKey);
941
+ const [mode, headers] = this.actingAuth(options?.user, "ingestDataPoint");
942
+ const sent = withIdempotencyHeader(headers, options?.idempotencyKey);
943
+ const send = () => this.request("POST", "/memory/ingest", mode, "ingestDataPoint", body, sent);
944
+ // Retry a lost answer only when the caller gave a key: without one a repeat could be a second send.
945
+ return options?.idempotencyKey ? this.retryOnceWhenLost(send) : send();
946
+ }
947
+ /** Send 1 to 1,000 data points at once. Send the same `idempotencyKey` and body again after a failure to resume. */
948
+ async ingestBatch(items, options) {
949
+ const body = (0, memoryV1_js_1.batchBody)(items, options?.idempotencyKey);
950
+ const [mode, headers] = this.actingAuth(options?.user, "ingestBatch");
951
+ const sent = withIdempotencyHeader(headers, options?.idempotencyKey);
952
+ const send = () => this.request("POST", "/memory/ingest/batch", mode, "ingestBatch", body, sent);
953
+ return options?.idempotencyKey ? this.retryOnceWhenLost(send) : send();
954
+ }
955
+ /** Where one send is: `processing`, `completed` or `failed`. */
956
+ async getIngestion(ingestionId, options) {
957
+ const [mode, headers] = this.actingAuth(options?.user, "getIngestion");
958
+ return this.request("GET", `/memory/ingest/${(0, memoryV1_js_1.pathPart)("ingestionId", ingestionId)}`, mode, "getIngestion", undefined, headers);
959
+ }
960
+ /** Poll `getIngestion` until the send is `completed` or `failed`; throws after `timeoutMs` (default 300000). A timeout does not mean the save failed. */
961
+ async waitForIngestion(ingestionId, options) {
962
+ const deadline = Date.now() + (options?.timeoutMs ?? 300000);
963
+ for (;;) {
964
+ const status = await this.getIngestion(ingestionId, { user: options?.user });
965
+ if (status.status === "completed" || status.status === "failed")
966
+ return status;
967
+ if (Date.now() >= deadline) {
968
+ const waitedS = (options?.timeoutMs ?? 300000) / 1000;
969
+ throw new HarnyError(0, "TIMEOUT", `Ingestion ${ingestionId} is still ${status.status} on Harny's side after waiting ${waitedS}s. `
970
+ + `This is not a failure: the save is still being processed and will usually complete. `
971
+ + `Call getIngestion("${ingestionId}") again later to check it.`, `Call getIngestion("${ingestionId}") again later to check the save.`, "", null);
972
+ }
973
+ await new Promise((resolve) => setTimeout(resolve, options?.pollMs ?? 2000));
974
+ }
975
+ }
976
+ /** Forget one send: its data points and every memory that rests only on them. Free; a repeat succeeds. */
977
+ async deleteIngestion(ingestionId, options) {
978
+ const [mode, headers] = this.actingAuth(options?.user, "deleteIngestion");
979
+ await this.request("DELETE", `/memory/ingest/${(0, memoryV1_js_1.pathPart)("ingestionId", ingestionId)}`, mode, "deleteIngestion", undefined, headers);
980
+ }
981
+ /** Delete one data point by its `sourceId`, and the memory built on it. */
982
+ async deleteDataPoint(sourceId, options) {
983
+ const [mode, headers] = this.actingAuth(options?.user, "deleteDataPoint");
984
+ return this.request("DELETE", `/memory/data-points/${(0, memoryV1_js_1.pathPart)("sourceId", sourceId)}`, mode, "deleteDataPoint", undefined, headers);
985
+ }
986
+ /** Delete 1 to 1,000 data points by `sourceId`. If one id is unknown, none is deleted and the error names it. */
987
+ async deleteDataPoints(ids, options) {
988
+ const body = (0, memoryV1_js_1.deleteBody)(ids);
989
+ const [mode, headers] = this.actingAuth(options?.user, "deleteDataPoints");
990
+ return this.request("POST", "/memory/data-points/delete", mode, "deleteDataPoints", body, headers);
991
+ }
992
+ /** Ranked, dated evidence for a question, as the person asking (`options.identity`, default `options.user`). */
993
+ async recall(query, options = {}) {
994
+ const identity = options.identity ?? options.user ?? this.userApiKeyIdentity();
995
+ const body = (0, memoryV1_js_1.recallBody)(query, identity, options);
996
+ const [mode, headers] = this.actingAuth(options.user, "recall");
997
+ return this.request("POST", "/memory/recall", mode, "recall", body, headers);
998
+ }
999
+ /** This month's usage for the app. Needs the app key. */
1000
+ async getUsage() {
1001
+ return this.request("GET", "/memory/usage", "app", "getUsage");
1002
+ }
1003
+ /** The recall identity when none is named: only a client that provisioned a user knows it. */
1004
+ userApiKeyIdentity() {
1005
+ return this.provisionedUserId ?? "";
1006
+ }
1007
+ }
1008
+ exports.HarnyClient = HarnyClient;
1009
+ // `console.log(client)` shows the key-free view, never a key.
1010
+ Object.defineProperty(HarnyClient.prototype, Symbol.for("nodejs.util.inspect.custom"), {
1011
+ value() {
1012
+ return `HarnyClient ${JSON.stringify(this.toJSON())}`;
1013
+ },
1014
+ enumerable: false,
1015
+ });
1016
+ // ── Webhook deliveries ──────────────────────────────────────────────────
1017
+ function toHex(bytes) {
1018
+ return Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
1019
+ }
1020
+ function asBytes(body) {
1021
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
1022
+ }
1023
+ /** The `X-Harny-Signature` value Harny sends for `body` under `secret`. */
1024
+ async function computeWebhookSignature(secret, body) {
1025
+ const key = await globalThis.crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1026
+ const digest = await globalThis.crypto.subtle.sign("HMAC", key, asBytes(body));
1027
+ return `sha256=${toHex(digest)}`;
1028
+ }
1029
+ /**
1030
+ * Constant-time check of `X-Harny-Signature` against one or more secrets
1031
+ * (pass the current and the previous one during a rotation). Verify the RAW
1032
+ * body bytes before parsing. Accepts the header with or without `sha256=`.
1033
+ */
1034
+ async function verifyWebhookSignature(body, signature, secrets) {
1035
+ if (!signature)
1036
+ return false;
1037
+ let provided = signature.trim().toLowerCase();
1038
+ if (provided.startsWith("sha256="))
1039
+ provided = provided.slice("sha256=".length);
1040
+ if (!provided)
1041
+ return false;
1042
+ for (const secret of Array.isArray(secrets) ? secrets : [secrets]) {
1043
+ if (!secret)
1044
+ continue;
1045
+ const expected = (await computeWebhookSignature(secret, body)).slice("sha256=".length);
1046
+ if (expected.length !== provided.length)
1047
+ continue;
1048
+ let diff = 0;
1049
+ for (let i = 0; i < expected.length; i += 1) {
1050
+ diff |= expected.charCodeAt(i) ^ provided.charCodeAt(i);
1051
+ }
1052
+ if (diff === 0)
1053
+ return true;
1054
+ }
1055
+ return false;
1056
+ }
1057
+ /**
1058
+ * Verify, then parse a `source.sync_complete` delivery. Throws when the
1059
+ * signature does not verify (answer 401 and do nothing) or the body is not a
1060
+ * `source.sync_complete` event.
1061
+ */
1062
+ async function parseSourceArrivalEvent(body, signature, secrets) {
1063
+ if (!(await verifyWebhookSignature(body, signature, secrets))) {
1064
+ throw new Error("X-Harny-Signature does not match");
1065
+ }
1066
+ const text = typeof body === "string" ? body : new TextDecoder().decode(body);
1067
+ const payload = JSON.parse(text);
1068
+ if (payload?.event_type !== "source.sync_complete") {
1069
+ throw new Error("not a source.sync_complete event");
1070
+ }
1071
+ return payload;
1072
+ }
1073
+ // ── Agent loop: your model, your key, Harny for context and actions ──
1074
+ var index_js_1 = require("./agent/index.js");
1075
+ Object.defineProperty(exports, "ActDecision", { enumerable: true, get: function () { return index_js_1.ActDecision; } });
1076
+ Object.defineProperty(exports, "ActionNote", { enumerable: true, get: function () { return index_js_1.ActionNote; } });
1077
+ Object.defineProperty(exports, "ActionTrace", { enumerable: true, get: function () { return index_js_1.ActionTrace; } });
1078
+ Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return index_js_1.Agent; } });
1079
+ Object.defineProperty(exports, "AgentConfig", { enumerable: true, get: function () { return index_js_1.AgentConfig; } });
1080
+ Object.defineProperty(exports, "AgentPendingConfirmation", { enumerable: true, get: function () { return index_js_1.AgentPendingConfirmation; } });
1081
+ Object.defineProperty(exports, "ChatCompletionsAdapter", { enumerable: true, get: function () { return index_js_1.ChatCompletionsAdapter; } });
1082
+ Object.defineProperty(exports, "FileSessionStore", { enumerable: true, get: function () { return index_js_1.FileSessionStore; } });
1083
+ Object.defineProperty(exports, "GenerateContentAdapter", { enumerable: true, get: function () { return index_js_1.GenerateContentAdapter; } });
1084
+ Object.defineProperty(exports, "HARNY_CALLS", { enumerable: true, get: function () { return index_js_1.HARNY_CALLS; } });
1085
+ Object.defineProperty(exports, "HarnyTools", { enumerable: true, get: function () { return index_js_1.HarnyTools; } });
1086
+ Object.defineProperty(exports, "HookBlocked", { enumerable: true, get: function () { return index_js_1.HookBlocked; } });
1087
+ Object.defineProperty(exports, "MODEL_REACHABLE_CALLS", { enumerable: true, get: function () { return index_js_1.MODEL_REACHABLE_CALLS; } });
1088
+ Object.defineProperty(exports, "MemorySessionStore", { enumerable: true, get: function () { return index_js_1.MemorySessionStore; } });
1089
+ Object.defineProperty(exports, "Message", { enumerable: true, get: function () { return index_js_1.Message; } });
1090
+ Object.defineProperty(exports, "MessagesAdapter", { enumerable: true, get: function () { return index_js_1.MessagesAdapter; } });
1091
+ Object.defineProperty(exports, "ModelProviderError", { enumerable: true, get: function () { return index_js_1.ModelProviderError; } });
1092
+ Object.defineProperty(exports, "ModelTurn", { enumerable: true, get: function () { return index_js_1.ModelTurn; } });
1093
+ Object.defineProperty(exports, "PRESET_NAMES", { enumerable: true, get: function () { return index_js_1.PRESET_NAMES; } });
1094
+ Object.defineProperty(exports, "PresetResolutionError", { enumerable: true, get: function () { return index_js_1.PresetResolutionError; } });
1095
+ Object.defineProperty(exports, "ResolvedModel", { enumerable: true, get: function () { return index_js_1.ResolvedModel; } });
1096
+ Object.defineProperty(exports, "RunResult", { enumerable: true, get: function () { return index_js_1.RunResult; } });
1097
+ Object.defineProperty(exports, "SpendCap", { enumerable: true, get: function () { return index_js_1.SpendCap; } });
1098
+ Object.defineProperty(exports, "SpendMeter", { enumerable: true, get: function () { return index_js_1.SpendMeter; } });
1099
+ Object.defineProperty(exports, "Tracer", { enumerable: true, get: function () { return index_js_1.Tracer; } });
1100
+ Object.defineProperty(exports, "Turn", { enumerable: true, get: function () { return index_js_1.Turn; } });
1101
+ Object.defineProperty(exports, "Usage", { enumerable: true, get: function () { return index_js_1.Usage; } });
1102
+ Object.defineProperty(exports, "buildAdapter", { enumerable: true, get: function () { return index_js_1.buildAdapter; } });
1103
+ Object.defineProperty(exports, "parseTraceparent", { enumerable: true, get: function () { return index_js_1.parseTraceparent; } });
1104
+ Object.defineProperty(exports, "resolvePreset", { enumerable: true, get: function () { return index_js_1.resolvePreset; } });
1105
+ var conflictQuestion_js_1 = require("./conflictQuestion.js");
1106
+ Object.defineProperty(exports, "ASKING_KINDS", { enumerable: true, get: function () { return conflictQuestion_js_1.ASKING_KINDS; } });
1107
+ Object.defineProperty(exports, "CONFLICT_QUESTION_MODES", { enumerable: true, get: function () { return conflictQuestion_js_1.CONFLICT_QUESTION_MODES; } });
1108
+ Object.defineProperty(exports, "DEFAULT_CONFLICT_QUESTION_MODE", { enumerable: true, get: function () { return conflictQuestion_js_1.DEFAULT_CONFLICT_QUESTION_MODE; } });
1109
+ Object.defineProperty(exports, "InMemoryConflictAskLedger", { enumerable: true, get: function () { return conflictQuestion_js_1.InMemoryConflictAskLedger; } });
1110
+ Object.defineProperty(exports, "QuestionPhrases", { enumerable: true, get: function () { return conflictQuestion_js_1.QuestionPhrases; } });
1111
+ Object.defineProperty(exports, "conflictKey", { enumerable: true, get: function () { return conflictQuestion_js_1.conflictKey; } });
1112
+ Object.defineProperty(exports, "decideConflictQuestion", { enumerable: true, get: function () { return conflictQuestion_js_1.decideConflictQuestion; } });
1113
+ Object.defineProperty(exports, "formatDay", { enumerable: true, get: function () { return conflictQuestion_js_1.formatDay; } });
1114
+ Object.defineProperty(exports, "normaliseMode", { enumerable: true, get: function () { return conflictQuestion_js_1.normaliseMode; } });
1115
+ Object.defineProperty(exports, "questionText", { enumerable: true, get: function () { return conflictQuestion_js_1.questionText; } });
1116
+ var memoryV1_js_2 = require("./memoryV1.js");
1117
+ Object.defineProperty(exports, "RECALL_LEVELS", { enumerable: true, get: function () { return memoryV1_js_2.RECALL_LEVELS; } });
1118
+ //# sourceMappingURL=index.js.map