@siftd/connect-agent 0.2.39 → 0.2.41

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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Connect Agent runs on your machine and bridges the Connect web app to Claude Cod
18
18
  ```bash
19
19
  # 1. Go to https://connect.siftd.app and get a pairing code
20
20
 
21
- # 2. Pair your machine (with API key for full orchestrator mode)
21
+ # 2. Pair your machine (with API key)
22
22
  npx @siftd/connect-agent pair <CODE> --api-key <YOUR_ANTHROPIC_API_KEY>
23
23
 
24
24
  # 3. Start the agent
@@ -33,17 +33,13 @@ On first start, the agent creates `~/Lia-Hub/` with:
33
33
 
34
34
  ## Modes
35
35
 
36
- ### Orchestrator Mode (Recommended)
37
- When you provide an Anthropic API key, the agent runs as a **master orchestrator**:
36
+ The agent runs as a **master orchestrator** (requires an Anthropic API key):
38
37
  - Uses Claude API directly for the orchestration layer
39
38
  - Maintains persistent memory about you and your projects
40
39
  - Delegates file/code work to Claude Code CLI workers
41
40
  - Schedules future tasks
42
41
  - Reads hub files for context on every startup
43
42
 
44
- ### Simple Relay Mode
45
- Without an API key, the agent acts as a simple relay to `claude -p --continue`.
46
-
47
43
  ## Commands
48
44
 
49
45
  ```bash
@@ -66,7 +62,7 @@ connect-agent logout
66
62
  ## Special Commands (via web chat)
67
63
 
68
64
  - `/reset` or `/clear` - Clear conversation history
69
- - `/mode` - Check current mode (orchestrator vs simple)
65
+ - `/mode` - Show runtime details
70
66
  - `/memory` - Show memory statistics
71
67
  - `/verbose` - Toggle verbose tool output
72
68
 
package/dist/agent.js CHANGED
@@ -12,10 +12,6 @@ import { startHeartbeat, stopHeartbeat, getHeartbeatState } from './heartbeat.js
12
12
  import { loadHubContext, readScratchpad } from './core/hub.js';
13
13
  import { PRODUCT_FULL_NAME } from './branding.js';
14
14
  import { startPreviewWorker, stopPreviewWorker } from './core/preview-worker.js';
