mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
Merge dev into slack-integration
This commit is contained in:
commit
05a2034d27
39 changed files with 1491 additions and 383 deletions
147
CLAUDE.md
Normal file
147
CLAUDE.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# CLAUDE.md - AI Coding Agent Context
|
||||
|
||||
This file provides context for AI coding agents working on the Rowboat monorepo.
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Electron App (apps/x)
|
||||
cd apps/x && pnpm install # Install dependencies
|
||||
cd apps/x && npm run deps # Build workspace packages (shared → core → preload)
|
||||
cd apps/x && npm run dev # Development mode (builds deps, runs app)
|
||||
cd apps/x && npm run lint # Lint check
|
||||
cd apps/x/apps/main && npm run package # Production build (.app)
|
||||
cd apps/x/apps/main && npm run make # Create DMG distributable
|
||||
```
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
rowboat/
|
||||
├── apps/
|
||||
│ ├── x/ # Electron desktop app (focus of this doc)
|
||||
│ ├── rowboat/ # Next.js web dashboard
|
||||
│ ├── rowboatx/ # Next.js frontend
|
||||
│ ├── cli/ # CLI tool
|
||||
│ ├── python-sdk/ # Python SDK
|
||||
│ └── docs/ # Documentation site
|
||||
├── CLAUDE.md # This file
|
||||
└── README.md # User-facing readme
|
||||
```
|
||||
|
||||
## Electron App Architecture (`apps/x`)
|
||||
|
||||
The Electron app is a **nested pnpm workspace** with its own package management.
|
||||
|
||||
```
|
||||
apps/x/
|
||||
├── package.json # Workspace root, dev scripts
|
||||
├── pnpm-workspace.yaml # Defines workspace packages
|
||||
├── pnpm-lock.yaml # Lockfile
|
||||
├── apps/
|
||||
│ ├── main/ # Electron main process
|
||||
│ │ ├── src/ # Main process source
|
||||
│ │ ├── forge.config.cjs # Electron Forge config
|
||||
│ │ └── bundle.mjs # esbuild bundler
|
||||
│ ├── renderer/ # React UI (Vite)
|
||||
│ │ ├── src/ # React components
|
||||
│ │ └── vite.config.ts
|
||||
│ └── preload/ # Electron preload scripts
|
||||
│ └── src/
|
||||
└── packages/
|
||||
├── shared/ # @x/shared - Types, utilities, validators
|
||||
└── core/ # @x/core - Business logic, AI, OAuth, MCP
|
||||
```
|
||||
|
||||
### Build Order (Dependencies)
|
||||
|
||||
```
|
||||
shared (no deps)
|
||||
↓
|
||||
core (depends on shared)
|
||||
↓
|
||||
preload (depends on shared)
|
||||
↓
|
||||
renderer (depends on shared)
|
||||
main (depends on shared, core)
|
||||
```
|
||||
|
||||
**The `npm run deps` command builds:** shared → core → preload
|
||||
|
||||
### Key Entry Points
|
||||
|
||||
| Component | Entry | Output |
|
||||
|-----------|-------|--------|
|
||||
| main | `apps/main/src/main.ts` | `.package/dist/main.cjs` |
|
||||
| renderer | `apps/renderer/src/main.tsx` | `apps/renderer/dist/` |
|
||||
| preload | `apps/preload/src/preload.ts` | `apps/preload/dist/preload.js` |
|
||||
|
||||
## Build System
|
||||
|
||||
- **Package manager:** pnpm (required for `workspace:*` protocol)
|
||||
- **Main bundler:** esbuild (bundles to single CommonJS file)
|
||||
- **Renderer bundler:** Vite
|
||||
- **Packaging:** Electron Forge
|
||||
- **TypeScript:** ES2022 target
|
||||
|
||||
### Why esbuild bundling?
|
||||
|
||||
pnpm uses symlinks for workspace packages. Electron Forge's dependency walker can't follow these symlinks. esbuild bundles everything into a single file, eliminating the need for node_modules in the packaged app.
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| Purpose | File |
|
||||
|---------|------|
|
||||
| Electron main entry | `apps/x/apps/main/src/main.ts` |
|
||||
| React app entry | `apps/x/apps/renderer/src/main.tsx` |
|
||||
| Forge config (packaging) | `apps/x/apps/main/forge.config.cjs` |
|
||||
| Main process bundler | `apps/x/apps/main/bundle.mjs` |
|
||||
| Vite config | `apps/x/apps/renderer/vite.config.ts` |
|
||||
| Shared types | `apps/x/packages/shared/src/` |
|
||||
| Core business logic | `apps/x/packages/core/src/` |
|
||||
| Workspace config | `apps/x/pnpm-workspace.yaml` |
|
||||
| Root scripts | `apps/x/package.json` |
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add a new shared type
|
||||
1. Edit `apps/x/packages/shared/src/`
|
||||
2. Run `cd apps/x && npm run deps` to rebuild
|
||||
|
||||
### Modify main process
|
||||
1. Edit `apps/x/apps/main/src/`
|
||||
2. Restart dev server (main doesn't hot-reload)
|
||||
|
||||
### Modify renderer (React UI)
|
||||
1. Edit `apps/x/apps/renderer/src/`
|
||||
2. Changes hot-reload automatically in dev mode
|
||||
|
||||
### Add a new dependency to main
|
||||
1. `cd apps/x/apps/main && pnpm add <package>`
|
||||
2. Import in source - esbuild will bundle it
|
||||
|
||||
### Verify compilation
|
||||
```bash
|
||||
cd apps/x && npm run deps && npm run lint
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Desktop | Electron 39.x |
|
||||
| UI | React 19, Vite 7 |
|
||||
| Styling | TailwindCSS, Radix UI |
|
||||
| State | React hooks |
|
||||
| AI | Vercel AI SDK, Anthropic/OpenAI/Google providers |
|
||||
| IPC | Electron contextBridge |
|
||||
| Build | TypeScript 5.9, esbuild, Electron Forge |
|
||||
|
||||
## Environment Variables (for packaging)
|
||||
|
||||
For production builds with code signing:
|
||||
- `APPLE_ID` - Apple Developer ID
|
||||
- `APPLE_PASSWORD` - App-specific password
|
||||
- `APPLE_TEAM_ID` - Team ID
|
||||
|
||||
Not required for local development.
|
||||
|
|
@ -296,7 +296,7 @@ export function setupIpcHandlers() {
|
|||
return { success: true };
|
||||
},
|
||||
'runs:stop': async (_event, args) => {
|
||||
await runsCore.stop(args.runId);
|
||||
await runsCore.stop(args.runId, args.force);
|
||||
return { success: true };
|
||||
},
|
||||
'runs:fetch': async (_event, args) => {
|
||||
|
|
@ -306,7 +306,7 @@ export function setupIpcHandlers() {
|
|||
return runsCore.listRuns(args.cursor);
|
||||
},
|
||||
'oauth:connect': async (_event, args) => {
|
||||
return await connectProvider(args.provider, args.clientId);
|
||||
return await connectProvider(args.provider);
|
||||
},
|
||||
'oauth:disconnect': async (_event, args) => {
|
||||
return await disconnectProvider(args.provider);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies
|
|||
import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
|
||||
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
|
||||
import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
|
||||
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -96,7 +97,7 @@ function createWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
// Register custom protocol before creating window (for production builds)
|
||||
if (app.isPackaged) {
|
||||
registerAppProtocol();
|
||||
|
|
@ -113,6 +114,9 @@ app.whenReady().then(() => {
|
|||
});
|
||||
}
|
||||
|
||||
// Initialize all config files before UI can access them
|
||||
await initConfigs();
|
||||
|
||||
setupIpcHandlers();
|
||||
|
||||
createWindow();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import { shell } from 'electron';
|
||||
import type { Server } from 'http';
|
||||
import { createAuthServer } from './auth-server.js';
|
||||
import * as oauthClient from '@x/core/dist/auth/oauth-client.js';
|
||||
import type { Configuration } from '@x/core/dist/auth/oauth-client.js';
|
||||
import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/providers.js';
|
||||
import {
|
||||
clearProviderClientIdOverride,
|
||||
getProviderClientIdOverride,
|
||||
hasProviderClientIdOverride,
|
||||
setProviderClientIdOverride,
|
||||
} from '@x/core/dist/auth/provider-client-id.js';
|
||||
import container from '@x/core/dist/di/container.js';
|
||||
import { IOAuthRepo } from '@x/core/dist/auth/repo.js';
|
||||
import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
|
||||
|
|
@ -20,12 +15,48 @@ import { emitOAuthEvent } from './ipc.js';
|
|||
const REDIRECT_URI = 'http://localhost:8080/oauth/callback';
|
||||
|
||||
// Store active OAuth flows (state -> { codeVerifier, provider, config })
|
||||
const activeFlows = new Map<string, {
|
||||
codeVerifier: string;
|
||||
const activeFlows = new Map<string, {
|
||||
codeVerifier: string;
|
||||
provider: string;
|
||||
config: Configuration;
|
||||
}>();
|
||||
|
||||
// Module-level state for tracking the active OAuth flow
|
||||
interface ActiveOAuthFlow {
|
||||
provider: string;
|
||||
state: string;
|
||||
server: Server;
|
||||
cleanupTimeout: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
let activeFlow: ActiveOAuthFlow | null = null;
|
||||
|
||||
/**
|
||||
* Cancel any active OAuth flow, cleaning up resources
|
||||
*/
|
||||
function cancelActiveFlow(reason: string = 'cancelled'): void {
|
||||
if (!activeFlow) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[OAuth] Cancelling active flow for ${activeFlow.provider}: ${reason}`);
|
||||
|
||||
clearTimeout(activeFlow.cleanupTimeout);
|
||||
activeFlow.server.close();
|
||||
activeFlows.delete(activeFlow.state);
|
||||
|
||||
// Only emit event for user-visible cancellations
|
||||
if (reason !== 'new_flow_started') {
|
||||
emitOAuthEvent({
|
||||
provider: activeFlow.provider,
|
||||
success: false,
|
||||
error: `OAuth flow ${reason}`
|
||||
});
|
||||
}
|
||||
|
||||
activeFlow = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth repository from DI container
|
||||
*/
|
||||
|
|
@ -45,25 +76,14 @@ function getClientRegistrationRepo(): IClientRegistrationRepo {
|
|||
*/
|
||||
async function getProviderConfiguration(provider: string): Promise<Configuration> {
|
||||
const config = getProviderConfig(provider);
|
||||
const resolveClientId = (): string => {
|
||||
const override = getProviderClientIdOverride(provider);
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
if (config.client.mode === 'static' && config.client.clientId) {
|
||||
return config.client.clientId;
|
||||
}
|
||||
throw new Error(`${provider} client ID not configured. Please provide a client ID.`);
|
||||
};
|
||||
|
||||
if (config.discovery.mode === 'issuer') {
|
||||
if (config.client.mode === 'static') {
|
||||
// Discover endpoints, use static client ID
|
||||
console.log(`[OAuth] ${provider}: Discovery from issuer with static client ID`);
|
||||
const clientId = resolveClientId();
|
||||
return await oauthClient.discoverConfiguration(
|
||||
config.discovery.issuer,
|
||||
clientId
|
||||
config.client.clientId
|
||||
);
|
||||
} else {
|
||||
// DCR mode - check for existing registration or register new
|
||||
|
|
@ -100,11 +120,10 @@ async function getProviderConfiguration(provider: string): Promise<Configuration
|
|||
}
|
||||
|
||||
console.log(`[OAuth] ${provider}: Using static endpoints (no discovery)`);
|
||||
const clientId = resolveClientId();
|
||||
return oauthClient.createStaticConfiguration(
|
||||
config.discovery.authorizationEndpoint,
|
||||
config.discovery.tokenEndpoint,
|
||||
clientId,
|
||||
config.client.clientId,
|
||||
config.discovery.revocationEndpoint
|
||||
);
|
||||
}
|
||||
|
|
@ -113,20 +132,16 @@ async function getProviderConfiguration(provider: string): Promise<Configuration
|
|||
/**
|
||||
* Initiate OAuth flow for a provider
|
||||
*/
|
||||
export async function connectProvider(provider: string, clientId?: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function connectProvider(provider: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log(`[OAuth] Starting connection flow for ${provider}...`);
|
||||
|
||||
// Cancel any existing flow before starting a new one
|
||||
cancelActiveFlow('new_flow_started');
|
||||
|
||||
const oauthRepo = getOAuthRepo();
|
||||
const providerConfig = getProviderConfig(provider);
|
||||
|
||||
if (provider === 'google') {
|
||||
const trimmedClientId = clientId?.trim();
|
||||
if (!trimmedClientId) {
|
||||
return { success: false, error: 'Google client ID is required to connect.' };
|
||||
}
|
||||
setProviderClientIdOverride(provider, trimmedClientId);
|
||||
}
|
||||
|
||||
// Get or create OAuth configuration
|
||||
const config = await getProviderConfiguration(provider);
|
||||
|
||||
|
|
@ -148,9 +163,6 @@ export async function connectProvider(provider: string, clientId?: string): Prom
|
|||
state,
|
||||
});
|
||||
|
||||
// Declare timeout variable (will be set after server is created)
|
||||
let cleanupTimeout: NodeJS.Timeout;
|
||||
|
||||
// Create callback server
|
||||
const { server } = await createAuthServer(8080, async (code, receivedState) => {
|
||||
// Validate state
|
||||
|
|
@ -166,7 +178,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom
|
|||
try {
|
||||
// Build callback URL for token exchange
|
||||
const callbackUrl = new URL(`${REDIRECT_URI}?code=${code}&state=${receivedState}`);
|
||||
|
||||
|
||||
// Exchange code for tokens
|
||||
console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`);
|
||||
const tokens = await oauthClient.exchangeCodeForTokens(
|
||||
|
|
@ -198,21 +210,30 @@ export async function connectProvider(provider: string, clientId?: string): Prom
|
|||
} finally {
|
||||
// Clean up
|
||||
activeFlows.delete(state);
|
||||
server.close();
|
||||
clearTimeout(cleanupTimeout);
|
||||
if (activeFlow && activeFlow.state === state) {
|
||||
clearTimeout(activeFlow.cleanupTimeout);
|
||||
activeFlow.server.close();
|
||||
activeFlow = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set timeout to clean up abandoned flows (5 minutes)
|
||||
// Set timeout to clean up abandoned flows (2 minutes)
|
||||
// This prevents memory leaks if user never completes the OAuth flow
|
||||
cleanupTimeout = setTimeout(() => {
|
||||
if (activeFlows.has(state)) {
|
||||
const cleanupTimeout = setTimeout(() => {
|
||||
if (activeFlow?.state === state) {
|
||||
console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`);
|
||||
activeFlows.delete(state);
|
||||
server.close();
|
||||
emitOAuthEvent({ provider, success: false, error: 'OAuth flow timed out' });
|
||||
cancelActiveFlow('timed_out');
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
}, 2 * 60 * 1000); // 2 minutes
|
||||
|
||||
// Store complete flow state for cleanup
|
||||
activeFlow = {
|
||||
provider,
|
||||
state,
|
||||
server,
|
||||
cleanupTimeout,
|
||||
};
|
||||
|
||||
// Open in system browser (shares cookies/sessions with user's regular browser)
|
||||
shell.openExternal(authUrl.toString());
|
||||
|
|
@ -235,9 +256,6 @@ export async function disconnectProvider(provider: string): Promise<{ success: b
|
|||
try {
|
||||
const oauthRepo = getOAuthRepo();
|
||||
await oauthRepo.clearTokens(provider);
|
||||
if (provider === 'google') {
|
||||
clearProviderClientIdOverride(provider);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('OAuth disconnect failed:', error);
|
||||
|
|
@ -251,9 +269,6 @@ export async function disconnectProvider(provider: string): Promise<{ success: b
|
|||
export async function isConnected(provider: string): Promise<{ isConnected: boolean }> {
|
||||
try {
|
||||
const oauthRepo = getOAuthRepo();
|
||||
if (provider === 'google' && !hasProviderClientIdOverride(provider)) {
|
||||
return { isConnected: false };
|
||||
}
|
||||
const connected = await oauthRepo.isConnected(provider);
|
||||
return { isConnected: connected };
|
||||
} catch (error) {
|
||||
|
|
@ -310,10 +325,7 @@ export async function getConnectedProviders(): Promise<{ providers: string[] }>
|
|||
try {
|
||||
const oauthRepo = getOAuthRepo();
|
||||
const providers = await oauthRepo.getConnectedProviders();
|
||||
const filteredProviders = providers.filter((provider) =>
|
||||
provider === 'google' ? hasProviderClientIdOverride(provider) : true
|
||||
);
|
||||
return { providers: filteredProviders };
|
||||
return { providers };
|
||||
} catch (error) {
|
||||
console.error('Get connected providers failed:', error);
|
||||
return { providers: [] };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import { contextBridge, ipcRenderer, webUtils } from 'electron';
|
||||
import { ipc as ipcShared } from '@x/shared';
|
||||
|
||||
type InvokeChannels = ipcShared.InvokeChannels;
|
||||
|
|
@ -51,4 +51,8 @@ const ipc = {
|
|||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('ipc', ipc);
|
||||
contextBridge.exposeInMainWorld('ipc', ipc);
|
||||
|
||||
contextBridge.exposeInMainWorld('electronUtils', {
|
||||
getPathForFile: (file: File) => webUtils.getPathForFile(file),
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
|||
import './App.css'
|
||||
import z from 'zod';
|
||||
import { Button } from './components/ui/button';
|
||||
import { CheckIcon, LoaderIcon, ArrowUp, PanelRightIcon, SquarePen } from 'lucide-react';
|
||||
import { CheckIcon, LoaderIcon, ArrowUp, PanelRightIcon, SquarePen, Square } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MarkdownEditor } from './components/markdown-editor';
|
||||
import { ChatInputBar } from './components/chat-button';
|
||||
|
|
@ -279,7 +279,9 @@ const collectFilePaths = (nodes: TreeNode[]): string[] =>
|
|||
// Inner component that uses the controller to access mentions
|
||||
interface ChatInputInnerProps {
|
||||
onSubmit: (message: PromptInputMessage, mentions?: FileMention[]) => void
|
||||
onStop?: () => void
|
||||
isProcessing: boolean
|
||||
isStopping?: boolean
|
||||
presetMessage?: string
|
||||
onPresetMessageConsumed?: () => void
|
||||
runId?: string | null
|
||||
|
|
@ -287,7 +289,9 @@ interface ChatInputInnerProps {
|
|||
|
||||
function ChatInputInner({
|
||||
onSubmit,
|
||||
onStop,
|
||||
isProcessing,
|
||||
isStopping,
|
||||
presetMessage,
|
||||
onPresetMessageConsumed,
|
||||
runId,
|
||||
|
|
@ -318,6 +322,37 @@ function ChatInputInner({
|
|||
}
|
||||
}, [handleSubmit])
|
||||
|
||||
useEffect(() => {
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.types?.includes("Files")) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDrop = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.types?.includes("Files")) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
const paths = Array.from(e.dataTransfer.files)
|
||||
.map((f) => window.electronUtils?.getPathForFile(f))
|
||||
.filter(Boolean)
|
||||
if (paths.length > 0) {
|
||||
const currentText = controller.textInput.value
|
||||
const pathText = paths.join(' ')
|
||||
controller.textInput.setInput(
|
||||
currentText ? `${currentText} ${pathText}` : pathText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("dragover", onDragOver)
|
||||
document.addEventListener("drop", onDrop)
|
||||
return () => {
|
||||
document.removeEventListener("dragover", onDragOver)
|
||||
document.removeEventListener("drop", onDrop)
|
||||
}
|
||||
}, [controller])
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
|
||||
<PromptInputTextarea
|
||||
|
|
@ -327,19 +362,39 @@ function ChatInputInner({
|
|||
focusTrigger={runId}
|
||||
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
canSubmit
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
{isProcessing ? (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onStop}
|
||||
title={isStopping ? "Click again to force stop" : "Stop generation"}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
isStopping
|
||||
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
)}
|
||||
>
|
||||
{isStopping ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Square className="h-3 w-3 fill-current" />
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
canSubmit
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -350,7 +405,9 @@ interface ChatInputWithMentionsProps {
|
|||
recentFiles: string[]
|
||||
visibleFiles: string[]
|
||||
onSubmit: (message: PromptInputMessage, mentions?: FileMention[]) => void
|
||||
onStop?: () => void
|
||||
isProcessing: boolean
|
||||
isStopping?: boolean
|
||||
presetMessage?: string
|
||||
onPresetMessageConsumed?: () => void
|
||||
runId?: string | null
|
||||
|
|
@ -361,7 +418,9 @@ function ChatInputWithMentions({
|
|||
recentFiles,
|
||||
visibleFiles,
|
||||
onSubmit,
|
||||
onStop,
|
||||
isProcessing,
|
||||
isStopping,
|
||||
presetMessage,
|
||||
onPresetMessageConsumed,
|
||||
runId,
|
||||
|
|
@ -370,7 +429,9 @@ function ChatInputWithMentions({
|
|||
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
||||
<ChatInputInner
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
isProcessing={isProcessing}
|
||||
isStopping={isStopping}
|
||||
presetMessage={presetMessage}
|
||||
onPresetMessageConsumed={onPresetMessageConsumed}
|
||||
runId={runId}
|
||||
|
|
@ -414,6 +475,8 @@ function App() {
|
|||
const [runId, setRunId] = useState<string | null>(null)
|
||||
const runIdRef = useRef<string | null>(null)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [isStopping, setIsStopping] = useState(false)
|
||||
const [stopClickedAt, setStopClickedAt] = useState<number | null>(null)
|
||||
const [agentId] = useState<string>('copilot')
|
||||
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
|
|
@ -758,6 +821,8 @@ function App() {
|
|||
|
||||
case 'run-processing-end':
|
||||
setIsProcessing(false)
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
break
|
||||
|
||||
case 'start':
|
||||
|
|
@ -936,8 +1001,32 @@ function App() {
|
|||
break
|
||||
}
|
||||
|
||||
case 'run-stopped':
|
||||
setIsProcessing(false)
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
// Clear pending requests since they've been aborted
|
||||
setPendingPermissionRequests(new Map())
|
||||
setPendingAskHumanRequests(new Map())
|
||||
// Flush any streaming content as a message
|
||||
setCurrentAssistantMessage(currentMsg => {
|
||||
if (currentMsg) {
|
||||
setConversation(prev => [...prev, {
|
||||
id: `assistant-stopped-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
content: currentMsg,
|
||||
timestamp: Date.now(),
|
||||
}])
|
||||
}
|
||||
return ''
|
||||
})
|
||||
setCurrentReasoning('')
|
||||
break
|
||||
|
||||
case 'error':
|
||||
setIsProcessing(false)
|
||||
setIsStopping(false)
|
||||
setStopClickedAt(null)
|
||||
console.error('Run error:', event.error)
|
||||
break
|
||||
}
|
||||
|
|
@ -1009,6 +1098,21 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!runId) return
|
||||
const now = Date.now()
|
||||
const isForce = isStopping && stopClickedAt !== null && (now - stopClickedAt) < 2000
|
||||
|
||||
setStopClickedAt(now)
|
||||
setIsStopping(true)
|
||||
|
||||
try {
|
||||
await window.ipc.invoke('runs:stop', { runId, force: isForce })
|
||||
} catch (error) {
|
||||
console.error('Failed to stop run:', error)
|
||||
}
|
||||
}, [runId, isStopping, stopClickedAt])
|
||||
|
||||
const handlePermissionResponse = useCallback(async (toolCallId: string, subflow: string[], response: 'approve' | 'deny') => {
|
||||
if (!runId) return
|
||||
|
||||
|
|
@ -1779,7 +1883,9 @@ function App() {
|
|||
recentFiles={recentWikiFiles}
|
||||
visibleFiles={visibleKnowledgeFiles}
|
||||
onSubmit={handlePromptSubmit}
|
||||
onStop={handleStop}
|
||||
isProcessing={isProcessing}
|
||||
isStopping={isStopping}
|
||||
presetMessage={presetMessage}
|
||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
||||
runId={runId}
|
||||
|
|
@ -1801,6 +1907,8 @@ function App() {
|
|||
currentAssistantMessage={currentAssistantMessage}
|
||||
currentReasoning={currentReasoning}
|
||||
isProcessing={isProcessing}
|
||||
isStopping={isStopping}
|
||||
onStop={handleStop}
|
||||
message={message}
|
||||
onMessageChange={setMessage}
|
||||
onSubmit={handlePromptSubmit}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ArrowUp, Expand, Plus } from 'lucide-react'
|
||||
import { ArrowUp, Expand, LoaderIcon, Plus, Square } from 'lucide-react'
|
||||
import type { ToolUIPart } from 'ai'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -115,6 +115,8 @@ interface ChatSidebarProps {
|
|||
currentAssistantMessage: string
|
||||
currentReasoning: string
|
||||
isProcessing: boolean
|
||||
isStopping?: boolean
|
||||
onStop?: () => void
|
||||
message: string
|
||||
onMessageChange: (message: string) => void
|
||||
onSubmit: (message: PromptInputMessage, mentions?: FileMention[]) => void
|
||||
|
|
@ -139,6 +141,8 @@ export function ChatSidebar({
|
|||
currentAssistantMessage,
|
||||
currentReasoning,
|
||||
isProcessing,
|
||||
isStopping,
|
||||
onStop,
|
||||
message,
|
||||
onMessageChange,
|
||||
onSubmit,
|
||||
|
|
@ -595,19 +599,39 @@ export function ChatSidebar({
|
|||
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
canSubmit
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
{isProcessing ? (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={onStop}
|
||||
title={isStopping ? "Click again to force stop" : "Stop generation"}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
isStopping
|
||||
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
)}
|
||||
>
|
||||
{isStopping ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Square className="h-3 w-3 fill-current" />
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className={cn(
|
||||
"h-7 w-7 rounded-full shrink-0 transition-all",
|
||||
canSubmit
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{knowledgeFiles.length > 0 && (
|
||||
<MentionPopover
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ import {
|
|||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
|
||||
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
|
||||
import { getGoogleClientId, setGoogleClientId, clearGoogleClientId } from "@/lib/google-client-id-store"
|
||||
import { toast } from "sonner"
|
||||
|
||||
interface ProviderState {
|
||||
|
|
@ -38,7 +36,6 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
const [providers, setProviders] = useState<string[]>([])
|
||||
const [providersLoading, setProvidersLoading] = useState(true)
|
||||
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
|
||||
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
|
||||
|
||||
// Granola state
|
||||
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
||||
|
|
@ -269,14 +266,15 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
return cleanup
|
||||
}, [])
|
||||
|
||||
const startConnect = useCallback(async (provider: string, clientId?: string) => {
|
||||
// Connect to a provider
|
||||
const handleConnect = useCallback(async (provider: string) => {
|
||||
setProviderStates(prev => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], isConnecting: true }
|
||||
}))
|
||||
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider, clientId })
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider })
|
||||
|
||||
if (result.success) {
|
||||
// OAuth flow started - keep isConnecting state, wait for event
|
||||
|
|
@ -299,27 +297,6 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Connect to a provider
|
||||
const handleConnect = useCallback(async (provider: string) => {
|
||||
if (provider === 'google') {
|
||||
const existingClientId = getGoogleClientId()
|
||||
if (!existingClientId) {
|
||||
setGoogleClientIdOpen(true)
|
||||
return
|
||||
}
|
||||
await startConnect(provider, existingClientId)
|
||||
return
|
||||
}
|
||||
|
||||
await startConnect(provider)
|
||||
}, [startConnect])
|
||||
|
||||
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
|
||||
setGoogleClientId(clientId)
|
||||
setGoogleClientIdOpen(false)
|
||||
startConnect('google', clientId)
|
||||
}, [startConnect])
|
||||
|
||||
// Disconnect from a provider
|
||||
const handleDisconnect = useCallback(async (provider: string) => {
|
||||
setProviderStates(prev => ({
|
||||
|
|
@ -331,9 +308,6 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
const result = await window.ipc.invoke('oauth:disconnect', { provider })
|
||||
|
||||
if (result.success) {
|
||||
if (provider === 'google') {
|
||||
clearGoogleClientId()
|
||||
}
|
||||
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
|
||||
toast.success(`Disconnected from ${displayName}`)
|
||||
setProviderStates(prev => ({
|
||||
|
|
@ -421,12 +395,6 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
|
|||
|
||||
return (
|
||||
<>
|
||||
<GoogleClientIdModal
|
||||
open={googleClientIdOpen}
|
||||
onOpenChange={setGoogleClientIdOpen}
|
||||
onSubmit={handleGoogleClientIdSubmit}
|
||||
isSubmitting={providerStates.google?.isConnecting ?? false}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{tooltip ? (
|
||||
<Tooltip open={open ? false : undefined}>
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
interface GoogleClientIdModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSubmit: (clientId: string) => void
|
||||
isSubmitting?: boolean
|
||||
}
|
||||
|
||||
export function GoogleClientIdModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
}: GoogleClientIdModalProps) {
|
||||
const [clientId, setClientId] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setClientId("")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const trimmedClientId = clientId.trim()
|
||||
const isValid = trimmedClientId.length > 0
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isValid || isSubmitting) return
|
||||
onSubmit(trimmedClientId)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enter Google Client ID</DialogTitle>
|
||||
<DialogDescription>
|
||||
This app does not store the client ID. You will be prompted each session.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground" htmlFor="google-client-id">
|
||||
Client ID
|
||||
</label>
|
||||
<Input
|
||||
id="google-client-id"
|
||||
placeholder="xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
|
||||
value={clientId}
|
||||
onChange={(event) => setClientId(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -14,9 +14,7 @@ import {
|
|||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { GoogleClientIdModal } from "@/components/google-client-id-modal"
|
||||
import { ComposioApiKeyModal } from "@/components/composio-api-key-modal"
|
||||
import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store"
|
||||
import { toast } from "sonner"
|
||||
|
||||
interface ProviderState {
|
||||
|
|
@ -39,7 +37,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
const [providers, setProviders] = useState<string[]>([])
|
||||
const [providersLoading, setProvidersLoading] = useState(true)
|
||||
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
|
||||
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
|
||||
|
||||
// Granola state
|
||||
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
||||
|
|
@ -248,14 +245,15 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
return cleanup
|
||||
}, [])
|
||||
|
||||
const startConnect = useCallback(async (provider: string, clientId?: string) => {
|
||||
// Connect to a provider
|
||||
const handleConnect = useCallback(async (provider: string) => {
|
||||
setProviderStates(prev => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], isConnecting: true }
|
||||
}))
|
||||
|
||||
try {
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider, clientId })
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider })
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error || `Failed to connect to ${provider}`)
|
||||
|
|
@ -274,27 +272,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// Connect to a provider
|
||||
const handleConnect = useCallback(async (provider: string) => {
|
||||
if (provider === 'google') {
|
||||
const existingClientId = getGoogleClientId()
|
||||
if (!existingClientId) {
|
||||
setGoogleClientIdOpen(true)
|
||||
return
|
||||
}
|
||||
await startConnect(provider, existingClientId)
|
||||
return
|
||||
}
|
||||
|
||||
await startConnect(provider)
|
||||
}, [startConnect])
|
||||
|
||||
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
|
||||
setGoogleClientId(clientId)
|
||||
setGoogleClientIdOpen(false)
|
||||
startConnect('google', clientId)
|
||||
}, [startConnect])
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < 2) {
|
||||
setCurrentStep((prev) => (prev + 1) as Step)
|
||||
|
|
@ -597,12 +574,6 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<GoogleClientIdModal
|
||||
open={googleClientIdOpen}
|
||||
onOpenChange={setGoogleClientIdOpen}
|
||||
onSubmit={handleGoogleClientIdSubmit}
|
||||
isSubmitting={providerStates.google?.isConnecting ?? false}
|
||||
/>
|
||||
<ComposioApiKeyModal
|
||||
open={composioApiKeyOpen}
|
||||
onOpenChange={setComposioApiKeyOpen}
|
||||
|
|
|
|||
3
apps/x/apps/renderer/src/global.d.ts
vendored
3
apps/x/apps/renderer/src/global.d.ts
vendored
|
|
@ -33,6 +33,9 @@ declare global {
|
|||
handler: (event: IPCChannels[K]['req']) => void
|
||||
): () => void;
|
||||
};
|
||||
electronUtils: {
|
||||
getPathForFile: (file: File) => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ export function useOAuth(provider: string) {
|
|||
return cleanup;
|
||||
}, [provider, checkConnection]);
|
||||
|
||||
const connect = useCallback(async (clientId?: string) => {
|
||||
const connect = useCallback(async () => {
|
||||
try {
|
||||
setIsConnecting(true);
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider, clientId });
|
||||
const result = await window.ipc.invoke('oauth:connect', { provider });
|
||||
if (result.success) {
|
||||
// OAuth flow started - keep isConnecting state, wait for event
|
||||
// Event listener will handle the actual completion
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
let googleClientId: string | null = null;
|
||||
|
||||
export function getGoogleClientId(): string | null {
|
||||
return googleClientId;
|
||||
}
|
||||
|
||||
export function setGoogleClientId(clientId: string): void {
|
||||
const trimmed = clientId.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
googleClientId = trimmed;
|
||||
}
|
||||
|
||||
export function clearGoogleClientId(): void {
|
||||
googleClientId = null;
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
"@google-cloud/local-auth": "^3.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.6",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@x/shared": "workspace:*",
|
||||
"ai": "^5.0.102",
|
||||
"awilix": "^12.0.5",
|
||||
|
|
@ -28,6 +30,7 @@
|
|||
"node-html-markdown": "^2.0.0",
|
||||
"ollama-ai-provider-v2": "^1.5.4",
|
||||
"openid-client": "^6.8.1",
|
||||
"react": "^19.2.3",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { IBus } from "../application/lib/bus.js";
|
|||
import { IMessageQueue } from "../application/lib/message-queue.js";
|
||||
import { IRunsRepo } from "../runs/repo.js";
|
||||
import { IRunsLock } from "../runs/lock.js";
|
||||
import { IAbortRegistry } from "../runs/abort-registry.js";
|
||||
import { PrefixLogger } from "@x/shared";
|
||||
import { parse } from "yaml";
|
||||
import { raw as noteCreationMediumRaw } from "../knowledge/note_creation_medium.js";
|
||||
|
|
@ -39,6 +40,7 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
private messageQueue: IMessageQueue;
|
||||
private modelConfigRepo: IModelConfigRepo;
|
||||
private runsLock: IRunsLock;
|
||||
private abortRegistry: IAbortRegistry;
|
||||
|
||||
constructor({
|
||||
runsRepo,
|
||||
|
|
@ -47,6 +49,7 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
messageQueue,
|
||||
modelConfigRepo,
|
||||
runsLock,
|
||||
abortRegistry,
|
||||
}: {
|
||||
runsRepo: IRunsRepo;
|
||||
idGenerator: IMonotonicallyIncreasingIdGenerator;
|
||||
|
|
@ -54,6 +57,7 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
messageQueue: IMessageQueue;
|
||||
modelConfigRepo: IModelConfigRepo;
|
||||
runsLock: IRunsLock;
|
||||
abortRegistry: IAbortRegistry;
|
||||
}) {
|
||||
this.runsRepo = runsRepo;
|
||||
this.idGenerator = idGenerator;
|
||||
|
|
@ -61,6 +65,7 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
this.messageQueue = messageQueue;
|
||||
this.modelConfigRepo = modelConfigRepo;
|
||||
this.runsLock = runsLock;
|
||||
this.abortRegistry = abortRegistry;
|
||||
}
|
||||
|
||||
async trigger(runId: string): Promise<void> {
|
||||
|
|
@ -68,6 +73,7 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
console.log(`unable to acquire lock on run ${runId}`);
|
||||
return;
|
||||
}
|
||||
const signal = this.abortRegistry.createForRun(runId);
|
||||
try {
|
||||
await this.bus.publish({
|
||||
runId,
|
||||
|
|
@ -75,6 +81,11 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
subflow: [],
|
||||
});
|
||||
while (true) {
|
||||
// Check for abort before each iteration
|
||||
if (signal.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
let eventCount = 0;
|
||||
const run = await this.runsRepo.fetch(runId);
|
||||
if (!run) {
|
||||
|
|
@ -84,18 +95,28 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
for (const event of run.log) {
|
||||
state.ingest(event);
|
||||
}
|
||||
for await (const event of streamAgent({
|
||||
state,
|
||||
idGenerator: this.idGenerator,
|
||||
runId,
|
||||
messageQueue: this.messageQueue,
|
||||
modelConfigRepo: this.modelConfigRepo,
|
||||
})) {
|
||||
eventCount++;
|
||||
if (event.type !== "llm-stream-event") {
|
||||
await this.runsRepo.appendEvents(runId, [event]);
|
||||
try {
|
||||
for await (const event of streamAgent({
|
||||
state,
|
||||
idGenerator: this.idGenerator,
|
||||
runId,
|
||||
messageQueue: this.messageQueue,
|
||||
modelConfigRepo: this.modelConfigRepo,
|
||||
signal,
|
||||
abortRegistry: this.abortRegistry,
|
||||
})) {
|
||||
eventCount++;
|
||||
if (event.type !== "llm-stream-event") {
|
||||
await this.runsRepo.appendEvents(runId, [event]);
|
||||
}
|
||||
await this.bus.publish(event);
|
||||
}
|
||||
await this.bus.publish(event);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
// Abort detected — exit cleanly
|
||||
break;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// if no events, break
|
||||
|
|
@ -103,7 +124,20 @@ export class AgentRuntime implements IAgentRuntime {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit run-stopped event if aborted
|
||||
if (signal.aborted) {
|
||||
const stoppedEvent: z.infer<typeof RunEvent> = {
|
||||
runId,
|
||||
type: "run-stopped",
|
||||
reason: "user-requested",
|
||||
subflow: [],
|
||||
};
|
||||
await this.runsRepo.appendEvents(runId, [stoppedEvent]);
|
||||
await this.bus.publish(stoppedEvent);
|
||||
}
|
||||
} finally {
|
||||
this.abortRegistry.cleanup(runId);
|
||||
await this.runsLock.release(runId);
|
||||
await this.bus.publish({
|
||||
runId,
|
||||
|
|
@ -428,6 +462,39 @@ export class AgentState {
|
|||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tool-result messages for all pending tool calls, marking them as aborted.
|
||||
* This is called when a run is stopped so the LLM knows what happened to its tool requests.
|
||||
*/
|
||||
getAbortedToolResults(): z.infer<typeof ToolMessage>[] {
|
||||
const results: z.infer<typeof ToolMessage>[] = [];
|
||||
for (const toolCallId of Object.keys(this.pendingToolCalls)) {
|
||||
const toolCall = this.toolCallIdMap[toolCallId];
|
||||
if (toolCall) {
|
||||
results.push({
|
||||
role: "tool",
|
||||
content: JSON.stringify({ error: "Tool execution aborted" }),
|
||||
toolCallId,
|
||||
toolName: toolCall.toolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending state (permissions, ask-human, tool calls).
|
||||
* Used when a run is stopped.
|
||||
*/
|
||||
clearAllPending(): void {
|
||||
this.pendingToolPermissionRequests = {};
|
||||
this.pendingAskHumanRequests = {};
|
||||
// Recursively clear subflows
|
||||
for (const subflow of Object.values(this.subflowStates)) {
|
||||
subflow.clearAllPending();
|
||||
}
|
||||
}
|
||||
|
||||
finalResponse(): string {
|
||||
if (!this.lastAssistantMsg) {
|
||||
return '';
|
||||
|
|
@ -526,12 +593,16 @@ export async function* streamAgent({
|
|||
runId,
|
||||
messageQueue,
|
||||
modelConfigRepo,
|
||||
signal,
|
||||
abortRegistry,
|
||||
}: {
|
||||
state: AgentState,
|
||||
idGenerator: IMonotonicallyIncreasingIdGenerator;
|
||||
runId: string;
|
||||
messageQueue: IMessageQueue;
|
||||
modelConfigRepo: IModelConfigRepo;
|
||||
signal: AbortSignal;
|
||||
abortRegistry: IAbortRegistry;
|
||||
}): AsyncGenerator<z.infer<typeof RunEvent>, void, unknown> {
|
||||
const logger = new PrefixLogger(`run-${runId}-${state.agentName}`);
|
||||
|
||||
|
|
@ -557,6 +628,9 @@ export async function* streamAgent({
|
|||
|
||||
let loopCounter = 0;
|
||||
while (true) {
|
||||
// Check abort at the top of each iteration
|
||||
signal.throwIfAborted();
|
||||
|
||||
loopCounter++;
|
||||
const loopLogger = logger.child(`iter-${loopCounter}`);
|
||||
loopLogger.log('starting loop iteration');
|
||||
|
|
@ -598,6 +672,11 @@ export async function* streamAgent({
|
|||
}
|
||||
|
||||
// execute approved tool
|
||||
// Check abort before starting tool execution
|
||||
if (signal.aborted) {
|
||||
_logger.log('skipping, reason: aborted');
|
||||
break;
|
||||
}
|
||||
_logger.log('executing tool');
|
||||
yield* processEvent({
|
||||
runId,
|
||||
|
|
@ -616,6 +695,8 @@ export async function* streamAgent({
|
|||
runId,
|
||||
messageQueue,
|
||||
modelConfigRepo,
|
||||
signal,
|
||||
abortRegistry,
|
||||
})) {
|
||||
yield* processEvent({
|
||||
...event,
|
||||
|
|
@ -626,7 +707,7 @@ export async function* streamAgent({
|
|||
result = subflowState.finalResponse();
|
||||
}
|
||||
} else {
|
||||
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments);
|
||||
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry });
|
||||
}
|
||||
const resultPayload = result === undefined ? null : result;
|
||||
const resultMsg: z.infer<typeof ToolMessage> = {
|
||||
|
|
@ -709,6 +790,7 @@ export async function* streamAgent({
|
|||
state.messages,
|
||||
instructionsWithDateTime,
|
||||
tools,
|
||||
signal,
|
||||
)) {
|
||||
// Only log significant events (not text-delta to reduce noise)
|
||||
if (event.type !== 'text-delta') {
|
||||
|
|
@ -791,6 +873,7 @@ async function* streamLlm(
|
|||
messages: z.infer<typeof MessageList>,
|
||||
instructions: string,
|
||||
tools: ToolSet,
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<z.infer<typeof LlmStepStreamEvent>, void, unknown> {
|
||||
const { fullStream } = streamText({
|
||||
model,
|
||||
|
|
@ -798,8 +881,11 @@ async function* streamLlm(
|
|||
system: instructions,
|
||||
tools,
|
||||
stopWhen: stepCountIs(1),
|
||||
abortSignal: signal,
|
||||
});
|
||||
for await (const event of fullStream) {
|
||||
// Check abort on every chunk for responsiveness
|
||||
signal?.throwIfAborted();
|
||||
// console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event));
|
||||
switch (event.type) {
|
||||
case "reasoning-start":
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
|
|||
|
||||
**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
|
||||
|
||||
**Create Presentations:** When users ask you to create a presentation, slide deck, pitch deck, or PDF slides, load the \`create-presentations\` skill first. It provides structured guidance for generating PDF presentations using context from the knowledge base.
|
||||
|
||||
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. This is required for any document creation or editing task. The skill provides structured guidance for creating, editing, and refining documents in the knowledge base.
|
||||
|
||||
**Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always check if Slack is connected first with \`slack-checkConnection\`, and always show message drafts to the user before sending.
|
||||
|
|
@ -150,15 +152,9 @@ When a user asks for ANY task that might require external capabilities (web sear
|
|||
- NEVER ask what OS the user is on - they are on macOS.
|
||||
- Load the \`organize-files\` skill for guidance on file organization tasks.
|
||||
|
||||
**Command Approval:**
|
||||
- Approved shell commands are listed in \`~/.rowboat/config/security.json\`. Read this file to see what commands are allowed.
|
||||
- Only use commands from the approved list. Commands not in the list will be blocked.
|
||||
- If you cannot accomplish a task with the approved commands, tell the user which command you need and ask them to add it to \`security.json\`.
|
||||
- Always confirm with the user before executing commands that modify files outside \`~/.rowboat/\` (e.g., "I'll move 12 screenshots to ~/Desktop/Screenshots. Proceed?").
|
||||
|
||||
## Builtin Tools vs Shell Commands
|
||||
|
||||
**IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require security allowlist entries:
|
||||
**IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require any user approval:
|
||||
- \`workspace-readFile\`, \`workspace-writeFile\`, \`workspace-edit\`, \`workspace-remove\` - File operations
|
||||
- \`workspace-readdir\`, \`workspace-exists\`, \`workspace-stat\`, \`workspace-glob\`, \`workspace-grep\` - Directory exploration and file search
|
||||
- \`workspace-mkdir\`, \`workspace-rename\`, \`workspace-copy\` - File/directory management
|
||||
|
|
@ -167,13 +163,20 @@ When a user asks for ANY task that might require external capabilities (web sear
|
|||
- \`loadSkill\` - Skill loading
|
||||
- \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them.
|
||||
|
||||
These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`.
|
||||
**Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
|
||||
|
||||
**Shell commands via \`executeCommand\`:**
|
||||
- You can run ANY shell command via \`executeCommand\`. Some commands are pre-approved in \`~/.rowboat/config/security.json\` and run immediately.
|
||||
- Commands not on the pre-approved list will trigger a one-time approval prompt for the user — this is fine and expected, just a minor friction. Do NOT let this stop you from running commands you need.
|
||||
- **Never say "I can't run this command"** or ask the user to run something manually. Just call \`executeCommand\` and let the approval flow handle it.
|
||||
- When calling \`executeCommand\`, do NOT provide the \`cwd\` parameter unless absolutely necessary. The default working directory is already set to the workspace root.
|
||||
- Always confirm with the user before executing commands that modify files outside \`~/.rowboat/\` (e.g., "I'll move 12 screenshots to ~/Desktop/Screenshots. Proceed?").
|
||||
|
||||
**CRITICAL: MCP Server Configuration**
|
||||
- ALWAYS use the \`addMcpServer\` builtin tool to add or update MCP servers—it validates the configuration before saving
|
||||
- NEVER manually edit \`config/mcp.json\` using \`workspace-writeFile\` for MCP servers
|
||||
- Invalid MCP configs will prevent the agent from starting with validation errors
|
||||
|
||||
**Only \`executeCommand\` (shell/bash commands) is filtered** by the security allowlist. If you need to delete a file, use the \`workspace-remove\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`workspace-writeFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`.
|
||||
**Only \`executeCommand\` (shell/bash commands) goes through the approval flow.** If you need to delete a file, use the \`workspace-remove\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`workspace-writeFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`.
|
||||
|
||||
The security allowlist in \`security.json\` only applies to shell commands executed via \`executeCommand\`, not to Rowboat's internal builtin tools.`;
|
||||
Rowboat's internal builtin tools never require approval — only shell commands via \`executeCommand\` do.`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
export const skill = String.raw`
|
||||
# PDF Presentation Generator Skill
|
||||
|
||||
## When to Use
|
||||
|
||||
Activate when the user wants to create presentations, slide decks, or pitch decks.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Check ~/.rowboat/knowledge/ for relevant context about the company, product, team, etc.
|
||||
2. Ensure Playwright is installed: 'npm install playwright && npx playwright install chromium'
|
||||
3. Create an HTML file (e.g., /tmp/presentation.html) with slides (1280x720px each)
|
||||
4. Create a Node.js script to convert HTML to PDF:
|
||||
|
||||
~~~javascript
|
||||
// save as /tmp/convert.js
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('file:///tmp/presentation.html', { waitUntil: 'networkidle' });
|
||||
await page.pdf({
|
||||
path: path.join(process.env.HOME, 'Desktop', 'presentation.pdf'),
|
||||
width: '1280px',
|
||||
height: '720px',
|
||||
printBackground: true,
|
||||
});
|
||||
await browser.close();
|
||||
console.log('Done: ~/Desktop/presentation.pdf');
|
||||
})();
|
||||
~~~
|
||||
|
||||
5. Run it: 'node /tmp/convert.js'
|
||||
6. Tell the user: "Your presentation is ready at ~/Desktop/presentation.pdf"
|
||||
|
||||
Do NOT show HTML code to the user. Do NOT explain how to export. Just create the PDF and deliver it.
|
||||
|
||||
## PDF Export Rules
|
||||
|
||||
**These rules prevent rendering issues in PDF. Violating them causes overlapping rectangles and broken layouts.**
|
||||
|
||||
1. **No layered elements** - Never create separate elements for backgrounds or shadows. Style content elements directly.
|
||||
2. **No box-shadow** - Use borders instead: \`border: 1px solid #e5e7eb\`
|
||||
3. **Bullets via CSS only** - Use \`li::before\` pseudo-elements, not separate DOM elements
|
||||
4. **Content must fit** - Slides are 1280x720px with 60px padding. Safe area is 1160x600px. Use \`overflow: hidden\`.
|
||||
|
||||
## Required CSS
|
||||
|
||||
~~~css
|
||||
@page { size: 1280px 720px; margin: 0; }
|
||||
html { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||
.slide {
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
padding: 60px;
|
||||
overflow: hidden;
|
||||
page-break-after: always;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.slide:last-child { page-break-after: auto; }
|
||||
~~~
|
||||
|
||||
## Playwright Export
|
||||
|
||||
~~~typescript
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
|
||||
await page.pdf({
|
||||
path: '~/Desktop/presentation.pdf',
|
||||
width: '1280px',
|
||||
height: '720px',
|
||||
printBackground: true,
|
||||
});
|
||||
await browser.close();
|
||||
~~~
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -9,6 +9,7 @@ import meetingPrepSkill from "./meeting-prep/skill.js";
|
|||
import organizeFilesSkill from "./organize-files/skill.js";
|
||||
import slackSkill from "./slack/skill.js";
|
||||
import workflowAuthoringSkill from "./workflow-authoring/skill.js";
|
||||
import createPresentationsSkill from "./create-presentations/skill.js";
|
||||
import workflowRunOpsSkill from "./workflow-run-ops/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -28,6 +29,13 @@ type ResolvedSkill = {
|
|||
};
|
||||
|
||||
const definitions: SkillDefinition[] = [
|
||||
{
|
||||
id: "create-presentations",
|
||||
title: "Create Presentations",
|
||||
folder: "create-presentations",
|
||||
summary: "Create PDF presentations and slide decks from natural language requests using knowledge base context.",
|
||||
content: createPresentationsSkill,
|
||||
},
|
||||
{
|
||||
id: "doc-collab",
|
||||
title: "Document Collaboration",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { z, ZodType } from "zod";
|
|||
import * as path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { glob } from "glob";
|
||||
import { executeCommand } from "./command-executor.js";
|
||||
import { executeCommand, executeCommandAbortable } from "./command-executor.js";
|
||||
import { resolveSkill, availableSkills } from "../assistant/skills/index.js";
|
||||
import { executeTool, listServers, listTools } from "../../mcp/mcp.js";
|
||||
import container from "../../di/container.js";
|
||||
|
|
@ -14,13 +14,14 @@ import { WorkDir } from "../../config/config.js";
|
|||
import { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, listToolkitTools } from "../../composio/client.js";
|
||||
import { slackToolCatalog } from "../assistant/skills/slack/tool-catalog.js";
|
||||
import type { ToolContext } from "./exec-tool.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
||||
description: z.string(),
|
||||
inputSchema: z.custom<ZodType>(),
|
||||
execute: z.function({
|
||||
input: z.any(),
|
||||
input: z.any(), // (input, ctx?) => Promise<any>
|
||||
output: z.promise(z.any()),
|
||||
}),
|
||||
}));
|
||||
|
|
@ -838,17 +839,17 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
||||
inputSchema: z.object({
|
||||
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
|
||||
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to workspace root)'),
|
||||
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to workspace root). You do not need to set this unless absolutely necessary.'),
|
||||
}),
|
||||
execute: async ({ command, cwd }: { command: string, cwd?: string }) => {
|
||||
execute: async ({ command, cwd }: { command: string, cwd?: string }, ctx?: ToolContext) => {
|
||||
try {
|
||||
const rootDir = path.resolve(WorkDir);
|
||||
const workingDir = cwd ? path.resolve(rootDir, cwd) : rootDir;
|
||||
const rootPrefix = rootDir.endsWith(path.sep)
|
||||
? rootDir
|
||||
: `${rootDir}${path.sep}`;
|
||||
|
||||
// TODO: Re-enable this check
|
||||
// const rootPrefix = rootDir.endsWith(path.sep)
|
||||
// ? rootDir
|
||||
// : `${rootDir}${path.sep}`;
|
||||
// if (workingDir !== rootDir && !workingDir.startsWith(rootPrefix)) {
|
||||
// return {
|
||||
// success: false,
|
||||
|
|
@ -858,6 +859,30 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
// };
|
||||
// }
|
||||
|
||||
// Use abortable version when we have a signal
|
||||
if (ctx?.signal) {
|
||||
const { promise, process: proc } = executeCommandAbortable(command, {
|
||||
cwd: workingDir,
|
||||
signal: ctx.signal,
|
||||
});
|
||||
|
||||
// Register process with abort registry for force-kill
|
||||
ctx.abortRegistry.registerProcess(ctx.runId, proc);
|
||||
|
||||
const result = await promise;
|
||||
|
||||
return {
|
||||
success: result.exitCode === 0 && !result.wasAborted,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
wasAborted: result.wasAborted,
|
||||
command,
|
||||
workingDir,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to original for backward compatibility
|
||||
const result = await executeCommand(command, { cwd: workingDir });
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { exec, execSync } from 'child_process';
|
||||
import { exec, execSync, spawn, ChildProcess } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getSecurityAllowList } from '../../config/security.js';
|
||||
|
||||
|
|
@ -110,6 +110,159 @@ export async function executeCommand(
|
|||
}
|
||||
}
|
||||
|
||||
export interface AbortableCommandResult extends CommandResult {
|
||||
wasAborted: boolean;
|
||||
}
|
||||
|
||||
const SIGKILL_GRACE_MS = 200;
|
||||
|
||||
/**
|
||||
* Kill a process tree using negative PID (process group kill on Unix).
|
||||
* Falls back to direct kill if group kill fails.
|
||||
*/
|
||||
function killProcessTree(proc: ChildProcess, signal: NodeJS.Signals): void {
|
||||
if (!proc.pid || proc.killed) return;
|
||||
try {
|
||||
// Negative PID kills the entire process group (Unix)
|
||||
process.kill(-proc.pid, signal);
|
||||
} catch {
|
||||
try {
|
||||
proc.kill(signal);
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a shell command with abort support.
|
||||
* Uses spawn with detached=true to create a process group for proper tree killing.
|
||||
* Returns both the promise and the child process handle.
|
||||
*/
|
||||
export function executeCommandAbortable(
|
||||
command: string,
|
||||
options?: {
|
||||
cwd?: string;
|
||||
timeout?: number;
|
||||
maxBuffer?: number;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): { promise: Promise<AbortableCommandResult>; process: ChildProcess } {
|
||||
// Check if already aborted before spawning
|
||||
if (options?.signal?.aborted) {
|
||||
// Return a dummy process and a resolved result
|
||||
const dummyProc = spawn('true', { shell: true });
|
||||
dummyProc.kill();
|
||||
return {
|
||||
process: dummyProc,
|
||||
promise: Promise.resolve({
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 130,
|
||||
wasAborted: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const proc = spawn(command, [], {
|
||||
shell: '/bin/sh',
|
||||
cwd: options?.cwd,
|
||||
detached: process.platform !== 'win32', // Create process group on Unix
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const promise = new Promise<AbortableCommandResult>((resolve) => {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let wasAborted = false;
|
||||
let exited = false;
|
||||
|
||||
// Collect output
|
||||
proc.stdout?.on('data', (chunk: Buffer) => {
|
||||
const maxBuffer = options?.maxBuffer || 1024 * 1024;
|
||||
if (stdout.length < maxBuffer) {
|
||||
stdout += chunk.toString();
|
||||
}
|
||||
});
|
||||
proc.stderr?.on('data', (chunk: Buffer) => {
|
||||
const maxBuffer = options?.maxBuffer || 1024 * 1024;
|
||||
if (stderr.length < maxBuffer) {
|
||||
stderr += chunk.toString();
|
||||
}
|
||||
});
|
||||
|
||||
// Abort handler
|
||||
const abortHandler = () => {
|
||||
wasAborted = true;
|
||||
killProcessTree(proc, 'SIGTERM');
|
||||
// Force kill after grace period
|
||||
setTimeout(() => {
|
||||
if (!exited) {
|
||||
killProcessTree(proc, 'SIGKILL');
|
||||
}
|
||||
}, SIGKILL_GRACE_MS);
|
||||
};
|
||||
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener('abort', abortHandler, { once: true });
|
||||
}
|
||||
|
||||
// Timeout handler
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
if (options?.timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
wasAborted = true;
|
||||
killProcessTree(proc, 'SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (!exited) {
|
||||
killProcessTree(proc, 'SIGKILL');
|
||||
}
|
||||
}, SIGKILL_GRACE_MS);
|
||||
}, options.timeout);
|
||||
}
|
||||
|
||||
proc.once('exit', (code) => {
|
||||
exited = true;
|
||||
// Cleanup listeners
|
||||
if (options?.signal) {
|
||||
options.signal.removeEventListener('abort', abortHandler);
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (wasAborted) {
|
||||
stdout += '\n\n(Command was aborted)';
|
||||
}
|
||||
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode: code ?? 1,
|
||||
wasAborted,
|
||||
});
|
||||
});
|
||||
|
||||
proc.once('error', (err) => {
|
||||
exited = true;
|
||||
if (options?.signal) {
|
||||
options.signal.removeEventListener('abort', abortHandler);
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
resolve({
|
||||
stdout: '',
|
||||
stderr: err.message,
|
||||
exitCode: 1,
|
||||
wasAborted,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { promise, process: proc };
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command synchronously (blocking)
|
||||
* Use with caution - prefer executeCommand for async execution
|
||||
|
|
|
|||
|
|
@ -2,22 +2,36 @@ import { ToolAttachment } from "@x/shared/dist/agent.js";
|
|||
import { z } from "zod";
|
||||
import { BuiltinTools } from "./builtin-tools.js";
|
||||
import { executeTool } from "../../mcp/mcp.js";
|
||||
import { IAbortRegistry } from "../../runs/abort-registry.js";
|
||||
|
||||
/**
|
||||
* Context passed to every tool execution, providing abort signal and run metadata.
|
||||
*/
|
||||
export interface ToolContext {
|
||||
runId: string;
|
||||
signal: AbortSignal;
|
||||
abortRegistry: IAbortRegistry;
|
||||
}
|
||||
|
||||
async function execMcpTool(agentTool: z.infer<typeof ToolAttachment> & { type: "mcp" }, input: Record<string, unknown>): Promise<unknown> {
|
||||
const result = await executeTool(agentTool.mcpServerName, agentTool.name, input);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function execTool(agentTool: z.infer<typeof ToolAttachment>, input: Record<string, unknown>): Promise<unknown> {
|
||||
export async function execTool(agentTool: z.infer<typeof ToolAttachment>, input: Record<string, unknown>, ctx?: ToolContext): Promise<unknown> {
|
||||
// Check abort before starting any tool
|
||||
ctx?.signal.throwIfAborted();
|
||||
|
||||
switch (agentTool.type) {
|
||||
case "mcp":
|
||||
// MCP tools: let complete on graceful stop (most are fast)
|
||||
return execMcpTool(agentTool, input);
|
||||
case "builtin": {
|
||||
const builtinTool = BuiltinTools[agentTool.name];
|
||||
if (!builtinTool || !builtinTool.execute) {
|
||||
throw new Error(`Unsupported builtin tool: ${agentTool.name}`);
|
||||
}
|
||||
return builtinTool.execute(input);
|
||||
return builtinTool.execute(input, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
type ProviderClientIdOverrides = Map<string, string>;
|
||||
|
||||
const providerClientIdOverrides: ProviderClientIdOverrides = new Map();
|
||||
|
||||
export function setProviderClientIdOverride(provider: string, clientId: string): void {
|
||||
const trimmed = clientId.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
providerClientIdOverrides.set(provider, trimmed);
|
||||
}
|
||||
|
||||
export function getProviderClientIdOverride(provider: string): string | undefined {
|
||||
return providerClientIdOverrides.get(provider);
|
||||
}
|
||||
|
||||
export function hasProviderClientIdOverride(provider: string): boolean {
|
||||
return providerClientIdOverrides.has(provider);
|
||||
}
|
||||
|
||||
export function clearProviderClientIdOverride(provider: string): void {
|
||||
providerClientIdOverrides.delete(provider);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ const DiscoverySchema = z.discriminatedUnion('mode', [
|
|||
const ClientSchema = z.discriminatedUnion('mode', [
|
||||
z.object({
|
||||
mode: z.literal('static'),
|
||||
clientId: z.string().min(1).optional(),
|
||||
clientId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
mode: z.literal('dcr'),
|
||||
|
|
@ -58,11 +58,11 @@ const providerConfigs: ProviderConfig = {
|
|||
},
|
||||
client: {
|
||||
mode: 'static',
|
||||
clientId: '797410052581-ibmmvqec0l68stv5fmgh0juqfvbg08fc.apps.googleusercontent.com',
|
||||
},
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/calendar.events.readonly',
|
||||
],
|
||||
},
|
||||
'fireflies-ai': {
|
||||
|
|
|
|||
20
apps/x/packages/core/src/config/initConfigs.ts
Normal file
20
apps/x/packages/core/src/config/initConfigs.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import container from "../di/container.js";
|
||||
import type { IModelConfigRepo } from "../models/repo.js";
|
||||
import type { IMcpConfigRepo } from "../mcp/repo.js";
|
||||
import { ensureSecurityConfig } from "./security.js";
|
||||
|
||||
/**
|
||||
* Initialize all config files at app startup.
|
||||
* Ensures config files exist before the UI might access them.
|
||||
*/
|
||||
export async function initConfigs(): Promise<void> {
|
||||
// Resolve repos and explicitly call their ensureConfig methods
|
||||
const modelConfigRepo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
||||
const mcpConfigRepo = container.resolve<IMcpConfigRepo>("mcpConfigRepo");
|
||||
|
||||
await Promise.all([
|
||||
modelConfigRepo.ensureConfig(),
|
||||
mcpConfigRepo.ensureConfig(),
|
||||
ensureSecurityConfig(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import path from "path";
|
||||
import fs from "fs";
|
||||
import fsPromises from "fs/promises";
|
||||
import { WorkDir } from "./config.js";
|
||||
|
||||
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
|
||||
|
|
@ -19,7 +20,26 @@ const DEFAULT_ALLOW_LIST = [
|
|||
let cachedAllowList: string[] | null = null;
|
||||
let cachedMtimeMs: number | null = null;
|
||||
|
||||
function ensureSecurityConfig() {
|
||||
/**
|
||||
* Async function to ensure security config file exists.
|
||||
* Called explicitly at app startup via initConfigs().
|
||||
*/
|
||||
export async function ensureSecurityConfig(): Promise<void> {
|
||||
try {
|
||||
await fsPromises.access(SECURITY_CONFIG_PATH);
|
||||
} catch {
|
||||
await fsPromises.writeFile(
|
||||
SECURITY_CONFIG_PATH,
|
||||
JSON.stringify(DEFAULT_ALLOW_LIST, null, 2) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync version for internal use by getSecurityAllowList() and readAllowList().
|
||||
*/
|
||||
function ensureSecurityConfigSync() {
|
||||
if (!fs.existsSync(SECURITY_CONFIG_PATH)) {
|
||||
fs.writeFileSync(
|
||||
SECURITY_CONFIG_PATH,
|
||||
|
|
@ -63,7 +83,7 @@ function parseSecurityPayload(payload: unknown): string[] {
|
|||
}
|
||||
|
||||
function readAllowList(): string[] {
|
||||
ensureSecurityConfig();
|
||||
ensureSecurityConfigSync();
|
||||
|
||||
try {
|
||||
const configContent = fs.readFileSync(SECURITY_CONFIG_PATH, "utf8");
|
||||
|
|
@ -76,7 +96,7 @@ function readAllowList(): string[] {
|
|||
}
|
||||
|
||||
export function getSecurityAllowList(): string[] {
|
||||
ensureSecurityConfig();
|
||||
ensureSecurityConfigSync();
|
||||
try {
|
||||
const stats = fs.statSync(SECURITY_CONFIG_PATH);
|
||||
if (cachedAllowList && cachedMtimeMs === stats.mtimeMs) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { IAgentRuntime, AgentRuntime } from "../agents/runtime.js";
|
|||
import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js";
|
||||
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
|
||||
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js";
|
||||
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
|
||||
|
||||
const container = createContainer({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
|
|
@ -22,6 +23,7 @@ container.register({
|
|||
messageQueue: asClass<IMessageQueue>(InMemoryMessageQueue).singleton(),
|
||||
bus: asClass<IBus>(InMemoryBus).singleton(),
|
||||
runsLock: asClass<IRunsLock>(InMemoryRunsLock).singleton(),
|
||||
abortRegistry: asClass<IAbortRegistry>(InMemoryAbortRegistry).singleton(),
|
||||
agentRuntime: asClass<IAgentRuntime>(AgentRuntime).singleton(),
|
||||
|
||||
mcpConfigRepo: asClass<IMcpConfigRepo>(FSMcpConfigRepo).singleton(),
|
||||
|
|
|
|||
|
|
@ -2,4 +2,7 @@
|
|||
export * as workspace from './workspace/workspace.js';
|
||||
|
||||
// Workspace watcher
|
||||
export * as watcher from './workspace/watcher.js';
|
||||
export * as watcher from './workspace/watcher.js';
|
||||
|
||||
// Config initialization
|
||||
export { initConfigs } from './config/initConfigs.js';
|
||||
|
|
@ -144,13 +144,9 @@ export class FirefliesClientFactory {
|
|||
if (providerConfig.client.mode === 'static') {
|
||||
// Discover endpoints, use static client ID
|
||||
console.log(`[Fireflies] Discovery mode: issuer with static client ID`);
|
||||
const clientId = providerConfig.client.clientId;
|
||||
if (!clientId) {
|
||||
throw new Error('Fireflies client ID not configured.');
|
||||
}
|
||||
this.cache.config = await oauthClient.discoverConfiguration(
|
||||
providerConfig.discovery.issuer,
|
||||
clientId
|
||||
providerConfig.client.clientId
|
||||
);
|
||||
} else {
|
||||
// DCR mode - need existing registration
|
||||
|
|
@ -174,14 +170,10 @@ export class FirefliesClientFactory {
|
|||
}
|
||||
|
||||
console.log(`[Fireflies] Using static endpoints (no discovery)`);
|
||||
const clientId = providerConfig.client.clientId;
|
||||
if (!clientId) {
|
||||
throw new Error('Fireflies client ID not configured.');
|
||||
}
|
||||
this.cache.config = oauthClient.createStaticConfiguration(
|
||||
providerConfig.discovery.authorizationEndpoint,
|
||||
providerConfig.discovery.tokenEndpoint,
|
||||
clientId,
|
||||
providerConfig.client.clientId,
|
||||
providerConfig.discovery.revocationEndpoint
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import container from '../di/container.js';
|
|||
import { IOAuthRepo } from '../auth/repo.js';
|
||||
import { IClientRegistrationRepo } from '../auth/client-repo.js';
|
||||
import { getProviderConfig } from '../auth/providers.js';
|
||||
import { getProviderClientIdOverride } from '../auth/provider-client-id.js';
|
||||
import * as oauthClient from '../auth/oauth-client.js';
|
||||
import type { Configuration } from '../auth/oauth-client.js';
|
||||
import { OAuthTokens } from '../auth/types.js';
|
||||
|
|
@ -18,22 +17,12 @@ export class GoogleClientFactory {
|
|||
config: Configuration | null;
|
||||
client: OAuth2Client | null;
|
||||
tokens: OAuthTokens | null;
|
||||
clientId: string | null;
|
||||
} = {
|
||||
config: null,
|
||||
client: null,
|
||||
tokens: null,
|
||||
clientId: null,
|
||||
};
|
||||
|
||||
private static resolveClientId(): string {
|
||||
const override = getProviderClientIdOverride(this.PROVIDER_NAME);
|
||||
if (!override) {
|
||||
throw new Error('Google client ID not provided for this session.');
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create OAuth2Client, reusing cached instance when possible
|
||||
*/
|
||||
|
|
@ -47,13 +36,7 @@ export class GoogleClientFactory {
|
|||
}
|
||||
|
||||
// Initialize config cache if needed
|
||||
try {
|
||||
await this.initializeConfigCache();
|
||||
} catch (error) {
|
||||
console.error("[OAuth] Failed to initialize Google OAuth configuration:", error);
|
||||
this.clearCache();
|
||||
return null;
|
||||
}
|
||||
await this.initializeConfigCache();
|
||||
if (!this.cache.config) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -112,10 +95,6 @@ export class GoogleClientFactory {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!getProviderClientIdOverride(this.PROVIDER_NAME)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokens = await oauthRepo.getTokens(this.PROVIDER_NAME);
|
||||
if (!tokens) {
|
||||
return false;
|
||||
|
|
@ -137,21 +116,14 @@ export class GoogleClientFactory {
|
|||
this.cache.config = null;
|
||||
this.cache.client = null;
|
||||
this.cache.tokens = null;
|
||||
this.cache.clientId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cached configuration (called once)
|
||||
*/
|
||||
private static async initializeConfigCache(): Promise<void> {
|
||||
const clientId = this.resolveClientId();
|
||||
|
||||
if (this.cache.config && this.cache.clientId === clientId) {
|
||||
return; // Already initialized for this client ID
|
||||
}
|
||||
|
||||
if (this.cache.clientId && this.cache.clientId !== clientId) {
|
||||
this.clearCache();
|
||||
if (this.cache.config) {
|
||||
return; // Already initialized
|
||||
}
|
||||
|
||||
console.log(`[OAuth] Initializing Google OAuth configuration...`);
|
||||
|
|
@ -163,7 +135,7 @@ export class GoogleClientFactory {
|
|||
console.log(`[OAuth] Discovery mode: issuer with static client ID`);
|
||||
this.cache.config = await oauthClient.discoverConfiguration(
|
||||
providerConfig.discovery.issuer,
|
||||
clientId
|
||||
providerConfig.client.clientId
|
||||
);
|
||||
} else {
|
||||
// DCR mode - need existing registration
|
||||
|
|
@ -190,12 +162,11 @@ export class GoogleClientFactory {
|
|||
this.cache.config = oauthClient.createStaticConfiguration(
|
||||
providerConfig.discovery.authorizationEndpoint,
|
||||
providerConfig.discovery.tokenEndpoint,
|
||||
clientId,
|
||||
providerConfig.client.clientId,
|
||||
providerConfig.discovery.revocationEndpoint
|
||||
);
|
||||
}
|
||||
|
||||
this.cache.clientId = clientId;
|
||||
console.log(`[OAuth] Google OAuth configuration initialized`);
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +174,17 @@ export class GoogleClientFactory {
|
|||
* Create OAuth2Client from OAuthTokens
|
||||
*/
|
||||
private static createClientFromTokens(tokens: OAuthTokens): OAuth2Client {
|
||||
const clientId = this.resolveClientId();
|
||||
const providerConfig = getProviderConfig(this.PROVIDER_NAME);
|
||||
|
||||
// Get client ID from config
|
||||
let clientId: string;
|
||||
if (providerConfig.client.mode === 'static') {
|
||||
clientId = providerConfig.client.clientId;
|
||||
} else {
|
||||
// For DCR, we'd need to look up the registered client ID
|
||||
// This is a fallback - normally initializeConfigCache handles this
|
||||
throw new Error('Cannot create client without static client ID');
|
||||
}
|
||||
|
||||
// Create OAuth2Client directly (PKCE flow doesn't use client secret)
|
||||
const client = new OAuth2Client(
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
|||
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
||||
const LOOKBACK_DAYS = 14;
|
||||
const REQUIRED_SCOPES = [
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
'https://www.googleapis.com/auth/drive.readonly'
|
||||
'https://www.googleapis.com/auth/calendar.events.readonly',
|
||||
// 'https://www.googleapis.com/auth/drive.readonly'
|
||||
];
|
||||
|
||||
const nhm = new NodeHtmlMarkdown();
|
||||
|
|
@ -186,7 +186,7 @@ async function syncCalendarWindow(auth: OAuth2Client, syncDir: string, lookbackD
|
|||
for (const event of events) {
|
||||
if (event.id) {
|
||||
await saveEvent(event, syncDir);
|
||||
await processAttachments(drive, event, syncDir);
|
||||
// await processAttachments(drive, event, syncDir);
|
||||
currentEventIds.add(event.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,22 @@ export async function cleanup() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-close all MCP client connections.
|
||||
* Used during force abort to immediately reject any pending MCP tool calls.
|
||||
* Clients will be lazily reconnected on next use.
|
||||
*/
|
||||
export async function forceCloseAllMcpClients(): Promise<void> {
|
||||
for (const [serverName, { client }] of Object.entries(clients)) {
|
||||
try {
|
||||
await client?.close();
|
||||
} catch {
|
||||
// Ignore errors during force close
|
||||
}
|
||||
delete clients[serverName];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listServers(): Promise<z.infer<typeof McpServerList>> {
|
||||
const repo = container.resolve<IMcpConfigRepo>('mcpConfigRepo');
|
||||
const { mcpServers } = await repo.getConfig();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path";
|
|||
import z from "zod";
|
||||
|
||||
export interface IMcpConfigRepo {
|
||||
ensureConfig(): Promise<void>;
|
||||
getConfig(): Promise<z.infer<typeof McpServerConfig>>;
|
||||
upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void>;
|
||||
delete(serverName: string): Promise<void>;
|
||||
|
|
@ -13,11 +14,7 @@ export interface IMcpConfigRepo {
|
|||
export class FSMcpConfigRepo implements IMcpConfigRepo {
|
||||
private readonly configPath = path.join(WorkDir, "config", "mcp.json");
|
||||
|
||||
constructor() {
|
||||
this.ensureDefaultConfig();
|
||||
}
|
||||
|
||||
private async ensureDefaultConfig(): Promise<void> {
|
||||
async ensureConfig(): Promise<void> {
|
||||
try {
|
||||
await fs.access(this.configPath);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from "path";
|
|||
import z from "zod";
|
||||
|
||||
export interface IModelConfigRepo {
|
||||
ensureConfig(): Promise<void>;
|
||||
getConfig(): Promise<z.infer<typeof ModelConfig>>;
|
||||
upsert(providerName: string, config: z.infer<typeof Provider>): Promise<void>;
|
||||
delete(providerName: string): Promise<void>;
|
||||
|
|
@ -26,11 +27,7 @@ const defaultConfig: z.infer<typeof ModelConfig> = {
|
|||
export class FSModelConfigRepo implements IModelConfigRepo {
|
||||
private readonly configPath = path.join(WorkDir, "config", "models.json");
|
||||
|
||||
constructor() {
|
||||
this.ensureDefaultConfig();
|
||||
}
|
||||
|
||||
private async ensureDefaultConfig(): Promise<void> {
|
||||
async ensureConfig(): Promise<void> {
|
||||
try {
|
||||
await fs.access(this.configPath);
|
||||
} catch {
|
||||
|
|
|
|||
170
apps/x/packages/core/src/runs/abort-registry.ts
Normal file
170
apps/x/packages/core/src/runs/abort-registry.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { ChildProcess } from "child_process";
|
||||
|
||||
export interface IAbortRegistry {
|
||||
/**
|
||||
* Create and track an AbortController for a run.
|
||||
* Returns the AbortSignal to thread through all operations.
|
||||
*/
|
||||
createForRun(runId: string): AbortSignal;
|
||||
|
||||
/**
|
||||
* Track a child process for a run (so we can kill it on abort).
|
||||
*/
|
||||
registerProcess(runId: string, process: ChildProcess): void;
|
||||
|
||||
/**
|
||||
* Untrack a child process after it exits.
|
||||
*/
|
||||
unregisterProcess(runId: string, process: ChildProcess): void;
|
||||
|
||||
/**
|
||||
* Graceful abort:
|
||||
* 1. Fires the AbortSignal (cancels LLM streaming, etc.)
|
||||
* 2. Sends SIGTERM to all tracked process groups
|
||||
* 3. Schedules SIGKILL fallback after grace period
|
||||
*/
|
||||
abort(runId: string): void;
|
||||
|
||||
/**
|
||||
* Force abort:
|
||||
* 1. Fires AbortSignal if not already fired
|
||||
* 2. Sends SIGKILL to all tracked process groups immediately
|
||||
*/
|
||||
forceAbort(runId: string): void;
|
||||
|
||||
/**
|
||||
* Check if a run has been aborted.
|
||||
*/
|
||||
isAborted(runId: string): boolean;
|
||||
|
||||
/**
|
||||
* Clean up tracking state after a run completes or is fully stopped.
|
||||
*/
|
||||
cleanup(runId: string): void;
|
||||
}
|
||||
|
||||
interface RunAbortState {
|
||||
controller: AbortController;
|
||||
processes: Set<ChildProcess>;
|
||||
killTimers: Set<ReturnType<typeof setTimeout>>;
|
||||
}
|
||||
|
||||
const SIGKILL_GRACE_MS = 200;
|
||||
|
||||
export class InMemoryAbortRegistry implements IAbortRegistry {
|
||||
private runs: Map<string, RunAbortState> = new Map();
|
||||
|
||||
createForRun(runId: string): AbortSignal {
|
||||
// If a previous run state exists, clean it up first
|
||||
this.cleanup(runId);
|
||||
|
||||
const state: RunAbortState = {
|
||||
controller: new AbortController(),
|
||||
processes: new Set(),
|
||||
killTimers: new Set(),
|
||||
};
|
||||
this.runs.set(runId, state);
|
||||
return state.controller.signal;
|
||||
}
|
||||
|
||||
registerProcess(runId: string, process: ChildProcess): void {
|
||||
const state = this.runs.get(runId);
|
||||
if (!state) return;
|
||||
state.processes.add(process);
|
||||
|
||||
// Auto-unregister when process exits
|
||||
const onExit = () => {
|
||||
state.processes.delete(process);
|
||||
};
|
||||
process.once("exit", onExit);
|
||||
process.once("error", onExit);
|
||||
}
|
||||
|
||||
unregisterProcess(runId: string, process: ChildProcess): void {
|
||||
const state = this.runs.get(runId);
|
||||
if (!state) return;
|
||||
state.processes.delete(process);
|
||||
}
|
||||
|
||||
abort(runId: string): void {
|
||||
const state = this.runs.get(runId);
|
||||
if (!state) return;
|
||||
|
||||
// 1. Fire the abort signal
|
||||
if (!state.controller.signal.aborted) {
|
||||
state.controller.abort();
|
||||
}
|
||||
|
||||
// 2. SIGTERM all tracked process groups
|
||||
for (const proc of state.processes) {
|
||||
this.killProcessTree(proc, "SIGTERM");
|
||||
|
||||
// 3. Schedule SIGKILL fallback
|
||||
const timer = setTimeout(() => {
|
||||
if (!proc.killed) {
|
||||
this.killProcessTree(proc, "SIGKILL");
|
||||
}
|
||||
state.killTimers.delete(timer);
|
||||
}, SIGKILL_GRACE_MS);
|
||||
state.killTimers.add(timer);
|
||||
}
|
||||
}
|
||||
|
||||
forceAbort(runId: string): void {
|
||||
const state = this.runs.get(runId);
|
||||
if (!state) return;
|
||||
|
||||
// 1. Fire abort signal if not already
|
||||
if (!state.controller.signal.aborted) {
|
||||
state.controller.abort();
|
||||
}
|
||||
|
||||
// 2. Clear any pending graceful kill timers
|
||||
for (const timer of state.killTimers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
state.killTimers.clear();
|
||||
|
||||
// 3. SIGKILL all tracked process groups immediately
|
||||
for (const proc of state.processes) {
|
||||
this.killProcessTree(proc, "SIGKILL");
|
||||
}
|
||||
}
|
||||
|
||||
isAborted(runId: string): boolean {
|
||||
const state = this.runs.get(runId);
|
||||
return state?.controller.signal.aborted ?? false;
|
||||
}
|
||||
|
||||
cleanup(runId: string): void {
|
||||
const state = this.runs.get(runId);
|
||||
if (!state) return;
|
||||
|
||||
// Clear any pending kill timers
|
||||
for (const timer of state.killTimers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
this.runs.delete(runId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process tree using negative PID (process group kill on Unix).
|
||||
* Falls back to direct kill if group kill fails.
|
||||
*/
|
||||
private killProcessTree(proc: ChildProcess, signal: NodeJS.Signals): void {
|
||||
if (!proc.pid || proc.killed) return;
|
||||
|
||||
try {
|
||||
// Negative PID kills the entire process group (Unix)
|
||||
process.kill(-proc.pid, signal);
|
||||
} catch {
|
||||
// Fallback: kill just the process directly
|
||||
try {
|
||||
proc.kill(signal);
|
||||
} catch {
|
||||
// Process may already be dead
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { AskHumanResponseEvent, ToolPermissionResponseEvent, CreateRunOptions, R
|
|||
import { IRunsRepo } from "./repo.js";
|
||||
import { IAgentRuntime } from "../agents/runtime.js";
|
||||
import { IBus } from "../application/lib/bus.js";
|
||||
import { IAbortRegistry } from "./abort-registry.js";
|
||||
import { forceCloseAllMcpClients } from "../mcp/mcp.js";
|
||||
|
||||
export async function createRun(opts: z.infer<typeof CreateRunOptions>): Promise<z.infer<typeof Run>> {
|
||||
const repo = container.resolve<IRunsRepo>('runsRepo');
|
||||
|
|
@ -46,9 +48,21 @@ export async function replyToHumanInputRequest(runId: string, ev: z.infer<typeof
|
|||
runtime.trigger(runId);
|
||||
}
|
||||
|
||||
export async function stop(runId: string): Promise<void> {
|
||||
console.log(`Stopping run ${runId}`);
|
||||
throw new Error('Not implemented');
|
||||
export async function stop(runId: string, force: boolean = false): Promise<void> {
|
||||
const abortRegistry = container.resolve<IAbortRegistry>('abortRegistry');
|
||||
|
||||
if (force && abortRegistry.isAborted(runId)) {
|
||||
// Second click: aggressive cleanup — SIGKILL + force close MCP clients
|
||||
console.log(`Force stopping run ${runId}`);
|
||||
abortRegistry.forceAbort(runId);
|
||||
await forceCloseAllMcpClients();
|
||||
} else {
|
||||
// First click: graceful — fires AbortSignal + SIGTERM
|
||||
console.log(`Gracefully stopping run ${runId}`);
|
||||
abortRegistry.abort(runId);
|
||||
}
|
||||
// Note: The run-stopped event is emitted by AgentRuntime.trigger() when it detects the abort.
|
||||
// This avoids duplicate events and ensures proper sequencing.
|
||||
}
|
||||
|
||||
export async function fetchRun(runId: string): Promise<z.infer<typeof Run>> {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
"types": ["node"],
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ const ipcSchemas = {
|
|||
'runs:stop': {
|
||||
req: z.object({
|
||||
runId: z.string(),
|
||||
force: z.boolean().optional().default(false),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.literal(true),
|
||||
|
|
@ -175,7 +176,6 @@ const ipcSchemas = {
|
|||
'oauth:connect': {
|
||||
req: z.object({
|
||||
provider: z.string(),
|
||||
clientId: z.string().optional(),
|
||||
}),
|
||||
res: z.object({
|
||||
success: z.boolean(),
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ export const RunErrorEvent = BaseRunEvent.extend({
|
|||
error: z.string(),
|
||||
});
|
||||
|
||||
export const RunStoppedEvent = BaseRunEvent.extend({
|
||||
type: z.literal("run-stopped"),
|
||||
reason: z.enum(["user-requested", "force-stopped"]).optional(),
|
||||
});
|
||||
|
||||
export const RunEvent = z.union([
|
||||
RunProcessingStartEvent,
|
||||
RunProcessingEndEvent,
|
||||
|
|
@ -94,6 +99,7 @@ export const RunEvent = z.union([
|
|||
ToolPermissionRequestEvent,
|
||||
ToolPermissionResponseEvent,
|
||||
RunErrorEvent,
|
||||
RunStoppedEvent,
|
||||
]);
|
||||
|
||||
export const ToolPermissionAuthorizePayload = ToolPermissionResponseEvent.pick({
|
||||
|
|
|
|||
398
apps/x/pnpm-lock.yaml
generated
398
apps/x/pnpm-lock.yaml
generated
|
|
@ -302,6 +302,12 @@ importers:
|
|||
'@openrouter/ai-sdk-provider':
|
||||
specifier: ^1.2.6
|
||||
version: 1.5.4(ai@5.0.117(zod@4.2.1))(zod@4.2.1)
|
||||
'@react-pdf/renderer':
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.2(react@19.2.3)
|
||||
'@types/react':
|
||||
specifier: ^19.2.7
|
||||
version: 19.2.7
|
||||
'@x/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
|
|
@ -332,6 +338,9 @@ importers:
|
|||
openid-client:
|
||||
specifier: ^6.8.1
|
||||
version: 6.8.1
|
||||
react:
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3
|
||||
yaml:
|
||||
specifier: ^2.8.2
|
||||
version: 2.8.2
|
||||
|
|
@ -634,6 +643,10 @@ packages:
|
|||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -1963,6 +1976,49 @@ packages:
|
|||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@react-pdf/fns@3.1.2':
|
||||
resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==}
|
||||
|
||||
'@react-pdf/font@4.0.4':
|
||||
resolution: {integrity: sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==}
|
||||
|
||||
'@react-pdf/image@3.0.4':
|
||||
resolution: {integrity: sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==}
|
||||
|
||||
'@react-pdf/layout@4.4.2':
|
||||
resolution: {integrity: sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==}
|
||||
|
||||
'@react-pdf/pdfkit@4.1.0':
|
||||
resolution: {integrity: sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==}
|
||||
|
||||
'@react-pdf/png-js@3.0.0':
|
||||
resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==}
|
||||
|
||||
'@react-pdf/primitives@4.1.1':
|
||||
resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==}
|
||||
|
||||
'@react-pdf/reconciler@2.0.0':
|
||||
resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@react-pdf/render@4.3.2':
|
||||
resolution: {integrity: sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==}
|
||||
|
||||
'@react-pdf/renderer@4.3.2':
|
||||
resolution: {integrity: sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@react-pdf/stylesheet@6.1.2':
|
||||
resolution: {integrity: sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==}
|
||||
|
||||
'@react-pdf/textkit@6.1.0':
|
||||
resolution: {integrity: sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==}
|
||||
|
||||
'@react-pdf/types@2.9.2':
|
||||
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
|
||||
|
||||
'@remirror/core-constants@3.0.0':
|
||||
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
||||
|
||||
|
|
@ -2323,6 +2379,9 @@ packages:
|
|||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
|
||||
|
||||
'@szmarczak/http-timer@4.0.6':
|
||||
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -2947,6 +3006,9 @@ packages:
|
|||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
abs-svg-path@0.1.1:
|
||||
resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -3087,6 +3149,10 @@ packages:
|
|||
base32-encode@1.2.0:
|
||||
resolution: {integrity: sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==}
|
||||
|
||||
base64-js@0.0.8:
|
||||
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
|
|
@ -3094,6 +3160,9 @@ packages:
|
|||
resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==}
|
||||
hasBin: true
|
||||
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
|
|
@ -3130,6 +3199,12 @@ packages:
|
|||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brotli@1.3.3:
|
||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
||||
|
||||
browserify-zlib@0.2.0:
|
||||
resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==}
|
||||
|
||||
browserslist@4.28.1:
|
||||
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
|
|
@ -3267,6 +3342,10 @@ packages:
|
|||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
clone@2.1.2:
|
||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -3287,6 +3366,9 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
|
|
@ -3385,6 +3467,9 @@ packages:
|
|||
resolution: {integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==}
|
||||
engines: {node: '>=12.10'}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
|
||||
|
|
@ -3628,6 +3713,9 @@ packages:
|
|||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
dfa@1.2.0:
|
||||
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
||||
|
||||
dir-compare@4.2.0:
|
||||
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
|
||||
|
||||
|
|
@ -3690,6 +3778,9 @@ packages:
|
|||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
emoji-regex-xs@1.0.0:
|
||||
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
|
|
@ -3991,6 +4082,9 @@ packages:
|
|||
debug:
|
||||
optional: true
|
||||
|
||||
fontkit@2.0.4:
|
||||
resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -4314,6 +4408,12 @@ packages:
|
|||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
hsl-to-hex@1.0.0:
|
||||
resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==}
|
||||
|
||||
hsl-to-rgb-for-reals@1.1.1:
|
||||
resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
|
|
@ -4346,6 +4446,9 @@ packages:
|
|||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
hyphen@1.14.1:
|
||||
resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -4435,6 +4538,9 @@ packages:
|
|||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-arrayish@0.3.4:
|
||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -4522,6 +4628,9 @@ packages:
|
|||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jay-peg@1.1.1:
|
||||
resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==}
|
||||
|
||||
jest-worker@27.5.1:
|
||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
|
@ -4695,6 +4804,9 @@ packages:
|
|||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
linebreak@1.1.0:
|
||||
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
|
|
@ -4751,6 +4863,10 @@ packages:
|
|||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
lower-case@2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
|
||||
|
|
@ -4871,6 +4987,9 @@ packages:
|
|||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
media-engine@1.0.3:
|
||||
resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -5216,6 +5335,9 @@ packages:
|
|||
normalize-package-data@2.5.0:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
|
||||
normalize-svg-path@1.1.0:
|
||||
resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==}
|
||||
|
||||
normalize-url@6.1.0:
|
||||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -5345,6 +5467,12 @@ packages:
|
|||
package-manager-detector@1.6.0:
|
||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
||||
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -5363,6 +5491,9 @@ packages:
|
|||
resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
parse-svg-path@0.1.2:
|
||||
resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
|
|
@ -5456,6 +5587,9 @@ packages:
|
|||
points-on-path@0.2.1:
|
||||
resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
|
@ -5500,6 +5634,9 @@ packages:
|
|||
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
|
|
@ -5596,6 +5733,9 @@ packages:
|
|||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
queue@6.0.2:
|
||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
||||
|
||||
quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -5619,6 +5759,9 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^19.2.3
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-refresh@0.18.0:
|
||||
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -5776,6 +5919,9 @@ packages:
|
|||
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
restructure@3.0.2:
|
||||
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
|
||||
|
||||
retry@0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
|
|
@ -5838,6 +5984,9 @@ packages:
|
|||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.25.0-rc-603e6108-20241029:
|
||||
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
|
|
@ -5928,6 +6077,9 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -6065,6 +6217,9 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
svg-arc-to-cubic-bezier@3.2.0:
|
||||
resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==}
|
||||
|
||||
tailwind-merge@3.4.0:
|
||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||
|
||||
|
|
@ -6108,6 +6263,9 @@ packages:
|
|||
tiny-each-async@2.0.3:
|
||||
resolution: {integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
tinyexec@1.0.2:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -6235,6 +6393,12 @@ packages:
|
|||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
|
||||
|
||||
unicode-trie@2.0.0:
|
||||
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
|
||||
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
|
||||
|
|
@ -6363,6 +6527,10 @@ packages:
|
|||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite-compatible-readable-stream@3.6.1:
|
||||
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
vite@7.3.0:
|
||||
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
|
@ -6546,6 +6714,9 @@ packages:
|
|||
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yoga-layout@3.2.1:
|
||||
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
|
|
@ -7197,6 +7368,8 @@ snapshots:
|
|||
'@babel/core': 7.28.5
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -8707,6 +8880,107 @@ snapshots:
|
|||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@react-pdf/fns@3.1.2': {}
|
||||
|
||||
'@react-pdf/font@4.0.4':
|
||||
dependencies:
|
||||
'@react-pdf/pdfkit': 4.1.0
|
||||
'@react-pdf/types': 2.9.2
|
||||
fontkit: 2.0.4
|
||||
is-url: 1.2.4
|
||||
|
||||
'@react-pdf/image@3.0.4':
|
||||
dependencies:
|
||||
'@react-pdf/png-js': 3.0.0
|
||||
jay-peg: 1.1.1
|
||||
|
||||
'@react-pdf/layout@4.4.2':
|
||||
dependencies:
|
||||
'@react-pdf/fns': 3.1.2
|
||||
'@react-pdf/image': 3.0.4
|
||||
'@react-pdf/primitives': 4.1.1
|
||||
'@react-pdf/stylesheet': 6.1.2
|
||||
'@react-pdf/textkit': 6.1.0
|
||||
'@react-pdf/types': 2.9.2
|
||||
emoji-regex-xs: 1.0.0
|
||||
queue: 6.0.2
|
||||
yoga-layout: 3.2.1
|
||||
|
||||
'@react-pdf/pdfkit@4.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@react-pdf/png-js': 3.0.0
|
||||
browserify-zlib: 0.2.0
|
||||
crypto-js: 4.2.0
|
||||
fontkit: 2.0.4
|
||||
jay-peg: 1.1.1
|
||||
linebreak: 1.1.0
|
||||
vite-compatible-readable-stream: 3.6.1
|
||||
|
||||
'@react-pdf/png-js@3.0.0':
|
||||
dependencies:
|
||||
browserify-zlib: 0.2.0
|
||||
|
||||
'@react-pdf/primitives@4.1.1': {}
|
||||
|
||||
'@react-pdf/reconciler@2.0.0(react@19.2.3)':
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
react: 19.2.3
|
||||
scheduler: 0.25.0-rc-603e6108-20241029
|
||||
|
||||
'@react-pdf/render@4.3.2':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@react-pdf/fns': 3.1.2
|
||||
'@react-pdf/primitives': 4.1.1
|
||||
'@react-pdf/textkit': 6.1.0
|
||||
'@react-pdf/types': 2.9.2
|
||||
abs-svg-path: 0.1.1
|
||||
color-string: 1.9.1
|
||||
normalize-svg-path: 1.1.0
|
||||
parse-svg-path: 0.1.2
|
||||
svg-arc-to-cubic-bezier: 3.2.0
|
||||
|
||||
'@react-pdf/renderer@4.3.2(react@19.2.3)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@react-pdf/fns': 3.1.2
|
||||
'@react-pdf/font': 4.0.4
|
||||
'@react-pdf/layout': 4.4.2
|
||||
'@react-pdf/pdfkit': 4.1.0
|
||||
'@react-pdf/primitives': 4.1.1
|
||||
'@react-pdf/reconciler': 2.0.0(react@19.2.3)
|
||||
'@react-pdf/render': 4.3.2
|
||||
'@react-pdf/types': 2.9.2
|
||||
events: 3.3.0
|
||||
object-assign: 4.1.1
|
||||
prop-types: 15.8.1
|
||||
queue: 6.0.2
|
||||
react: 19.2.3
|
||||
|
||||
'@react-pdf/stylesheet@6.1.2':
|
||||
dependencies:
|
||||
'@react-pdf/fns': 3.1.2
|
||||
'@react-pdf/types': 2.9.2
|
||||
color-string: 1.9.1
|
||||
hsl-to-hex: 1.0.0
|
||||
media-engine: 1.0.3
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
'@react-pdf/textkit@6.1.0':
|
||||
dependencies:
|
||||
'@react-pdf/fns': 3.1.2
|
||||
bidi-js: 1.0.3
|
||||
hyphen: 1.14.1
|
||||
unicode-properties: 1.4.1
|
||||
|
||||
'@react-pdf/types@2.9.2':
|
||||
dependencies:
|
||||
'@react-pdf/font': 4.0.4
|
||||
'@react-pdf/primitives': 4.1.1
|
||||
'@react-pdf/stylesheet': 6.1.2
|
||||
|
||||
'@remirror/core-constants@3.0.0': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||
|
|
@ -9152,6 +9426,10 @@ snapshots:
|
|||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@swc/helpers@0.5.18':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@szmarczak/http-timer@4.0.6':
|
||||
dependencies:
|
||||
defer-to-connect: 2.0.1
|
||||
|
|
@ -9886,6 +10164,8 @@ snapshots:
|
|||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
abs-svg-path@0.1.1: {}
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
|
|
@ -10025,10 +10305,16 @@ snapshots:
|
|||
to-data-view: 1.1.0
|
||||
optional: true
|
||||
|
||||
base64-js@0.0.8: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.9.11: {}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
bl@4.1.0:
|
||||
|
|
@ -10078,6 +10364,14 @@ snapshots:
|
|||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
brotli@1.3.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
|
||||
browserify-zlib@0.2.0:
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.11
|
||||
|
|
@ -10239,6 +10533,8 @@ snapshots:
|
|||
|
||||
clone@1.0.4: {}
|
||||
|
||||
clone@2.1.2: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
|
|
@ -10262,6 +10558,11 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
color-string@1.9.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.4
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
|
|
@ -10342,6 +10643,8 @@ snapshots:
|
|||
|
||||
cross-zip@4.0.1: {}
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
css-select@5.2.2:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
|
@ -10601,6 +10904,8 @@ snapshots:
|
|||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
dfa@1.2.0: {}
|
||||
|
||||
dir-compare@4.2.0:
|
||||
dependencies:
|
||||
minimatch: 3.1.2
|
||||
|
|
@ -10714,6 +11019,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
emoji-regex-xs@1.0.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
|
@ -11105,6 +11412,18 @@ snapshots:
|
|||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
fontkit@2.0.4:
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.18
|
||||
brotli: 1.3.3
|
||||
clone: 2.1.2
|
||||
dfa: 1.2.0
|
||||
fast-deep-equal: 3.1.3
|
||||
restructure: 3.0.2
|
||||
tiny-inflate: 1.0.3
|
||||
unicode-properties: 1.4.1
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
|
@ -11594,6 +11913,12 @@ snapshots:
|
|||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
hsl-to-hex@1.0.0:
|
||||
dependencies:
|
||||
hsl-to-rgb-for-reals: 1.1.1
|
||||
|
||||
hsl-to-rgb-for-reals@1.1.1: {}
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
|
@ -11639,6 +11964,8 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
hyphen@1.14.1: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
|
@ -11704,6 +12031,8 @@ snapshots:
|
|||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-arrayish@0.3.4: {}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
|
@ -11771,6 +12100,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jay-peg@1.1.1:
|
||||
dependencies:
|
||||
restructure: 3.0.2
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
|
@ -11924,6 +12257,11 @@ snapshots:
|
|||
lightningcss-win32-arm64-msvc: 1.30.2
|
||||
lightningcss-win32-x64-msvc: 1.30.2
|
||||
|
||||
linebreak@1.1.0:
|
||||
dependencies:
|
||||
base64-js: 0.0.8
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
|
@ -11984,6 +12322,10 @@ snapshots:
|
|||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lower-case@2.0.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
|
@ -12232,6 +12574,8 @@ snapshots:
|
|||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
media-engine@1.0.3: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
mem@4.3.0:
|
||||
|
|
@ -12687,6 +13031,10 @@ snapshots:
|
|||
semver: 5.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-svg-path@1.1.0:
|
||||
dependencies:
|
||||
svg-arc-to-cubic-bezier: 3.2.0
|
||||
|
||||
normalize-url@6.1.0: {}
|
||||
|
||||
npm-run-path@2.0.2:
|
||||
|
|
@ -12805,6 +13153,10 @@ snapshots:
|
|||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
|
@ -12832,6 +13184,8 @@ snapshots:
|
|||
dependencies:
|
||||
error-ex: 1.3.4
|
||||
|
||||
parse-svg-path@0.1.2: {}
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
|
@ -12908,6 +13262,8 @@ snapshots:
|
|||
path-data-parser: 0.1.0
|
||||
points-on-curve: 0.2.0
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
|
|
@ -12951,6 +13307,12 @@ snapshots:
|
|||
err-code: 2.0.3
|
||||
retry: 0.12.0
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
prosemirror-changeset@2.3.1:
|
||||
|
|
@ -13099,6 +13461,10 @@ snapshots:
|
|||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
queue@6.0.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
||||
quick-lru@5.1.1: {}
|
||||
|
||||
random-path@0.1.2:
|
||||
|
|
@ -13125,6 +13491,8 @@ snapshots:
|
|||
react: 19.2.3
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-refresh@0.18.0: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
|
||||
|
|
@ -13315,6 +13683,8 @@ snapshots:
|
|||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
restructure@3.0.2: {}
|
||||
|
||||
retry@0.12.0: {}
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
|
@ -13407,6 +13777,8 @@ snapshots:
|
|||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.25.0-rc-603e6108-20241029: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
schema-utils@4.3.3:
|
||||
|
|
@ -13520,6 +13892,10 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-swizzle@0.2.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.4
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
|
|
@ -13685,6 +14061,8 @@ snapshots:
|
|||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svg-arc-to-cubic-bezier@3.2.0: {}
|
||||
|
||||
tailwind-merge@3.4.0: {}
|
||||
|
||||
tailwindcss@4.1.18: {}
|
||||
|
|
@ -13727,6 +14105,8 @@ snapshots:
|
|||
tiny-each-async@2.0.3:
|
||||
optional: true
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
|
|
@ -13839,6 +14219,16 @@ snapshots:
|
|||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
unicode-trie@2.0.0:
|
||||
dependencies:
|
||||
pako: 0.2.9
|
||||
tiny-inflate: 1.0.3
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
@ -13974,6 +14364,12 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-compatible-readable-stream@3.6.1:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
|
|
@ -14153,6 +14549,8 @@ snapshots:
|
|||
|
||||
yoctocolors-cjs@2.1.3: {}
|
||||
|
||||
yoga-layout@3.2.1: {}
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.2.1):
|
||||
dependencies:
|
||||
zod: 4.2.1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue