# Conflicts: # apps/mobile/lib/api.test.ts # apps/web/src/locales/de/messages.po # apps/web/src/locales/en/messages.po # apps/web/src/locales/es/messages.po # apps/web/src/locales/hi/messages.po # apps/web/src/locales/ko/messages.po # apps/web/src/locales/pt-BR/messages.po # apps/web/src/locales/ru/messages.po # apps/web/src/locales/tr/messages.po # apps/web/src/locales/zh-CN/messages.po # apps/web/src/pages/SettingsOverlay.tsx # apps/web/src/pages/shell/message-cards.tsx # packages/adapter-kit/src/index.ts # packages/adapters/src/builtin-tools.ts # packages/adapters/src/executor-tools.ts # packages/adapters/src/index.ts
972 lines
34 KiB
TypeScript
972 lines
34 KiB
TypeScript
import { eventIterator, oc } from "@orpc/contract";
|
|
import * as z from "zod";
|
|
import { AiConsentQuerySchema, AiConsentStatusSchema } from "./ai-consent.js";
|
|
import { ATTACHMENT_MAX_BASE64_LENGTH, ATTACHMENT_MAX_COUNT } from "./attachments.js";
|
|
import {
|
|
CheckInSchema,
|
|
COACH_TITLE_MAX_LENGTH,
|
|
CoachRhythmSchema,
|
|
CoachTodaySchema,
|
|
CreateGoalInput,
|
|
CreateHabitInput,
|
|
GoalSchema,
|
|
HabitSchema,
|
|
ListCheckInsInput,
|
|
ListGoalsInput,
|
|
ListHabitsInput,
|
|
LogCheckInInput,
|
|
UpdateGoalInput,
|
|
UpdateHabitInput,
|
|
} from "./coach.js";
|
|
import {
|
|
ActionApprovalRuleSchema,
|
|
ActionAutoReviewSettingsSchema,
|
|
AgentSecretInputSchema,
|
|
AgentSecretSchema,
|
|
AgentSkillCatalogEntrySchema,
|
|
AgentSkillSchema,
|
|
AppBootstrapSchema,
|
|
ArtifactSchema,
|
|
ArtifactWithContentSchema,
|
|
AvatarStyleSchema,
|
|
BotMcpServerSchema,
|
|
BotSchema,
|
|
BotSectionSchema,
|
|
CapabilityInstallSchema,
|
|
ComputerModeSchema,
|
|
ComputerReleaseReasonSchema,
|
|
ComputerStatusSchema,
|
|
ComputerUpdateSchema,
|
|
ConnectionCatalogItemSchema,
|
|
ConnectionSchema,
|
|
CreateAgentSkillInput,
|
|
CreateBotInput,
|
|
CreateGroupInput,
|
|
CreateRoutineInput,
|
|
CreateScratchpadItemInput,
|
|
DelegatedTaskSchema,
|
|
DeploymentSettingsSchema,
|
|
ExportManifestSchema,
|
|
ExternalConversationPolicySchema,
|
|
GroupDetailSchema,
|
|
GroupSchema,
|
|
IntegrationCatalogResultSchema,
|
|
MainConversationRefSchema,
|
|
McpServerConfigInput,
|
|
McpServerSchema,
|
|
MemoryDocumentSchema,
|
|
MemoryScopeSchema,
|
|
MeSchema,
|
|
MessagingAgentConnectionSchema,
|
|
MessagingChannelMembershipSchema,
|
|
MessagingLinkedIdentitySchema,
|
|
MessagingStatusSchema,
|
|
ModelCatalogEntrySchema,
|
|
ModelConnectInputSchema,
|
|
ModelCredentialSchema,
|
|
ModelOAuthBeginSchema,
|
|
REPLY_QUOTE_MAX_LENGTH,
|
|
ReorderBotsInput,
|
|
RoutineSchema,
|
|
ScratchpadItemSchema,
|
|
ScratchpadItemStatusSchema,
|
|
ServerUpdateCheckSchema,
|
|
ServerUpdateRequestSchema,
|
|
ServerUpdateRunSchema,
|
|
ServerUpdateStatusSchema,
|
|
SkillPlaybookSchema,
|
|
SpaceMemoryConfigSchema,
|
|
SpaceNavigationSchema,
|
|
SpaceSchema,
|
|
TaughtSkillSchema,
|
|
TeachRecordingEventSchema,
|
|
ThreadMessagePageSchema,
|
|
ThreadSnapshotSchema,
|
|
UpdateAgentSkillInput,
|
|
UpdateBotInput,
|
|
UpdateExternalConversationPolicyInput,
|
|
UpdateGroupInput,
|
|
UsageRecordSchema,
|
|
VoiceCatalogEntrySchema,
|
|
VoiceCredentialSchema,
|
|
VoiceInfoSchema,
|
|
VoiceStatusSchema,
|
|
} from "./domain.js";
|
|
import { DelegatedTaskStatusSchema, OnboardingStepSchema, ProductEventSchema } from "./events.js";
|
|
import { Id, IsoDate } from "./ids.js";
|
|
import { InboxListOutputSchema } from "./inbox.js";
|
|
import {
|
|
IntegrationProviderConfigSchema,
|
|
IntegrationSetupStateSchema,
|
|
} from "./integration-settings.js";
|
|
import {
|
|
MediaCapabilitiesSchema,
|
|
MediaJobSnapshotSchema,
|
|
MediaJobStartInputSchema,
|
|
MediaJobStartOutputSchema,
|
|
} from "./media-jobs.js";
|
|
import { MediaSettingsUpdateSchema, MediaSettingsViewSchema } from "./media-settings.js";
|
|
import { ModelTierMappingSchema, SetModelTierInput } from "./model-tiers.js";
|
|
import {
|
|
ExpoPushTokenSchema,
|
|
NotificationSettingsSchema,
|
|
UpdateNotificationSettingsInput,
|
|
WebPushConfigSchema,
|
|
WebPushEndpointSchema,
|
|
WebPushSubscriptionInput,
|
|
} from "./notifications.js";
|
|
import {
|
|
OnboardingScanApplyInputSchema,
|
|
OnboardingScanApplyResultSchema,
|
|
OnboardingScanStatusSchema,
|
|
OnboardingScanTargetSchema,
|
|
} from "./onboarding-scan.js";
|
|
import {
|
|
CreateProjectGoalInput,
|
|
ProjectGoalSchema,
|
|
UpdateProjectGoalInput,
|
|
} from "./project-goals.js";
|
|
import { ProjectSessionsOutputSchema } from "./project-sessions.js";
|
|
import { MessageReactionSchema } from "./reactions.js";
|
|
import { RunsListOutputSchema } from "./runs.js";
|
|
import { SearchQueryOutputSchema } from "./search.js";
|
|
import {
|
|
ForgetUserFactsInput,
|
|
UpdateUserProfileInput,
|
|
UserProfileStateSchema,
|
|
} from "./user-profile.js";
|
|
|
|
const botId = z.object({ botId: Id });
|
|
const groupId = z.object({ groupId: Id });
|
|
|
|
const threadTarget = z
|
|
.object({
|
|
botId: Id.optional(),
|
|
groupId: Id.optional(),
|
|
})
|
|
.superRefine((input, ctx) => {
|
|
const hasBot = Boolean(input.botId);
|
|
const hasGroup = Boolean(input.groupId);
|
|
if (hasBot === hasGroup) {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Provide exactly one of botId or groupId",
|
|
path: ["botId"],
|
|
});
|
|
}
|
|
});
|
|
|
|
const structuredMentionTarget = z.discriminatedUnion("kind", [
|
|
z.object({ kind: z.literal("bot"), id: Id }),
|
|
z.object({ kind: z.literal("group"), id: Id }),
|
|
z.object({ kind: z.literal("routine"), id: Id }),
|
|
z.object({ kind: z.literal("connector"), id: Id }),
|
|
]);
|
|
|
|
const threadSendInput = threadTarget
|
|
.safeExtend({
|
|
text: z.string().optional(),
|
|
artifactIds: z.array(Id).max(ATTACHMENT_MAX_COUNT).optional(),
|
|
/** Bare bot ids (legacy) or typed mention chips from the composer. */
|
|
mentions: z
|
|
.array(z.union([Id, structuredMentionTarget]))
|
|
.max(64)
|
|
.optional(),
|
|
replyToMessageId: Id.optional(),
|
|
replyQuote: z.string().trim().min(1).max(REPLY_QUOTE_MAX_LENGTH).optional(),
|
|
clientNonce: z.string().min(1).max(200).optional(),
|
|
})
|
|
.superRefine((input, ctx) => {
|
|
const text = input.text?.trim() ?? "";
|
|
const artifactIds = input.artifactIds ?? [];
|
|
if (!text && artifactIds.length === 0) {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Provide text or at least one attachment",
|
|
path: ["text"],
|
|
});
|
|
}
|
|
if (input.replyQuote && !input.replyToMessageId) {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "replyQuote requires replyToMessageId",
|
|
path: ["replyQuote"],
|
|
});
|
|
}
|
|
});
|
|
|
|
export const appContract = {
|
|
aiConsent: {
|
|
status: oc.input(AiConsentQuerySchema).output(AiConsentStatusSchema),
|
|
allow: oc
|
|
.input(
|
|
z.object({
|
|
scope: z.string(),
|
|
version: z.string(),
|
|
keys: z.array(z.string()).min(1).max(200),
|
|
}),
|
|
)
|
|
.output(AiConsentStatusSchema),
|
|
revoke: oc.input(z.object({ key: z.string().nullable() })).output(AiConsentStatusSchema),
|
|
},
|
|
health: oc.output(z.object({ ok: z.literal(true), version: z.string() })),
|
|
me: oc.output(MeSchema),
|
|
preferences: {
|
|
update: oc.input(z.object({ avatarStyle: AvatarStyleSchema })).output(MeSchema),
|
|
},
|
|
/** What the user's bots know about the user, across spaces. */
|
|
profile: {
|
|
get: oc.output(UserProfileStateSchema),
|
|
update: oc.input(UpdateUserProfileInput).output(UserProfileStateSchema),
|
|
forget: oc.input(ForgetUserFactsInput).output(UserProfileStateSchema),
|
|
},
|
|
/** The user's goals, habits, check-ins and check-in times; owner-only, across spaces. */
|
|
coach: {
|
|
today: oc.output(CoachTodaySchema),
|
|
goals: {
|
|
list: oc.input(ListGoalsInput).output(z.array(GoalSchema)),
|
|
create: oc.input(CreateGoalInput).output(GoalSchema),
|
|
update: oc.input(UpdateGoalInput).output(GoalSchema),
|
|
remove: oc.input(z.object({ goalId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
habits: {
|
|
list: oc.input(ListHabitsInput).output(z.array(HabitSchema)),
|
|
create: oc.input(CreateHabitInput).output(HabitSchema),
|
|
update: oc.input(UpdateHabitInput).output(HabitSchema),
|
|
remove: oc.input(z.object({ habitId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
checkIns: {
|
|
log: oc.input(LogCheckInInput).output(CheckInSchema),
|
|
list: oc.input(ListCheckInsInput).output(z.array(CheckInSchema)),
|
|
},
|
|
rhythm: {
|
|
get: oc.output(CoachRhythmSchema),
|
|
set: oc.input(CoachRhythmSchema).output(CoachRhythmSchema),
|
|
},
|
|
},
|
|
spaces: {
|
|
list: oc.output(SpaceNavigationSchema),
|
|
create: oc
|
|
.input(
|
|
z.object({
|
|
name: z.string().trim().min(1).max(60),
|
|
workspacePath: z.string().trim().min(1).max(500).optional(),
|
|
}),
|
|
)
|
|
.output(SpaceSchema),
|
|
remove: oc
|
|
.input(z.object({ spaceId: Id }))
|
|
.output(z.object({ ok: z.literal(true), activeSpaceId: Id })),
|
|
/** The space's main conversation, created with its main bot when the user first opens it. */
|
|
ensureMain: oc.input(z.object({ spaceId: Id })).output(MainConversationRefSchema),
|
|
},
|
|
bootstrap: oc.input(z.object({ botId: Id.optional() })).output(AppBootstrapSchema),
|
|
deployment: {
|
|
get: oc.output(DeploymentSettingsSchema),
|
|
update: oc
|
|
.input(
|
|
z.object({
|
|
signupsEnabled: z.boolean().optional(),
|
|
signupAllowlist: z.array(z.string()).optional(),
|
|
computerHost: z.enum(["docker", "this-mac"]).nullable().optional(),
|
|
}),
|
|
)
|
|
.output(DeploymentSettingsSchema),
|
|
},
|
|
/**
|
|
* Deployment-owner product updates. When the Compose updater sidecar is reachable, these proxy
|
|
* to its `/state` `/plan` `/apply` contract. Rollback stays on the sidecar for ops only and is
|
|
* not exposed here. Never git-fetch from the API process.
|
|
*/
|
|
updater: {
|
|
status: oc.output(ServerUpdateStatusSchema),
|
|
check: oc.input(ServerUpdateRequestSchema).output(ServerUpdateCheckSchema),
|
|
apply: oc.input(ServerUpdateRequestSchema).output(ServerUpdateRunSchema),
|
|
},
|
|
models: {
|
|
list: oc.output(z.array(ModelCatalogEntrySchema)),
|
|
credentials: oc.output(z.array(ModelCredentialSchema)),
|
|
connect: oc.input(ModelConnectInputSchema).output(ModelCredentialSchema),
|
|
probeOpenAiCompatible: oc
|
|
.input(
|
|
z.union([
|
|
z.object({
|
|
baseUrl: z.string(),
|
|
apiKey: z.string().optional(),
|
|
}),
|
|
// A connected server, probed with its stored address and key.
|
|
z.object({ credentialId: Id }),
|
|
]),
|
|
)
|
|
.output(z.object({ models: z.array(z.string()) })),
|
|
beginOAuth: oc
|
|
.input(
|
|
z.object({
|
|
provider: z.string(),
|
|
label: z.string().optional(),
|
|
modelId: z.string().optional(),
|
|
}),
|
|
)
|
|
.output(ModelOAuthBeginSchema),
|
|
submitOAuthCode: oc
|
|
.input(z.object({ loginId: z.string(), code: z.string().trim().min(1).max(8_192) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
completeOAuth: oc
|
|
.input(z.object({ loginId: z.string() }))
|
|
.output(
|
|
z.discriminatedUnion("status", [
|
|
z.object({ status: z.literal("pending") }),
|
|
z.object({ status: z.literal("ready") }),
|
|
z.object({ status: z.literal("error"), error: z.string() }),
|
|
]),
|
|
),
|
|
finishOAuth: oc.input(z.object({ loginId: z.string() })).output(ModelCredentialSchema),
|
|
cancelOAuth: oc
|
|
.input(z.object({ loginId: z.string() }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
setDefault: oc
|
|
.input(z.object({ provider: z.string(), modelId: z.string() }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
tiers: {
|
|
list: oc.output(z.array(ModelTierMappingSchema)),
|
|
set: oc.input(SetModelTierInput).output(z.array(ModelTierMappingSchema)),
|
|
},
|
|
},
|
|
bots: {
|
|
list: oc.output(z.array(BotSchema)),
|
|
listArchived: oc.output(z.array(BotSchema)),
|
|
get: oc.input(botId).output(BotSchema),
|
|
create: oc.input(CreateBotInput).output(BotSchema),
|
|
/** The user's main bot, created with its main conversation in the home space when missing. */
|
|
ensureMain: oc.output(BotSchema),
|
|
duplicate: oc.input(botId).output(BotSchema),
|
|
reorder: oc.input(ReorderBotsInput).output(z.object({ ok: z.literal(true) })),
|
|
update: oc.input(UpdateBotInput).output(BotSchema),
|
|
/** Builtin tool names this bot's allowlist can choose from. */
|
|
builtinTools: oc.input(botId).output(z.array(z.string())),
|
|
setComputer: oc.input(z.object({ botId: Id, mode: ComputerModeSchema })).output(BotSchema),
|
|
archive: oc.input(botId).output(z.object({ ok: z.literal(true) })),
|
|
restore: oc.input(botId).output(z.object({ ok: z.literal(true) })),
|
|
remove: oc
|
|
.input(z.object({ botId: Id, deleteMemories: z.boolean().default(false) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
rotateWebhookSecret: oc.input(botId).output(
|
|
z.object({
|
|
secret: z.string(),
|
|
path: z.string(),
|
|
webhookConfigured: z.literal(true),
|
|
}),
|
|
),
|
|
},
|
|
groups: {
|
|
create: oc.input(CreateGroupInput).output(GroupSchema),
|
|
list: oc.output(z.array(GroupSchema)),
|
|
listArchived: oc.output(z.array(GroupSchema)),
|
|
get: oc.input(groupId).output(GroupDetailSchema),
|
|
listSessions: oc.input(groupId).output(ProjectSessionsOutputSchema),
|
|
duplicate: oc.input(groupId).output(GroupSchema),
|
|
update: oc.input(UpdateGroupInput).output(GroupSchema),
|
|
archive: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
|
|
restore: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
|
|
remove: oc.input(groupId).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
botSections: {
|
|
list: oc.output(z.array(BotSectionSchema)),
|
|
create: oc
|
|
.input(threadTarget.safeExtend({ name: z.string().trim().min(1).max(60) }))
|
|
.output(BotSectionSchema),
|
|
update: oc
|
|
.input(z.object({ sectionId: Id, name: z.string().trim().min(1).max(60) }))
|
|
.output(BotSectionSchema),
|
|
},
|
|
projectGoals: {
|
|
list: oc
|
|
.input(
|
|
z.object({
|
|
sectionId: Id,
|
|
groupId: Id.optional(),
|
|
seedDefaults: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.output(z.array(ProjectGoalSchema)),
|
|
create: oc.input(CreateProjectGoalInput).output(ProjectGoalSchema),
|
|
update: oc.input(UpdateProjectGoalInput).output(ProjectGoalSchema),
|
|
},
|
|
threads: {
|
|
head: oc.input(threadTarget).output(
|
|
z.object({
|
|
threadId: Id,
|
|
cursor: z.number().int().min(-1),
|
|
}),
|
|
),
|
|
get: oc.input(threadTarget).output(ThreadSnapshotSchema),
|
|
messages: oc
|
|
.input(
|
|
threadTarget.safeExtend({
|
|
before: z.number().int().nonnegative().optional(),
|
|
includePeerRuns: z.boolean().optional(),
|
|
includePeerReceipts: z.boolean().optional(),
|
|
around: z
|
|
.object({
|
|
messageId: Id.optional(),
|
|
seq: z.number().int().nonnegative().optional(),
|
|
})
|
|
.optional(),
|
|
}),
|
|
)
|
|
.output(ThreadMessagePageSchema),
|
|
subscribe: oc
|
|
.input(threadTarget.safeExtend({ cursor: z.number().int().min(-1) }))
|
|
.output(eventIterator(ProductEventSchema)),
|
|
send: oc.input(threadSendInput).output(
|
|
z.object({
|
|
taskId: Id,
|
|
runId: Id,
|
|
seq: z.number().int(),
|
|
runIds: z.array(Id).optional(),
|
|
}),
|
|
),
|
|
react: oc
|
|
.input(
|
|
threadTarget.safeExtend({
|
|
messageId: Id,
|
|
reaction: MessageReactionSchema,
|
|
clientNonce: z.string().min(1).max(200),
|
|
}),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
|
followUp: oc
|
|
.input(threadTarget.safeExtend({ text: z.string().min(1) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
clear: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
|
answer: oc
|
|
.input(
|
|
threadTarget.safeExtend({
|
|
runId: Id,
|
|
messageId: Id,
|
|
answer: z.string().min(1),
|
|
}),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
markRead: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
|
markUnread: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
computer: {
|
|
status: oc.input(botId).output(ComputerStatusSchema),
|
|
boot: oc.input(botId).output(ComputerStatusSchema),
|
|
stop: oc.input(botId).output(ComputerStatusSchema),
|
|
recover: oc.input(botId).output(ComputerUpdateSchema),
|
|
reset: oc.input(botId).output(ComputerStatusSchema),
|
|
update: oc.input(botId).output(ComputerUpdateSchema),
|
|
updates: oc.output(z.array(ComputerUpdateSchema)),
|
|
releaseInterrupted: oc
|
|
.input(z.object({ id: Id, workersStopped: z.literal(true) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
dismissUpdate: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
|
|
takeover: oc.input(botId).output(z.object({ leaseId: Id, expiresAt: z.string() })),
|
|
release: oc
|
|
.input(
|
|
z.object({
|
|
botId: Id,
|
|
reason: ComputerReleaseReasonSchema.optional(),
|
|
}),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
input: oc
|
|
.input(
|
|
z.object({
|
|
botId: Id,
|
|
kind: z.enum(["key", "pointer", "clipboard", "scroll"]),
|
|
payload: z.record(z.string(), z.unknown()),
|
|
}),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
files: oc
|
|
.input(z.object({ botId: Id, path: z.string().default("/") }))
|
|
.output(
|
|
z.array(z.object({ path: z.string(), kind: z.enum(["file", "dir"]), size: z.number() })),
|
|
),
|
|
readFile: oc
|
|
.input(z.object({ botId: Id, path: z.string() }))
|
|
.output(z.object({ path: z.string(), content: z.string() })),
|
|
screenUrl: oc.input(botId).output(z.object({ url: z.string().nullable() })),
|
|
heartbeat: oc.input(botId).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
memory: {
|
|
list: oc
|
|
.input(z.object({ botId: Id.optional(), scope: z.enum(["bot", "user"]).optional() }))
|
|
.output(z.array(MemoryDocumentSchema)),
|
|
update: oc
|
|
.input(z.object({ documentId: Id, content: z.string() }))
|
|
.output(MemoryDocumentSchema),
|
|
exportMarkdown: oc.input(z.object({ botId: Id.optional() })).output(z.string()),
|
|
providerConfig: oc.output(SpaceMemoryConfigSchema.nullable()),
|
|
connectProvider: oc
|
|
.input(
|
|
z.object({
|
|
provider: z.string().min(1),
|
|
settings: z.record(z.string(), z.string()),
|
|
credentials: z.record(z.string(), z.string()),
|
|
defaultMemoryScope: MemoryScopeSchema.default("isolated"),
|
|
}),
|
|
)
|
|
.output(SpaceMemoryConfigSchema),
|
|
setDefaultScope: oc
|
|
.input(z.object({ defaultMemoryScope: MemoryScopeSchema }))
|
|
.output(SpaceMemoryConfigSchema),
|
|
disconnectProvider: oc.output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
routines: {
|
|
list: oc.input(botId).output(z.array(RoutineSchema)),
|
|
create: oc.input(CreateRoutineInput).output(RoutineSchema),
|
|
update: oc
|
|
.input(
|
|
z
|
|
.object({
|
|
routineId: Id,
|
|
name: z.string().optional(),
|
|
prompt: z.string().optional(),
|
|
crons: z.array(z.string().min(1)).optional(),
|
|
timezone: z.string().optional(),
|
|
active: z.boolean().optional(),
|
|
notify: z.boolean().optional(),
|
|
webhookEnabled: z.boolean().optional(),
|
|
githubEnabled: z.boolean().optional(),
|
|
messageProvider: z
|
|
.string()
|
|
.min(1)
|
|
.max(50)
|
|
.regex(/^[a-z0-9._-]+$/i)
|
|
.nullable()
|
|
.optional(),
|
|
/** ISO datetime to arm a never-run one-shot. */
|
|
runAt: IsoDate.optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
if (
|
|
value.crons &&
|
|
value.crons.length === 0 &&
|
|
value.webhookEnabled === false &&
|
|
value.githubEnabled === false &&
|
|
value.messageProvider === null
|
|
) {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Add a schedule, webhook, GitHub, or message trigger",
|
|
path: ["crons"],
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.output(RoutineSchema),
|
|
remove: oc.input(z.object({ routineId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
testRun: oc
|
|
.input(
|
|
z.object({
|
|
routineId: Id,
|
|
clientNonce: z.string().min(1).max(200).optional(),
|
|
}),
|
|
)
|
|
.output(z.object({ runId: Id })),
|
|
},
|
|
scratchpad: {
|
|
list: oc
|
|
.input(
|
|
z.object({
|
|
botId: Id,
|
|
status: ScratchpadItemStatusSchema.optional(),
|
|
includeDone: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.output(z.array(ScratchpadItemSchema)),
|
|
create: oc.input(CreateScratchpadItemInput).output(ScratchpadItemSchema),
|
|
update: oc
|
|
.input(
|
|
z.object({
|
|
itemId: Id,
|
|
title: z.string().min(1).max(200).optional(),
|
|
status: ScratchpadItemStatusSchema.optional(),
|
|
notes: z.string().max(4_000).optional(),
|
|
}),
|
|
)
|
|
.output(ScratchpadItemSchema),
|
|
remove: oc.input(z.object({ itemId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
skills: {
|
|
list: oc.input(botId).output(z.array(TaughtSkillSchema)),
|
|
get: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
|
|
start: oc
|
|
.input(z.object({ botId: Id, goal: z.string().min(1).max(4000) }))
|
|
.output(TaughtSkillSchema),
|
|
appendEvent: oc
|
|
.input(z.object({ skillId: Id, event: TeachRecordingEventSchema }))
|
|
.output(TaughtSkillSchema),
|
|
snapshot: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
|
|
stop: oc.input(z.object({ skillId: Id })).output(TaughtSkillSchema),
|
|
updateDraft: oc
|
|
.input(
|
|
z.object({
|
|
skillId: Id,
|
|
name: z.string().optional(),
|
|
playbook: SkillPlaybookSchema,
|
|
}),
|
|
)
|
|
.output(TaughtSkillSchema),
|
|
save: oc
|
|
.input(z.object({ skillId: Id, name: z.string().optional() }))
|
|
.output(TaughtSkillSchema),
|
|
testRun: oc
|
|
.input(z.object({ skillId: Id, prompt: z.string().optional() }))
|
|
.output(z.object({ runId: Id })),
|
|
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
/** Claude Agent Skills (SKILL.md recipes) shared across assistants (not taught/demo skills). Pi already understands this format; we persist and inject them. */
|
|
agentSkills: {
|
|
list: oc.output(z.array(AgentSkillCatalogEntrySchema)),
|
|
get: oc
|
|
.input(
|
|
z
|
|
.object({ skillId: Id.optional(), name: z.string().min(1).max(80).optional() })
|
|
.superRefine((input, ctx) => {
|
|
if (!input.skillId && !input.name?.trim()) {
|
|
ctx.addIssue({
|
|
code: "custom",
|
|
message: "Provide skillId or name",
|
|
path: ["skillId"],
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.output(AgentSkillSchema),
|
|
create: oc.input(CreateAgentSkillInput).output(AgentSkillSchema),
|
|
update: oc.input(UpdateAgentSkillInput).output(AgentSkillSchema),
|
|
remove: oc.input(z.object({ skillId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
capabilities: {
|
|
list: oc.output(z.array(CapabilityInstallSchema)),
|
|
catalogSearch: oc
|
|
.input(
|
|
z.object({
|
|
query: z.string().trim().max(253).default(""),
|
|
usePublicCatalog: z.boolean().default(false),
|
|
}),
|
|
)
|
|
.output(
|
|
z.object({
|
|
enabled: z.boolean(),
|
|
results: z.array(IntegrationCatalogResultSchema),
|
|
}),
|
|
),
|
|
install: oc
|
|
.input(
|
|
z.object({
|
|
kind: z.enum(["skill", "plugin", "mcp", "api", "graphql"]),
|
|
name: z.string().min(1).max(120),
|
|
source: z.string().min(1).max(2048),
|
|
config: z.record(z.string(), z.unknown()).default({}),
|
|
credential: z.string().max(16_384).optional(),
|
|
}),
|
|
)
|
|
.output(CapabilityInstallSchema),
|
|
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
mcp: {
|
|
servers: {
|
|
list: oc.output(z.array(McpServerSchema)),
|
|
create: oc.input(McpServerConfigInput).output(McpServerSchema),
|
|
update: oc
|
|
.input(
|
|
z.union([
|
|
z.object({ id: Id, config: McpServerConfigInput }),
|
|
z.object({ id: Id, secret: z.string().min(1).max(16384) }),
|
|
]),
|
|
)
|
|
.output(McpServerSchema),
|
|
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
assignments: {
|
|
list: oc.input(botId).output(z.array(BotMcpServerSchema)),
|
|
all: oc.output(z.array(BotMcpServerSchema)),
|
|
approve: oc.input(z.object({ botId: Id, serverId: Id })).output(BotMcpServerSchema),
|
|
replace: oc
|
|
.input(
|
|
z.object({
|
|
botId: Id,
|
|
assignments: z.array(
|
|
z.object({
|
|
serverId: Id,
|
|
allowAllTools: z.boolean().default(true),
|
|
allowedTools: z.array(z.string().min(1).max(200)).max(500).default([]),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.output(z.array(BotMcpServerSchema)),
|
|
},
|
|
oauth: {
|
|
begin: oc.input(z.object({ serverId: Id, redirectUri: z.string().url() })).output(
|
|
z.discriminatedUnion("status", [
|
|
z.object({
|
|
status: z.literal("authorization_required"),
|
|
sessionId: Id,
|
|
authorizationUrl: z.string().url(),
|
|
}),
|
|
z.object({
|
|
status: z.enum(["already_connected", "authorization_not_requested"]),
|
|
}),
|
|
]),
|
|
),
|
|
complete: oc
|
|
.input(z.object({ sessionId: Id, code: z.string().min(1), state: z.string().min(1) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
disconnect: oc.input(z.object({ serverId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
},
|
|
onboarding: {
|
|
/** Checks that the caller may onboard this bot's thread. Posts nothing. */
|
|
start: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/**
|
|
* Posts the first card: for the main bot the first onboarding question, once per user;
|
|
* for another bot its focus card while the thread is still idle.
|
|
*/
|
|
promptFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/**
|
|
* Answers the pending card and posts what follows it: the focus card's app suggestions,
|
|
* then the next onboarding question. A stale answer (another step is pending) is ignored.
|
|
*/
|
|
choose: oc
|
|
.input(
|
|
z.object({
|
|
botId: Id,
|
|
step: OnboardingStepSchema.optional(),
|
|
optionId: z.string(),
|
|
/** The answer in the user's words, with `optionId` "_text"; only the goal card takes one. */
|
|
text: z.string().trim().min(1).max(COACH_TITLE_MAX_LENGTH).optional(),
|
|
/** This device could still turn on push notifications. */
|
|
canEnablePush: z.boolean().optional(),
|
|
}),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
/** Dismisses the pending card; an onboarding question counts as skipped. */
|
|
dismissFocus: oc.input(z.object({ botId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/** Flip an app_connect card to connected after authorization completes. */
|
|
appConnected: oc
|
|
.input(
|
|
z.object({ botId: Id, provider: z.string(), connectorId: z.string().default("composio") }),
|
|
)
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
/** The folder a scan would read; null when this account cannot scan, so the step is skipped. */
|
|
scanTarget: oc.output(OnboardingScanTargetSchema),
|
|
/** Start scanning that folder. Only called after the user agreed. */
|
|
scan: oc.output(z.object({ scanId: z.string() })),
|
|
scanStatus: oc.input(z.object({ scanId: z.string() })).output(OnboardingScanStatusSchema),
|
|
/** Create a project space per chosen project and save the chosen facts. */
|
|
applyScan: oc.input(OnboardingScanApplyInputSchema).output(OnboardingScanApplyResultSchema),
|
|
},
|
|
integrationSetup: {
|
|
get: oc.output(IntegrationSetupStateSchema),
|
|
save: oc.input(IntegrationProviderConfigSchema).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
media: {
|
|
/** Deployment-wide media backends; deployment owner only. */
|
|
settings: {
|
|
get: oc.output(MediaSettingsViewSchema),
|
|
update: oc.input(MediaSettingsUpdateSchema).output(MediaSettingsViewSchema),
|
|
},
|
|
/** With a job id, what the job's space can run (a card's follow-ups). */
|
|
capabilities: oc
|
|
.input(z.object({ jobId: Id.optional() }).optional())
|
|
.output(MediaCapabilitiesSchema),
|
|
jobs: {
|
|
start: oc.input(MediaJobStartInputSchema).output(MediaJobStartOutputSchema),
|
|
get: oc.input(z.object({ jobId: Id })).output(MediaJobSnapshotSchema),
|
|
cancel: oc.input(z.object({ jobId: Id })).output(MediaJobSnapshotSchema),
|
|
},
|
|
},
|
|
connections: {
|
|
catalog: oc
|
|
.input(z.object({ query: z.string().optional(), connectorId: z.string().optional() }))
|
|
.output(z.array(ConnectionCatalogItemSchema)),
|
|
list: oc.output(z.array(ConnectionSchema)),
|
|
begin: oc
|
|
.input(
|
|
z.object({
|
|
connectorId: z.string().default("composio"),
|
|
provider: z.string(),
|
|
displayName: z.string(),
|
|
}),
|
|
)
|
|
.output(z.object({ connectionId: Id, authorizationUrl: z.string().nullable() })),
|
|
complete: oc
|
|
.input(z.object({ connectionId: Id, code: z.string().optional() }))
|
|
.output(ConnectionSchema),
|
|
rename: oc
|
|
.input(z.object({ connectionId: Id, displayName: z.string().trim().min(1).max(80) }))
|
|
.output(ConnectionSchema),
|
|
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
/** Tools the connected provider exposes. Read-only; no per-tool allowlist yet. */
|
|
tools: oc.input(z.object({ connectorId: z.string(), provider: z.string() })).output(
|
|
z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
description: z.string(),
|
|
}),
|
|
),
|
|
),
|
|
},
|
|
/** External messaging surface: link state, group channels, agent connections. */
|
|
messaging: {
|
|
status: oc.output(MessagingStatusSchema),
|
|
link: {
|
|
/** Issue a short-lived code the user sends to the line from a chat app. */
|
|
start: oc
|
|
.input(z.object({ botId: Id }))
|
|
.output(z.object({ code: z.string(), expiresAt: z.string() })),
|
|
},
|
|
identities: {
|
|
setBot: oc
|
|
.input(z.object({ identityId: Id, botId: Id }))
|
|
.output(MessagingLinkedIdentitySchema),
|
|
unlink: oc.input(z.object({ identityId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
channels: {
|
|
list: oc.output(z.array(MessagingChannelMembershipSchema)),
|
|
// Addressed by membership, not channel: one user can have two linked
|
|
// chat apps in the same group, and each answers for its own agent.
|
|
respond: oc
|
|
.input(z.object({ membershipId: Id, accept: z.boolean() }))
|
|
.output(MessagingChannelMembershipSchema),
|
|
leave: oc.input(z.object({ membershipId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
connections: {
|
|
list: oc.output(z.array(MessagingAgentConnectionSchema)),
|
|
respond: oc
|
|
.input(z.object({ connectionId: Id, accept: z.boolean() }))
|
|
.output(MessagingAgentConnectionSchema),
|
|
revoke: oc.input(z.object({ connectionId: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
},
|
|
approvalRules: {
|
|
list: oc.output(z.array(ActionApprovalRuleSchema)),
|
|
set: oc
|
|
.input(
|
|
z.object({
|
|
effect: z.enum(["always_allow", "require_approval"]),
|
|
matchKind: z.enum(["tool", "connector", "category"]),
|
|
matchValue: z.string().min(1),
|
|
}),
|
|
)
|
|
.output(ActionApprovalRuleSchema),
|
|
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
autoReview: {
|
|
get: oc.output(ActionAutoReviewSettingsSchema),
|
|
set: oc.input(z.object({ enabled: z.boolean() })).output(ActionAutoReviewSettingsSchema),
|
|
},
|
|
artifacts: {
|
|
list: oc.input(botId).output(z.array(ArtifactSchema)),
|
|
create: oc
|
|
.input(
|
|
threadTarget.and(
|
|
z.object({
|
|
name: z.string().min(1).max(255),
|
|
mimeType: z.string().min(1),
|
|
contentBase64: z.string().min(1).max(ATTACHMENT_MAX_BASE64_LENGTH),
|
|
}),
|
|
),
|
|
)
|
|
.output(ArtifactSchema),
|
|
get: oc.input(threadTarget.and(z.object({ artifactId: Id }))).output(ArtifactWithContentSchema),
|
|
},
|
|
usage: {
|
|
list: oc.output(z.array(UsageRecordSchema)),
|
|
summary: oc.output(
|
|
z.object({
|
|
inputTokens: z.number(),
|
|
outputTokens: z.number(),
|
|
runs: z.number(),
|
|
}),
|
|
),
|
|
},
|
|
export: {
|
|
bot: oc.input(botId).output(ExportManifestSchema),
|
|
},
|
|
notifications: {
|
|
registerPush: oc
|
|
.input(z.object({ token: ExpoPushTokenSchema }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
unregisterPush: oc.output(z.object({ ok: z.literal(true) })),
|
|
settings: {
|
|
get: oc.output(NotificationSettingsSchema),
|
|
update: oc.input(UpdateNotificationSettingsInput).output(NotificationSettingsSchema),
|
|
},
|
|
webPush: {
|
|
config: oc.output(WebPushConfigSchema),
|
|
subscribe: oc.input(WebPushSubscriptionInput).output(z.object({ ok: z.literal(true) })),
|
|
unsubscribe: oc
|
|
.input(z.object({ endpoint: WebPushEndpointSchema }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
},
|
|
search: {
|
|
query: oc.input(z.object({ q: z.string().max(200) })).output(SearchQueryOutputSchema),
|
|
},
|
|
runs: {
|
|
list: oc.input(z.object({ filter: z.enum(["active", "recent"]) })).output(RunsListOutputSchema),
|
|
},
|
|
tasks: {
|
|
list: oc
|
|
.input(z.object({ threadId: Id.optional(), status: DelegatedTaskStatusSchema.optional() }))
|
|
.output(z.array(DelegatedTaskSchema)),
|
|
},
|
|
/** Asks, approvals, takeovers and unread threads outside the main conversation, in all spaces. */
|
|
inbox: {
|
|
list: oc.output(InboxListOutputSchema),
|
|
},
|
|
voice: {
|
|
catalog: oc.output(z.array(VoiceCatalogEntrySchema)),
|
|
status: oc.output(VoiceStatusSchema),
|
|
credentials: oc.output(z.array(VoiceCredentialSchema)),
|
|
connect: oc
|
|
.input(
|
|
z.object({
|
|
provider: z.string(),
|
|
apiKey: z.string().min(8),
|
|
voiceId: z.string().max(120).optional(),
|
|
}),
|
|
)
|
|
.output(VoiceCredentialSchema),
|
|
disconnect: oc
|
|
.input(z.object({ provider: z.string().min(1) }))
|
|
.output(z.object({ ok: z.literal(true) })),
|
|
setVoice: oc
|
|
.input(z.object({ voiceId: z.string().min(1).max(120), provider: z.string().optional() }))
|
|
.output(VoiceStatusSchema),
|
|
voices: oc
|
|
.input(z.object({ provider: z.string().optional() }))
|
|
.output(z.array(VoiceInfoSchema)),
|
|
prepare: oc
|
|
.input(
|
|
z.object({
|
|
text: z.string().max(20000),
|
|
voiceId: z.string().max(120).optional(),
|
|
botId: Id.optional(),
|
|
}),
|
|
)
|
|
.output(z.object({ ready: z.boolean(), utterances: z.array(z.string()) })),
|
|
},
|
|
externalConversations: {
|
|
updatePolicy: oc
|
|
.input(UpdateExternalConversationPolicyInput)
|
|
.output(ExternalConversationPolicySchema),
|
|
},
|
|
agentSecrets: {
|
|
list: oc.output(z.array(AgentSecretSchema)),
|
|
put: oc.input(AgentSecretInputSchema).output(AgentSecretSchema),
|
|
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
|
|
},
|
|
};
|
|
|
|
export type AppContract = typeof appContract;
|