Skip to content

Commit 3f508e4

Browse files
authored
v0.6.28: new docs, delete confirmation standardization, dagster integration, signup method feature flags, SSO improvements
2 parents 316bc8c + e2d4d0e commit 3f508e4

File tree

165 files changed

+10002
-1290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

165 files changed

+10002
-1290
lines changed

.agents/skills/add-block/SKILL.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ When the user asks you to create a block:
1919
```typescript
2020
import { {ServiceName}Icon } from '@/components/icons'
2121
import type { BlockConfig } from '@/blocks/types'
22-
import { AuthMode } from '@/blocks/types'
22+
import { AuthMode, IntegrationType } from '@/blocks/types'
2323
import { getScopesForService } from '@/lib/oauth/utils'
2424

2525
export const {ServiceName}Block: BlockConfig = {
@@ -29,6 +29,8 @@ export const {ServiceName}Block: BlockConfig = {
2929
longDescription: 'Detailed description for docs',
3030
docsLink: 'https://docs.sim.ai/tools/{service}',
3131
category: 'tools', // 'tools' | 'blocks' | 'triggers'
32+
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
33+
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
3234
bgColor: '#HEXCOLOR', // Brand color
3335
icon: {ServiceName}Icon,
3436

@@ -629,7 +631,7 @@ export const registry: Record<string, BlockConfig> = {
629631
```typescript
630632
import { ServiceIcon } from '@/components/icons'
631633
import type { BlockConfig } from '@/blocks/types'
632-
import { AuthMode } from '@/blocks/types'
634+
import { AuthMode, IntegrationType } from '@/blocks/types'
633635
import { getScopesForService } from '@/lib/oauth/utils'
634636

