@siftd/connect-agent 0.2.39 → 0.2.40

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,9 +684,8 @@ 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) {
@@ -686,7 +733,7 @@ ${hubContextStr}
686
733
  { role: 'user', content: message }
687
734
  ];
688
735
  try {
689
- const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
736
+ const response = await this.runAgentLoop(messages, systemWithContext, sendMessage);
690
737
  // Auto-remember important things from the conversation
691
738
  await this.autoRemember(message, response);
692
739
  void this.autoCaptureContext(message);
@@ -1087,22 +1134,19 @@ ${hubContextStr}
1087
1134
  }
1088
1135
  /**
1089
1136
  * Run the agentic loop
1090
- * @param apiKey Optional per-request API key (creates temporary client)
1091
1137
  */
1092
- async runAgentLoop(messages, system, sendMessage, apiKey) {
1138
+ async runAgentLoop(messages, system, sendMessage) {
1093
1139
  const tools = this.getToolDefinitions();
1094
1140
  let currentMessages = [...messages];
1095
1141
  let iterations = 0;
1096
1142
  const maxIterations = 30; // Increased for complex multi-tool tasks
1097
1143
  const requestTimeoutMs = 60000;
1098
- // Use per-request client if apiKey provided, otherwise use default
1099
- const client = apiKey ? new Anthropic({ apiKey }) : this.client;
1100
1144
  while (iterations < maxIterations) {
1101
1145
  iterations++;
1102
1146
  const requestStart = Date.now();
1103
1147
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1104
1148
  const response = await Promise.race([
1105
- client.messages.create({
1149
+ this.client.messages.create({
1106
1150
  model: this.model,
1107
1151
  max_tokens: this.maxTokens,
1108
1152
  system,
@@ -1205,7 +1249,18 @@ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the director
1205
1249
  name: 'todo_upsert_items',
1206
1250
  description: `Create or update todo items for /todo.
1207
1251
 
1208
- Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
1252
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).
1253
+
1254
+ UPSERT BEHAVIOR:
1255
+ - Items with matching 'id' are REPLACED (use this to update or mark done)
1256
+ - Items with new 'id' are ADDED
1257
+ - To "delete" an item, set done: true or exclude it when replacing
1258
+
1259
+ BREAKING DOWN A TODO:
1260
+ When asked to split/parse a todo into multiple items:
1261
+ 1. Include the original item with done: true (marks it complete)
1262
+ 2. Add the new parsed items with unique ids
1263
+ 3. Extract deadlines, priorities, and notes from the original content`,
1209
1264
  input_schema: {
1210
1265
  type: 'object',
1211
1266
  properties: {
@@ -1676,6 +1731,37 @@ Be specific about what you want done.`,
1676
1731
  properties: {},
1677
1732
  required: []
1678
1733
  }
1734
+ },
1735
+ {
1736
+ name: 'lia_todo_write',
1737
+ description: `Update your visible todo list displayed inline in the chat. Use this to show the user what you're working on.
1738
+
1739
+ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears in the chat conversation.
1740
+ - Use for multi-step tasks to show real-time progress
1741
+ - Update status as you complete each item
1742
+ - Only have ONE item as in_progress at a time
1743
+ - Link workerId to show actual worker progress percentage`,
1744
+ input_schema: {
1745
+ type: 'object',
1746
+ properties: {
1747
+ todos: {
1748
+ type: 'array',
1749
+ items: {
1750
+ type: 'object',
1751
+ properties: {
1752
+ id: { type: 'string', description: 'Unique identifier for the todo (optional, auto-generated if not provided)' },
1753
+ content: { type: 'string', description: 'Imperative form: "Run tests", "Build component"' },
1754
+ activeForm: { type: 'string', description: 'Present continuous: "Running tests", "Building component"' },
1755
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
1756
+ workerId: { type: 'string', description: 'Optional: link to a spawned worker ID for real progress tracking' }
1757
+ },
1758
+ required: ['content', 'activeForm', 'status']
1759
+ },
1760
+ description: 'The full todo list (replaces existing list)'
1761
+ }
1762
+ },
1763
+ required: ['todos']
1764
+ }
1679
1765
  }
1680
1766
  ];
1681
1767
  }
@@ -1829,6 +1915,9 @@ Be specific about what you want done.`,
1829
1915
  case 'lia_get_queue':
1830
1916
  result = this.executeLiaGetQueue();
1831
1917
  break;
1918
+ case 'lia_todo_write':
1919
+ result = this.executeLiaTodoWrite(input.todos);
1920
+ break;
1832
1921
  default:
1833
1922
  result = { success: false, output: `Unknown tool: ${toolUse.name}` };
1834
1923
  }
@@ -2392,6 +2481,42 @@ Be specific about what you want done.`,
2392
2481
  return { success: false, output: `Failed to get queue: ${error}` };
2393
2482
  }
2394
2483
  }
2484
+ /**
2485
+ * Update visible todo list (displayed inline in chat)
2486
+ */
2487
+ executeLiaTodoWrite(todos) {
2488
+ try {
2489
+ // Assign IDs if missing
2490
+ this.currentTodos = todos.map((todo, i) => ({
2491
+ ...todo,
2492
+ id: todo.id || `todo_${Date.now()}_${i}`,
2493
+ }));
2494
+ // Update progress from linked workers
2495
+ const allWorkers = this.getWorkerStatus();
2496
+ for (const todo of this.currentTodos) {
2497
+ if (todo.workerId && todo.status === 'in_progress') {
2498
+ const workerStatus = allWorkers.find(w => w.id === todo.workerId);
2499
+ if (workerStatus) {
2500
+ todo.progress = workerStatus.progress;
2501
+ }
2502
+ }
2503
+ }
2504
+ // Broadcast to UI
2505
+ this.todoUpdateCallback?.(this.currentTodos);
2506
+ const counts = {
2507
+ pending: this.currentTodos.filter(t => t.status === 'pending').length,
2508
+ inProgress: this.currentTodos.filter(t => t.status === 'in_progress').length,
2509
+ completed: this.currentTodos.filter(t => t.status === 'completed').length,
2510
+ };
2511
+ return {
2512
+ success: true,
2513
+ output: `Todo list updated: ${counts.completed}/${this.currentTodos.length} completed, ${counts.inProgress} in progress`
2514
+ };
2515
+ }
2516
+ catch (error) {
2517
+ return { success: false, output: `Failed to update todo list: ${error}` };
2518
+ }
2519
+ }
2395
2520
  /**
2396
2521
  * Format tool preview for user
2397
2522
  */
@@ -2447,6 +2572,8 @@ Be specific about what you want done.`,
2447
2572
  return `Queuing task: ${input.task.slice(0, 50)}...`;
2448
2573
  case 'lia_get_queue':
2449
2574
  return 'Checking my task queue...';
2575
+ case 'lia_todo_write':
2576
+ return null; // Don't show preview - the todo list itself is the UI
2450
2577
  default:
2451
2578
  return null;
2452
2579
  }
@@ -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.40",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",