UNPKG

configure

Version:

Identity layer SDK for AI agents

1,267 lines (1,251 loc) 80.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CONFIGURE_GUIDELINES: () => CONFIGURE_GUIDELINES, Configure: () => Configure, ConfigureError: () => ConfigureError, ErrorCode: () => ErrorCode, classifyError: () => classifyError, getActionTools: () => getActionTools, getAdvancedTools: () => getAdvancedTools, getConnectorTools: () => getConnectorTools, getDefaultProfileTools: () => getDefaultProfileTools, getToolsForOptions: () => getToolsForOptions, getUITools: () => getUITools, toOpenAIFunctions: () => toOpenAIFunctions, validateAgentHandle: () => validateAgentHandle }); module.exports = __toCommonJS(index_exports); // 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/tool-definitions.ts function cloneTool(tool) { return { name: tool.name, description: tool.description, input_schema: { ...tool.input_schema, properties: { ...tool.input_schema.properties }, required: tool.input_schema.required ? [...tool.input_schema.required] : void 0 } }; } var DEFAULT_PROFILE_TOOLS = [ { name: "configure_profile_read", description: 'Read compact approved profile context for the current user. Use this for a broad profile overview; use sections ["agents"] to inspect readable agent/source namespaces. This is not a raw file/path reader and not a connector list. Use configure_profile_search for source-attributed memories or facts.', input_schema: { type: "object", properties: { sections: { type: "array", items: { type: "string", enum: ["identity", "preferences", "integrations", "agents", "summary"] }, description: "Optional section filter. Omit to read the approved compact default profile. Allowed values: identity, preferences, integrations, agents, summary. This is not a path selector." } } } }, { name: "configure_profile_search", description: 'Search or list permitted attributed profile data for the current user. Omit query or pass "*" to list bounded permitted compact results. For "what does <source> know about me?", pass query "*" plus source, e.g. "tempo". For relative-date questions, resolve dates first and pass from/to. Compact results omit raw CFS paths; pass detail "full" only when you need inspectable metadata.', input_schema: { type: "object", properties: { query: { type: "string", description: 'Optional search query. Omit or use "*" to list bounded permitted attributed profile results.' }, source: { type: "string", description: 'Optional explicit source handle filter, such as "tempo". This is not a path or connector and there is no magic self value.' }, from: { type: "string", description: "Optional inclusive start date filter in YYYY-MM-DD format for date-attributed results." }, to: { type: "string", description: "Optional inclusive end date filter in YYYY-MM-DD format for date-attributed results." }, limit: { type: "number", description: "Optional maximum result count. The backend enforces a default and hard cap." }, detail: { type: "string", enum: ["compact", "full"], description: "Optional result detail. Defaults to compact. Full includes safe path, markers, provenance, and updated_at metadata." } } } }, { name: "configure_profile_remember", description: "Save one explicit durable user fact, preference, or memory for the current user under the API-key-resolved acting agent handle's namespace. Do not use for raw transcripts or message arrays.", input_schema: { type: "object", properties: { fact: { type: "string", description: "Required explicit durable memory text to save for the current user." } }, required: ["fact"] } } ]; var CONNECTOR_TOOL_BY_CONNECTOR = { gmail: { name: "configure_gmail_search", description: "Search the user's connected Gmail account when Gmail is connected and this connector tool is enabled. Returns email search results, not stored profile memories.", input_schema: { type: "object", properties: { query: { type: "string", description: "Required Gmail search query." }, max_results: { type: "number", description: "Optional maximum result count. The backend enforces a default and hard cap." } }, required: ["query"] } }, calendar: { name: "configure_calendar_get", description: "Get events from the user's connected calendar when Calendar is connected and this connector tool is enabled.", input_schema: { type: "object", properties: { range: { type: "string", enum: ["today", "tomorrow", "week", "month"], description: "Optional bounded calendar range to retrieve. Omit to use week." } } } }, drive: { name: "configure_drive_search", description: "Search the user's connected Google Drive files when Drive is connected and this connector tool is enabled. Returns file search results, not stored profile memories.", input_schema: { type: "object", properties: { query: { type: "string", description: "Required Drive search query." }, max_results: { type: "number", description: "Optional maximum result count. The backend enforces a default and hard cap." } }, required: ["query"] } }, notion: { name: "configure_notion_search", description: "Search the user's connected Notion pages when Notion is connected and this connector tool is enabled. Returns page search results, not stored profile memories.", input_schema: { type: "object", properties: { query: { type: "string", description: "Required Notion search query." }, max_results: { type: "number", description: "Optional maximum result count. The backend enforces a default and hard cap." } }, required: ["query"] } } }; var ACTION_TOOL_BY_ACTION = { "email.send": { name: "configure_email_send", description: "Send an email from the user's connected Gmail account. This is an action and may require user or runtime approval.", input_schema: { type: "object", properties: { to: { type: "string", description: "Required recipient email address." }, subject: { type: "string", description: "Required email subject." }, body: { type: "string", description: "Required email body." } }, required: ["to", "subject", "body"] } }, "calendar.create_event": { name: "configure_calendar_create_event", description: "Create an event in the user's connected calendar. This is an action and may require user or runtime approval.", input_schema: { type: "object", properties: { title: { type: "string", description: "Required event title." }, start_time: { type: "string", description: "Required event start time in ISO 8601 format, including timezone or offset when known." }, end_time: { type: "string", description: "Required event end time in ISO 8601 format, including timezone or offset when known." }, description: { type: "string", description: "Optional event description." }, location: { type: "string", description: "Optional event location." } }, required: ["title", "start_time", "end_time"] } } }; var ADVANCED_COMMIT_TOOL = { name: "configure_profile_commit", description: "Submit bounded source material for a prior Configure profile read/search. This is advanced adapter/runtime plumbing, not a default model tool and not a bulk import tool.", input_schema: { type: "object", properties: { read_id: { type: "string", description: "Optional read id returned by a prior profile read/search when the runtime provides one." }, messages: { type: "array", items: { type: "object" }, description: "Bounded surrounding turn messages to process for durable user memories." }, memories: { type: "array", items: { type: "string" }, description: "Explicit memory candidates to attribute to the API-key-resolved acting agent." } } } }; var ADVANCED_FILE_TOOLS = [ { name: "configure_file_read", description: "Advanced: read a raw profile file path.", input_schema: { type: "object", properties: { path: { type: "string", description: "Profile file path to read." } }, required: ["path"] } }, { name: "configure_file_write", description: "Advanced: write raw content to a profile file path.", input_schema: { type: "object", properties: { path: { type: "string", description: "Profile file path to write." }, content: { type: "string", description: "Content to write." }, type: { type: "string", enum: ["markdown", "json"], description: "Content type." }, mode: { type: "string", enum: ["overwrite", "append", "merge"], description: "Write mode." } }, required: ["path", "content"] } }, { name: "configure_file_list", description: "Advanced: list raw profile files under a profile file path.", input_schema: { type: "object", properties: { path: { type: "string", description: "Profile file path to list." }, depth: { type: "number", description: "Listing depth." }, limit: { type: "number", description: "Maximum number of entries." } } } }, { name: "configure_file_search", description: "Advanced: search raw profile file contents.", input_schema: { type: "object", properties: { query: { type: "string", description: "Search query." }, path: { type: "string", description: "Optional profile file path prefix." }, limit: { type: "number", description: "Maximum number of results." } }, required: ["query"] } }, { name: "configure_file_delete", description: "Advanced: delete a raw profile file path.", input_schema: { type: "object", properties: { path: { type: "string", description: "Profile file path to delete." } }, required: ["path"] } } ]; var UTILITY_TOOLS = [ { name: "configure_web_search", description: "Search the web for current information. This is an opt-in utility, not a connector.", input_schema: { type: "object", properties: { query: { type: "string", description: "Required web search query." }, max_results: { type: "number", description: "Optional maximum result count. The backend enforces a default and hard cap." } }, required: ["query"] } }, { name: "configure_url_fetch", description: "Fetch readable text from a URL. This is an opt-in utility, not a connector.", input_schema: { type: "object", properties: { url: { type: "string", description: "Required URL to fetch." }, max_length: { type: "number", description: "Optional maximum content length. The backend enforces a default and hard cap." } }, required: ["url"] } } ]; function getDefaultProfileTools() { return DEFAULT_PROFILE_TOOLS.map(cloneTool); } function getConnectorTools(connectors = []) { return unique(connectors).map((connector) => cloneTool(CONNECTOR_TOOL_BY_CONNECTOR[connector])); } function getActionTools(actions = []) { return unique(actions).map((action) => cloneTool(ACTION_TOOL_BY_ACTION[action])); } function getAdvancedTools(options = {}) { const tools = []; if (options.commit) tools.push(cloneTool(ADVANCED_COMMIT_TOOL)); if (options.files) tools.push(...ADVANCED_FILE_TOOLS.map(cloneTool)); if (options.utilitySearch) tools.push(...UTILITY_TOOLS.map(cloneTool)); return tools; } function getUITools() { return [ { name: "configure_show_ui_component", description: "Render a Configure-hosted UI component when the host runtime supports UI tools.", input_schema: { type: "object", properties: { component_type: { type: "string", description: "Component type to render." }, props: { type: "object", description: "Component props." } }, required: ["component_type"] } } ]; } function getToolsForOptions(options = {}) { return [ ...getDefaultProfileTools(), ...getConnectorTools(options.connectors), ...getActionTools(options.actions), ...getAdvancedTools(options.advanced) ]; } function toOpenAIFunctions(tools) { return tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.input_schema } })); } function unique(items) { return [...new Set(items)]; } // 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; }