@siftd/connect-agent 0.2.36 → 0.2.38

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/dist/agent.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { spawn, execSync } from 'child_process';
2
+ import { createWriteStream } from 'fs';
3
+ import { mkdir, stat } from 'fs/promises';
4
+ import { Readable } from 'stream';
5
+ import { pipeline } from 'stream/promises';
6
+ import { basename, join, parse } from 'path';
2
7
  import { pollMessages, sendResponse } from './api.js';
3
- import { getUserId, getAnthropicApiKey, isCloudMode, getDeploymentInfo } from './config.js';
8
+ import { getUserId, getOrgId, getAnthropicApiKey, isCloudMode, getDeploymentInfo, getInstanceMode, getUserOrgIds } from './config.js';
4
9
  import { MasterOrchestrator } from './orchestrator.js';
5
10
  import { AgentWebSocket } from './websocket.js';
6
11
  import { startHeartbeat, stopHeartbeat, getHeartbeatState } from './heartbeat.js';
@@ -11,6 +16,97 @@ import { startPreviewWorker, stopPreviewWorker } from './core/preview-worker.js'
11
16
  function stripAnsi(str) {
12
17
  return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
13
18
  }
19
+ function parseAttachments(content) {
20
+ const marker = '📎 Attached files:';
21
+ const index = content.indexOf(marker);
22
+ if (index === -1)
23
+ return [];
24
+ const lines = content
25
+ .slice(index + marker.length)
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean);
29
+ const attachments = [];
30
+ for (const line of lines) {
31
+ const match = line.match(/^(.+?):\s*(https?:\/\/\S+)$/);
32
+ if (!match)
33
+ continue;
34
+ attachments.push({ name: match[1].trim(), url: match[2].trim() });
35
+ }
36
+ return attachments;
37
+ }
38
+ function stripAttachmentBlock(content) {
39
+ const marker = '📎 Attached files:';
40
+ const index = content.indexOf(marker);
41
+ if (index === -1)
42
+ return content;
43
+ return content.slice(0, index).trim();
44
+ }
45
+ function isSaveIntent(text) {
46
+ const lower = text.toLowerCase();
47
+ if (!lower)
48
+ return true;
49
+ const analysis = /(analy|summarize|review|explain|inspect|diff|lint|test|build|run)/;
50
+ if (analysis.test(lower))
51
+ return false;
52
+ return /(save|store|archive|upload|attach|add|file|keep)/.test(lower);
53
+ }
54
+ async function fileExists(path) {
55
+ try {
56
+ await stat(path);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ async function getUniquePath(dir, filename) {
64
+ const safeName = basename(filename).replace(/[\\/:*?"<>|]/g, '_');
65
+ const parsed = parse(safeName);
66
+ let candidate = join(dir, safeName);
67
+ let counter = 1;
68
+ while (await fileExists(candidate)) {
69
+ candidate = join(dir, `${parsed.name}-${counter}${parsed.ext}`);
70
+ counter += 1;
71
+ }
72
+ return candidate;
73
+ }
74
+ function humanizePath(path) {
75
+ const home = process.env.HOME;
76
+ if (home && path.startsWith(home)) {
77
+ return `~${path.slice(home.length)}`;
78
+ }
79
+ return path;
80
+ }
81
+ async function saveAttachments(attachments, onProgress) {
82
+ const home = process.env.HOME || '/tmp';
83
+ const targetDir = join(home, 'Lia-Hub', 'shared', 'outputs', 'safe-files');
84
+ await mkdir(targetDir, { recursive: true });
85
+ const results = [];
86
+ for (const attachment of attachments) {
87
+ const targetPath = await getUniquePath(targetDir, attachment.name);
88
+ onProgress?.(`Saving ${attachment.name}...`);
89
+ try {
90
+ const response = await fetch(attachment.url);
91
+ if (!response.ok) {
92
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
93
+ }
94
+ if (!response.body) {
95
+ throw new Error('Empty response body');
96
+ }
97
+ const stream = Readable.fromWeb(response.body);
98
+ await pipeline(stream, createWriteStream(targetPath));
99
+ results.push({ name: attachment.name, path: targetPath });
100
+ onProgress?.(`Saved ${attachment.name} → ${humanizePath(targetPath)}`);
101
+ }
102
+ catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ results.push({ name: attachment.name, path: targetPath, error: message });
105
+ onProgress?.(`Failed ${attachment.name}: ${message}`);
106
+ }
107
+ }
108
+ return results;
109
+ }
14
110
  /**
15
111
  * Self-update: npm install latest and auto-restart
16
112
  * Called from webapp banner or update command
@@ -62,6 +158,9 @@ function initOrchestrator() {
62
158
  // Check env first, then stored config
63
159
  const apiKey = process.env.ANTHROPIC_API_KEY || getAnthropicApiKey();
64
160
  const userId = getUserId();
161
+ const orgId = getOrgId();
162
+ const instanceMode = getInstanceMode();
163
+ const userOrgIds = getUserOrgIds();
65
164
  if (!apiKey) {
66
165
  console.log('[AGENT] No API key - using simple relay mode');
67
166
  console.log('[AGENT] To enable orchestrator: pair with --api-key flag');
@@ -71,12 +170,21 @@ function initOrchestrator() {
71
170
  console.log('[AGENT] No userId configured - using simple relay mode');
72
171
  return null;
73
172
  }
74
- console.log('[AGENT] Initializing Master Orchestrator...');
173
+ console.log(`[AGENT] Initializing Master Orchestrator (mode: ${instanceMode})...`);
174
+ if (instanceMode === 'personal' && userOrgIds.length > 0) {
175
+ console.log(`[AGENT] Personal mode with access to ${userOrgIds.length} team(s): ${userOrgIds.join(', ')}`);
176
+ }
177
+ if (instanceMode === 'team' && orgId) {
178
+ console.log(`[AGENT] Team mode for org: ${orgId}`);
179
+ }
75
180
  return new MasterOrchestrator({
76
181
  apiKey,
77
182
  userId,
183
+ orgId: orgId || undefined,
78
184
  workspaceDir: process.env.HOME || '/tmp',
79
- voyageApiKey: process.env.VOYAGE_API_KEY
185
+ voyageApiKey: process.env.VOYAGE_API_KEY,
186
+ instanceMode,
187
+ userOrgIds: instanceMode === 'personal' ? userOrgIds : undefined,
80
188
  });
81
189
  }
82
190
  /**
@@ -137,6 +245,17 @@ async function sendToOrchestrator(input, orch, messageId, apiKey) {
137
245
  if (wsClient?.connected()) {
138
246
  wsClient.sendTyping(true);
139
247
  }
248
+ const workerLogHandler = (workerId, line, stream) => {
249
+ if (!wsClient?.connected() || !messageId)
250
+ return;
251
+ const prefix = stream === 'stderr' ? `${workerId} stderr` : workerId;
252
+ const chunks = line.split(/\r?\n/).map(part => part.trim()).filter(Boolean);
253
+ for (const chunk of chunks) {
254
+ const trimmed = chunk.slice(0, 400);
255
+ wsClient.sendProgress(messageId, `[${prefix}] ${trimmed}`);
256
+ }
257
+ };
258
+ orch.setWorkerLogCallback(workerLogHandler);
140
259
  try {
141
260
  const response = await orch.processMessage(input, conversationHistory, async (msg) => {
142
261
  // Progress callback - log to console and send via WebSocket
@@ -172,6 +291,9 @@ async function sendToOrchestrator(input, orch, messageId, apiKey) {
172
291
  }
173
292
  return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
174
293
  }
294
+ finally {
295
+ orch.setWorkerLogCallback(null);
296
+ }
175
297
  }
176
298
  export async function processMessage(message) {
177
299
  console.log(`\n[WEB] >>> ${message.content}`);
@@ -200,6 +322,42 @@ export async function processMessage(message) {
200
322
  console.log('[AGENT] === SYSTEM UPDATE COMMAND ===');
201
323
  return await performSelfUpdate();
202
324
  }
325
+ const attachments = parseAttachments(message.content);
326
+ if (attachments.length > 0) {
327
+ const progress = (msgText) => {
328
+ if (wsClient?.connected() && message.id) {
329
+ wsClient.sendProgress(message.id, msgText);
330
+ }
331
+ };
332
+ const results = await saveAttachments(attachments, progress);
333
+ const saved = results.filter((entry) => !entry.error);
334
+ const failed = results.filter((entry) => entry.error);
335
+ const savedLines = saved.map((entry) => `- ${humanizePath(entry.path)}`).join('\n');
336
+ const failedLines = failed
337
+ .map((entry) => `- ${entry.name}: ${entry.error}`)
338
+ .join('\n');
339
+ const baseText = stripAttachmentBlock(message.content);
340
+ if (isSaveIntent(baseText)) {
341
+ const responseLines = [
342
+ saved.length > 0 ? `Saved ${saved.length} file${saved.length === 1 ? '' : 's'}:` : 'No files saved.',
343
+ savedLines,
344
+ failed.length > 0 ? `\nFailed ${failed.length} file${failed.length === 1 ? '' : 's'}:\n${failedLines}` : ''
345
+ ].filter(Boolean);
346
+ const response = responseLines.join('\n');
347
+ conversationHistory.push({ role: 'user', content: message.content });
348
+ conversationHistory.push({ role: 'assistant', content: response });
349
+ return response;
350
+ }
351
+ const savedNote = [
352
+ 'Saved attachments locally:',
353
+ savedLines || '- (none)',
354
+ failed.length > 0 ? `Failed downloads:\n${failedLines}` : ''
355
+ ].filter(Boolean).join('\n');
356
+ message = {
357
+ ...message,
358
+ content: `${message.content}\n\n${savedNote}`
359
+ };
360
+ }
203
361
  try {
204
362
  if (orchestrator) {
205
363
  return await sendToOrchestrator(message.content, orchestrator, message.id, message.apiKey);
@@ -339,19 +497,21 @@ export async function runAgent(pollInterval = 2000) {
339
497
  });
340
498
  // Keep process alive - WebSocket handles messages
341
499
  console.log('[AGENT] Listening for WebSocket messages...\n');
342
- // Always poll for messages (socket-store.ts stores messages that WebSocket doesn't see)
500
+ // Poll only if WebSocket is not connected (WS input is preferred for latency).
343
501
  while (true) {
344
502
  await new Promise(resolve => setTimeout(resolve, pollInterval));
345
- try {
346
- const { messages } = await pollMessages();
347
- for (const msg of messages) {
348
- const response = await processMessage(msg);
349
- await sendResponse(msg.id, response);
350
- console.log(`[AGENT] Response sent (${response.length} chars)`);
503
+ if (!wsClient?.connected()) {
504
+ try {
505
+ const { messages } = await pollMessages();
506
+ for (const msg of messages) {
507
+ const response = await processMessage(msg);
508
+ await sendResponse(msg.id, response);
509
+ console.log(`[AGENT] Response sent (${response.length} chars)`);
510
+ }
511
+ }
512
+ catch (error) {
513
+ console.error('[AGENT] Poll error:', error.message);
351
514
  }
352
- }
353
- catch (error) {
354
- console.error('[AGENT] Poll error:', error.message);
355
515
  }
356
516
  }
357
517
  }
package/dist/api.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface ConnectResponse {
8
8
  success: boolean;
9
9
  agentToken?: string;
10
10
  userId?: string;
11
+ orgId?: string;
12
+ orgRole?: 'owner' | 'member';
11
13
  error?: string;
12
14
  }
13
15
  export interface MessagesResponse {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander';
3
3
  import ora from 'ora';
4
- import { getServerUrl, setServerUrl, setAgentToken, setUserId, setAnthropicApiKey, getAnthropicApiKey, isConfigured, clearConfig, getConfigPath, } from './config.js';
4
+ import { getServerUrl, setServerUrl, setAgentToken, setUserId, setOrgId, setAnthropicApiKey, getAnthropicApiKey, isConfigured, clearConfig, getConfigPath, } from './config.js';
5
5
  import { connectWithPairingCode, checkConnection } from './api.js';
6
6
  import { runAgent } from './agent.js';
7
7
  import { ensureLiaHub, getLiaHubPaths } from './core/hub.js';
@@ -29,6 +29,9 @@ program
29
29
  }
30
30
  setAgentToken(result.agentToken);
31
31
  setUserId(result.userId);
32
+ if (result.orgId) {
33
+ setOrgId(result.orgId);
34
+ }
32
35
  // Store API key if provided
33
36
  if (options.apiKey) {
34
37
  setAnthropicApiKey(options.apiKey);
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type InstanceMode = 'personal' | 'team';
1
2
  /**
2
3
  * Check if running in cloud mode (environment variables take precedence)
3
4
  */
@@ -8,6 +9,12 @@ export declare function getAgentToken(): string | null;
8
9
  export declare function setAgentToken(token: string | null): void;
9
10
  export declare function getUserId(): string | null;
10
11
  export declare function setUserId(id: string | null): void;
12
+ export declare function getOrgId(): string | null;
13
+ export declare function setOrgId(id: string | null): void;
14
+ export declare function getInstanceMode(): InstanceMode;
15
+ export declare function setInstanceMode(mode: InstanceMode | null): void;
16
+ export declare function getUserOrgIds(): string[];
17
+ export declare function setUserOrgIds(ids: string[] | null): void;
11
18
  export declare function getAnthropicApiKey(): string | null;
12
19
  export declare function setAnthropicApiKey(key: string | null): void;
13
20
  export declare function isConfigured(): boolean;
package/dist/config.js CHANGED
@@ -42,6 +42,46 @@ export function setUserId(id) {
42
42
  config.set('userId', id);
43
43
  }
44
44
  }
45
+ export function getOrgId() {
46
+ return process.env.CONNECT_ORG_ID || config.get('orgId') || null;
47
+ }
48
+ export function setOrgId(id) {
49
+ if (id === null) {
50
+ config.delete('orgId');
51
+ }
52
+ else {
53
+ config.set('orgId', id);
54
+ }
55
+ }
56
+ export function getInstanceMode() {
57
+ const mode = process.env.CONNECT_INSTANCE_MODE || config.get('instanceMode');
58
+ return mode === 'team' ? 'team' : 'personal';
59
+ }
60
+ export function setInstanceMode(mode) {
61
+ if (mode === null) {
62
+ config.delete('instanceMode');
63
+ }
64
+ else {
65
+ config.set('instanceMode', mode);
66
+ }
67
+ }
68
+ export function getUserOrgIds() {
69
+ // From env: comma-separated list
70
+ const envOrgIds = process.env.CONNECT_USER_ORG_IDS;
71
+ if (envOrgIds) {
72
+ return envOrgIds.split(',').map(id => id.trim()).filter(Boolean);
73
+ }
74
+ // From config
75
+ return config.get('userOrgIds') || [];
76
+ }
77
+ export function setUserOrgIds(ids) {
78
+ if (ids === null || ids.length === 0) {
79
+ config.delete('userOrgIds');
80
+ }
81
+ else {
82
+ config.set('userOrgIds', ids);
83
+ }
84
+ }
45
85
  export function getAnthropicApiKey() {
46
86
  // Environment variable takes precedence (cloud mode)
47
87
  // Note: ANTHROPIC_API_KEY is also checked in orchestrator.ts
@@ -61,6 +101,7 @@ export function isConfigured() {
61
101
  export function clearConfig() {
62
102
  config.delete('agentToken');
63
103
  config.delete('userId');
104
+ config.delete('orgId');
64
105
  config.delete('anthropicApiKey');
65
106
  }
66
107
  export function getConfigPath() {
@@ -7,6 +7,11 @@
7
7
  import { createHash } from 'crypto';
8
8
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, realpathSync } from 'fs';
9
9
  import { basename, extname, join, relative, sep } from 'path';
10
+ const ASSET_MANIFEST_SUFFIX = '.asset.json';
11
+ const ASSET_PREVIEW_SUFFIX = '.preview.json';
12
+ function isAssetMetadataFile(filePath) {
13
+ return filePath.includes(ASSET_MANIFEST_SUFFIX) || filePath.includes(ASSET_PREVIEW_SUFFIX);
14
+ }
10
15
  /**
11
16
  * Convert internal manifest to client-safe version (strips path)
12
17
  */
@@ -123,6 +128,8 @@ export function createAssetManifest(filePath, options = {}) {
123
128
  * Write asset manifest alongside the file
124
129
  */
125
130
  export function writeAssetManifest(manifest) {
131
+ if (isAssetMetadataFile(manifest.path))
132
+ return;
126
133
  const manifestPath = manifest.path + '.asset.json';
127
134
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
128
135
  }
@@ -130,7 +137,11 @@ export function writeAssetManifest(manifest) {
130
137
  * Read asset manifest for a file
131
138
  */
132
139
  export function readAssetManifest(filePath) {
133
- const manifestPath = filePath + '.asset.json';
140
+ if (filePath.includes(ASSET_PREVIEW_SUFFIX))
141
+ return null;
142
+ const manifestPath = filePath.includes(ASSET_MANIFEST_SUFFIX)
143
+ ? filePath
144
+ : filePath + '.asset.json';
134
145
  if (!existsSync(manifestPath))
135
146
  return null;
136
147
  try {
@@ -0,0 +1,94 @@
1
+ type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'working';
2
+ type MemoryRecord = {
3
+ id: string;
4
+ content: string;
5
+ timestamp: string;
6
+ source: string;
7
+ tags: string[];
8
+ };
9
+ type MemoryStore = {
10
+ remember: (content: string, options?: {
11
+ type?: MemoryType;
12
+ source?: string;
13
+ importance?: number;
14
+ tags?: string[];
15
+ }) => Promise<string>;
16
+ search: (query: string, options?: {
17
+ limit?: number;
18
+ type?: MemoryType;
19
+ minImportance?: number;
20
+ }) => Promise<MemoryRecord[]>;
21
+ findByTags?: (tags: string[], options?: {
22
+ limit?: number;
23
+ type?: MemoryType;
24
+ minImportance?: number;
25
+ }) => Promise<MemoryRecord[]>;
26
+ };
27
+ export type ContextEntityKind = 'person' | 'team' | 'project' | 'org' | 'role' | 'tool' | 'document' | 'task' | 'system' | 'topic';
28
+ export type ContextRelationKind = 'member_of' | 'leads' | 'works_on' | 'owns' | 'depends_on' | 'blocked_by' | 'related_to' | 'uses' | 'reports_to' | 'supports' | 'delivers_to' | 'requested_by' | 'scheduled_for';
29
+ export type ContextEntity = {
30
+ key: string;
31
+ kind: ContextEntityKind;
32
+ name: string;
33
+ description?: string;
34
+ attributes?: Record<string, string>;
35
+ source?: string;
36
+ updatedAt: string;
37
+ };
38
+ export type ContextRelation = {
39
+ from: string;
40
+ to: string;
41
+ type: ContextRelationKind;
42
+ description?: string;
43
+ source?: string;
44
+ updatedAt: string;
45
+ };
46
+ type EntityInput = {
47
+ key?: string;
48
+ kind: ContextEntityKind;
49
+ name: string;
50
+ description?: string;
51
+ attributes?: Record<string, string>;
52
+ tags?: string[];
53
+ source?: string;
54
+ importance?: number;
55
+ };
56
+ type RelationInput = {
57
+ from: string;
58
+ to: string;
59
+ type: ContextRelationKind;
60
+ description?: string;
61
+ source?: string;
62
+ fromKind?: ContextEntityKind;
63
+ toKind?: ContextEntityKind;
64
+ };
65
+ export declare class ContextGraph {
66
+ private memory;
67
+ private scope;
68
+ constructor(memory: MemoryStore, options?: {
69
+ scope?: string;
70
+ });
71
+ upsertEntity(input: EntityInput): Promise<ContextEntity>;
72
+ linkEntities(input: RelationInput): Promise<ContextRelation>;
73
+ searchEntities(query: string, options?: {
74
+ limit?: number;
75
+ kind?: ContextEntityKind;
76
+ }): Promise<ContextEntity[]>;
77
+ getRelationsForEntity(key: string, options?: {
78
+ limit?: number;
79
+ }): Promise<ContextRelation[]>;
80
+ getContextSummary(query: string, options?: {
81
+ limit?: number;
82
+ }): Promise<string | null>;
83
+ private normalizeKey;
84
+ private resolveEntity;
85
+ private inferKind;
86
+ private findEntityByKey;
87
+ private findByTags;
88
+ private pickMostRecent;
89
+ }
90
+ export declare const ContextGraphKinds: {
91
+ entities: ContextEntityKind[];
92
+ relations: ContextRelationKind[];
93
+ };
94
+ export {};