mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-30 20:39:46 +02:00
Merge branch 'dev' into feat/skill-system
Bring the new skill system branch up to date with dev. Conflicts resolved in favor of the new skill-system architecture: built-in skill .ts files (including dev-added tracks, browser-control, composio-integration) are deleted in favor of SKILL.md content sourced from outside the source tree. buildCopilotInstructions now sources the catalog from SkillResolver and filters composio-integration when Composio is not configured. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
66c0bc5fa7
171 changed files with 17719 additions and 2984 deletions
|
|
@ -1,5 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const IFRAME_LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
||||
|
||||
export function isAllowedIframeUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'https:') return true;
|
||||
if (parsed.protocol !== 'http:') return false;
|
||||
return IFRAME_LOCAL_HOSTS.has(parsed.hostname.toLowerCase());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageBlockSchema = z.object({
|
||||
src: z.string(),
|
||||
alt: z.string().optional(),
|
||||
|
|
@ -16,6 +29,18 @@ export const EmbedBlockSchema = z.object({
|
|||
|
||||
export type EmbedBlock = z.infer<typeof EmbedBlockSchema>;
|
||||
|
||||
export const IframeBlockSchema = z.object({
|
||||
url: z.string().url().refine(isAllowedIframeUrl, {
|
||||
message: 'Iframe URLs must use https:// or local http://localhost / 127.0.0.1.',
|
||||
}),
|
||||
title: z.string().optional(),
|
||||
caption: z.string().optional(),
|
||||
height: z.number().int().min(240).max(1600).optional(),
|
||||
allow: z.string().optional(),
|
||||
});
|
||||
|
||||
export type IframeBlock = z.infer<typeof IframeBlockSchema>;
|
||||
|
||||
export const ChartBlockSchema = z.object({
|
||||
chart: z.enum(['line', 'bar', 'pie']),
|
||||
title: z.string().optional(),
|
||||
|
|
@ -63,6 +88,7 @@ export type CalendarBlock = z.infer<typeof CalendarBlockSchema>;
|
|||
|
||||
export const EmailBlockSchema = z.object({
|
||||
threadId: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
from: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
|
|
@ -70,6 +96,21 @@ export const EmailBlockSchema = z.object({
|
|||
latest_email: z.string(),
|
||||
past_summary: z.string().optional(),
|
||||
draft_response: z.string().optional(),
|
||||
response_mode: z.enum(['inline', 'assistant', 'both']).optional(),
|
||||
});
|
||||
|
||||
export type EmailBlock = z.infer<typeof EmailBlockSchema>;
|
||||
|
||||
export const TranscriptBlockSchema = z.object({
|
||||
transcript: z.string(),
|
||||
});
|
||||
|
||||
export type TranscriptBlock = z.infer<typeof TranscriptBlockSchema>;
|
||||
|
||||
export const SuggestedTopicBlockSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
category: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SuggestedTopicBlock = z.infer<typeof SuggestedTopicBlockSchema>;
|
||||
|
|
|
|||
134
apps/x/packages/shared/src/browser-control.ts
Normal file
134
apps/x/packages/shared/src/browser-control.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const BrowserTabStateSchema = z.object({
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
canGoBack: z.boolean(),
|
||||
canGoForward: z.boolean(),
|
||||
loading: z.boolean(),
|
||||
});
|
||||
|
||||
export const BrowserStateSchema = z.object({
|
||||
activeTabId: z.string().nullable(),
|
||||
tabs: z.array(BrowserTabStateSchema),
|
||||
});
|
||||
|
||||
export const BrowserPageElementSchema = z.object({
|
||||
index: z.number().int().positive(),
|
||||
tagName: z.string(),
|
||||
role: z.string().nullable(),
|
||||
type: z.string().nullable(),
|
||||
label: z.string().nullable(),
|
||||
text: z.string().nullable(),
|
||||
placeholder: z.string().nullable(),
|
||||
href: z.string().nullable(),
|
||||
disabled: z.boolean(),
|
||||
});
|
||||
|
||||
export const BrowserPageSnapshotSchema = z.object({
|
||||
snapshotId: z.string(),
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
loading: z.boolean(),
|
||||
text: z.string(),
|
||||
elements: z.array(BrowserPageElementSchema),
|
||||
});
|
||||
|
||||
export const BrowserControlActionSchema = z.enum([
|
||||
'open',
|
||||
'get-state',
|
||||
'new-tab',
|
||||
'switch-tab',
|
||||
'close-tab',
|
||||
'navigate',
|
||||
'back',
|
||||
'forward',
|
||||
'reload',
|
||||
'read-page',
|
||||
'click',
|
||||
'type',
|
||||
'press',
|
||||
'scroll',
|
||||
'wait',
|
||||
]);
|
||||
|
||||
const BrowserElementTargetFields = {
|
||||
index: z.number().int().positive().optional(),
|
||||
selector: z.string().min(1).optional(),
|
||||
snapshotId: z.string().optional(),
|
||||
} as const;
|
||||
|
||||
export const BrowserControlInputSchema = z.object({
|
||||
action: BrowserControlActionSchema,
|
||||
target: z.string().min(1).optional(),
|
||||
tabId: z.string().min(1).optional(),
|
||||
text: z.string().optional(),
|
||||
key: z.string().min(1).optional(),
|
||||
direction: z.enum(['up', 'down']).optional(),
|
||||
amount: z.number().int().positive().max(5000).optional(),
|
||||
ms: z.number().int().positive().max(30000).optional(),
|
||||
maxElements: z.number().int().positive().max(100).optional(),
|
||||
maxTextLength: z.number().int().positive().max(20000).optional(),
|
||||
...BrowserElementTargetFields,
|
||||
}).strict().superRefine((value, ctx) => {
|
||||
const needsElementTarget = value.action === 'click' || value.action === 'type';
|
||||
const hasElementTarget = value.index !== undefined || value.selector !== undefined;
|
||||
|
||||
if ((value.action === 'switch-tab' || value.action === 'close-tab') && !value.tabId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['tabId'],
|
||||
message: 'tabId is required for this action.',
|
||||
});
|
||||
}
|
||||
|
||||
if ((value.action === 'navigate') && !value.target) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['target'],
|
||||
message: 'target is required for navigate.',
|
||||
});
|
||||
}
|
||||
|
||||
if (value.action === 'type' && value.text === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['text'],
|
||||
message: 'text is required for type.',
|
||||
});
|
||||
}
|
||||
|
||||
if (value.action === 'press' && !value.key) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['key'],
|
||||
message: 'key is required for press.',
|
||||
});
|
||||
}
|
||||
|
||||
if (needsElementTarget && !hasElementTarget) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['index'],
|
||||
message: 'Provide an element index or selector.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const BrowserControlResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
action: BrowserControlActionSchema,
|
||||
message: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
browser: BrowserStateSchema,
|
||||
page: BrowserPageSnapshotSchema.optional(),
|
||||
});
|
||||
|
||||
export type BrowserTabState = z.infer<typeof BrowserTabStateSchema>;
|
||||
export type BrowserState = z.infer<typeof BrowserStateSchema>;
|
||||
export type BrowserPageElement = z.infer<typeof BrowserPageElementSchema>;
|
||||
export type BrowserPageSnapshot = z.infer<typeof BrowserPageSnapshotSchema>;
|
||||
export type BrowserControlAction = z.infer<typeof BrowserControlActionSchema>;
|
||||
export type BrowserControlInput = z.infer<typeof BrowserControlInputSchema>;
|
||||
export type BrowserControlResult = z.infer<typeof BrowserControlResultSchema>;
|
||||
78
apps/x/packages/shared/src/composio.ts
Normal file
78
apps/x/packages/shared/src/composio.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schemas for Composio IPC responses.
|
||||
* Defined here in shared so both ipc.ts and core/composio/types.ts can reference them.
|
||||
*/
|
||||
export const ZToolkitMeta = z.object({
|
||||
description: z.string(),
|
||||
logo: z.string(),
|
||||
tools_count: z.number(),
|
||||
triggers_count: z.number(),
|
||||
});
|
||||
|
||||
export const ZToolkitItem = z.object({
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
meta: ZToolkitMeta,
|
||||
no_auth: z.boolean().optional(),
|
||||
auth_schemes: z.array(z.string()).optional(),
|
||||
composio_managed_auth_schemes: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const ZListToolkitsResponse = z.object({
|
||||
items: z.array(ZToolkitItem),
|
||||
nextCursor: z.string().nullable(),
|
||||
totalItems: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Curated Composio toolkits available to Rowboat users.
|
||||
* Single source of truth for slugs, display names, and categories.
|
||||
* Sorted by slug (ASC) for maintainability.
|
||||
*/
|
||||
|
||||
export type ToolkitCategory = 'communication' | 'productivity' | 'development' | 'crm' | 'social' | 'storage' | 'support';
|
||||
|
||||
export interface CuratedToolkit {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
category: ToolkitCategory;
|
||||
}
|
||||
|
||||
export const CURATED_TOOLKITS: CuratedToolkit[] = [
|
||||
{ slug: 'airtable', displayName: 'Airtable', category: 'productivity' },
|
||||
{ slug: 'asana', displayName: 'Asana', category: 'productivity' },
|
||||
{ slug: 'cal', displayName: 'Cal.com', category: 'productivity' },
|
||||
{ slug: 'calendly', displayName: 'Calendly', category: 'productivity' },
|
||||
{ slug: 'dropbox', displayName: 'Dropbox', category: 'storage' },
|
||||
{ slug: 'github', displayName: 'GitHub', category: 'development' },
|
||||
{ slug: 'gmail', displayName: 'Gmail', category: 'communication' },
|
||||
{ slug: 'googlecalendar', displayName: 'Google Calendar', category: 'productivity' },
|
||||
{ slug: 'googledocs', displayName: 'Google Docs', category: 'productivity' },
|
||||
{ slug: 'googledrive', displayName: 'Google Drive', category: 'storage' },
|
||||
{ slug: 'googlesheets', displayName: 'Google Sheets', category: 'productivity' },
|
||||
{ slug: 'hubspot', displayName: 'HubSpot', category: 'crm' },
|
||||
{ slug: 'intercom', displayName: 'Intercom', category: 'support' },
|
||||
{ slug: 'jira', displayName: 'Jira', category: 'development' },
|
||||
{ slug: 'linear', displayName: 'Linear', category: 'development' },
|
||||
{ slug: 'linkedin', displayName: 'LinkedIn', category: 'social' },
|
||||
{ slug: 'microsoft_outlook', displayName: 'Microsoft Outlook', category: 'communication' },
|
||||
{ slug: 'microsoft_teams', displayName: 'Microsoft Teams', category: 'communication' },
|
||||
{ slug: 'notion', displayName: 'Notion', category: 'productivity' },
|
||||
{ slug: 'onedrive', displayName: 'OneDrive', category: 'storage' },
|
||||
{ slug: 'reddit', displayName: 'Reddit', category: 'social' },
|
||||
{ slug: 'salesforce', displayName: 'Salesforce', category: 'crm' },
|
||||
{ slug: 'slack', displayName: 'Slack', category: 'communication' },
|
||||
{ slug: 'trello', displayName: 'Trello', category: 'productivity' },
|
||||
{ slug: 'twitter', displayName: 'X', category: 'social' },
|
||||
{ slug: 'zendesk', displayName: 'Zendesk', category: 'support' },
|
||||
];
|
||||
|
||||
/** Slug → display-name lookup. */
|
||||
export const COMPOSIO_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
|
||||
CURATED_TOOLKITS.map(t => [t.slug, t.displayName])
|
||||
);
|
||||
|
||||
/** Set of curated slugs for fast membership checks. */
|
||||
export const CURATED_TOOLKIT_SLUGS = new Set(CURATED_TOOLKITS.map(t => t.slug));
|
||||
|
|
@ -9,7 +9,10 @@ export * as agentScheduleState from './agent-schedule-state.js';
|
|||
export * as serviceEvents from './service-events.js'
|
||||
export * as inlineTask from './inline-task.js';
|
||||
export * as blocks from './blocks.js';
|
||||
export * as trackBlock from './track-block.js';
|
||||
export * as promptBlock from './prompt-block.js';
|
||||
export * as frontmatter from './frontmatter.js';
|
||||
export * as bases from './bases.js';
|
||||
export * as skill from './skill.js';
|
||||
export * as browserControl from './browser-control.js';
|
||||
export { PrefixLogger };
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ import { LlmModelConfig } from './models.js';
|
|||
import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js';
|
||||
import { AgentScheduleState } from './agent-schedule-state.js';
|
||||
import { ServiceEvent } from './service-events.js';
|
||||
import { TrackEvent } from './track-block.js';
|
||||
import { UserMessageContent } from './message.js';
|
||||
import { ResolvedSkill, SkillOverride } from './skill.js';
|
||||
import { RowboatApiConfig } from './rowboat-account.js';
|
||||
import { ZListToolkitsResponse } from './composio.js';
|
||||
import { BrowserStateSchema } from './browser-control.js';
|
||||
|
||||
// ============================================================================
|
||||
// Runtime Validation Schemas (Single Source of Truth)
|
||||
|
|
@ -134,6 +138,18 @@ const ipcSchemas = {
|
|||
voiceInput: z.boolean().optional(),
|
||||
voiceOutput: z.enum(['summary', 'full']).optional(),
|
||||
searchEnabled: z.boolean().optional(),
|
||||
middlePaneContext: z.discriminatedUnion('kind', [
|
||||
z.object({
|
||||
kind: z.literal('note'),
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('browser'),
|
||||
url: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
]).optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
messageId: z.string(),
|
||||
|
|
@ -192,6 +208,10 @@ const ipcSchemas = {
|
|||
req: ServiceEvent,
|
||||
res: z.null(),
|
||||
},
|
||||
'tracks:events': {
|
||||
req: TrackEvent,
|
||||
res: z.null(),
|
||||
},
|
||||
'models:list': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
|
|
@ -224,6 +244,7 @@ const ipcSchemas = {
|
|||
req: z.object({
|
||||
provider: z.string(),
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
|
|
@ -250,14 +271,25 @@ const ipcSchemas = {
|
|||
config: z.record(z.string(), z.object({
|
||||
connected: z.boolean(),
|
||||
error: z.string().nullable().optional(),
|
||||
userId: z.string().optional(),
|
||||
clientId: z.string().nullable().optional(),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
'account:getRowboat': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
signedIn: z.boolean(),
|
||||
accessToken: z.string().nullable(),
|
||||
config: RowboatApiConfig.nullable(),
|
||||
}),
|
||||
},
|
||||
'oauth:didConnect': {
|
||||
req: z.object({
|
||||
provider: z.string(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
}),
|
||||
res: z.null(),
|
||||
},
|
||||
|
|
@ -369,18 +401,6 @@ const ipcSchemas = {
|
|||
toolkits: z.array(z.string()),
|
||||
}),
|
||||
},
|
||||
'composio:execute-action': {
|
||||
req: z.object({
|
||||
actionSlug: z.string(),
|
||||
toolkitSlug: z.string(),
|
||||
input: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
res: z.object({
|
||||
data: z.unknown(),
|
||||
successful: z.boolean(),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
},
|
||||
'composio:use-composio-for-google': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
|
|
@ -401,6 +421,11 @@ const ipcSchemas = {
|
|||
}),
|
||||
res: z.null(),
|
||||
},
|
||||
// Composio Tools Library channels
|
||||
'composio:list-toolkits': {
|
||||
req: z.object({}),
|
||||
res: ZListToolkitsResponse,
|
||||
},
|
||||
// Agent schedule channels
|
||||
'agent-schedule:getConfig': {
|
||||
req: z.null(),
|
||||
|
|
@ -493,11 +518,15 @@ const ipcSchemas = {
|
|||
mimeType: z.string(),
|
||||
}),
|
||||
},
|
||||
'voice:getDeepgramToken': {
|
||||
'meeting:checkScreenPermission': {
|
||||
req: z.null(),
|
||||
res: z.object({
|
||||
token: z.string(),
|
||||
}).nullable(),
|
||||
granted: z.boolean(),
|
||||
}),
|
||||
},
|
||||
'meeting:openScreenRecordingSettings': {
|
||||
req: z.null(),
|
||||
res: z.object({ success: z.boolean() }),
|
||||
},
|
||||
'meeting:summarize': {
|
||||
req: z.object({
|
||||
|
|
@ -550,6 +579,148 @@ const ipcSchemas = {
|
|||
response: z.string().nullable(),
|
||||
}),
|
||||
},
|
||||
// Track channels
|
||||
'track:run': {
|
||||
req: z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
summary: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'track:get': {
|
||||
req: z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
// Fresh, authoritative YAML of the track block from disk.
|
||||
// Renderer should use this for display/edit — never its Tiptap node attr.
|
||||
yaml: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'track:update': {
|
||||
req: z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
// Partial TrackBlock updates — merged into the block's YAML on disk.
|
||||
// Backend is the sole writer; avoids races with scheduler/runner writes.
|
||||
updates: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
yaml: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'track:replaceYaml': {
|
||||
req: z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
yaml: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
yaml: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'track:delete': {
|
||||
req: z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
// Embedded browser (WebContentsView) channels
|
||||
'browser:setBounds': {
|
||||
req: z.object({
|
||||
x: z.number().int(),
|
||||
y: z.number().int(),
|
||||
width: z.number().int().nonnegative(),
|
||||
height: z.number().int().nonnegative(),
|
||||
}),
|
||||
res: z.object({ ok: z.literal(true) }),
|
||||
},
|
||||
'browser:setVisible': {
|
||||
req: z.object({ visible: z.boolean() }),
|
||||
res: z.object({ ok: z.literal(true) }),
|
||||
},
|
||||
'browser:newTab': {
|
||||
req: z.object({
|
||||
url: z.string().min(1).refine(
|
||||
(u) => {
|
||||
const lower = u.trim().toLowerCase();
|
||||
if (lower.startsWith('javascript:')) return false;
|
||||
if (lower.startsWith('file://')) return false;
|
||||
if (lower.startsWith('chrome://')) return false;
|
||||
if (lower.startsWith('chrome-extension://')) return false;
|
||||
return true;
|
||||
},
|
||||
{ message: 'Unsafe URL scheme' },
|
||||
).optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
ok: z.boolean(),
|
||||
tabId: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'browser:switchTab': {
|
||||
req: z.object({ tabId: z.string().min(1) }),
|
||||
res: z.object({ ok: z.boolean() }),
|
||||
},
|
||||
'browser:closeTab': {
|
||||
req: z.object({ tabId: z.string().min(1) }),
|
||||
res: z.object({ ok: z.boolean() }),
|
||||
},
|
||||
'browser:navigate': {
|
||||
req: z.object({
|
||||
url: z.string().min(1).refine(
|
||||
(u) => {
|
||||
const lower = u.trim().toLowerCase();
|
||||
if (lower.startsWith('javascript:')) return false;
|
||||
if (lower.startsWith('file://')) return false;
|
||||
if (lower.startsWith('chrome://')) return false;
|
||||
if (lower.startsWith('chrome-extension://')) return false;
|
||||
return true;
|
||||
},
|
||||
{ message: 'Unsafe URL scheme' },
|
||||
),
|
||||
}),
|
||||
res: z.object({
|
||||
ok: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
'browser:back': {
|
||||
req: z.null(),
|
||||
res: z.object({ ok: z.boolean() }),
|
||||
},
|
||||
'browser:forward': {
|
||||
req: z.null(),
|
||||
res: z.object({ ok: z.boolean() }),
|
||||
},
|
||||
'browser:reload': {
|
||||
req: z.null(),
|
||||
res: z.object({ ok: z.literal(true) }),
|
||||
},
|
||||
'browser:getState': {
|
||||
req: z.null(),
|
||||
res: BrowserStateSchema,
|
||||
},
|
||||
'browser:didUpdateState': {
|
||||
req: BrowserStateSchema,
|
||||
res: z.null(),
|
||||
},
|
||||
// Billing channels
|
||||
// Skills channels
|
||||
'skills:list': {
|
||||
|
|
@ -595,6 +766,7 @@ const ipcSchemas = {
|
|||
userId: z.string().nullable(),
|
||||
subscriptionPlan: z.string().nullable(),
|
||||
subscriptionStatus: z.string().nullable(),
|
||||
trialExpiresAt: z.string().nullable(),
|
||||
sanctionedCredits: z.number(),
|
||||
availableCredits: z.number(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const UserAttachmentPart = z.object({
|
|||
filename: z.string(), // display name ("photo.png")
|
||||
mimeType: z.string(), // MIME type ("image/png", "text/plain")
|
||||
size: z.number().optional(), // bytes
|
||||
lineNumber: z.number().int().min(1).optional(), // 1-indexed line in source file (for editor-context references)
|
||||
});
|
||||
|
||||
// Any single part of a user message (text or attachment)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ export const LlmModelConfig = z.object({
|
|||
model: z.string(),
|
||||
models: z.array(z.string()).optional(),
|
||||
knowledgeGraphModel: z.string().optional(),
|
||||
meetingNotesModel: z.string().optional(),
|
||||
});
|
||||
|
|
|
|||
8
apps/x/packages/shared/src/prompt-block.ts
Normal file
8
apps/x/packages/shared/src/prompt-block.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import z from 'zod';
|
||||
|
||||
export const PromptBlockSchema = z.object({
|
||||
label: z.string().min(1).describe('Short title shown on the card'),
|
||||
instruction: z.string().min(1).describe('Full prompt sent to Copilot when Run is clicked'),
|
||||
});
|
||||
|
||||
export type PromptBlock = z.infer<typeof PromptBlockSchema>;
|
||||
7
apps/x/packages/shared/src/rowboat-account.ts
Normal file
7
apps/x/packages/shared/src/rowboat-account.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const RowboatApiConfig = z.object({
|
||||
appUrl: z.string(),
|
||||
websocketApiUrl: z.string(),
|
||||
supabaseUrl: z.string(),
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ export const ServiceName = z.enum([
|
|||
'voice_memo',
|
||||
'email_labeling',
|
||||
'note_tagging',
|
||||
'agent_notes',
|
||||
]);
|
||||
|
||||
const ServiceEventBase = z.object({
|
||||
|
|
|
|||
87
apps/x/packages/shared/src/track-block.ts
Normal file
87
apps/x/packages/shared/src/track-block.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import z from 'zod';
|
||||
|
||||
export const TrackScheduleSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('cron').describe('Fires at exact cron times'),
|
||||
expression: z.string().describe('5-field cron expression, quoted (e.g. "0 * * * *")'),
|
||||
}).describe('Recurring at exact times'),
|
||||
z.object({
|
||||
type: z.literal('window').describe('Fires at most once per cron occurrence, only within a time-of-day window'),
|
||||
cron: z.string().describe('5-field cron expression, quoted'),
|
||||
startTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe('24h HH:MM, local time'),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe('24h HH:MM, local time'),
|
||||
}).describe('Recurring within a time-of-day window'),
|
||||
z.object({
|
||||
type: z.literal('once').describe('Fires once and never again'),
|
||||
runAt: z.string().describe('ISO 8601 datetime, local time, no Z suffix (e.g. "2026-04-14T09:00:00")'),
|
||||
}).describe('One-shot future run'),
|
||||
]).describe('Optional schedule. Omit entirely for manual-only tracks.');
|
||||
|
||||
export type TrackSchedule = z.infer<typeof TrackScheduleSchema>;
|
||||
|
||||
export const TrackBlockSchema = z.object({
|
||||
trackId: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).describe('Kebab-case identifier, unique within the note file'),
|
||||
instruction: z.string().min(1).describe('What the agent should produce each run — specific, single-focus, imperative'),
|
||||
eventMatchCriteria: z.string().optional().describe('When set, this track participates in event-based triggering. Describe what kinds of events should consider this track for an update (e.g. "Emails about Q3 planning"). Omit to disable event triggers — the track will only run on schedule or manually.'),
|
||||
active: z.boolean().default(true).describe('Set false to pause without deleting'),
|
||||
schedule: TrackScheduleSchema.optional(),
|
||||
lastRunAt: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
lastRunId: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
lastRunSummary: z.string().optional().describe('Runtime-managed — never write this yourself'),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knowledge events (event-driven track triggering pipeline)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const KnowledgeEventSchema = z.object({
|
||||
id: z.string().describe('Monotonically increasing ID; also the filename in events/pending/'),
|
||||
source: z.string().describe('Producer of the event (e.g. "gmail", "calendar")'),
|
||||
type: z.string().describe('Event type (e.g. "email.synced")'),
|
||||
createdAt: z.string().describe('ISO timestamp when the event was produced'),
|
||||
payload: z.string().describe('Human-readable event body, usually markdown'),
|
||||
targetTrackId: z.string().optional().describe('If set, skip routing and target this track directly (used for re-runs)'),
|
||||
targetFilePath: z.string().optional(),
|
||||
// Enriched on move from pending/ to done/
|
||||
processedAt: z.string().optional(),
|
||||
candidates: z.array(z.object({
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
})).optional(),
|
||||
runIds: z.array(z.string()).optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type KnowledgeEvent = z.infer<typeof KnowledgeEventSchema>;
|
||||
|
||||
export const Pass1OutputSchema = z.object({
|
||||
candidates: z.array(z.object({
|
||||
trackId: z.string().describe('The track block identifier'),
|
||||
filePath: z.string().describe('The note file path the track lives in'),
|
||||
})).describe('Tracks that may be relevant to this event. trackIds are only unique within a file, so always return both fields.'),
|
||||
});
|
||||
|
||||
export type Pass1Output = z.infer<typeof Pass1OutputSchema>;
|
||||
|
||||
// Track bus events
|
||||
export const TrackRunStartEvent = z.object({
|
||||
type: z.literal('track_run_start'),
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
trigger: z.enum(['timed', 'manual', 'event']),
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
export const TrackRunCompleteEvent = z.object({
|
||||
type: z.literal('track_run_complete'),
|
||||
trackId: z.string(),
|
||||
filePath: z.string(),
|
||||
runId: z.string(),
|
||||
error: z.string().optional(),
|
||||
summary: z.string().optional(),
|
||||
});
|
||||
|
||||
export const TrackEvent = z.union([TrackRunStartEvent, TrackRunCompleteEvent]);
|
||||
|
||||
export type TrackBlock = z.infer<typeof TrackBlockSchema>;
|
||||
export type TrackEventType = z.infer<typeof TrackEvent>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue