@siftd/connect-agent 0.2.50 → 0.2.52

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.
@@ -49,6 +49,7 @@ export declare class MasterOrchestrator {
49
49
  private client;
50
50
  private model;
51
51
  private maxTokens;
52
+ private toolMaxTokens;
52
53
  private memory;
53
54
  private contextGraph;
54
55
  private orgMemory?;
@@ -202,11 +203,13 @@ export declare class MasterOrchestrator {
202
203
  private getLocalTimeZone;
203
204
  private getTodoCalSystemPrompt;
204
205
  private getToolChoice;
206
+ private recordUsage;
205
207
  private withAttachments;
206
208
  private updateFileScope;
207
209
  private getTeamFilesDir;
208
210
  private getFileScopeOverrides;
209
211
  private rewriteFilesAlias;
212
+ private resolveFilesWritePath;
210
213
  private getFileScopeSystemNote;
211
214
  /**
212
215
  * Check if verbose mode is enabled
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import Anthropic from '@anthropic-ai/sdk';
8
8
  import { spawn, execSync } from 'child_process';
9
- import { existsSync, readFileSync } from 'fs';
10
- import { join } from 'path';
9
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
10
+ import { join, resolve, dirname, sep } from 'path';
11
11
  import { AdvancedMemoryStore } from './core/memory-advanced.js';
12
12
  import { PostgresMemoryStore, isPostgresConfigured } from './core/memory-postgres.js';
13
13
  import { TaskScheduler } from './core/scheduler.js';
@@ -22,6 +22,35 @@ import { getKnowledgeForPrompt } from './genesis/index.js';
22
22
  import { loadHubContext, formatHubContext, logAction, logWorker, getSharedOutputPath } from './core/hub.js';
23
23
  import { buildWorkerPrompt } from './prompts/worker-system.js';
24
24
  import { LiaTaskQueue } from './core/task-queue.js';
25
+ const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-5-20251101';
26
+ const DEFAULT_TOOL_MAX_TOKENS = 1024;
27
+ function readBooleanEnv(value, fallback) {
28
+ if (value === undefined)
29
+ return fallback;
30
+ return !['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
31
+ }
32
+ function resolveClaudeModel() {
33
+ const raw = (process.env.LIA_MODEL || process.env.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL).trim();
34
+ if (!raw)
35
+ return DEFAULT_CLAUDE_MODEL;
36
+ if (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && raw !== DEFAULT_CLAUDE_MODEL) {
37
+ return DEFAULT_CLAUDE_MODEL;
38
+ }
39
+ return raw;
40
+ }
41
+ function resolveClaudeBudgetUsd() {
42
+ const raw = process.env.LIA_MAX_BUDGET_USD || process.env.CLAUDE_MAX_BUDGET_USD;
43
+ if (!raw)
44
+ return null;
45
+ const trimmed = raw.trim();
46
+ return trimmed ? trimmed : null;
47
+ }
48
+ function resolveToolMaxTokens() {
49
+ const raw = Number(process.env.LIA_TOOL_MAX_TOKENS);
50
+ if (Number.isFinite(raw) && raw >= 256)
51
+ return Math.min(raw, 4096);
52
+ return DEFAULT_TOOL_MAX_TOKENS;
53
+ }
25
54
  /**
26
55
  * Extract file paths from worker output
27
56
  * Workers naturally mention files they create: "Created /tmp/foo.html", "Saved to /path/file"
@@ -103,7 +132,8 @@ TOOL RULES:
103
132
  - Installing packages
104
133
  - Running builds or tests
105
134
 
106
- delegate_to_worker / spawn_worker - ALL file operations:
135
+ files_write for simple /files writes
136
+ ✅ delegate_to_worker / spawn_worker - complex file operations:
107
137
  - Creating, editing, deleting files
108
138
  - Running npm/pip/cargo install
109
139
  - Building, testing, deploying
@@ -142,6 +172,7 @@ FILES BROWSER:
142
172
  Users can type /files to open the Finder UI (cloud mode).
143
173
  When asked to browse or locate files, point them to /files.
144
174
  Refer to /files instead of internal Lia-Hub paths in responses.
175
+ When users ask you to create or update a file in /files, use files_write.
145
176
 
146
177
  FILES SCOPES:
147
178
  - "My Files" are private to the user (default /files view)
@@ -186,7 +217,7 @@ WORKFLOW:
186
217
  Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
187
218
  After completing work: Update LANDMARKS.md → Remember learnings
188
219
 
189
- You orchestrate through workers. You remember through memory. You never do arbitrary file operations directly (calendar_upsert_events and todo_upsert_items are the safe exceptions).`;
220
+ You orchestrate through workers. You remember through memory. You never do arbitrary file operations directly (calendar_upsert_events, todo_upsert_items, and files_write are the safe exceptions).`;
190
221
  const TODO_CAL_SYSTEM_PROMPT_BASE = `You are Lia. Your ONLY job is to update /todo and /cal using tools.
191
222
 
192
223
  Rules:
@@ -201,6 +232,7 @@ export class MasterOrchestrator {
201
232
  client;
202
233
  model;
203
234
  maxTokens;
235
+ toolMaxTokens;
204
236
  memory;
205
237
  contextGraph;
206
238
  orgMemory;
@@ -242,8 +274,14 @@ export class MasterOrchestrator {
242
274
  todoUpdateCallback;
243
275
  constructor(options) {
244
276
  this.client = new Anthropic({ apiKey: options.apiKey });
245
- this.model = options.model || process.env.ANTHROPIC_MODEL || 'claude-opus-4-5-20251101';
277
+ const requestedModel = options.model?.trim();
278
+ this.model = requestedModel
279
+ ? (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && requestedModel !== DEFAULT_CLAUDE_MODEL
280
+ ? DEFAULT_CLAUDE_MODEL
281
+ : requestedModel)
282
+ : resolveClaudeModel();
246
283
  this.maxTokens = options.maxTokens || 4096;
284
+ this.toolMaxTokens = resolveToolMaxTokens();
247
285
  this.userId = options.userId;
248
286
  this.orgId = options.orgId;
249
287
  this.orgRole = options.orgRole;
@@ -696,7 +734,7 @@ export class MasterOrchestrator {
696
734
  }
697
735
  hasCalendarMutation(message) {
698
736
  const lower = this.stripTodoSnapshot(message).toLowerCase();
699
- const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b/.test(lower);
737
+ const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b|\bcal\b/.test(lower);
700
738
  const action = /\b(add|create|schedule|book|move|reschedule|update|change|cancel|delete|remove)\b/.test(lower);
701
739
  const query = /\b(what|show|list|open|view|see)\b/.test(lower);
702
740
  return target && action && !query;
@@ -783,10 +821,36 @@ export class MasterOrchestrator {
783
821
  return { type: 'tool', name: 'calendar_upsert_events' };
784
822
  }
785
823
  if (wantsFiles) {
786
- return { type: 'tool', name: 'delegate_to_worker' };
824
+ return { type: 'tool', name: 'files_write' };
787
825
  }
788
826
  return undefined;
789
827
  }
828
+ recordUsage(response, meta) {
829
+ const usage = response.usage;
830
+ if (!usage)
831
+ return;
832
+ try {
833
+ const usageDir = join(getSharedOutputPath(), '.lia');
834
+ mkdirSync(usageDir, { recursive: true });
835
+ const entry = {
836
+ ts: new Date().toISOString(),
837
+ model: this.model,
838
+ input_tokens: usage.input_tokens,
839
+ output_tokens: usage.output_tokens,
840
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
841
+ cache_read_input_tokens: usage.cache_read_input_tokens,
842
+ tool_choice: meta.toolChoice || null,
843
+ iterations: meta.iterations,
844
+ messages: meta.messages,
845
+ stop_reason: response.stop_reason || null,
846
+ response_id: response.id || null,
847
+ };
848
+ appendFileSync(join(usageDir, 'usage.jsonl'), `${JSON.stringify(entry)}\n`, 'utf8');
849
+ }
850
+ catch (error) {
851
+ console.warn('[ORCHESTRATOR] Usage log failed:', error);
852
+ }
853
+ }
790
854
  withAttachments(task, context) {
791
855
  if (!this.attachmentContext)
792
856
  return { task, context };
@@ -849,6 +913,22 @@ export class MasterOrchestrator {
849
913
  return `${targetDir}${suffix}`;
850
914
  });
851
915
  }
916
+ resolveFilesWritePath(rawPath) {
917
+ const trimmed = rawPath.trim();
918
+ if (!trimmed)
919
+ throw new Error('File path is required');
920
+ const teamDir = this.currentFileScope === 'team' ? this.getTeamFilesDir() : null;
921
+ const baseDir = teamDir || getSharedOutputPath();
922
+ const normalized = this.rewriteFilesAlias(trimmed);
923
+ const relative = normalized.replace(/^\/+/, '');
924
+ const candidate = normalized.startsWith(baseDir) ? normalized : join(baseDir, relative);
925
+ const resolved = resolve(candidate);
926
+ const baseResolved = resolve(baseDir);
927
+ if (resolved !== baseResolved && !resolved.startsWith(`${baseResolved}${sep}`)) {
928
+ throw new Error('Invalid file path');
929
+ }
930
+ return resolved;
931
+ }
852
932
  getFileScopeSystemNote() {
853
933
  if (this.currentFileScope !== 'team')
854
934
  return null;
@@ -889,25 +969,6 @@ export class MasterOrchestrator {
889
969
  this.attachmentContext = null;
890
970
  }
891
971
  }
892
- const wantsFiles = this.hasFileMutation(message);
893
- if (wantsFiles) {
894
- this.attachmentContext = this.extractAttachmentContext(message);
895
- const { task } = this.withAttachments(message);
896
- const normalizedTask = this.rewriteFilesAlias(task);
897
- const fileScope = this.getFileScopeOverrides();
898
- const scopedTask = fileScope.instructions ? `${fileScope.instructions}\n\n${normalizedTask}` : normalizedTask;
899
- try {
900
- await this.delegateToWorker(scopedTask, undefined, fileScope.workingDir, fileScope.instructions);
901
- return 'Working on it. Check /files shortly.';
902
- }
903
- catch (error) {
904
- const errorMessage = error instanceof Error ? error.message : String(error);
905
- return `Error: ${errorMessage}`;
906
- }
907
- finally {
908
- this.attachmentContext = null;
909
- }
910
- }
911
972
  // DISABLED: Dumb regex extraction was creating garbage todos
912
973
  // Let the AI use calendar_upsert_events and todo_upsert_items tools properly
913
974
  // const quickWrite = this.tryHandleCalendarTodo(message);
@@ -1386,12 +1447,13 @@ ${hubContextStr}
1386
1447
  while (iterations < maxIterations) {
1387
1448
  iterations++;
1388
1449
  const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
1450
+ const requestMaxTokens = wantsTodoOrCal ? this.toolMaxTokens : this.maxTokens;
1389
1451
  const requestStart = Date.now();
1390
1452
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1391
1453
  const response = await Promise.race([
1392
1454
  this.client.messages.create({
1393
1455
  model: this.model,
1394
- max_tokens: this.maxTokens,
1456
+ max_tokens: requestMaxTokens,
1395
1457
  system,
1396
1458
  tools,
1397
1459
  messages: currentMessages,
@@ -1404,6 +1466,11 @@ ${hubContextStr}
1404
1466
  })
1405
1467
  ]);
1406
1468
  console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
1469
+ this.recordUsage(response, {
1470
+ iterations,
1471
+ messages: currentMessages.length,
1472
+ toolChoice: toolChoice?.name
1473
+ });
1407
1474
  // Check if done
1408
1475
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
1409
1476
  if (forcedToolChoice && !retriedForcedTool) {
@@ -1412,7 +1479,9 @@ ${hubContextStr}
1412
1479
  const needsNoWorkers = toolName === 'todo_upsert_items' || toolName === 'calendar_upsert_events';
1413
1480
  const followup = needsNoWorkers
1414
1481
  ? `You must call the ${toolName} tool now. Use the exact task/event titles and any bracketed tags exactly as provided. Do not spawn workers.`
1415
- : `You must call the ${toolName} tool now. Use the user's request as the task details.`;
1482
+ : toolName === 'files_write'
1483
+ ? 'You must call the files_write tool now. Provide the file path under /files and the full content.'
1484
+ : `You must call the ${toolName} tool now. Use the user's request as the task details.`;
1416
1485
  currentMessages = [
1417
1486
  ...currentMessages,
1418
1487
  { role: 'assistant', content: response.content },
@@ -1573,6 +1642,26 @@ and preserve explicit due dates and priority from the user.`,
1573
1642
  required: ['items']
1574
1643
  }
1575
1644
  },
1645
+ {
1646
+ name: 'files_write',
1647
+ description: `Create or overwrite a file in /files.
1648
+
1649
+ Only write to /files (shared outputs). Use a filename or /files/path/to/file.ext.`,
1650
+ input_schema: {
1651
+ type: 'object',
1652
+ properties: {
1653
+ path: {
1654
+ type: 'string',
1655
+ description: 'Target file path (e.g. /files/report.txt or report.txt)'
1656
+ },
1657
+ content: {
1658
+ type: 'string',
1659
+ description: 'File contents to write'
1660
+ }
1661
+ },
1662
+ required: ['path', 'content']
1663
+ }
1664
+ },
1576
1665
  // Web tools
1577
1666
  {
1578
1667
  name: 'web_search',
@@ -2115,6 +2204,22 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2115
2204
  result = this.calendarTools.upsertTodoItems(taggedItems);
2116
2205
  }
2117
2206
  break;
2207
+ case 'files_write':
2208
+ {
2209
+ try {
2210
+ const pathValue = String(input.path || '').trim();
2211
+ const content = String(input.content || '');
2212
+ const targetPath = this.resolveFilesWritePath(pathValue);
2213
+ mkdirSync(dirname(targetPath), { recursive: true });
2214
+ writeFileSync(targetPath, content, 'utf8');
2215
+ result = { success: true, output: 'Saved file to /files.' };
2216
+ }
2217
+ catch (error) {
2218
+ const message = error instanceof Error ? error.message : String(error);
2219
+ result = { success: false, output: '', error: message };
2220
+ }
2221
+ }
2222
+ break;
2118
2223
  case 'web_search':
2119
2224
  result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
2120
2225
  break;
@@ -2310,7 +2415,9 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2310
2415
  };
2311
2416
  // Escape single quotes in prompt for shell safety
2312
2417
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
2313
- const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
2418
+ const budget = resolveClaudeBudgetUsd();
2419
+ const budgetArg = budget ? ` --max-budget-usd ${budget}` : '';
2420
+ const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions --model ${this.model}${budgetArg}`;
2314
2421
  const isRoot = process.getuid?.() === 0;
2315
2422
  const shellCmd = isRoot
2316
2423
  ? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
@@ -12,7 +12,7 @@ export declare const WORKER_IDENTITY = "## Worker Identity\nYou are a Claude Cod
12
12
  /**
13
13
  * Output format workers should follow
14
14
  */
15
- export declare const WORKER_OUTPUT_FORMAT = "## Output Format\nWhen done, clearly list:\n- Files created: /path/to/file.ext\n- Files modified: /path/to/file.ext\n- Key findings: brief summary\n\nBe concise. Execute efficiently.";
15
+ export declare const WORKER_OUTPUT_FORMAT = "## Output Format\nWhen done, clearly list:\n- Files created: /path/to/file.ext\n- Files modified: /path/to/file.ext\n- Key findings: 1-3 short bullets\n\nBe concise. Do not restate the task.";
16
16
  /**
17
17
  * Instructions for workers to contribute back to memory
18
18
  * Workers can add to orchestrator's memory using these patterns
@@ -44,9 +44,9 @@ export const WORKER_OUTPUT_FORMAT = `## Output Format
44
44
  When done, clearly list:
45
45
  - Files created: /path/to/file.ext
46
46
  - Files modified: /path/to/file.ext
47
- - Key findings: brief summary
47
+ - Key findings: 1-3 short bullets
48
48
 
49
- Be concise. Execute efficiently.`;
49
+ Be concise. Do not restate the task.`;
50
50
  /**
51
51
  * Instructions for workers to contribute back to memory
52
52
  * Workers can add to orchestrator's memory using these patterns
@@ -71,6 +71,15 @@ Share data with other workers:
71
71
  */
72
72
  export function getWorkerLoggingInstructions(jobId) {
73
73
  const logFile = `/tmp/worker-${jobId}-log.txt`;
74
+ const verbose = process.env.LIA_WORKER_VERBOSE === '1';
75
+ if (!verbose) {
76
+ return `## Progress & Logging
77
+ - Keep output minimal; report only key steps and final results
78
+
79
+ REQUIRED - Log Export:
80
+ At the END of your work, create a final log file at: ${logFile}
81
+ Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.`;
82
+ }
74
83
  return `## Progress & Logging
75
84
  - Output findings as you go, don't wait until the end
76
85
  - Print discoveries and insights immediately as you find them
@@ -48,6 +48,28 @@ function getFileType(ext) {
48
48
  return 'text';
49
49
  return 'other';
50
50
  }
51
+ const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-5-20251101';
52
+ function readBooleanEnv(value, fallback) {
53
+ if (value === undefined)
54
+ return fallback;
55
+ return !['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
56
+ }
57
+ function resolveClaudeModel() {
58
+ const raw = (process.env.LIA_MODEL || process.env.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL).trim();
59
+ if (!raw)
60
+ return DEFAULT_CLAUDE_MODEL;
61
+ if (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && raw !== DEFAULT_CLAUDE_MODEL) {
62
+ return DEFAULT_CLAUDE_MODEL;
63
+ }
64
+ return raw;
65
+ }
66
+ function resolveClaudeBudgetUsd() {
67
+ const raw = process.env.LIA_MAX_BUDGET_USD || process.env.CLAUDE_MAX_BUDGET_USD;
68
+ if (!raw)
69
+ return null;
70
+ const trimmed = raw.trim();
71
+ return trimmed ? trimmed : null;
72
+ }
51
73
  export class WorkerManager {
52
74
  config;
53
75
  activeWorkers = new Map();
@@ -166,22 +188,30 @@ export class WorkerManager {
166
188
  try {
167
189
  // Add checkpoint and logging instructions to prevent data loss
168
190
  const logFile = `/tmp/worker-${jobId}-log.txt`;
169
- const enhancedTask = `${task}
170
-
171
- IMPORTANT - Progress & Logging:
191
+ const verbose = process.env.LIA_WORKER_VERBOSE === '1';
192
+ const loggingBlock = verbose
193
+ ? `IMPORTANT - Progress & Logging:
172
194
  - Output findings as you go, don't wait until the end
173
195
  - Print discoveries and insights immediately as you find them
174
196
  - Report on each file/step before moving to the next
175
- - Do NOT open browsers or start servers - just create files and report paths
197
+ - Do NOT open browsers or start servers - just create files and report paths`
198
+ : `IMPORTANT - Progress & Logging:
199
+ - Keep output minimal; report only key steps and final results
200
+ - Do NOT open browsers or start servers - just create files and report paths`;
201
+ const enhancedTask = `${task}
202
+
203
+ ${loggingBlock}
176
204
 
177
205
  REQUIRED - Log Export:
178
206
  At the END of your work, create a final log file at: ${logFile}
179
- Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.
180
- This ensures nothing is lost even if your output gets truncated.`;
207
+ Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.`;
181
208
  // Escape single quotes in task for shell safety
182
209
  const escapedTask = enhancedTask.replace(/'/g, "'\\''");
183
210
  // Build the claude command
184
- const claudeCmd = `claude -p '${escapedTask}' --output-format text --dangerously-skip-permissions`;
211
+ const model = resolveClaudeModel();
212
+ const budget = resolveClaudeBudgetUsd();
213
+ const budgetArg = budget ? ` --max-budget-usd ${budget}` : '';
214
+ const claudeCmd = `claude -p '${escapedTask}' --output-format text --dangerously-skip-permissions --model ${model}${budgetArg}`;
185
215
  // If running as root, run workers as 'lia' user to avoid --dangerously-skip-permissions being blocked
186
216
  // The Claude CLI blocks this flag for root users as a security measure
187
217
  const isRoot = process.getuid?.() === 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",