@siftd/connect-agent 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,354 @@
1
+ /**
2
+ * System Indexer
3
+ *
4
+ * Indexes the user's filesystem INTO the memory system.
5
+ * When user says "that project" or "save in downloads",
6
+ * semantic search finds the relevant paths.
7
+ *
8
+ * No MCP. No new protocols. Just smart memory use.
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { execSync } from 'child_process';
13
+ export class SystemIndexer {
14
+ memory;
15
+ home;
16
+ indexed = new Set();
17
+ constructor(memory) {
18
+ this.memory = memory;
19
+ this.home = process.env.HOME || '/Users/jimjordan';
20
+ }
21
+ /**
22
+ * Index the user's home directory structure into memory
23
+ */
24
+ async indexHome() {
25
+ console.log('[INDEXER] Starting filesystem indexing...');
26
+ const startTime = Date.now();
27
+ let projects = 0;
28
+ let directories = 0;
29
+ // Key directories to index - these are the IMPORTANT ones users will reference
30
+ const keyDirs = [
31
+ { path: `${this.home}/Documents`, depth: 3, importance: 0.7, alias: 'documents folder' },
32
+ { path: `${this.home}/Projects`, depth: 3, importance: 0.8, alias: 'projects folder' },
33
+ { path: `${this.home}/Downloads`, depth: 1, importance: 0.6, alias: 'downloads folder' },
34
+ { path: `${this.home}/Desktop`, depth: 1, importance: 0.6, alias: 'desktop folder' },
35
+ { path: `${this.home}/.claude`, depth: 2, importance: 0.6, alias: 'claude config' },
36
+ ];
37
+ // First, index the key directories themselves with high importance
38
+ for (const dir of keyDirs) {
39
+ if (fs.existsSync(dir.path)) {
40
+ await this.rememberKeyDirectory(dir.path, dir.importance, dir.alias);
41
+ directories++;
42
+ }
43
+ }
44
+ // Then index subdirectories
45
+ for (const dir of keyDirs) {
46
+ if (fs.existsSync(dir.path)) {
47
+ const result = await this.indexDirectory(dir.path, dir.depth, dir.importance);
48
+ directories += result.directories;
49
+ }
50
+ }
51
+ // Find and index all git repos (these are projects)
52
+ for (const dir of keyDirs) {
53
+ if (fs.existsSync(dir.path)) {
54
+ const found = await this.indexGitRepos(dir.path);
55
+ projects += found;
56
+ }
57
+ }
58
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
59
+ console.log(`[INDEXER] Indexed ${projects} projects, ${directories} directories in ${elapsed}s`);
60
+ return { projects, directories, total: projects + directories };
61
+ }
62
+ /**
63
+ * Index a directory and its subdirectories
64
+ */
65
+ async indexDirectory(dirPath, maxDepth, baseImportance, currentDepth = 0) {
66
+ if (currentDepth > maxDepth)
67
+ return { directories: 0 };
68
+ if (this.indexed.has(dirPath))
69
+ return { directories: 0 };
70
+ let directories = 0;
71
+ try {
72
+ const stat = fs.statSync(dirPath);
73
+ if (!stat.isDirectory())
74
+ return { directories: 0 };
75
+ // Skip hidden directories (except .claude)
76
+ const basename = path.basename(dirPath);
77
+ if (basename.startsWith('.') && basename !== '.claude')
78
+ return { directories: 0 };
79
+ // Skip node_modules, .git, etc.
80
+ const skipDirs = ['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv'];
81
+ if (skipDirs.includes(basename))
82
+ return { directories: 0 };
83
+ this.indexed.add(dirPath);
84
+ // Create memory for this directory
85
+ const relativePath = dirPath.replace(this.home, '~');
86
+ const importance = Math.max(0.3, baseImportance - (currentDepth * 0.1));
87
+ const contents = fs.readdirSync(dirPath);
88
+ const subdirs = contents.filter(f => {
89
+ try {
90
+ return fs.statSync(path.join(dirPath, f)).isDirectory() && !f.startsWith('.');
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ });
96
+ const description = this.describeDirectory(dirPath, contents);
97
+ await this.memory.remember(`Directory: ${basename} at ${relativePath} - ${description}`, {
98
+ type: 'semantic',
99
+ source: 'system-indexer',
100
+ importance,
101
+ tags: ['filesystem', 'directory', ...this.extractTags(dirPath, contents)]
102
+ });
103
+ directories++;
104
+ // Recursively index subdirectories
105
+ for (const subdir of subdirs) {
106
+ const subPath = path.join(dirPath, subdir);
107
+ const result = await this.indexDirectory(subPath, maxDepth, baseImportance, currentDepth + 1);
108
+ directories += result.directories;
109
+ }
110
+ }
111
+ catch (error) {
112
+ // Skip inaccessible directories
113
+ }
114
+ return { directories };
115
+ }
116
+ /**
117
+ * Find and index git repositories as projects
118
+ */
119
+ async indexGitRepos(basePath) {
120
+ let projects = 0;
121
+ try {
122
+ // Find all .git directories
123
+ const result = execSync(`find "${basePath}" -maxdepth 5 -name ".git" -type d 2>/dev/null`, { encoding: 'utf8', timeout: 30000 });
124
+ const gitDirs = result.trim().split('\n').filter(Boolean);
125
+ for (const gitDir of gitDirs) {
126
+ const projectDir = path.dirname(gitDir);
127
+ if (this.indexed.has(`project:${projectDir}`))
128
+ continue;
129
+ this.indexed.add(`project:${projectDir}`);
130
+ const projectInfo = this.getProjectInfo(projectDir);
131
+ if (projectInfo) {
132
+ await this.rememberProject(projectInfo);
133
+ projects++;
134
+ }
135
+ }
136
+ }
137
+ catch (error) {
138
+ // find command failed, skip
139
+ }
140
+ return projects;
141
+ }
142
+ /**
143
+ * Get detailed info about a git project
144
+ */
145
+ getProjectInfo(projectPath) {
146
+ try {
147
+ const name = path.basename(projectPath);
148
+ const stat = fs.statSync(projectPath);
149
+ let gitBranch;
150
+ let gitRemote;
151
+ try {
152
+ gitBranch = execSync('git branch --show-current', {
153
+ cwd: projectPath,
154
+ encoding: 'utf8',
155
+ timeout: 5000
156
+ }).trim();
157
+ }
158
+ catch { }
159
+ try {
160
+ gitRemote = execSync('git remote get-url origin 2>/dev/null', {
161
+ cwd: projectPath,
162
+ encoding: 'utf8',
163
+ timeout: 5000
164
+ }).trim();
165
+ }
166
+ catch { }
167
+ // Detect project type
168
+ const description = this.detectProjectType(projectPath);
169
+ return {
170
+ path: projectPath,
171
+ type: 'project',
172
+ name,
173
+ lastModified: stat.mtime,
174
+ gitBranch,
175
+ gitRemote,
176
+ description
177
+ };
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * Detect project type by looking at files
185
+ */
186
+ detectProjectType(projectPath) {
187
+ const files = fs.readdirSync(projectPath);
188
+ const parts = [];
189
+ if (files.includes('package.json')) {
190
+ parts.push('Node.js');
191
+ try {
192
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8'));
193
+ if (pkg.dependencies?.next || pkg.devDependencies?.next)
194
+ parts.push('Next.js');
195
+ if (pkg.dependencies?.react || pkg.devDependencies?.react)
196
+ parts.push('React');
197
+ if (pkg.dependencies?.express)
198
+ parts.push('Express');
199
+ if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript)
200
+ parts.push('TypeScript');
201
+ }
202
+ catch { }
203
+ }
204
+ if (files.includes('requirements.txt') || files.includes('pyproject.toml') || files.includes('setup.py')) {
205
+ parts.push('Python');
206
+ }
207
+ if (files.includes('Cargo.toml'))
208
+ parts.push('Rust');
209
+ if (files.includes('go.mod'))
210
+ parts.push('Go');
211
+ if (files.includes('Gemfile'))
212
+ parts.push('Ruby');
213
+ if (files.includes('docker-compose.yml') || files.includes('Dockerfile'))
214
+ parts.push('Docker');
215
+ if (files.includes('.github'))
216
+ parts.push('GitHub Actions');
217
+ return parts.length > 0 ? parts.join(', ') + ' project' : 'project';
218
+ }
219
+ /**
220
+ * Remember a key directory (Downloads, Documents, Desktop, etc.)
221
+ * These get high importance and multiple aliases for better search
222
+ */
223
+ async rememberKeyDirectory(dirPath, importance, alias) {
224
+ const name = path.basename(dirPath);
225
+ const relativePath = dirPath.replace(this.home, '~');
226
+ // Get contents preview
227
+ let contentsPreview = '';
228
+ try {
229
+ const contents = fs.readdirSync(dirPath);
230
+ const visibleItems = contents.filter(f => !f.startsWith('.'));
231
+ contentsPreview = ` - contains ${visibleItems.length} items`;
232
+ // For Downloads, show recent items
233
+ if (name.toLowerCase() === 'downloads' && visibleItems.length > 0) {
234
+ const recent = visibleItems.slice(0, 5).join(', ');
235
+ contentsPreview += ` including ${recent}`;
236
+ }
237
+ }
238
+ catch { }
239
+ // Create memory with multiple search-friendly terms
240
+ const content = `${alias.charAt(0).toUpperCase() + alias.slice(1)}: ${relativePath}${contentsPreview}. ` +
241
+ `This is the user's ${alias} at ${dirPath}. ` +
242
+ `Use this path when user mentions "${name.toLowerCase()}" or "${alias}".`;
243
+ await this.memory.remember(content, {
244
+ type: 'semantic',
245
+ source: 'system-indexer',
246
+ importance,
247
+ tags: ['filesystem', 'key-directory', name.toLowerCase(), alias]
248
+ });
249
+ this.indexed.add(dirPath);
250
+ }
251
+ /**
252
+ * Store a project in memory
253
+ */
254
+ async rememberProject(project) {
255
+ const relativePath = project.path.replace(this.home, '~');
256
+ const ageHours = (Date.now() - project.lastModified.getTime()) / (1000 * 60 * 60);
257
+ const recentlyModified = ageHours < 24;
258
+ // Build memory content
259
+ let content = `Project: ${project.name} at ${relativePath}`;
260
+ if (project.description)
261
+ content += ` - ${project.description}`;
262
+ if (project.gitBranch)
263
+ content += `, branch: ${project.gitBranch}`;
264
+ if (project.gitRemote) {
265
+ // Extract repo name from URL
266
+ const repoMatch = project.gitRemote.match(/([^/]+\/[^/]+?)(?:\.git)?$/);
267
+ if (repoMatch)
268
+ content += `, repo: ${repoMatch[1]}`;
269
+ }
270
+ if (recentlyModified)
271
+ content += ' (recently modified)';
272
+ const tags = ['filesystem', 'project', 'git'];
273
+ if (project.description) {
274
+ // Add tech stack tags
275
+ const techTags = project.description.toLowerCase().split(/[,\s]+/).filter(t => ['node.js', 'next.js', 'react', 'typescript', 'python', 'rust', 'go', 'docker'].includes(t));
276
+ tags.push(...techTags);
277
+ }
278
+ await this.memory.remember(content, {
279
+ type: 'semantic',
280
+ source: 'system-indexer',
281
+ importance: recentlyModified ? 0.9 : 0.75,
282
+ tags
283
+ });
284
+ }
285
+ /**
286
+ * Generate a description for a directory
287
+ */
288
+ describeDirectory(dirPath, contents) {
289
+ const parts = [];
290
+ const fileCount = contents.filter(f => !f.startsWith('.')).length;
291
+ parts.push(`contains ${fileCount} items`);
292
+ // Check for specific content types
293
+ const hasImages = contents.some(f => /\.(png|jpg|jpeg|gif|svg)$/i.test(f));
294
+ const hasDocuments = contents.some(f => /\.(pdf|doc|docx|md|txt)$/i.test(f));
295
+ const hasCode = contents.some(f => /\.(js|ts|py|go|rs|java|cpp|c|h)$/i.test(f));
296
+ if (hasImages)
297
+ parts.push('images');
298
+ if (hasDocuments)
299
+ parts.push('documents');
300
+ if (hasCode)
301
+ parts.push('code files');
302
+ return parts.join(', ');
303
+ }
304
+ /**
305
+ * Extract relevant tags from directory contents
306
+ */
307
+ extractTags(dirPath, contents) {
308
+ const tags = [];
309
+ const basename = path.basename(dirPath).toLowerCase();
310
+ // Directory name based tags
311
+ if (basename.includes('download'))
312
+ tags.push('downloads');
313
+ if (basename.includes('document'))
314
+ tags.push('documents');
315
+ if (basename.includes('project'))
316
+ tags.push('projects');
317
+ if (basename.includes('desktop'))
318
+ tags.push('desktop');
319
+ if (basename.includes('picture') || basename.includes('image'))
320
+ tags.push('images');
321
+ if (basename.includes('video') || basename.includes('movie'))
322
+ tags.push('videos');
323
+ if (basename.includes('music') || basename.includes('audio'))
324
+ tags.push('audio');
325
+ // Content based tags
326
+ if (contents.some(f => /\.(png|jpg|jpeg|gif|svg)$/i.test(f)))
327
+ tags.push('images');
328
+ if (contents.some(f => /\.(mp4|mov|avi|mkv)$/i.test(f)))
329
+ tags.push('videos');
330
+ if (contents.some(f => /\.(mp3|wav|m4a)$/i.test(f)))
331
+ tags.push('audio');
332
+ if (contents.some(f => /\.(pdf|doc|docx)$/i.test(f)))
333
+ tags.push('documents');
334
+ return [...new Set(tags)];
335
+ }
336
+ /**
337
+ * Re-index (clear old filesystem memories and re-scan)
338
+ */
339
+ async reindex() {
340
+ console.log('[INDEXER] Re-indexing filesystem...');
341
+ this.indexed.clear();
342
+ // Clear old filesystem memories
343
+ const oldMemories = await this.memory.search('filesystem directory project', {
344
+ limit: 1000,
345
+ type: 'semantic'
346
+ });
347
+ for (const mem of oldMemories) {
348
+ if (mem.tags.includes('filesystem')) {
349
+ await this.memory.forget(mem.id);
350
+ }
351
+ }
352
+ return this.indexHome();
353
+ }
354
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Genesis Knowledge Module
3
+ *
4
+ * System-level knowledge that is shared across all users and deployments.
5
+ * This is NOT user-specific data - it's operational knowledge about how
6
+ * the Master Orchestrator works.
7
+ */
8
+ export interface KnowledgeItem {
9
+ id: string;
10
+ category: string;
11
+ title: string;
12
+ content: string;
13
+ importance: 'critical' | 'high' | 'medium' | 'low';
14
+ }
15
+ export interface ToolPattern {
16
+ scenario: string;
17
+ approach: string;
18
+ example: string;
19
+ notes: string;
20
+ }
21
+ export interface AntiPattern {
22
+ wrong: string;
23
+ right: string;
24
+ reason: string;
25
+ }
26
+ export interface SystemKnowledge {
27
+ version: string;
28
+ description: string;
29
+ knowledge: KnowledgeItem[];
30
+ }
31
+ export interface ToolPatterns {
32
+ version: string;
33
+ description: string;
34
+ patterns: ToolPattern[];
35
+ antipatterns: AntiPattern[];
36
+ }
37
+ /**
38
+ * Load system knowledge from JSON
39
+ */
40
+ export declare function loadSystemKnowledge(): SystemKnowledge;
41
+ /**
42
+ * Load tool patterns from JSON
43
+ */
44
+ export declare function loadToolPatterns(): ToolPatterns;
45
+ /**
46
+ * Get critical knowledge items for system prompt injection
47
+ */
48
+ export declare function getCriticalKnowledge(): string[];
49
+ /**
50
+ * Get all knowledge formatted for system prompt
51
+ */
52
+ export declare function getKnowledgeForPrompt(): string;
53
+ /**
54
+ * Search knowledge by category or keyword
55
+ */
56
+ export declare function searchKnowledge(query: string): KnowledgeItem[];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Genesis Knowledge Module
3
+ *
4
+ * System-level knowledge that is shared across all users and deployments.
5
+ * This is NOT user-specific data - it's operational knowledge about how
6
+ * the Master Orchestrator works.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ /**
14
+ * Load system knowledge from JSON
15
+ */
16
+ export function loadSystemKnowledge() {
17
+ const filePath = path.join(__dirname, 'system-knowledge.json');
18
+ const data = fs.readFileSync(filePath, 'utf8');
19
+ return JSON.parse(data);
20
+ }
21
+ /**
22
+ * Load tool patterns from JSON
23
+ */
24
+ export function loadToolPatterns() {
25
+ const filePath = path.join(__dirname, 'tool-patterns.json');
26
+ const data = fs.readFileSync(filePath, 'utf8');
27
+ return JSON.parse(data);
28
+ }
29
+ /**
30
+ * Get critical knowledge items for system prompt injection
31
+ */
32
+ export function getCriticalKnowledge() {
33
+ const knowledge = loadSystemKnowledge();
34
+ return knowledge.knowledge
35
+ .filter(k => k.importance === 'critical')
36
+ .map(k => `${k.title}: ${k.content}`);
37
+ }
38
+ /**
39
+ * Get all knowledge formatted for system prompt
40
+ */
41
+ export function getKnowledgeForPrompt() {
42
+ const knowledge = loadSystemKnowledge();
43
+ const patterns = loadToolPatterns();
44
+ let prompt = '\n\n## OPERATIONAL KNOWLEDGE\n\n';
45
+ // Add critical and high importance knowledge
46
+ const important = knowledge.knowledge.filter(k => k.importance === 'critical' || k.importance === 'high');
47
+ for (const item of important) {
48
+ prompt += `### ${item.title}\n${item.content}\n\n`;
49
+ }
50
+ // Add key patterns
51
+ prompt += '## TOOL USAGE PATTERNS\n\n';
52
+ for (const pattern of patterns.patterns.slice(0, 5)) {
53
+ prompt += `- **${pattern.scenario}**: ${pattern.approach}\n`;
54
+ }
55
+ // Add antipatterns as warnings
56
+ prompt += '\n## AVOID THESE MISTAKES\n\n';
57
+ for (const anti of patterns.antipatterns) {
58
+ prompt += `- DON'T: ${anti.wrong} → DO: ${anti.right}\n`;
59
+ }
60
+ return prompt;
61
+ }
62
+ /**
63
+ * Search knowledge by category or keyword
64
+ */
65
+ export function searchKnowledge(query) {
66
+ const knowledge = loadSystemKnowledge();
67
+ const lowerQuery = query.toLowerCase();
68
+ return knowledge.knowledge.filter(k => k.category.toLowerCase().includes(lowerQuery) ||
69
+ k.title.toLowerCase().includes(lowerQuery) ||
70
+ k.content.toLowerCase().includes(lowerQuery));
71
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Core operational knowledge for the Master Orchestrator",
4
+ "knowledge": [
5
+ {
6
+ "id": "architecture",
7
+ "category": "core",
8
+ "title": "Orchestrator Architecture",
9
+ "content": "You are the MASTER ORCHESTRATOR. You use the Anthropic API directly and delegate ALL file/code work to Claude Code CLI workers. Never try to do file operations yourself - always delegate to workers. Workers run autonomously and return results. You synthesize and respond to the user.",
10
+ "importance": "critical"
11
+ },
12
+ {
13
+ "id": "worker_spawning",
14
+ "category": "workers",
15
+ "title": "Worker Spawning",
16
+ "content": "Workers are spawned using: spawn('bash', ['-l', '-c', 'claude -p <task> --dangerously-skip-permissions']). The --dangerously-skip-permissions flag is CRITICAL for non-interactive execution. The -l flag ensures proper PATH resolution for NVM-installed binaries. Use spawn_worker for background tasks, delegate_to_worker for blocking tasks.",
17
+ "importance": "critical"
18
+ },
19
+ {
20
+ "id": "shell_quoting",
21
+ "category": "workers",
22
+ "title": "Shell Quoting",
23
+ "content": "When building worker prompts, escape single quotes: task.replace(/'/g, \"'\\\\''\"). Never use shell: true with spawn as it has quote escaping issues. Always use bash -l -c with proper escaping.",
24
+ "importance": "high"
25
+ },
26
+ {
27
+ "id": "polling_architecture",
28
+ "category": "networking",
29
+ "title": "Message Polling",
30
+ "content": "The socket-store.ts (HTTP) and WebSocket server are SEPARATE systems that do not communicate. The agent MUST always poll for HTTP messages regardless of WebSocket connection status. Never skip polling when WebSocket is connected.",
31
+ "importance": "high"
32
+ },
33
+ {
34
+ "id": "tool_errors",
35
+ "category": "api",
36
+ "title": "Tool Error Handling",
37
+ "content": "When returning tool_result with is_error: true, the content CANNOT be empty. Always use fallback: result.output || result.error || 'Unknown error'. This prevents 400 errors from the Anthropic API.",
38
+ "importance": "high"
39
+ },
40
+ {
41
+ "id": "iteration_limits",
42
+ "category": "api",
43
+ "title": "Iteration Limits",
44
+ "content": "The orchestrator has a 30 iteration limit per request. If hitting max iterations, check if the agent is stuck in a loop or if tools are returning errors. Complex multi-tool tasks may need careful planning to stay within limits.",
45
+ "importance": "medium"
46
+ },
47
+ {
48
+ "id": "delegation_philosophy",
49
+ "category": "core",
50
+ "title": "Delegation Philosophy",
51
+ "content": "Simple commands (ls, cat, git status) can use bash directly. Complex multi-file changes, builds, and deployments should use spawn_worker or delegate_to_worker. Web research uses web_search + fetch_url. Always choose the right tool for the task.",
52
+ "importance": "high"
53
+ },
54
+ {
55
+ "id": "memory_usage",
56
+ "category": "memory",
57
+ "title": "Memory System",
58
+ "content": "Use remember() to store important facts, preferences, and context about the user. Use search_memory() before taking action to recall relevant context. Memories persist across conversations. High importance memories (0.8+) decay slowly.",
59
+ "importance": "medium"
60
+ }
61
+ ]
62
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Common tool usage patterns for the Master Orchestrator",
4
+ "patterns": [
5
+ {
6
+ "scenario": "Create a file on user's system",
7
+ "approach": "spawn_worker or delegate_to_worker",
8
+ "example": "spawn_worker('Create a file called example.txt on the desktop with content: Hello World')",
9
+ "notes": "Never use bash echo/cat for file creation - delegate to worker"
10
+ },
11
+ {
12
+ "scenario": "Read a file",
13
+ "approach": "delegate_to_worker for complex, bash for simple",
14
+ "example": "bash('cat ~/Desktop/example.txt') OR delegate_to_worker('Read and summarize the contents of ~/Projects/readme.md')",
15
+ "notes": "Use bash for quick reads, delegate for analysis"
16
+ },
17
+ {
18
+ "scenario": "Run a build or deployment",
19
+ "approach": "spawn_worker (background)",
20
+ "example": "spawn_worker('Run npm build in ~/Projects/my-app and report any errors')",
21
+ "notes": "Non-blocking, check status with check_worker"
22
+ },
23
+ {
24
+ "scenario": "Git operations",
25
+ "approach": "bash for status/log, delegate for commits",
26
+ "example": "bash('git status') then delegate_to_worker('Commit changes with message: Fix bug')",
27
+ "notes": "Simple git commands via bash, complex operations via worker"
28
+ },
29
+ {
30
+ "scenario": "Web research",
31
+ "approach": "web_search + fetch_url",
32
+ "example": "web_search('latest React 19 features') then fetch_url('https://react.dev/blog')",
33
+ "notes": "Search first, then fetch specific pages for details"
34
+ },
35
+ {
36
+ "scenario": "Multiple parallel tasks",
37
+ "approach": "Multiple spawn_worker calls",
38
+ "example": "spawn_worker('Task A'), spawn_worker('Task B'), spawn_worker('Task C')",
39
+ "notes": "Workers run in parallel, use wait_worker to collect results"
40
+ },
41
+ {
42
+ "scenario": "Remember user preferences",
43
+ "approach": "remember with high importance",
44
+ "example": "remember('User prefers dark mode and concise responses', importance=0.8)",
45
+ "notes": "High importance = longer retention"
46
+ },
47
+ {
48
+ "scenario": "Find user's project",
49
+ "approach": "search_memory first",
50
+ "example": "search_memory('connect app project path')",
51
+ "notes": "Always check memory before asking user for paths"
52
+ },
53
+ {
54
+ "scenario": "Preview HTML/web project",
55
+ "approach": "start_local_server + open_browser",
56
+ "example": "start_local_server('/path/to/project', 8080) then open_browser('http://localhost:8080')",
57
+ "notes": "Serve files and open in browser for preview"
58
+ },
59
+ {
60
+ "scenario": "Debug failing workers",
61
+ "approach": "check_worker for status, list_workers for overview",
62
+ "example": "check_worker('job_xxx') to see error details",
63
+ "notes": "Workers may fail due to permissions, paths, or timeouts"
64
+ }
65
+ ],
66
+ "antipatterns": [
67
+ {
68
+ "wrong": "Using bash to create/edit files directly",
69
+ "right": "Delegate file operations to workers",
70
+ "reason": "Workers have full Claude Code capabilities for safe file ops"
71
+ },
72
+ {
73
+ "wrong": "Doing file operations yourself as orchestrator",
74
+ "right": "Always delegate to workers",
75
+ "reason": "Orchestrator focuses on understanding and coordinating"
76
+ },
77
+ {
78
+ "wrong": "Blocking on spawn_worker completion",
79
+ "right": "Use spawn_worker for background, delegate_to_worker for blocking",
80
+ "reason": "spawn_worker returns immediately with job ID"
81
+ },
82
+ {
83
+ "wrong": "Guessing file paths",
84
+ "right": "Use search_memory or ask user",
85
+ "reason": "Memory stores indexed filesystem, use it"
86
+ }
87
+ ]
88
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Runner Heartbeat Module
3
+ *
4
+ * Sends periodic heartbeats to the Connect server to maintain presence.
5
+ * This is what makes a runner "connected" - NOT the pairing token.
6
+ *
7
+ * Both local and VM runners use this same module.
8
+ */
9
+ export type RunnerType = 'local' | 'vm';
10
+ export interface HeartbeatConfig {
11
+ runnerType: RunnerType;
12
+ capabilities?: string[];
13
+ onError?: (error: Error) => void;
14
+ onSuccess?: () => void;
15
+ }
16
+ /**
17
+ * Start the heartbeat loop
18
+ */
19
+ export declare function startHeartbeat(config: HeartbeatConfig): void;
20
+ /**
21
+ * Stop the heartbeat loop
22
+ */
23
+ export declare function stopHeartbeat(): void;
24
+ /**
25
+ * Get current heartbeat state (for diagnostics)
26
+ */
27
+ export declare function getHeartbeatState(): {
28
+ isRunning: boolean;
29
+ runnerId: string | null;
30
+ lastSuccess: Date | null;
31
+ consecutiveFailures: number;
32
+ };