UNPKG

configure

Version:

Identity layer SDK for AI agents

1,281 lines (1,266 loc) 67 kB
import { getActionTools, getAdvancedTools, getConnectorTools, getDefaultProfileTools, getToolsForOptions, getUITools, toOpenAIFunctions } from "./chunk-NWNVII7P.mjs"; // src/errors.ts var ErrorCode = { /** No API key provided to Configure */ API_KEY_MISSING: "API_KEY_MISSING", /** Token is missing, invalid, or expired (HTTP 401/403) */ AUTH_REQUIRED: "AUTH_REQUIRED", /** Input failed validation before reaching the API */ INVALID_INPUT: "INVALID_INPUT", /** The requested tool is not connected for this user (HTTP 400) */ TOOL_NOT_CONNECTED: "TOOL_NOT_CONNECTED", /** Network request failed (DNS, connection refused, etc.) */ NETWORK_ERROR: "NETWORK_ERROR", /** Too many requests (HTTP 429) */ RATE_LIMITED: "RATE_LIMITED", /** Resource not found (HTTP 404) */ NOT_FOUND: "NOT_FOUND", /** Server error (HTTP 5xx) */ SERVER_ERROR: "SERVER_ERROR", /** Request timed out */ TIMEOUT: "TIMEOUT", /** Not authorized for this resource (HTTP 403, distinct from AUTH_REQUIRED) */ ACCESS_DENIED: "ACCESS_DENIED", /** Tool operation failed (provider error, misconfiguration) */ TOOL_ERROR: "TOOL_ERROR", /** Billing/quota limit reached (HTTP 402) */ PAYMENT_REQUIRED: "PAYMENT_REQUIRED", /** A prior portable profile read must be committed before another portable read. */ COMMIT_REQUIRED: "COMMIT_REQUIRED" }; var ConfigureError = class _ConfigureError extends Error { constructor(code, message, statusCode, details) { super(message); this.name = "ConfigureError"; this.code = code; this.statusCode = statusCode; if (details) { this.type = details.type; this.param = details.param; this.retryable = details.retryable; this.suggestedAction = details.suggestedAction; this.docUrl = details.docUrl; this.retryAfter = details.retryAfter; this.requestId = details.requestId; } } /** * Create a ConfigureError from an HTTP response body. * Parses structured error responses ({ error: { type, code, message, ... } }) * and falls back to status-code mapping for legacy string responses. */ static fromResponse(status, body) { const err = body?.error; if (isStructuredError(err)) { const sdkCode = mapBackendToSdkCode(err.type, err.code); const message2 = typeof err.message === "string" ? err.message : "Request failed"; return new _ConfigureError(sdkCode, message2, status, { type: err.type, param: typeof err.param === "string" ? err.param : err.param === null ? null : void 0, retryable: typeof err.retryable === "boolean" ? err.retryable : void 0, suggestedAction: typeof err.suggested_action === "string" ? err.suggested_action : void 0, docUrl: typeof err.doc_url === "string" ? err.doc_url : void 0, retryAfter: typeof err.retry_after === "number" ? err.retry_after : void 0, requestId: typeof body.request_id === "string" ? body.request_id : void 0 }); } const message = typeof err === "string" ? err : typeof body?.message === "string" ? body.message : "Request failed"; return new _ConfigureError(mapStatusToSdkCode(status, message), message, status); } /** * Create a ConfigureError from a caught exception (network failures, timeouts). */ static fromCatch(error, fallbackMessage) { if (error instanceof _ConfigureError) return error; if (error instanceof Error) { if (error.name === "AbortError") { return new _ConfigureError(ErrorCode.TIMEOUT, "Request timed out"); } return new _ConfigureError(ErrorCode.NETWORK_ERROR, error.message); } return new _ConfigureError(ErrorCode.NETWORK_ERROR, fallbackMessage); } }; function isStructuredError(err) { return !!err && typeof err === "object" && !Array.isArray(err) && typeof err.type === "string" && typeof err.code === "string"; } function mapBackendToSdkCode(type, code) { switch (type) { case "authentication_error": return ErrorCode.AUTH_REQUIRED; case "invalid_request_error": return ErrorCode.INVALID_INPUT; case "permission_error": return code === "commit_required" ? ErrorCode.COMMIT_REQUIRED : ErrorCode.ACCESS_DENIED; case "tool_error": return code === "tool_not_connected" ? ErrorCode.TOOL_NOT_CONNECTED : ErrorCode.TOOL_ERROR; case "rate_limit_error": return code === "quota_exceeded" ? ErrorCode.PAYMENT_REQUIRED : ErrorCode.RATE_LIMITED; case "api_error": return ErrorCode.SERVER_ERROR; default: return ErrorCode.SERVER_ERROR; } } function mapStatusToSdkCode(status, message) { if (status === 401 || status === 403) return ErrorCode.AUTH_REQUIRED; if (status === 404) return ErrorCode.NOT_FOUND; if (status === 409 && message.toLowerCase().includes("commit")) return ErrorCode.COMMIT_REQUIRED; if (status === 429) return ErrorCode.RATE_LIMITED; if (status >= 500) return ErrorCode.SERVER_ERROR; const lower = message.toLowerCase(); if (lower.includes("not connected") || lower.includes("not linked")) return ErrorCode.TOOL_NOT_CONNECTED; return ErrorCode.INVALID_INPUT; } function classifyError(error) { if (error instanceof ConfigureError) { const friendly = { [ErrorCode.AUTH_REQUIRED]: "your session expired. please sign in again.", [ErrorCode.RATE_LIMITED]: "taking a breather \u2014 try again in a moment.", [ErrorCode.TIMEOUT]: "request timed out. try again.", [ErrorCode.NETWORK_ERROR]: "connection issue. check your network and try again.", [ErrorCode.SERVER_ERROR]: "something went wrong. try again.", [ErrorCode.ACCESS_DENIED]: "you don't have permission to do that.", [ErrorCode.TOOL_ERROR]: "something went wrong with the tool. try again.", [ErrorCode.PAYMENT_REQUIRED]: "usage quota exceeded. check your plan.", [ErrorCode.COMMIT_REQUIRED]: "commit the prior profile read before reading again." }; const msg = friendly[error.code]; if (msg) return new ConfigureError(error.code, msg, error.statusCode); return error; } const raw = error instanceof Error ? error.message : String(error); const lower = raw.toLowerCase(); if (lower.includes("rate_limit") || lower.includes("429") || lower.includes("too many")) { return new ConfigureError(ErrorCode.RATE_LIMITED, "taking a breather \u2014 try again in a moment."); } if (lower.includes("invalid_token") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("401")) { return new ConfigureError(ErrorCode.AUTH_REQUIRED, "your session expired. please sign in again."); } if (lower.includes("timeout") || lower.includes("aborted")) { return new ConfigureError(ErrorCode.TIMEOUT, "request timed out. try again."); } if (lower.includes("network") || lower.includes("fetch") || lower.includes("econnrefused")) { return new ConfigureError(ErrorCode.NETWORK_ERROR, "connection issue. check your network and try again."); } return new ConfigureError(ErrorCode.SERVER_ERROR, "something went wrong. try again."); } var E164_REGEX = /^\+[1-9]\d{6,14}$/; var OTP_REGEX = /^\d{6}$/; var PATH_TRAVERSAL_REGEX = /\.\.\//; function validateRequired(value, name) { if (!value || !value.trim()) { throw new ConfigureError(ErrorCode.INVALID_INPUT, `${name} is required`); } } function validatePhone(phone) { validateRequired(phone, "phone"); let s = phone.trim().replace(/[\s\-.()\u00A0]+/g, ""); if (!s.startsWith("+")) s = "+" + s; if (!E164_REGEX.test(s)) { throw new ConfigureError( ErrorCode.INVALID_INPUT, `Invalid phone number "${phone}". Must be in E.164 format (e.g., "+14155551234").` ); } return s; } function validateOtpCode(code) { validateRequired(code, "code"); if (!OTP_REGEX.test(code)) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "OTP code must be exactly 6 digits"); } } function validatePath(path) { validateRequired(path, "path"); if (PATH_TRAVERSAL_REGEX.test(path)) { throw new ConfigureError(ErrorCode.INVALID_INPUT, 'Path must not contain "../" (path traversal)'); } } var VALID_CONNECTOR_TYPES = /* @__PURE__ */ new Set(["gmail", "calendar", "drive", "notion"]); function validateConnectorType(connector) { validateRequired(connector, "connector"); if (!VALID_CONNECTOR_TYPES.has(connector)) { throw new ConfigureError( ErrorCode.INVALID_INPUT, `Invalid connector "${connector}". Must be one of: ${[...VALID_CONNECTOR_TYPES].join(", ")}` ); } } // src/auth.ts var AuthModule = class { constructor(baseUrl, appKey, fetchFn, timeout, agent) { this.baseUrl = baseUrl; this.appKey = appKey; this.fetchFn = fetchFn; this.timeout = timeout; this.agent = agent; } createAbortSignal() { if (!this.timeout) return { clear: () => { } }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), this.timeout); return { signal: controller.signal, clear: () => clearTimeout(id) }; } /** * Server/headless OTP method. Browser integrations should use `Configure.link()` * from the hosted iframe script instead. * * Sends an OTP to a phone number. * @param phone - Phone number in E.164 format (e.g., "+14155551234") */ async sendOtp(phone) { const normalized = validatePhone(phone); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/auth/otp/start`, { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": this.appKey }, body: JSON.stringify({ phone: normalized }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return { ok: true }; } catch (error) { throw ConfigureError.fromCatch(error, "OTP start failed"); } finally { abort.clear(); } } /** * Server/headless OTP method. Browser integrations should use `Configure.link()` * from the hosted iframe script instead. * * Verifies an OTP code. * @param phone - Phone number in E.164 format * @param code - 6-digit OTP code */ async verifyOtp(phone, code, options) { const normalized = validatePhone(phone); validateOtpCode(code); const abort = this.createAbortSignal(); try { const headers = { "Content-Type": "application/json", "X-API-Key": this.appKey }; if (options?.embed) { headers["X-Configure-Embed"] = "1"; } const response = await this.fetchFn(`${this.baseUrl}/v1/auth/otp/verify`, { method: "POST", headers, body: JSON.stringify({ phone: normalized, code, ...this.agent ? { agent: this.agent } : {}, ...options?.externalId ? { external_id: options.externalId } : {} }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const json = await response.json(); return { token: json.token, userId: json.user_id, ...json.embed_receipt ? { embedReceipt: json.embed_receipt } : {} }; } catch (error) { throw ConfigureError.fromCatch(error, "OTP verification failed"); } finally { abort.clear(); } } /** * Get a demo authentication token (for development only) * @returns Promise resolving to demo token */ async getDemo() { const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/auth/demo`, { method: "POST", headers: { "X-API-Key": this.appKey }, signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const json = await response.json(); return json.user_token; } catch (error) { throw ConfigureError.fromCatch(error, "Demo auth failed"); } finally { abort.clear(); } } }; // src/files.ts var FilesModule = class { constructor(baseUrl, appKey, fetchFn, timeout, defaultExternalId) { this.baseUrl = baseUrl; this.appKey = appKey; this.fetchFn = fetchFn; this.timeout = timeout; this.defaultExternalId = defaultExternalId; } getHeaders(token, externalId) { const headers = { "Content-Type": "application/json", "X-API-Key": this.appKey }; if (token) { headers.Authorization = `Bearer ${token}`; } else if (externalId) { headers["X-User-Id"] = externalId; } return headers; } resolveUserId(userId, externalId) { const resolved = userId || externalId || this.defaultExternalId; if (!resolved) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "externalId or userId is required for raw file operations"); } return resolved; } profileUrl(userId, suffix) { return `${this.baseUrl}/v1/profile/${encodeURIComponent(userId)}${suffix}`; } createAbortSignal() { if (!this.timeout) return { clear: () => { } }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), this.timeout); return { signal: controller.signal, clear: () => clearTimeout(id) }; } async list(options = {}) { const userId = this.resolveUserId(options.userId, options.externalId); const path = options.path ?? "/"; const headers = this.getHeaders(options.token, userId); if (!path || path === "/") { const abort2 = this.createAbortSignal(); try { const params2 = new URLSearchParams({ path: "/" }); if (options.depth !== void 0) params2.append("depth", String(options.depth)); if (options.limit !== void 0) params2.append("limit", String(options.limit)); const response = await this.fetchFn(`${this.profileUrl(userId, "")}?${params2.toString()}`, { headers, signal: abort2.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to list files"); } finally { abort2.clear(); } } const params = new URLSearchParams({ path }); if (options.depth !== void 0) params.append("depth", String(options.depth)); if (options.limit !== void 0) params.append("limit", String(options.limit)); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.profileUrl(userId, "")}?${params.toString()}`, { headers, signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to list files"); } finally { abort.clear(); } } async read(options) { const userId = this.resolveUserId(options.userId, options.externalId); validateRequired(options.path, "path"); const params = new URLSearchParams({ path: options.path }); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.profileUrl(userId, "/read")}?${params.toString()}`, { headers: this.getHeaders(options.token, userId), signal: abort.signal }); if (!response.ok) { if (response.status === 404) return null; const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const result = await response.json(); return result || null; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to read file"); } finally { abort.clear(); } } async write(options) { const userId = this.resolveUserId(options.userId, options.externalId); validateRequired(options.path, "path"); validatePath(options.path); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(this.profileUrl(userId, "/write"), { method: "PUT", headers: this.getHeaders(options.token, userId), body: JSON.stringify({ path: options.path, content: options.content, type: options.type || "markdown", mode: options.mode || "overwrite" }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to write file"); } finally { abort.clear(); } } async search(options) { const userId = this.resolveUserId(options.userId, options.externalId); validateRequired(options.query, "query"); const params = new URLSearchParams({ query: options.query }); const scope = options.path || options.scope; if (scope) params.append("scope", scope); if (options.limit !== void 0) params.append("limit", String(options.limit)); if (options.filesOnly !== void 0) params.append("files_only", String(options.filesOnly)); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.profileUrl(userId, "/search")}?${params.toString()}`, { headers: this.getHeaders(options.token, userId), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to search files"); } finally { abort.clear(); } } async delete(options) { const userId = this.resolveUserId(options.userId, options.externalId); validateRequired(options.path, "path"); const params = new URLSearchParams({ path: options.path }); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.profileUrl(userId, "")}?${params.toString()}`, { method: "DELETE", headers: this.getHeaders(options.token, userId), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to delete file"); } finally { abort.clear(); } } }; // src/imports.ts var ImportRequester = class { constructor(baseUrl, apiKey, fetchFn, timeout) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.fetchFn = fetchFn; this.timeout = timeout; } async importProfiles(input) { const response = await this.request("/v1/import/profiles", { method: "POST", body: JSON.stringify(input) }); return response.json(); } async getJob(jobId) { const response = await this.request(`/v1/import/jobs/${encodeURIComponent(jobId)}`, { method: "GET" }); return response.json(); } async request(path, init) { const abort = this.createAbortSignal(); try { const headers = new Headers(init.headers); headers.set("Content-Type", "application/json"); headers.set("X-API-Key", this.apiKey); const response = await this.fetchFn(`${this.baseUrl}${path}`, { ...init, headers, signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to import profiles"); } finally { abort.clear(); } } createAbortSignal() { if (!this.timeout) return { clear: () => { } }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), this.timeout); return { signal: controller.signal, clear: () => clearTimeout(id) }; } }; var ImportJobsModule = class { constructor(requester) { this.requester = requester; } get(jobId) { return this.requester.getJob(jobId); } }; // src/guidelines.ts var CONFIGURE_GUIDELINES = `CONFIGURE GUIDELINES \u2014 handling personal data responsibly GROUNDING: - Only state specific facts (dates, names, amounts, locations) if they appear in the user's profile context or tool results. - Never fabricate or assume personal information. If you lack data, say so. - If the profile context seems incomplete, use profile or connector tools to retrieve data \u2014 do not guess. - Resolve relative dates like "yesterday", "last week", and "last month" against the runtime's current date/time before searching profile or connector data. TRANSPARENCY: - When referencing personal data, briefly cite your source: "from your profile", "from another agent", "from your Gmail", "from your calendar", etc. - If the user asks how you know something, explain clearly \u2014 you found it in their connected data. - When using web search results, include source URLs so users can verify. PROFILE SOURCES: - For "what does <agent/source> know about me?", use configure_profile_search with query "*" and the explicit source handle. - For "what did I talk about last week/month/yesterday?", search with date filters after resolving the date range. - For "what agents have profile data about me?", use configure_profile_read with sections ["agents"] or list-style configure_profile_search with query "*". - If results are filtered or hiddenSources is present, do not reveal hidden contents. It is okay to say that a source exists but is hidden from this agent. - Do not confuse profile sources with connectors. Profile sources are agents that wrote profile data; connectors are external accounts like Gmail, Calendar, Drive, and Notion. CONNECTORS: - When a connector-backed tool returns a connection error or "not connected" status, do not echo the error. Let the user know naturally \u2014 e.g. "I'd need access to your calendar for that \u2014 would you like to connect it?" - Do not repeatedly prompt the user to connect accounts they have already declined or been asked about. CONVERSATION EFFICIENCY: - Check conversation history before re-searching \u2014 you may have already retrieved the data. - If a search didn't find what the user needs, try different queries with synonyms or alternative phrasing.`; // src/format.ts function unwrapField(field) { if (field === null || field === void 0) return void 0; if (typeof field === "string") return field; if (typeof field === "object" && "value" in field) { return field.value; } return void 0; } function formatProfile(profile, options) { const parts = []; const includeTools = options?.includeTools ?? false; const identity = profile.identity || {}; const identityParts = []; const fields = [ ["Name", unwrapField(identity.name) || ""], ["Email", unwrapField(identity.email) || ""], ["Phone", unwrapField(identity.phone_last4) ? `...${unwrapField(identity.phone_last4)}` : ""], ["Occupation", unwrapField(identity.occupation) || ""], ["Location", unwrapField(identity.location) || ""], ["Bio", unwrapField(identity.bio) || ""] ]; for (const [label, value] of fields) { if (value) identityParts.push(`${label}: ${value}`); } const interests = identity.interests; if (Array.isArray(interests) && interests.length > 0) { identityParts.push(`Interests: ${interests.join(", ")}`); } if (identityParts.length > 0) { parts.push(`User: ${identityParts.join("\n")}`); } const connectedConnectors = Object.entries(profile.integrations || {}).filter(([, data]) => data.connected).map(([name]) => name); if (connectedConnectors.length > 0) { parts.push(`Connected: ${connectedConnectors.join(", ")}`); } if (profile.preferences && profile.preferences.length > 0) { parts.push(`Preferences: ${profile.preferences.map((p) => `- ${p}`).join("\n")}`); } if (profile.summary) { parts.push(`About this user: ${profile.summary}`); } if (includeTools) { const gmail = profile.integrations?.gmail; if (gmail?.connected) { if (gmail.synthesis?.summary) { const factLines = (gmail.synthesis.facts || []).map((f) => `- ${f.fact}`).join("\n"); parts.push(`Gmail insights: ${gmail.synthesis.summary}${factLines ? ` Key facts: ${factLines}` : ""}`); } if (gmail.ranked && gmail.ranked.length > 0) { const gmailLines = gmail.ranked.slice(0, 15).map((item) => { const subject = item.subject || ""; const from = item.from || ""; const date = item.date || ""; return `\u2022 ${subject}${from ? ` (from: ${from})` : ""}${date ? ` [${date}]` : ""}`; }).filter(Boolean); if (gmailLines.length > 0) { parts.push(`Gmail profile (top emails by importance): ${gmailLines.join("\n")}`); } } } const calendar = profile.integrations?.calendar; if (calendar?.connected && calendar.events && calendar.events.length > 0) { const events = calendar.events.slice(0, 5); parts.push(`Calendar: ${events.map((e) => `\u2022 ${e.summary || e.title}`).join("\n")}`); } const drive = profile.integrations?.drive; if (drive?.connected && drive.files && drive.files.length > 0) { const files = drive.files.slice(0, 5); parts.push(`Drive files: ${files.map((f) => `\u2022 ${f.name || f.title}`).join("\n")}`); } const notion = profile.integrations?.notion; if (notion?.connected && notion.pages && notion.pages.length > 0) { const pages = notion.pages.slice(0, 5); parts.push(`Notion pages: ${pages.map((p) => `\u2022 ${p.title || p.name}`).join("\n")}`); } } const agents = profile.agents || {}; const allMemories = []; for (const [agent, agentData] of Object.entries(agents)) { const memories = agentData.memories; if (memories && memories.length > 0) { allMemories.push({ app: agent, content: memories.map((m) => m.content).join("\n") }); } } if (allMemories.length > 0) { const memsText = allMemories.map((m) => `${m.app}: ${m.content}`).join("\n"); parts.push(`Memories: ${memsText}`); } const result = parts.join("\n\n"); const includeGuidelines = options?.guidelines ?? true; if (includeGuidelines) { return result ? `${result} ${CONFIGURE_GUIDELINES}` : CONFIGURE_GUIDELINES; } return result; } // src/profile.ts var MAX_COMMIT_MESSAGES = 20; var MAX_COMMIT_MESSAGE_CHARS = 16e3; var MAX_COMMIT_TOTAL_CHARS = 5e4; var OBLIGATION_METADATA = /* @__PURE__ */ Symbol("configure.obligationMetadata"); var ProfileRequester = class { constructor(baseUrl, appKey, fetchFn, timeout, defaultExternalId) { this.baseUrl = baseUrl; this.appKey = appKey; this.fetchFn = fetchFn; this.timeout = timeout; this.defaultExternalId = defaultExternalId; } getHeaders(token, externalId, correlation) { const headers = { "Content-Type": "application/json", "X-API-Key": this.appKey }; if (token) { headers.Authorization = `Bearer ${token}`; } else if (externalId) { headers["X-User-Id"] = externalId; } if (correlation?.sessionId) { headers["X-Configure-Session-Id"] = correlation.sessionId; } if (correlation?.runtimeScopeId) { headers["X-Configure-Runtime-Scope-Id"] = correlation.runtimeScopeId; } if (correlation?.readId) { headers["X-Configure-Read-Id"] = correlation.readId; } return headers; } resolveSubject(runtime) { if (runtime.token) { return { token: runtime.token }; } const externalId = runtime.externalId || this.defaultExternalId; if (!externalId) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "profile requires token for linked users or externalId for unlinked users"); } return { externalId }; } createAbortSignal() { if (!this.timeout) return { clear: () => { } }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), this.timeout); return { signal: controller.signal, clear: () => clearTimeout(id) }; } async read(runtime, options = {}, correlation) { const subject = this.resolveSubject(runtime); const params = new URLSearchParams(); if (options.sections) params.append("sections", options.sections.join(",")); const abort = this.createAbortSignal(); try { const query = params.toString(); const response = await this.fetchFn(`${this.baseUrl}/v1/profile${query ? `?${query}` : ""}`, { method: "GET", headers: this.getHeaders(subject.token, subject.externalId, correlation), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const data = await response.json(); const { obligations: _ignoredObligations, ...rawProfileData } = data; const profileData = rawProfileData; const profile = Object.assign(profileData, { format(formatOptions) { return formatProfile(profileData, formatOptions); } }); const result = { profile, portable: Boolean(rawProfileData.portable ?? rawProfileData.linked ?? subject.token), filtered: Boolean(rawProfileData.filtered), hiddenSources: arrayValue(rawProfileData.hidden_sources) || arrayValue(rawProfileData.hiddenSources) }; attachObligationMetadata(result, obligationMetadataFromHeaders(response.headers)); return result; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to read profile"); } finally { abort.clear(); } } async search(runtime, options = {}, correlation) { const subject = this.resolveSubject(runtime); const searchOptions = typeof options === "string" ? { query: options } : options; const params = new URLSearchParams(); if (searchOptions.query) params.append("query", searchOptions.query); if (searchOptions.source) params.append("source", searchOptions.source); if (searchOptions.from) params.append("from", searchOptions.from); if (searchOptions.to) params.append("to", searchOptions.to); if (searchOptions.limit !== void 0) params.append("limit", String(searchOptions.limit)); if (searchOptions.detail) params.append("detail", searchOptions.detail); const abort = this.createAbortSignal(); try { const query = params.toString(); const response = await this.fetchFn(`${this.baseUrl}/v1/profile/search${query ? `?${query}` : ""}`, { method: "GET", headers: this.getHeaders(subject.token, subject.externalId, correlation), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const data = await response.json(); const rawResults = Array.isArray(data.results) ? data.results : Array.isArray(data.memories) ? data.memories : []; const result = { results: rawResults.map((result2, index) => toProfileSearchHit(result2, index)), filtered: Boolean(data.filtered), hiddenSources: arrayValue(data.hidden_sources) || arrayValue(data.hiddenSources) || arrayValue(data.hiddenAgents) }; attachObligationMetadata(result, obligationMetadataFromHeaders(response.headers)); return result; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to search profile"); } finally { abort.clear(); } } async remember(runtime, fact) { const subject = this.resolveSubject(runtime); if (typeof fact !== "string") { throw new ConfigureError(ErrorCode.INVALID_INPUT, "profile.remember accepts one fact string, not messages"); } validateRequired(fact, "fact"); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/profile/remember`, { method: "POST", headers: this.getHeaders(subject.token, subject.externalId), body: JSON.stringify({ fact }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to remember"); } finally { abort.clear(); } } async commit(runtime, input, correlation) { const subject = this.resolveSubject(runtime); const packet = normalizeCommitPacket(input); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/profile/commit`, { method: "POST", headers: this.getHeaders(subject.token, subject.externalId, correlation), body: JSON.stringify({ ...packet.messages.length > 0 ? { messages: packet.messages } : {}, ...packet.memories.length > 0 ? { memories: packet.memories } : {}, ...packet.toolResults.length > 0 ? { toolResults: packet.toolResults } : {}, ...correlation?.readId ? { read_id: correlation.readId } : {}, ...correlation?.sessionId ? { session_id: correlation.sessionId } : {}, ...correlation?.runtimeScopeId ? { runtime_scope_id: correlation.runtimeScopeId } : {}, memory_criteria: "Extract durable user memories from this bounded runtime commit.", sync: input.sync }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const result = await response.json(); return { status: result.status, facts_written: result.facts_written || result.memories_written || [], user_summary: result.user_summary, memories_written: result.memories_written || [], obligations_committed: result.obligations_committed || [], rejected_memories: result.rejected_memories || [] }; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to commit profile packet"); } finally { abort.clear(); } } async generateDocuments(runtime, documents) { const subject = this.resolveSubject(runtime); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/profile/documents/generate`, { method: "POST", headers: this.getHeaders(subject.token, subject.externalId), body: JSON.stringify({ documents }), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } return response.json(); } catch (error) { throw ConfigureError.fromCatch(error, "Failed to generate documents"); } finally { abort.clear(); } } }; var ProfileRuntime = class { constructor(requester, connectorRequester, filesRequester, runtime) { this.requester = requester; this.connectorRequester = connectorRequester; this.filesRequester = filesRequester; this.runtime = runtime; this.enabledToolNames = new Set(getDefaultProfileTools().map((tool) => tool.name)); this.pendingReadIds = /* @__PURE__ */ new Set(); this.read = async (options) => { const result = await this.requester.read(this.runtime, options, this.correlation()); this.recordObligations(getObligationMetadata(result)); return result; }; this.search = async (options = {}) => { const result = await this.requester.search(this.runtime, options, this.correlation()); this.recordObligations(getObligationMetadata(result)); return result; }; this.remember = (fact) => { return this.requester.remember(this.runtime, fact); }; this.commit = async (input) => { const result = await this.requester.commit(this.runtime, input, this.correlation()); if (result.status === "completed" || result.obligations_committed?.length || input.memories?.length) { this.pendingReadIds.clear(); this.runtimeScopeId = createRuntimeId("scope"); } return result; }; this.tools = (options = {}) => { const tools = getToolsForOptions(options); this.enabledToolNames = new Set(tools.map((tool) => tool.name)); return tools; }; this.executeTool = async (toolCall) => { const { name, args } = parseToolCall(toolCall); if (!this.enabledToolNames.has(name)) { throw new ConfigureError(ErrorCode.ACCESS_DENIED, `Tool ${name} is not enabled for this profile runtime`); } switch (name) { case "configure_profile_read": return this.read({ sections: args.sections }); case "configure_profile_search": return this.search({ query: typeof args.query === "string" ? args.query : void 0, source: args.source, from: args.from, to: args.to, limit: numberValue(args.limit), detail: args.detail === "full" ? "full" : args.detail === "compact" ? "compact" : void 0 }); case "configure_profile_remember": return this.remember(String(args.fact || "")); case "configure_gmail_search": return this.connectorRequester.searchEmails(this.runtime.token, String(args.query || ""), { maxResults: numberValue(args.max_results) }); case "configure_calendar_get": return this.connectorRequester.getCalendar( this.runtime.token, args.range || "week" ); case "configure_drive_search": return this.connectorRequester.searchFiles(this.runtime.token, String(args.query || ""), { maxResults: numberValue(args.max_results) }); case "configure_notion_search": return this.connectorRequester.searchNotes(this.runtime.token, String(args.query || ""), { maxResults: numberValue(args.max_results) }); case "configure_email_send": return this.connectorRequester.sendEmail(this.runtime.token, { to: String(args.to || ""), subject: String(args.subject || ""), body: String(args.body || "") }); case "configure_calendar_create_event": return this.connectorRequester.createCalendarEvent(this.runtime.token, { title: String(args.title || ""), startTime: String(args.start_time || ""), endTime: String(args.end_time || ""), description: args.description, location: args.location }); case "configure_profile_commit": return this.commit({ messages: args.messages, memories: args.memories }); case "configure_file_read": return this.filesRequester.read({ token: this.runtime.token, externalId: this.runtime.externalId, path: String(args.path || "") }); case "configure_file_write": return this.filesRequester.write({ token: this.runtime.token, externalId: this.runtime.externalId, path: String(args.path || ""), content: String(args.content || ""), type: args.type, mode: args.mode }); case "configure_file_list": return this.filesRequester.list({ token: this.runtime.token, externalId: this.runtime.externalId, path: args.path, depth: numberValue(args.depth), limit: numberValue(args.limit) }); case "configure_file_search": return this.filesRequester.search({ token: this.runtime.token, externalId: this.runtime.externalId, query: String(args.query || ""), path: args.path, limit: numberValue(args.limit) }); case "configure_file_delete": return this.filesRequester.delete({ token: this.runtime.token, externalId: this.runtime.externalId, path: String(args.path || "") }); default: throw new ConfigureError(ErrorCode.INVALID_INPUT, `Unknown Configure tool: ${name}`); } }; this.sessionId = internalString(runtime, "sessionId") || createRuntimeId("session"); this.runtimeScopeId = internalString(runtime, "runtimeScopeId") || createRuntimeId("scope"); } correlation() { const readId = this.pendingReadIds.values().next().value; return { sessionId: this.sessionId, runtimeScopeId: this.runtimeScopeId, ...readId ? { readId } : {} }; } recordObligations(obligations) { if (!obligations || typeof obligations !== "object") return; const readIds = obligations.readIds; if (!Array.isArray(readIds)) return; for (const readId of readIds) { if (typeof readId === "string" && readId) { this.pendingReadIds.add(readId); } } } }; function parseToolCall(toolCall) { const name = toolCall.name || toolCall.function?.name; if (!name) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "Tool call name is required"); } const rawArgs = toolCall.arguments ?? toolCall.input ?? toolCall.function?.arguments ?? {}; if (typeof rawArgs === "string") { try { return { name, args: JSON.parse(rawArgs) }; } catch { throw new ConfigureError(ErrorCode.INVALID_INPUT, "Tool call arguments must be valid JSON"); } } return { name, args: rawArgs }; } function normalizeCommitPacket(input) { if (!input || typeof input !== "object") { throw new ConfigureError(ErrorCode.INVALID_INPUT, "commit input is required"); } const messages = input.messages ? [...input.messages] : []; if (input.response !== void 0) { messages.push({ role: "assistant", content: typeof input.response === "string" ? input.response : JSON.stringify(input.response) }); } if (messages.length > MAX_COMMIT_MESSAGES) { throw new ConfigureError(ErrorCode.INVALID_INPUT, `profile.commit accepts at most ${MAX_COMMIT_MESSAGES} messages`); } let totalChars = 0; const normalizedMessages = messages.map((message) => { if (!message.content) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "commit messages must include content"); } if (message.content.length > MAX_COMMIT_MESSAGE_CHARS) { throw new ConfigureError(ErrorCode.INVALID_INPUT, `commit message content must be ${MAX_COMMIT_MESSAGE_CHARS} characters or fewer`); } totalChars += message.content.length; return { role: message.role, content: message.content, ...message.toolName ? { toolName: message.toolName } : {} }; }); const memories = (input.memories || []).map((memory) => { if (typeof memory !== "string" || !memory.trim()) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "commit memories must be non-empty strings"); } if (memory.length > 1e3) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "commit memories must be 1000 characters or fewer"); } totalChars += memory.length; return memory.trim(); }); if (memories.length > 20) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "profile.commit accepts at most 20 memories"); } const toolResults = (input.toolResults || []).map((result) => { const content = typeof result.content === "string" ? result.content : JSON.stringify(result.content); if (!result.toolName || !content) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "commit toolResults require toolName and content"); } totalChars += result.toolName.length + content.length; return { toolName: result.toolName, content: result.content }; }); if (totalChars === 0) { throw new ConfigureError(ErrorCode.INVALID_INPUT, "profile.commit requires bounded source material"); } if (totalChars > MAX_COMMIT_TOTAL_CHARS) { throw new ConfigureError(ErrorCode.INVALID_INPUT, `profile.commit packet must be ${MAX_COMMIT_TOTAL_CHARS} characters or fewer`); } return { messages: normalizedMessages, memories, toolResults }; } function attachObligationMetadata(result, obligations) { if (!obligations) return; Object.defineProperty(result, OBLIGATION_METADATA, { value: obligations, enumerable: false, configurable: false }); } function obligationMetadataFromHeaders(headers) { const readIdHeader = headers.get("X-Configure-Read-Id") || headers.get("X-Configure-Read-Ids"); const readIds = readIdHeader ? readIdHeader.split(",").map((readId) => readId.trim()).filter(Boolean) : []; const sessionId = headers.get("X-Configure-Session-Id") || void 0; const runtimeScopeId = headers.get("X-Configure-Runtime-Scope-Id") || void 0; const commitRequired = headers.get("X-Configure-Commit-Required") === "1"; if (!commitRequired && readIds.length === 0 && !sessionId && !runtimeScopeId) { return void 0; } return { requiresCommit: commitRequired || readIds.length > 0, readIds, sessionId, runtimeScopeId }; } function getObligationMetadata(result) { return result[OBLIGATION_METADATA]; } function toProfileSearchHit(result, index) { const source = stringValue(result.source) || stringValue(result.agent) || sourceFromPath(stringValue(result.path)) || "unknown"; const text = stringValue(result.text) || stringValue(result.content) || stringValue(result.snippet) || ""; const hit = { id: stringValue(result.id) || `${source}:${stringValue(result.date) || index}`, title: stringValue(result.title), text, snippet: stringValue(result.snippet) || text.slice(0, 240), source, written_by: stringValue(result.written_by), created_at: stringValue(result.created_at), updated_at: stringValue(result.updated_at), event_at: stringValue(result.event_at) || stringValue(result.date), score: numberValue(result.score), type: stringValue(result.type) }; const path = stringValue(result.path); if (path) hit.path = path; const markers = arrayValue(result.markers); if (markers) hit.markers = markers; if (result.provenance && typeof result.provenance === "object" && !Array.isArray(result.provenance)) { hit.provenance = result.provenance; } return hit; } function sourceFromPath(path) { const match = path?.match(/^\/agents\/([^/]+)/); return match?.[1]; } function stringValue(value) { return typeof value === "string" && value.length > 0 ? value : void 0; } function numberValue(value) { return typeof value === "number" ? value : void 0; } function arrayValue(value) { if (!Array.isArray(value)) return void 0; return value.filter((item) => typeof item === "string"); } function createRuntimeId(prefix) { const random = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; return `${prefix}_${random}`; } function internalString(runtime, key) { const value = runtime[key]; return typeof value === "string" && value.trim() ? value.trim() : void 0; } // src/tools.ts var ConnectorsModule = class { constructor(baseUrl, appKey, fetchFn, timeout) { this.baseUrl = baseUrl; this.appKey = appKey; this.fetchFn = fetchFn; this.timeout = timeout; } getHeaders(authToken) { const headers = { "Content-Type": "application/json", "X-API-Key": this.appKey }; if (authToken) { headers["Authorization"] = `Bearer ${authToken}`; } return headers; } createAbortSignal() { if (!this.timeout) return { clear: () => { } }; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), this.timeout); return { signal: controller.signal, clear: () => clearTimeout(id) }; } // ============================================ // Connection Management // ============================================ /** * List all available connectors and their connection status */ async list(authToken) { validateRequired(authToken, "authToken"); const abort = this.createAbortSignal(); try { const response = await this.fetchFn(`${this.baseUrl}/v1/connectors`, { headers: this.getHeaders(authToken), signal: abort.signal }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw ConfigureError.fromResponse(response.status, body); } const result = await response.json(); return { connectors: result.connectors || [] }; } catch (error) { throw ConfigureError.fromCatch(error, "Failed to list connectors"); } finally { abort.clear(); } } /** * Start connecting a connector (initiates OAuth flow) * * @example * ```typescript * const { url, connectionRequestId } = await configure.connectors.connect(token, 'gmail', 'myapp://callback'); * ``` */ async connect(authToken, connector, callbackUrl, forceNew)