15
- // Strip ANSI escape codes for clean output
16
- function stripAnsi(str) {
17
- return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
18
- }
19
15
  function parseAttachments(content) {
20
16
  const marker = '📎 Attached files:';
21
17
  const index = content.indexOf(marker);
@@ -162,13 +158,10 @@ function initOrchestrator() {
162
158
  const instanceMode = getInstanceMode();
163
159
  const userOrgIds = getUserOrgIds();
164
160
  if (!apiKey) {
165
- console.log('[AGENT] No API key - using simple relay mode');
166
- console.log('[AGENT] To enable orchestrator: pair with --api-key flag');
167
- return null;
161
+ throw new Error('Missing Anthropic API key. Set ANTHROPIC_API_KEY or run `lia-agent set-key <key>`.');
168
162
  }
169
163
  if (!userId) {
170
- console.log('[AGENT] No userId configured - using simple relay mode');
171
- return null;
164
+ throw new Error('Missing userId configuration. Re-pair the agent.');
172
165
  }
173
166
  console.log(`[AGENT] Initializing Master Orchestrator (mode: ${instanceMode})...`);
174
167
  if (instanceMode === 'personal' && userOrgIds.length > 0) {
@@ -187,59 +180,11 @@ function initOrchestrator() {
187
180
  userOrgIds: instanceMode === 'personal' ? userOrgIds : undefined,
188
181
  });
189
182
  }
190
- /**
191
- * Simple relay mode - just pipes to Claude Code CLI
192
- */
193
- async function sendToClaudeSimple(input) {
194
- return new Promise((resolve) => {
195
- console.log(`\n[CLAUDE] Sending: ${input.substring(0, 80)}...`);
196
- const claude = spawn('claude', ['-p', '--continue'], {
197
- cwd: process.env.HOME,
198
- env: process.env,
199
- shell: true,
200
- });
201
- let output = '';
202
- let errorOutput = '';
203
- claude.stdout?.on('data', (data) => {
204
- const text = data.toString();
205
- output += text;
206
- process.stdout.write(text);
207
- });
208
- claude.stderr?.on('data', (data) => {
209
- const text = data.toString();
210
- errorOutput += text;
211
- if (!text.includes('Checking') && !text.includes('Connected')) {
212
- process.stderr.write(text);
213
- }
214
- });
215
- claude.on('close', (code) => {
216
- if (code === 0) {
217
- const response = stripAnsi(output).trim();
218
- console.log(`\n[CLAUDE] Response: ${response.substring(0, 80)}...`);
219
- resolve(response || 'No response from Claude');
220
- }
221
- else {
222
- const error = errorOutput || `Claude exited with code ${code}`;
223
- console.error(`\n[CLAUDE] Error: ${error}`);
224
- resolve(`Error: ${stripAnsi(error).trim()}`);
225
- }
226
- });
227
- claude.on('error', (err) => {
228
- console.error(`\n[CLAUDE] Failed to start: ${err.message}`);
229
- resolve(`Error: Failed to start Claude - ${err.message}`);
230
- });
231
- if (claude.stdin) {
232
- claude.stdin.write(input);
233
- claude.stdin.end();
234
- }
235
- });
236
- }
237
183
  /**
238
184
  * Orchestrator mode - uses the master orchestrator with memory
239
185
  * Supports WebSocket streaming for real-time progress updates
240
- * @param apiKey Optional per-request API key from user
241
186
  */
242
- async function sendToOrchestrator(input, orch, messageId, apiKey) {
187
+ async function sendToOrchestrator(input, orch, messageId) {
243
188
  console.log(`\n[ORCHESTRATOR] Processing: ${input.substring(0, 80)}...`);
244
189
  // Send typing indicator via WebSocket
245
190
  if (wsClient?.connected()) {
@@ -263,8 +208,7 @@ async function sendToOrchestrator(input, orch, messageId, apiKey) {
263
208
  if (wsClient?.connected() && messageId) {
264
209
  wsClient.sendProgress(messageId, msg);
265
210
  }
266
- }, apiKey // Pass per-request API key
267
- );
211
+ });
268
212
  // Update conversation history (only if response is non-empty)
269
213
  if (response && response.trim()) {
270
214
  conversationHistory.push({ role: 'user', content: input });
@@ -307,14 +251,11 @@ export async function processMessage(message) {
307
251
  const wsStatus = wsClient?.connected() ? 'WebSocket' : 'Polling';
308
252
  const hbState = getHeartbeatState();
309
253
  const runnerInfo = hbState.runnerId ? ` [${hbState.runnerId}]` : '';
310
- return orchestrator
311
- ? `Running in ORCHESTRATOR mode (${wsStatus})${runnerInfo} with memory and delegation`
312
- : `Running in SIMPLE mode (${wsStatus})${runnerInfo} - direct Claude Code relay`;
254
+ return `Running in ORCHESTRATOR mode (${wsStatus})${runnerInfo} with memory and delegation`;
313
255
  }
314
256
  if (content === '/memory' && orchestrator) {
315
257
  // Trigger memory stats
316
- const response = await orchestrator.processMessage('Show me my memory stats', [], undefined, message.apiKey // Use per-message API key if provided
317
- );
258
+ const response = await orchestrator.processMessage('Show me my memory stats', [], undefined);
318
259
  return response;
319
260
  }
320
261
  // System command: force update (sent by webapp banner)
@@ -359,12 +300,7 @@ export async function processMessage(message) {
359
300
  };
360
301
  }
361
302
  try {
362
- if (orchestrator) {
363
- return await sendToOrchestrator(message.content, orchestrator, message.id, message.apiKey);
364
- }
365
- else {
366
- return await sendToClaudeSimple(message.content);
367
- }
303
+ return await sendToOrchestrator(message.content, orchestrator, message.id);
368
304
  }
369
305
  catch (error) {
370
306
  console.error('Error:', error);
@@ -372,9 +308,15 @@ export async function processMessage(message) {
372
308
  }
373
309
  }
374
310
  export async function runAgent(pollInterval = 2000) {
375
- // Initialize orchestrator
376
- orchestrator = initOrchestrator();
377
- const mode = orchestrator ? 'ORCHESTRATOR' : 'SIMPLE RELAY';
311
+ try {
312
+ orchestrator = initOrchestrator();
313
+ }
314
+ catch (error) {
315
+ const msg = error instanceof Error ? error.message : String(error);
316
+ console.error(`[AGENT] ${msg}`);
317
+ process.exit(1);
318
+ }
319
+ const mode = 'ORCHESTRATOR';
378
320
  const deployment = getDeploymentInfo();
379
321
  console.log('╔══════════════════════════════════════════════════╗');
380
322
  console.log(`║ ${PRODUCT_FULL_NAME.padEnd(47)}║`);
@@ -385,29 +327,27 @@ export async function runAgent(pollInterval = 2000) {
385
327
  console.log(`[AGENT] Running in CLOUD mode`);
386
328
  console.log(`[AGENT] Server: ${deployment.server}`);
387
329
  }
388
- if (orchestrator) {
389
- console.log('[AGENT] Features: Memory, Scheduling, Worker Delegation');
390
- // Initialize orchestrator (indexes filesystem into memory)
391
- await orchestrator.initialize();
392
- // Load hub context on startup
393
- const hubContext = loadHubContext();
394
- if (hubContext.agentIdentity) {
395
- console.log('[AGENT] Hub: AGENTS.md loaded');
396
- }
397
- if (hubContext.claudeMd) {
398
- console.log('[AGENT] Hub: CLAUDE.md loaded');
399
- }
400
- if (hubContext.landmarks) {
401
- console.log('[AGENT] Hub: LANDMARKS.md loaded');
402
- }
403
- // Check scratchpad for pending plans
404
- const scratchpad = readScratchpad();
405
- if (scratchpad && scratchpad.length > 100) {
406
- console.log(`[AGENT] Scratchpad: ${scratchpad.length} chars of pending notes`);
407
- }
330
+ console.log('[AGENT] Features: Memory, Scheduling, Worker Delegation');
331
+ // Initialize orchestrator (indexes filesystem into memory)
332
+ await orchestrator.initialize();
333
+ // Load hub context on startup
334
+ const hubContext = loadHubContext();
335
+ if (hubContext.agentIdentity) {
336
+ console.log('[AGENT] Hub: AGENTS.md loaded');
337
+ }
338
+ if (hubContext.claudeMd) {
339
+ console.log('[AGENT] Hub: CLAUDE.md loaded');
340
+ }
341
+ if (hubContext.landmarks) {
342
+ console.log('[AGENT] Hub: LANDMARKS.md loaded');
343
+ }
344
+ // Check scratchpad for pending plans
345
+ const scratchpad = readScratchpad();
346
+ if (scratchpad && scratchpad.length > 100) {
347
+ console.log(`[AGENT] Scratchpad: ${scratchpad.length} chars of pending notes`);
408
348
  }
409
349
  // Start preview worker for fast-path asset previews (no LLM)
410
- const previewWorker = startPreviewWorker({
350
+ startPreviewWorker({
411
351
  verbose: true,
412
352
  onAsset: (manifest) => {
413
353
  console.log(`[PREVIEW] New asset: ${manifest.name} (${manifest.id})`);
@@ -425,7 +365,7 @@ export async function runAgent(pollInterval = 2000) {
425
365
  const runnerType = isCloudMode() ? 'vm' : 'local';
426
366
  startHeartbeat({
427
367
  runnerType,
428
- capabilities: orchestrator ? ['orchestrator', 'memory'] : [],
368
+ capabilities: ['orchestrator', 'memory'],
429
369
  onError: (error) => {
430
370
  // Log only significant errors
431
371
  if (error.message.includes('401') || error.message.includes('403')) {
@@ -449,35 +389,38 @@ export async function runAgent(pollInterval = 2000) {
449
389
  // Try WebSocket first
450
390
  wsClient = new AgentWebSocket();
451
391
  const wsConnected = await wsClient.connect();
452
- // Set up worker callbacks
453
- if (orchestrator) {
454
- // Progress bars
455
- orchestrator.setWorkerStatusCallback((workers) => {
456
- if (wsClient?.connected()) {
457
- wsClient.sendWorkersUpdate(workers);
458
- }
459
- const running = workers.filter(w => w.status === 'running');
460
- if (running.length > 0) {
461
- console.log(`[WORKERS] ${running.length} running`);
462
- }
463
- });
464
- // Gallery updates - send worker assets for UI gallery view
465
- orchestrator.setGalleryCallback((galleryWorkers) => {
466
- if (wsClient?.connected()) {
467
- wsClient.sendGalleryWorkers(galleryWorkers);
468
- const totalAssets = galleryWorkers.reduce((sum, w) => sum + w.assets.length, 0);
469
- console.log(`[GALLERY] ${galleryWorkers.length} workers, ${totalAssets} assets`);
470
- }
471
- });
472
- // Worker results - send to user when workers complete
473
- orchestrator.setWorkerResultCallback((workerId, result) => {
474
- console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
475
- if (wsClient?.connected()) {
476
- // Send as response with worker ID as message ID
477
- wsClient.sendResponse(workerId, `**Worker completed:**\n\n${result}`);
478
- }
479
- });
480
- }
392
+ // Progress bars
393
+ orchestrator.setWorkerStatusCallback((workers) => {
394
+ if (wsClient?.connected()) {
395
+ wsClient.sendWorkersUpdate(workers);
396
+ }
397
+ const running = workers.filter(w => w.status === 'running');
398
+ if (running.length > 0) {
399
+ console.log(`[WORKERS] ${running.length} running`);
400
+ }
401
+ });
402
+ // Todo updates - send to user for inline display in chat
403
+ orchestrator.setTodoUpdateCallback((todos) => {
404
+ if (wsClient?.connected()) {
405
+ wsClient.sendTodoUpdate(todos);
406
+ }
407
+ });
408
+ // Gallery updates - send worker assets for UI gallery view
409
+ orchestrator.setGalleryCallback((galleryWorkers) => {
410
+ if (wsClient?.connected()) {
411
+ wsClient.sendGalleryWorkers(galleryWorkers);
412
+ const totalAssets = galleryWorkers.reduce((sum, w) => sum + w.assets.length, 0);
413
+ console.log(`[GALLERY] ${galleryWorkers.length} workers, ${totalAssets} assets`);
414
+ }
415
+ });
416
+ // Worker results - send to user when workers complete
417
+ orchestrator.setWorkerResultCallback((workerId, result) => {
418
+ console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
419
+ if (wsClient?.connected()) {
420
+ // Send as response with worker ID as message ID
421
+ wsClient.sendResponse(workerId, `**Worker completed:**\n\n${result}`);
422
+ }
423
+ });
481
424
  if (wsConnected) {
482
425
  console.log('[AGENT] Using WebSocket for real-time communication\n');
483
426
  // Handle messages via WebSocket
@@ -486,8 +429,7 @@ export async function runAgent(pollInterval = 2000) {
486
429
  const message = {
487
430
  id: wsMsg.id,
488
431
  content: wsMsg.content,
489
- timestamp: wsMsg.timestamp || Date.now(),
490
- apiKey: wsMsg.apiKey // Pass through per-request API key
432
+ timestamp: wsMsg.timestamp || Date.now()
491
433
  };
492
434
  const response = await processMessage(message);
493
435
  // Send response via WebSocket
package/dist/api.d.ts CHANGED
@@ -2,7 +2,6 @@ export interface Message {
2
2
  id: string;
3
3
  content: string;
4
4
  timestamp: number;
5
- apiKey?: string;
6
5
  }
7
6
  export interface ConnectResponse {
8
7
  success: boolean;
package/dist/cli.js CHANGED
@@ -84,7 +84,7 @@ program
84
84
  console.log(`Config file: ${getConfigPath()}`);
85
85
  console.log(`Server URL: ${getServerUrl()}`);
86
86
  console.log(`Configured: ${isConfigured() ? 'Yes' : 'No'}`);
87
- console.log(`Orchestrator mode: ${getAnthropicApiKey() ? 'Yes (API key saved)' : 'No (simple relay)'}`);
87
+ console.log(`Anthropic API key: ${getAnthropicApiKey() ? 'Configured' : 'Missing'}`);
88
88
  if (isConfigured()) {
89
89
  const spinner = ora('Checking connection...').start();
90
90
  const connected = await checkConnection();
@@ -22,7 +22,6 @@ export interface LiaTask {
22
22
  userId?: string;
23
23
  messageId?: string;
24
24
  parentTaskId?: string;
25
- apiKey?: string;
26
25
  context?: 'personal' | 'team';
27
26
  orgId?: string;
28
27
  };
@@ -17,6 +17,15 @@ export interface WorkerStatus {
17
17
  }
18
18
  export type WorkerStatusCallback = (workers: WorkerStatus[]) => void;
19
19
  export type WorkerLogCallback = (workerId: string, line: string, stream: 'stdout' | 'stderr') => void;
20
+ export interface LiaTodoItem {
21
+ id: string;
22
+ content: string;
23
+ activeForm: string;
24
+ status: 'pending' | 'in_progress' | 'completed';
25
+ workerId?: string;
26
+ progress?: number;
27
+ }
28
+ export type TodoUpdateCallback = (todos: LiaTodoItem[]) => void;
20
29
  export interface WorkerAsset {
21
30
  path: string;
22
31
  name: string;
@@ -71,6 +80,8 @@ export declare class MasterOrchestrator {
71
80
  private taskQueue;
72
81
  private taskUpdateCallback?;
73
82
  private planUpdateCallback?;
83
+ private currentTodos;
84
+ private todoUpdateCallback?;
74
85
  constructor(options: {
75
86
  apiKey: string;
76
87
  userId: string;
@@ -107,13 +118,16 @@ export declare class MasterOrchestrator {
107
118
  * Set callback for plan updates (for real-time UI updates)
108
119
  */
109
120
  setPlanUpdateCallback(callback: ((plan: LiaPlan) => void) | null): void;
121
+ /**
122
+ * Set callback for visible todo list updates (displayed inline in chat)
123
+ */
124
+ setTodoUpdateCallback(callback: TodoUpdateCallback | null): void;
110
125
  /**
111
126
  * Queue a message for async processing (non-blocking)
112
127
  * Returns immediately with task ID - use callbacks or getTaskQueueStatus for results
113
128
  */
114
129
  queueMessage(message: string, options?: {
115
130
  priority?: 'urgent' | 'high' | 'normal' | 'low';
116
- apiKey?: string;
117
131
  messageId?: string;
118
132
  }): LiaTask;
119
133
  /**
@@ -181,9 +195,8 @@ export declare class MasterOrchestrator {
181
195
  isVerbose(): boolean;
182
196
  /**
183
197
  * Process a user message
184
- * @param apiKey Optional per-request API key (overrides default)
185
198
  */
186
- processMessage(message: string, conversationHistory?: MessageParam[], sendMessage?: MessageSender, apiKey?: string): Promise<string>;
199
+ processMessage(message: string, conversationHistory?: MessageParam[], sendMessage?: MessageSender): Promise<string>;
187
200
  private tryHandleCalendarTodo;
188
201
  private extractTodoItems;
189
202
  private extractCalendarEvents;
@@ -211,7 +224,6 @@ export declare class MasterOrchestrator {
211
224
  private isIgnoredEntity;
212
225
  /**
213
226
  * Run the agentic loop
214
- * @param apiKey Optional per-request API key (creates temporary client)
215
227
  */
216
228
  private runAgentLoop;
217
229
  /**
@@ -300,6 +312,10 @@ export declare class MasterOrchestrator {
300
312
  * Get Lia's task queue status
301
313
  */
302
314
  private executeLiaGetQueue;
315
+ /**
316
+ * Update visible todo list (displayed inline in chat)
317
+ */
318
+ private executeLiaTodoWrite;
303
319
  /**
304
320
  * Format tool preview for user
305
321
  */
@@ -75,6 +75,13 @@ YOUR IDENTITY:
75
75
  HOW YOU WORK:
76
76
  You are the orchestrator, not the executor. You coordinate and remember while workers do the hands-on work.
77
77
 
78
+ CRITICAL - DATE AWARENESS:
79
+ ALWAYS run \`date\` via bash FIRST before referencing ANY temporal context from memory.
80
+ - Before mentioning meetings, deadlines, or scheduled items: check today's date
81
+ - Cross-reference memory timestamps with current date
82
+ - Only surface RELEVANT, CURRENT information - never reference past events as if they're upcoming
83
+ - A personal assistant that gives outdated information is worse than useless - it's actively misleading
84
+
78
85
  TASK QUEUE (Always-Listening):
79
86
  You have an internal task queue. Users can keep sending messages while you work - you're interruptible.
80
87
  - Use lia_plan to break down complex goals into steps
@@ -150,6 +157,23 @@ CALENDAR + TODO (Lia-managed data):
150
157
  - calendar.json: { "version": 1, "calendars": [...], "events": [...] }
151
158
  - todos.json: { "version": 1, "items": [...] }
152
159
 
160
+ COMPLEX TODO/CALENDAR OPERATIONS:
161
+ When users ask to "break down", "split", or "parse" a todo item:
162
+ 1. READ THE NOTES FIELD - the TODO SNAPSHOT includes notes with full content
163
+ 2. Parse the notes into distinct actionable items (grants, meetings, tasks, etc.)
164
+ 3. Call todo_upsert_items with MULTIPLE new items extracted from the notes
165
+ 4. Mark the original item as done: true OR delete by not including it in upsert
166
+ 5. Each new item should have: id (unique), title, priority, notes (if details needed), due (if deadline mentioned)
167
+
168
+ Example: If a todo has notes listing "NIH R01 deadline Jan 25, AASLD pilot Feb, ADA Mar":
169
+ → Create 3 separate todos: one for NIH R01 (due: 2026-01-25), one for AASLD, one for ADA
170
+ → Extract deadlines, descriptions, requirements from the original notes
171
+
172
+ When users reference a todo by number (e.g., "#3", "item 3", "the third one"):
173
+ - Match it to the items in the TODO SNAPSHOT (which is ordered)
174
+ - READ its notes field carefully - that's where the real content lives
175
+ - Don't guess what it contains - the snapshot shows you exactly what's there
176
+
153
177
  WORKFLOW:
154
178
  Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
155
179
  After completing work: Update LANDMARKS.md → Remember learnings
@@ -192,9 +216,12 @@ export class MasterOrchestrator {
192
216
  taskQueue;
193
217
  taskUpdateCallback;
194
218
  planUpdateCallback;
219
+ // Lia's visible todo list (displayed inline in chat)
220
+ currentTodos = [];
221
+ todoUpdateCallback;
195
222
  constructor(options) {
196
223
  this.client = new Anthropic({ apiKey: options.apiKey });
197
- this.model = options.model || 'claude-sonnet-4-20250514';
224
+ this.model = options.model || process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
198
225
  this.maxTokens = options.maxTokens || 4096;
199
226
  this.userId = options.userId;
200
227
  this.orgId = options.orgId;
@@ -349,6 +376,12 @@ export class MasterOrchestrator {
349
376
  setPlanUpdateCallback(callback) {
350
377
  this.planUpdateCallback = callback || undefined;
351
378
  }
379
+ /**
380
+ * Set callback for visible todo list updates (displayed inline in chat)
381
+ */
382
+ setTodoUpdateCallback(callback) {
383
+ this.todoUpdateCallback = callback || undefined;
384
+ }
352
385
  /**
353
386
  * Queue a message for async processing (non-blocking)
354
387
  * Returns immediately with task ID - use callbacks or getTaskQueueStatus for results
@@ -361,7 +394,6 @@ export class MasterOrchestrator {
361
394
  metadata: {
362
395
  userId: this.userId,
363
396
  messageId: options?.messageId,
364
- apiKey: options?.apiKey,
365
397
  context: this.instanceMode,
366
398
  orgId: this.orgId,
367
399
  },
@@ -373,8 +405,8 @@ export class MasterOrchestrator {
373
405
  async processQueuedTask(task) {
374
406
  // This is where the actual work happens
375
407
  return this.processMessage(task.content, [], // Fresh conversation for each task
376
- undefined, // No sendMessage callback for queued tasks
377
- task.metadata?.apiKey);
408
+ undefined // No sendMessage callback for queued tasks
409
+ );
378
410
  }
379
411
  /**
380
412
  * Get task queue status (for UI)
@@ -517,6 +549,22 @@ export class MasterOrchestrator {
517
549
  if (hasRunning || workers.length > 0) {
518
550
  this.workerStatusCallback(workers);
519
551
  }
552
+ // Update linked todos with worker progress
553
+ if (this.currentTodos.length > 0 && this.todoUpdateCallback) {
554
+ let todoUpdated = false;
555
+ for (const todo of this.currentTodos) {
556
+ if (todo.workerId && todo.status === 'in_progress') {
557
+ const worker = workers.find(w => w.id === todo.workerId);
558
+ if (worker && todo.progress !== worker.progress) {
559
+ todo.progress = worker.progress;
560
+ todoUpdated = true;
561
+ }
562
+ }
563
+ }
564
+ if (todoUpdated) {
565
+ this.todoUpdateCallback(this.currentTodos);
566
+ }
567
+ }
520
568
  // Clean up completed jobs after 3 seconds
521
569
  const now = Date.now();
522
570
  for (const [id, job] of this.jobs) {
@@ -636,19 +684,19 @@ export class MasterOrchestrator {
636
684
  }
637
685
  /**
638
686
  * Process a user message
639
- * @param apiKey Optional per-request API key (overrides default)
640
687
  */
641
- async processMessage(message, conversationHistory = [], sendMessage, apiKey) {
688
+ async processMessage(message, conversationHistory = [], sendMessage) {
642
689
  // Handle slash commands first
643
690
  const slashResponse = this.handleSlashCommand(message);
644
691
  if (slashResponse) {
645
692
  return slashResponse;
646
693
  }
647
- // Deterministic calendar/todo writes (do not rely on the model calling tools)
648
- const quickWrite = this.tryHandleCalendarTodo(message);
649
- if (quickWrite) {
650
- return quickWrite;
651
- }
694
+ // DISABLED: Dumb regex extraction was creating garbage todos
695
+ // Let the AI use calendar_upsert_events and todo_upsert_items tools properly
696
+ // const quickWrite = this.tryHandleCalendarTodo(message);
697
+ // if (quickWrite) {
698
+ // return quickWrite;
699
+ // }
652
700
  // Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
653
701
  const hubContext = loadHubContext(message);
654
702
  const hubContextStr = formatHubContext(hubContext);
@@ -686,7 +734,7 @@ ${hubContextStr}
686
734
  { role: 'user', content: message }
687
735
  ];
688
736
  try {
689
- const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
737
+ const response = await this.runAgentLoop(messages, systemWithContext, sendMessage);
690
738
  // Auto-remember important things from the conversation
691
739
  await this.autoRemember(message, response);
692
740
  void this.autoCaptureContext(message);
@@ -1087,22 +1135,19 @@ ${hubContextStr}
1087
1135
  }
1088
1136
  /**
1089
1137
  * Run the agentic loop
1090
- * @param apiKey Optional per-request API key (creates temporary client)
1091
1138
  */
1092
- async runAgentLoop(messages, system, sendMessage, apiKey) {
1139
+ async runAgentLoop(messages, system, sendMessage) {
1093
1140
  const tools = this.getToolDefinitions();
1094
1141
  let currentMessages = [...messages];
1095
1142
  let iterations = 0;
1096
1143
  const maxIterations = 30; // Increased for complex multi-tool tasks
1097
1144
  const requestTimeoutMs = 60000;
1098
- // Use per-request client if apiKey provided, otherwise use default
1099
- const client = apiKey ? new Anthropic({ apiKey }) : this.client;
1100
1145
  while (iterations < maxIterations) {
1101
1146
  iterations++;
1102
1147
  const requestStart = Date.now();
1103
1148
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1104
1149
  const response = await Promise.race([
1105
- client.messages.create({
1150
+ this.client.messages.create({
1106
1151
  model: this.model,
1107
1152
  max_tokens: this.maxTokens,
1108
1153
  system,
@@ -1205,7 +1250,18 @@ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the director
1205
1250
  name: 'todo_upsert_items',
1206
1251
  description: `Create or update todo items for /todo.
1207
1252
 
1208
- Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
1253
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).
1254
+
1255
+ UPSERT BEHAVIOR:
1256
+ - Items with matching 'id' are REPLACED (use this to update or mark done)
1257
+ - Items with new 'id' are ADDED
1258
+ - To "delete" an item, set done: true or exclude it when replacing
1259
+
1260
+ BREAKING DOWN A TODO:
1261
+ When asked to split/parse a todo into multiple items:
1262
+ 1. Include the original item with done: true (marks it complete)
1263
+ 2. Add the new parsed items with unique ids
1264
+ 3. Extract deadlines, priorities, and notes from the original content`,
1209
1265
  input_schema: {
1210
1266
  type: 'object',
1211
1267
  properties: {
@@ -1676,6 +1732,37 @@ Be specific about what you want done.`,
1676
1732
  properties: {},
1677
1733
  required: []
1678
1734
  }
1735
+ },
1736
+ {
1737
+ name: 'lia_todo_write',
1738
+ description: `Update your visible todo list displayed inline in the chat. Use this to show the user what you're working on.
1739
+
1740
+ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears in the chat conversation.
1741
+ - Use for multi-step tasks to show real-time progress
1742
+ - Update status as you complete each item
1743
+ - Only have ONE item as in_progress at a time
1744
+ - Link workerId to show actual worker progress percentage`,
1745
+ input_schema: {
1746
+ type: 'object',
1747
+ properties: {
1748
+ todos: {
1749
+ type: 'array',
1750
+ items: {
1751
+ type: 'object',
1752
+ properties: {
1753
+ id: { type: 'string', description: 'Unique identifier for the todo (optional, auto-generated if not provided)' },
1754
+ content: { type: 'string', description: 'Imperative form: "Run tests", "Build component"' },
1755
+ activeForm: { type: 'string', description: 'Present continuous: "Running tests", "Building component"' },
1756
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
1757
+ workerId: { type: 'string', description: 'Optional: link to a spawned worker ID for real progress tracking' }
1758
+ },
1759
+ required: ['content', 'activeForm', 'status']
1760
+ },
1761
+ description: 'The full todo list (replaces existing list)'
1762
+ }
1763
+ },
1764
+ required: ['todos']
1765
+ }
1679
1766
  }
1680
1767
  ];
1681
1768
  }
@@ -1829,6 +1916,9 @@ Be specific about what you want done.`,
1829
1916
  case 'lia_get_queue':
1830
1917
  result = this.executeLiaGetQueue();
1831
1918
  break;
1919
+ case 'lia_todo_write':
1920
+ result = this.executeLiaTodoWrite(input.todos);
1921
+ break;
1832
1922
  default:
1833
1923
  result = { success: false, output: `Unknown tool: ${toolUse.name}` };
1834
1924
  }
@@ -2392,6 +2482,42 @@ Be specific about what you want done.`,
2392
2482
  return { success: false, output: `Failed to get queue: ${error}` };
2393
2483
  }
2394
2484
  }
2485
+ /**
2486
+ * Update visible todo list (displayed inline in chat)
2487
+ */
2488
+ executeLiaTodoWrite(todos) {
2489
+ try {
2490
+ // Assign IDs if missing
2491
+ this.currentTodos = todos.map((todo, i) => ({
2492
+ ...todo,
2493
+ id: todo.id || `todo_${Date.now()}_${i}`,
2494
+ }));
2495
+ // Update progress from linked workers
2496
+ const allWorkers = this.getWorkerStatus();
2497
+ for (const todo of this.currentTodos) {
2498
+ if (todo.workerId && todo.status === 'in_progress') {
2499
+ const workerStatus = allWorkers.find(w => w.id === todo.workerId);
2500
+ if (workerStatus) {
2501
+ todo.progress = workerStatus.progress;
2502
+ }
2503
+ }
2504
+ }
2505
+ // Broadcast to UI
2506
+ this.todoUpdateCallback?.(this.currentTodos);
2507
+ const counts = {
2508
+ pending: this.currentTodos.filter(t => t.status === 'pending').length,
2509
+ inProgress: this.currentTodos.filter(t => t.status === 'in_progress').length,
2510
+ completed: this.currentTodos.filter(t => t.status === 'completed').length,
2511
+ };
2512
+ return {
2513
+ success: true,
2514
+ output: `Todo list updated: ${counts.completed}/${this.currentTodos.length} completed, ${counts.inProgress} in progress`
2515
+ };
2516
+ }
2517
+ catch (error) {
2518
+ return { success: false, output: `Failed to update todo list: ${error}` };
2519
+ }
2520
+ }
2395
2521
  /**
2396
2522
  * Format tool preview for user
2397
2523
  */
@@ -2447,6 +2573,8 @@ Be specific about what you want done.`,
2447
2573
  return `Queuing task: ${input.task.slice(0, 50)}...`;
2448
2574
  case 'lia_get_queue':
2449
2575
  return 'Checking my task queue...';
2576
+ case 'lia_todo_write':
2577
+ return null; // Don't show preview - the todo list itself is the UI
2450
2578
  default:
2451
2579
  return null;
2452
2580
  }
@@ -6,7 +6,7 @@
6
6
  * - Streams responses as they're generated
7
7
  * - Supports interruption and progress updates
8
8
  */
9
- import type { WorkerStatus } from './orchestrator.js';
9
+ import type { WorkerStatus, LiaTodoItem } from './orchestrator.js';
10
10
  import { AssetResponse } from './core/asset-api.js';
11
11
  export type MessageHandler = (message: WebSocketMessage) => Promise<void>;
12
12
  export type StreamHandler = (chunk: string) => void;
@@ -15,7 +15,6 @@ export interface WebSocketMessage {
15
15
  id?: string;
16
16
  content?: string;
17
17
  timestamp?: number;
18
- apiKey?: string;
19
18
  requestId?: string;
20
19
  assetId?: string;
21
20
  groupId?: string;
@@ -67,6 +66,10 @@ export declare class AgentWebSocket {
67
66
  * Send workers status update for progress bars
68
67
  */
69
68
  sendWorkersUpdate(workers: WorkerStatus[]): void;
69
+ /**
70
+ * Send todo list update for inline display in chat
71
+ */
72
+ sendTodoUpdate(todos: LiaTodoItem[]): void;
70
73
  /**
71
74
  * Send gallery command (AI controls the gallery)
72
75
  */
package/dist/websocket.js CHANGED
@@ -169,6 +169,16 @@ export class AgentWebSocket {
169
169
  workers
170
170
  });
171
171
  }
172
+ /**
173
+ * Send todo list update for inline display in chat
174
+ */
175
+ sendTodoUpdate(todos) {
176
+ this.sendToServer({
177
+ type: 'lia_todo_update',
178
+ todos,
179
+ timestamp: Date.now()
180
+ });
181
+ }
172
182
  /**
173
183
  * Send gallery command (AI controls the gallery)
174
184
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",