635637
export const ServiceBlock: BlockConfig = {
@@ -639,6 +641,8 @@ export const ServiceBlock: BlockConfig = {
639641
longDescription: 'Full description for documentation...',
640642
docsLink: 'https://docs.sim.ai/tools/service',
641643
category: 'tools',
644+
integrationType: IntegrationType.DeveloperTools,
645+
tags: ['oauth', 'api'],
642646
bgColor: '#FF6B6B',
643647
icon: ServiceIcon,
644648
authMode: AuthMode.OAuth,
@@ -796,6 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
796800

797801
## Checklist Before Finishing
798802

803+
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
804+
- [ ] `tags` array includes all applicable `IntegrationTag` values
799805
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
800806
- [ ] Conditions use correct syntax (field, value, not, and)
801807
- [ ] DependsOn set for fields that need other values

.agents/skills/add-tools/SKILL.md

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ export * from './types'
266266

267267
## Registering Tools
268268

269-
After creating tools, remind the user to:
269+
After creating tools:
270270
1. Import tools in `apps/sim/tools/registry.ts`
271-
2. Add to the `tools` object with snake_case keys:
271+
2. Add to the `tools` object with snake_case keys (alphabetically):
272272
```typescript
273273
import { serviceActionTool } from '@/tools/{service}'
274274

@@ -278,6 +278,130 @@ export const tools = {
278278
}
279279
```
280280

281+
## Wiring Tools into the Block (Required)
282+
283+
After registering in `tools/registry.ts`, you MUST also update the block definition at `apps/sim/blocks/blocks/{service}.ts`. This is not optional — tools are only usable from the UI if they are wired into the block.
284+
285+
### 1. Add to `tools.access`
286+
287+
```typescript
288+
tools: {
289+
access: [
290+
// existing tools...
291+
'service_new_action', // Add every new tool ID here
292+
],
293+
config: { ... }
294+
}
295+
```
296+
297+
### 2. Add operation dropdown options
298+
299+
If the block uses an operation dropdown, add an option for each new tool:
300+
301+
```typescript
302+
{
303+
id: 'operation',
304+
type: 'dropdown',
305+
options: [
306+
// existing options...
307+
{ label: 'New Action', id: 'new_action' }, // id maps to what tools.config.tool returns
308+
],
309+
}
310+
```
311+
312+
### 3. Add subBlocks for new tool params
313+
314+
For each new tool, add subBlocks covering all its required params (and optional ones where useful). Apply `condition` to show them only for the right operation, and mark required params with `required`:
315+
316+
```typescript
317+
// Required param for new_action
318+
{
319+
id: 'someParam',
320+
title: 'Some Param',
321+
type: 'short-input',
322+
placeholder: 'e.g., value',
323+
condition: { field: 'operation', value: 'new_action' },
324+
required: { field: 'operation', value: 'new_action' },
325+
},
326+
// Optional param — put in advanced mode
327+
{
328+
id: 'optionalParam',
329+
title: 'Optional Param',
330+
type: 'short-input',
331+
condition: { field: 'operation', value: 'new_action' },
332+
mode: 'advanced',
333+
},
334+
```
335+
336+
### 4. Update `tools.config.tool`
337+
338+
Ensure the tool selector returns the correct tool ID for every new operation. The simplest pattern:
339+
340+
```typescript
341+
tool: (params) => `service_${params.operation}`,
342+
// If operation dropdown IDs already match tool IDs, this requires no change.
343+
```
344+
345+
If the dropdown IDs differ from tool IDs, add explicit mappings:
346+
347+
```typescript
348+
tool: (params) => {
349+
const map: Record<string, string> = {
350+
new_action: 'service_new_action',
351+
// ...
352+
}
353+
return map[params.operation] ?? `service_${params.operation}`
354+
},
355+
```
356+
357+
### 5. Update `tools.config.params`
358+
359+
Add any type coercions needed for new params (runs at execution time, after variable resolution):
360+
361+
```typescript
362+
params: (params) => {
363+
const result: Record<string, unknown> = {}
364+
if (params.limit != null && params.limit !== '') result.limit = Number(params.limit)
365+
if (params.newParamName) result.toolParamName = params.newParamName // rename if IDs differ
366+
return result
367+
},
368+
```
369+
370+
### 6. Add new outputs
371+
372+
Add any new fields returned by the new tools to the block `outputs`:
373+
374+
```typescript
375+
outputs: {
376+
// existing outputs...
377+
newField: { type: 'string', description: 'Description of new field' },
378+
}
379+
```
380+
381+
### 7. Add new inputs
382+
383+
Add new subBlock param IDs to the block `inputs` section:
384+
385+
```typescript
386+
inputs: {
387+
// existing inputs...
388+
someParam: { type: 'string', description: 'Param description' },
389+
optionalParam: { type: 'string', description: 'Optional param description' },
390+
}
391+
```
392+
393+
### Block wiring checklist
394+
395+
- [ ] New tool IDs added to `tools.access`
396+
- [ ] Operation dropdown has an option for each new tool
397+
- [ ] SubBlocks cover all required params for each new tool
398+
- [ ] SubBlocks have correct `condition` (only show for the right operation)
399+
- [ ] Optional/rarely-used params set to `mode: 'advanced'`
400+
- [ ] `tools.config.tool` returns correct ID for every new operation
401+
- [ ] `tools.config.params` handles any ID remapping or type coercions
402+
- [ ] New outputs added to block `outputs`
403+
- [ ] New params added to block `inputs`
404+
281405
## V2 Tool Pattern
282406

283407
If creating V2 tools (API-aligned outputs), use `_v2` suffix:
@@ -299,7 +423,9 @@ All tool IDs MUST use `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`
299423
- [ ] All optional outputs have `optional: true`
300424
- [ ] No raw JSON dumps in outputs
301425
- [ ] Types file has all interfaces
302-
- [ ] Index.ts exports all tools
426+
- [ ] Index.ts exports all tools and re-exports types (`export * from './types'`)
427+
- [ ] Tools registered in `tools/registry.ts`
428+
- [ ] Block wired: `tools.access`, dropdown options, subBlocks, `tools.config`, outputs, inputs
303429

304430
## Final Validation (Required)
305431

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
name: you-might-not-need-an-effect
3+
description: Analyze and fix useEffect anti-patterns in your code
4+
---
5+
6+
# You Might Not Need an Effect
7+
8+
Arguments:
9+
- scope: what to analyze (default: your current changes). Examples: "diff to main", "PR #123", "src/components/", "whole codebase"
10+
- fix: whether to apply fixes (default: true). Set to false to only propose changes.
11+
12+
User arguments: $ARGUMENTS
13+
14+
Steps:
15+
1. Read https://react.dev/learn/you-might-not-need-an-effect to understand the guidelines
16+
2. Analyze the specified scope for useEffect anti-patterns
17+
3. If fix=true, apply the fixes. If fix=false, propose the fixes without applying.

.claude/commands/add-connector.md

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ export const {service}Connector: ConnectorConfig = {
7171
],
7272

7373
listDocuments: async (accessToken, sourceConfig, cursor) => {
74-
// Paginate via cursor, extract text, compute SHA-256 hash
74+
// Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
75+
// Or full documents with content (if list API returns content inline)
7576
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
7677
},
7778

7879
getDocument: async (accessToken, sourceConfig, externalId) => {
79-
// Return ExternalDocument or null
80+
// Fetch full content for a single document
81+
// Return ExternalDocument with contentDeferred: false, or null
8082
},
8183

8284
validateConfig: async (accessToken, sourceConfig) => {
@@ -281,26 +283,110 @@ Every document returned from `listDocuments`/`getDocument` must include:
281283
{
282284
externalId: string // Source-specific unique ID
283285
title: string // Document title
284-
content: string // Extracted plain text
286+
content: string // Extracted plain text (or '' if contentDeferred)
287+
contentDeferred?: boolean // true = content will be fetched via getDocument
285288
mimeType: 'text/plain' // Always text/plain (content is extracted)
286-
contentHash: string // SHA-256 of content (change detection)
289+
contentHash: string // Metadata-based hash for change detection
287290
sourceUrl?: string // Link back to original (stored on document record)
288291
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
289292
}
290293
```
291294

292-
## Content Hashing (Required)
295+
## Content Deferral (Required for file/content-download connectors)
293296

294-
The sync engine uses content hashes for change detection:
297+
**All connectors that require per-document API calls to fetch content MUST use `contentDeferred: true`.** This is the standard pattern — `listDocuments` returns lightweight metadata stubs, and content is fetched lazily by the sync engine via `getDocument` only for new/changed documents.
298+
299+
This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
300+
301+
### When to use `contentDeferred: true`
302+
303+
- The service's list API does NOT return document content (only metadata)
304+
- Content requires a separate download/export API call per document
305+
- Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
306+
307+
### When NOT to use `contentDeferred`
308+
309+
- The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
310+
- No per-document API call is needed to get content
311+
312+
### Content Hash Strategy
313+
314+
Use a **metadata-based** `contentHash` — never a content-based hash. The hash must be derivable from the list response metadata alone, so the sync engine can detect changes without downloading content.
315+
316+
Good metadata hash sources:
317+
- `modifiedTime` / `lastModifiedDateTime` — changes when file is edited
318+
- Git blob SHA — unique per content version
319+
- API-provided content hash (e.g., Dropbox `content_hash`)
320+
- Version number (e.g., Confluence page version)
321+
322+
Format: `{service}:{id}:{changeIndicator}`
295323

296324
```typescript
297-
async function computeContentHash(content: string): Promise<string> {
298-
const data = new TextEncoder().encode(content)
299-
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
300-
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
325+
// Google Drive: modifiedTime changes on edit
326+
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`
327+
328+
// GitHub: blob SHA is a content-addressable hash
329+
contentHash: `gitsha:${item.sha}`
330+
331+
// Dropbox: API provides content_hash
332+
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`
333+
334+
// Confluence: version number increments on edit
335+
contentHash: `confluence:${page.id}:${page.version.number}`
336+
```
337+
338+
**Critical invariant:** The `contentHash` MUST be identical whether produced by `listDocuments` (stub) or `getDocument` (full doc). Both should use the same stub function to guarantee this.
339+
340+
### Implementation Pattern
341+
342+
```typescript
343+
// 1. Create a stub function (sync, no API calls)
344+
function fileToStub(file: ServiceFile): ExternalDocument {
345+
return {
346+
externalId: file.id,
347+
title: file.name || 'Untitled',
348+
content: '',
349+
contentDeferred: true,
350+
mimeType: 'text/plain',
351+
sourceUrl: `https://service.com/file/${file.id}`,
352+
contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
353+
metadata: { /* fields needed by mapTags */ },
354+
}
355+
}
356+
357+
// 2. listDocuments returns stubs (fast, metadata only)
358+
listDocuments: async (accessToken, sourceConfig, cursor) => {
359+
const response = await fetchWithRetry(listUrl, { ... })
360+
const files = (await response.json()).files
361+
const documents = files.map(fileToStub)
362+
return { documents, nextCursor, hasMore }
363+
}
364+
365+
// 3. getDocument fetches content and returns full doc with SAME contentHash
366+
getDocument: async (accessToken, sourceConfig, externalId) => {
367+
const metadata = await fetchWithRetry(metadataUrl, { ... })
368+
const file = await metadata.json()
369+
if (file.trashed) return null
370+
371+
try {
372+
const content = await fetchContent(accessToken, file)
373+
if (!content.trim()) return null
374+
const stub = fileToStub(file)
375+
return { ...stub, content, contentDeferred: false }
376+
} catch (error) {
377+
logger.warn(`Failed to fetch content for: ${file.name}`, { error })
378+
return null
379+
}
301380
}
302381
```
303382

383+
### Reference Implementations
384+
385+
- **Google Drive**: `connectors/google-drive/google-drive.ts` — file download/export with `modifiedTime` hash
386+
- **GitHub**: `connectors/github/github.ts` — git blob SHA hash
387+
- **Notion**: `connectors/notion/notion.ts` — blocks API with `last_edited_time` hash
388+
- **Confluence**: `connectors/confluence/confluence.ts` — version number hash
389+
304390
## tagDefinitions — Declared Tag Definitions
305391

306392
Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes.
@@ -409,7 +495,10 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
409495

410496
## Reference Implementations
411497

412-
- **OAuth**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
498+
- **OAuth + contentDeferred**: `apps/sim/connectors/google-drive/google-drive.ts` — file download with metadata-based hash, `orderBy` for deterministic pagination
499+
- **OAuth + contentDeferred (blocks API)**: `apps/sim/connectors/notion/notion.ts` — complex block content extraction deferred to `getDocument`
500+
- **OAuth + contentDeferred (git)**: `apps/sim/connectors/github/github.ts` — blob SHA hash, tree listing
501+
- **OAuth + inline content**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
413502
- **API key**: `apps/sim/connectors/fireflies/fireflies.ts` — GraphQL API with Bearer token auth
414503

415504
## Checklist
@@ -425,7 +514,9 @@ export const CONNECTOR_REGISTRY: ConnectorRegistry = {
425514
- `selectorKey` exists in `hooks/selectors/registry.ts`
426515
- `dependsOn` references selector field IDs (not `canonicalParamId`)
427516
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
428-
- [ ] `listDocuments` handles pagination and computes content hashes
517+
- [ ] `listDocuments` handles pagination with metadata-based content hashes
518+
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
519+
- [ ] `contentHash` is metadata-based (not content-based) and identical between stub and `getDocument`
429520
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
430521
- [ ] `metadata` includes source-specific data for tag mapping
431522
- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags`

0 commit comments

Comments
 (0)