Skip to content

Commit f8ee325

Browse files
committed
chore(config): align .claude, .cursor, and .agents configs
1 parent ec5725a commit f8ee325

File tree

18 files changed

+4457
-28
lines changed

18 files changed

+4457
-28
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
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
name: you-might-not-need-an-effect
3+
description: Analyze code for useEffect anti-patterns and fix them based on React guidelines
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+
Steps:
13+
1. Read https://react.dev/learn/you-might-not-need-an-effect to understand the guidelines
14+
2. Analyze the specified scope for useEffect anti-patterns
15+
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)