@siftd/connect-agent 0.2.37 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ const ENTITY_PREFIX = 'CTX_ENTITY ';
2
+ const RELATION_PREFIX = 'CTX_REL ';
3
+ const DEFAULT_SCOPE = 'user';
4
+ const ENTITY_KINDS = [
5
+ 'person',
6
+ 'team',
7
+ 'project',
8
+ 'org',
9
+ 'role',
10
+ 'tool',
11
+ 'document',
12
+ 'task',
13
+ 'system',
14
+ 'topic'
15
+ ];
16
+ const RELATION_KINDS = [
17
+ 'member_of',
18
+ 'leads',
19
+ 'works_on',
20
+ 'owns',
21
+ 'depends_on',
22
+ 'blocked_by',
23
+ 'related_to',
24
+ 'uses',
25
+ 'reports_to',
26
+ 'supports',
27
+ 'delivers_to',
28
+ 'requested_by',
29
+ 'scheduled_for'
30
+ ];
31
+ function slugify(value) {
32
+ return value
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, '-')
35
+ .replace(/(^-|-$)/g, '');
36
+ }
37
+ function buildEntityKey(kind, name) {
38
+ return `${kind}:${slugify(name)}`;
39
+ }
40
+ function normalizeTags(tags) {
41
+ return Array.from(new Set(tags.filter(Boolean)));
42
+ }
43
+ export class ContextGraph {
44
+ memory;
45
+ scope;
46
+ constructor(memory, options) {
47
+ this.memory = memory;
48
+ this.scope = options?.scope || DEFAULT_SCOPE;
49
+ }
50
+ async upsertEntity(input) {
51
+ const name = input.name.trim();
52
+ const kind = input.kind;
53
+ const key = input.key ? this.normalizeKey(input.key, kind, name) : buildEntityKey(kind, name);
54
+ const existing = await this.findEntityByKey(key);
55
+ if (existing) {
56
+ const parsed = parseEntity(existing.content);
57
+ if (parsed)
58
+ return parsed;
59
+ }
60
+ const payload = {
61
+ key,
62
+ kind,
63
+ name,
64
+ description: input.description?.trim() || undefined,
65
+ attributes: input.attributes,
66
+ source: input.source || 'context-graph',
67
+ updatedAt: new Date().toISOString()
68
+ };
69
+ const tags = normalizeTags([
70
+ 'ctx:entity',
71
+ `ctx:kind:${kind}`,
72
+ `ctx:key:${key}`,
73
+ `ctx:name:${slugify(name)}`,
74
+ `ctx:scope:${this.scope}`,
75
+ ...(input.tags || [])
76
+ ]);
77
+ await this.memory.remember(`${ENTITY_PREFIX}${JSON.stringify(payload)}`, {
78
+ type: 'semantic',
79
+ source: payload.source,
80
+ importance: input.importance ?? 0.7,
81
+ tags
82
+ });
83
+ return payload;
84
+ }
85
+ async linkEntities(input) {
86
+ const fromEntity = await this.resolveEntity(input.from, input.fromKind);
87
+ const toEntity = await this.resolveEntity(input.to, input.toKind);
88
+ const relationKeyTags = [
89
+ 'ctx:edge',
90
+ `ctx:rel:${input.type}`,
91
+ `ctx:from:${fromEntity.key}`,
92
+ `ctx:to:${toEntity.key}`,
93
+ `ctx:scope:${this.scope}`
94
+ ];
95
+ const existing = await this.findByTags(relationKeyTags, 1);
96
+ if (existing.length > 0) {
97
+ const parsed = parseRelation(existing[0].content);
98
+ if (parsed)
99
+ return parsed;
100
+ }
101
+ const payload = {
102
+ from: fromEntity.key,
103
+ to: toEntity.key,
104
+ type: input.type,
105
+ description: input.description?.trim() || undefined,
106
+ source: input.source || 'context-graph',
107
+ updatedAt: new Date().toISOString()
108
+ };
109
+ await this.memory.remember(`${RELATION_PREFIX}${JSON.stringify(payload)}`, {
110
+ type: 'semantic',
111
+ source: payload.source,
112
+ importance: 0.6,
113
+ tags: relationKeyTags
114
+ });
115
+ return payload;
116
+ }
117
+ async searchEntities(query, options) {
118
+ const limit = options?.limit ?? 6;
119
+ const results = await this.memory.search(query, { limit: limit * 2 });
120
+ const entities = results
121
+ .map(result => parseEntity(result.content))
122
+ .filter((entity) => !!entity)
123
+ .filter(entity => !options?.kind || entity.kind === options.kind);
124
+ const deduped = new Map();
125
+ for (const entity of entities) {
126
+ if (!deduped.has(entity.key)) {
127
+ deduped.set(entity.key, entity);
128
+ }
129
+ }
130
+ return Array.from(deduped.values()).slice(0, limit);
131
+ }
132
+ async getRelationsForEntity(key, options) {
133
+ const limit = options?.limit ?? 6;
134
+ const outgoing = await this.findByTags(['ctx:edge', `ctx:from:${key}`], limit);
135
+ const incoming = await this.findByTags(['ctx:edge', `ctx:to:${key}`], limit);
136
+ const relations = [...outgoing, ...incoming]
137
+ .map(result => parseRelation(result.content))
138
+ .filter((rel) => !!rel);
139
+ const deduped = new Map();
140
+ for (const relation of relations) {
141
+ const relationKey = `${relation.from}|${relation.type}|${relation.to}`;
142
+ if (!deduped.has(relationKey)) {
143
+ deduped.set(relationKey, relation);
144
+ }
145
+ }
146
+ return Array.from(deduped.values()).slice(0, limit);
147
+ }
148
+ async getContextSummary(query, options) {
149
+ const entities = await this.searchEntities(query, { limit: options?.limit ?? 4 });
150
+ if (entities.length === 0)
151
+ return null;
152
+ const lines = [];
153
+ for (const entity of entities) {
154
+ const relations = await this.getRelationsForEntity(entity.key, { limit: 3 });
155
+ const relSummary = relations.map(relation => {
156
+ const other = relation.from === entity.key ? relation.to : relation.from;
157
+ return `${relation.type} -> ${other}`;
158
+ });
159
+ const desc = entity.description ? ` - ${entity.description}` : '';
160
+ const relText = relSummary.length > 0 ? ` (${relSummary.join('; ')})` : '';
161
+ lines.push(`[${entity.kind}] ${entity.name}${desc}${relText}`);
162
+ }
163
+ return lines.join('\n');
164
+ }
165
+ normalizeKey(value, kind, fallbackName) {
166
+ const trimmed = value.trim();
167
+ const match = trimmed.split(':');
168
+ if (match.length >= 2 && ENTITY_KINDS.includes(match[0])) {
169
+ return `${match[0]}:${slugify(match.slice(1).join(':'))}`;
170
+ }
171
+ if (trimmed.length > 0 && trimmed.includes(':')) {
172
+ return trimmed;
173
+ }
174
+ return buildEntityKey(kind, trimmed || fallbackName);
175
+ }
176
+ async resolveEntity(value, kind) {
177
+ const name = value.trim();
178
+ const inferredKind = kind || this.inferKind(name);
179
+ const key = this.normalizeKey(name, inferredKind, name);
180
+ const existing = await this.findEntityByKey(key);
181
+ if (existing) {
182
+ const parsed = parseEntity(existing.content);
183
+ if (parsed)
184
+ return parsed;
185
+ }
186
+ return this.upsertEntity({ kind: inferredKind, name });
187
+ }
188
+ inferKind(value) {
189
+ const lower = value.toLowerCase();
190
+ if (lower.includes('team'))
191
+ return 'team';
192
+ if (lower.includes('project'))
193
+ return 'project';
194
+ if (lower.includes('org') || lower.includes('company'))
195
+ return 'org';
196
+ return 'topic';
197
+ }
198
+ async findEntityByKey(key) {
199
+ const results = await this.findByTags(['ctx:entity', `ctx:key:${key}`], 2);
200
+ if (results.length === 0)
201
+ return null;
202
+ return this.pickMostRecent(results);
203
+ }
204
+ async findByTags(tags, limit) {
205
+ if (this.memory.findByTags) {
206
+ return this.memory.findByTags(tags, { limit });
207
+ }
208
+ const query = tags.join(' ');
209
+ const results = await this.memory.search(query, { limit: limit * 2 });
210
+ return results.filter(result => tags.every(tag => result.tags.includes(tag))).slice(0, limit);
211
+ }
212
+ pickMostRecent(records) {
213
+ return records.sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
214
+ }
215
+ }
216
+ function parseEntity(content) {
217
+ if (!content.startsWith(ENTITY_PREFIX))
218
+ return null;
219
+ const raw = content.slice(ENTITY_PREFIX.length).trim();
220
+ try {
221
+ const data = JSON.parse(raw);
222
+ if (!data.key || !data.kind || !data.name)
223
+ return null;
224
+ return data;
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ }
230
+ function parseRelation(content) {
231
+ if (!content.startsWith(RELATION_PREFIX))
232
+ return null;
233
+ const raw = content.slice(RELATION_PREFIX.length).trim();
234
+ try {
235
+ const data = JSON.parse(raw);
236
+ if (!data.from || !data.to || !data.type)
237
+ return null;
238
+ return data;
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ export const ContextGraphKinds = {
245
+ entities: ENTITY_KINDS,
246
+ relations: RELATION_KINDS
247
+ };
@@ -23,7 +23,7 @@ const TEXT_EXTENSIONS = new Set([
23
23
  // Directories to skip
24
24
  const SKIP_DIRS = new Set([
25
25
  'node_modules', '.git', '.next', 'dist', 'build', '__pycache__',
26
- '.cache', 'coverage', '.nyc_output', 'vendor', 'target'
26
+ '.cache', 'coverage', '.nyc_output', 'vendor', 'target', '.lia'
27
27
  ]);
28
28
  // Max files to track (performance limit)
29
29
  const MAX_FILES = 500;
@@ -165,6 +165,8 @@ function getFileType(ext) {
165
165
  * Check if a file is worth tracking
166
166
  */
167
167
  function isInterestingFile(name, ext) {
168
+ if (name.includes('.asset.json') || name.includes('.preview.json'))
169
+ return false;
168
170
  // Skip hidden files (except some config files)
169
171
  if (name.startsWith('.') && !TEXT_EXTENSIONS.has(ext))
170
172
  return false;
package/dist/core/hub.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * Manages structured memory files in ~/.connect-hub/
5
5
  * Human-readable markdown files that the orchestrator reads/writes.
6
6
  */
7
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
7
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, chmodSync, chownSync } from 'fs';
8
+ import { execSync } from 'child_process';
8
9
  import { join } from 'path';
9
10
  import { homedir } from 'os';
10
11
  const HUB_DIR = join(homedir(), '.connect-hub');
@@ -15,6 +16,38 @@ const NOTEBOOK_A_DIR = join(LIA_HUB_DIR, 'notebook-a');
15
16
  const DRAFTS_DIR = join(NOTEBOOK_A_DIR, 'drafts');
16
17
  const SHARED_DIR = join(LIA_HUB_DIR, 'shared');
17
18
  const OUTPUTS_DIR = join(SHARED_DIR, 'outputs');
19
+ const LIA_STATE_DIR = join(OUTPUTS_DIR, '.lia');
20
+ let cachedLiaGid = null;
21
+ function getLiaGid() {
22
+ if (cachedLiaGid !== null)
23
+ return cachedLiaGid;
24
+ try {
25
+ const gidText = execSync('id -g lia', { stdio: ['ignore', 'pipe', 'ignore'] })
26
+ .toString()
27
+ .trim();
28
+ const gid = Number(gidText);
29
+ cachedLiaGid = Number.isFinite(gid) ? gid : null;
30
+ }
31
+ catch {
32
+ cachedLiaGid = null;
33
+ }
34
+ return cachedLiaGid;
35
+ }
36
+ function ensureDirPermissions(dir) {
37
+ if (process.getuid?.() !== 0)
38
+ return;
39
+ const liaGid = getLiaGid();
40
+ if (liaGid === null)
41
+ return;
42
+ try {
43
+ chownSync(dir, 0, liaGid);
44
+ chmodSync(dir, 0o775);
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ console.warn(`[HUB] Permission fix failed for ${dir}: ${message}`);
49
+ }
50
+ }
18
51
  /**
19
52
  * Ensure Lia-Hub directory structure exists
20
53
  * Called on agent start to create the orchestrator's workspace
@@ -26,6 +59,7 @@ export function ensureLiaHub() {
26
59
  DRAFTS_DIR,
27
60
  SHARED_DIR,
28
61
  OUTPUTS_DIR,
62
+ LIA_STATE_DIR,
29
63
  join(LIA_HUB_DIR, 'projects')
30
64
  ];
31
65
  for (const dir of dirs) {
@@ -33,6 +67,7 @@ export function ensureLiaHub() {
33
67
  mkdirSync(dir, { recursive: true });
34
68
  console.log(`[HUB] Created: ${dir}`);
35
69
  }
70
+ ensureDirPermissions(dir);
36
71
  }
37
72
  // Create template files if they don't exist
38
73
  const agentsFile = join(LIA_HUB_DIR, 'AGENTS.md');
@@ -53,6 +53,11 @@ export declare class AdvancedMemoryStore {
53
53
  includeAssociations?: boolean;
54
54
  }): Promise<Memory[]>;
55
55
  private textSearch;
56
+ findByTags(tags: string[], options?: {
57
+ limit?: number;
58
+ type?: MemoryType;
59
+ minImportance?: number;
60
+ }): Promise<Memory[]>;
56
61
  getById(id: string): Memory | null;
57
62
  getByType(type: MemoryType, limit?: number): Memory[];
58
63
  recall(id: string): Memory | null;
@@ -242,6 +242,33 @@ export class AdvancedMemoryStore {
242
242
  const rows = stmt.all(...params, limit);
243
243
  return rows.map(row => this.rowToMemory(row));
244
244
  }
245
+ async findByTags(tags, options = {}) {
246
+ if (tags.length === 0)
247
+ return [];
248
+ const limit = options.limit || 20;
249
+ const conditions = ['1=1'];
250
+ const params = [];
251
+ if (options.type) {
252
+ conditions.push('type = ?');
253
+ params.push(options.type);
254
+ }
255
+ if (options.minImportance) {
256
+ conditions.push('importance >= ?');
257
+ params.push(options.minImportance);
258
+ }
259
+ for (const tag of tags) {
260
+ conditions.push('tags LIKE ?');
261
+ params.push(`%"${tag}"%`);
262
+ }
263
+ const stmt = this.db.prepare(`
264
+ SELECT * FROM memories
265
+ WHERE ${conditions.join(' AND ')}
266
+ ORDER BY lastAccessed DESC
267
+ LIMIT ?
268
+ `);
269
+ const rows = stmt.all(...params, limit);
270
+ return rows.map(row => this.rowToMemory(row));
271
+ }
245
272
  getById(id) {
246
273
  const stmt = this.db.prepare('SELECT * FROM memories WHERE id = ?');
247
274
  const row = stmt.get(id);
@@ -58,10 +58,19 @@ export declare class PostgresMemoryStore {
58
58
  limit?: number;
59
59
  minImportance?: number;
60
60
  }): Promise<Memory[]>;
61
+ /**
62
+ * Find memories that contain all provided tags
63
+ */
64
+ findByTags(tags: string[], options?: {
65
+ type?: MemoryType;
66
+ limit?: number;
67
+ minImportance?: number;
68
+ }): Promise<Memory[]>;
61
69
  /**
62
70
  * Get memory by ID
63
71
  */
64
72
  get(id: string): Promise<Memory | null>;
73
+ getById(id: string): Promise<Memory | null>;
65
74
  /**
66
75
  * Delete a memory
67
76
  */
@@ -197,6 +197,51 @@ export class PostgresMemoryStore {
197
197
  hasEmbedding: true
198
198
  }));
199
199
  }
200
+ /**
201
+ * Find memories that contain all provided tags
202
+ */
203
+ async findByTags(tags, options) {
204
+ await this.initialize();
205
+ if (tags.length === 0)
206
+ return [];
207
+ const limit = options?.limit || 20;
208
+ const minImportance = options?.minImportance || 0;
209
+ const params = [
210
+ this.userId,
211
+ tags,
212
+ minImportance
213
+ ];
214
+ let sql = `
215
+ SELECT id, type, content, summary, source, timestamp, last_accessed,
216
+ importance, access_count, decay_rate, associations, tags
217
+ FROM memories
218
+ WHERE user_id = $1
219
+ AND tags @> $2::text[]
220
+ AND importance >= $3
221
+ `;
222
+ if (options?.type) {
223
+ sql += ` AND type = $${params.length + 1}`;
224
+ params.push(options.type);
225
+ }
226
+ sql += ` ORDER BY last_accessed DESC LIMIT $${params.length + 1}`;
227
+ params.push(limit);
228
+ const result = await this.pool.query(sql, params);
229
+ return result.rows.map(row => ({
230
+ id: row.id,
231
+ type: row.type,
232
+ content: row.content,
233
+ summary: row.summary,
234
+ source: row.source,
235
+ timestamp: row.timestamp.toISOString(),
236
+ lastAccessed: row.last_accessed.toISOString(),
237
+ importance: row.importance,
238
+ accessCount: row.access_count,
239
+ decayRate: row.decay_rate,
240
+ associations: row.associations || [],
241
+ tags: row.tags || [],
242
+ hasEmbedding: true
243
+ }));
244
+ }
200
245
  /**
201
246
  * Get memory by ID
202
247
  */
@@ -224,6 +269,9 @@ export class PostgresMemoryStore {
224
269
  hasEmbedding: true
225
270
  };
226
271
  }
272
+ async getById(id) {
273
+ return this.get(id);
274
+ }
227
275
  /**
228
276
  * Delete a memory
229
277
  */
@@ -23,6 +23,7 @@ import { getSharedOutputPath } from './hub.js';
23
23
  const IGNORE_PATTERNS = [
24
24
  '**/*.asset.json',
25
25
  '**/*.preview.json',
26
+ '**/.lia/**',
26
27
  '**/.thumbs/**',
27
28
  '**/thumbs/**',
28
29
  '**/*.tmp',
@@ -140,6 +141,8 @@ export class PreviewWorker {
140
141
  // Skip if file no longer exists
141
142
  if (!existsSync(filePath))
142
143
  return;
144
+ if (filePath.includes('.asset.json') || filePath.includes('.preview.json'))
145
+ return;
143
146
  const startTime = Date.now();
144
147
  // Check for existing manifest
145
148
  let manifest = readAssetManifest(filePath);
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Lia's Internal Task Queue
3
+ *
4
+ * This manages Lia's own todo list - separate from user /todos.
5
+ * Allows async message processing where users can keep sending
6
+ * while Lia works through her queue.
7
+ */
8
+ export type TaskPriority = 'urgent' | 'high' | 'normal' | 'low';
9
+ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
10
+ export interface LiaTask {
11
+ id: string;
12
+ type: 'user_message' | 'follow_up' | 'scheduled' | 'background';
13
+ content: string;
14
+ priority: TaskPriority;
15
+ status: TaskStatus;
16
+ createdAt: Date;
17
+ startedAt?: Date;
18
+ completedAt?: Date;
19
+ result?: string;
20
+ error?: string;
21
+ metadata?: {
22
+ userId?: string;
23
+ messageId?: string;
24
+ parentTaskId?: string;
25
+ apiKey?: string;
26
+ context?: 'personal' | 'team';
27
+ orgId?: string;
28
+ };
29
+ }
30
+ export interface LiaPlan {
31
+ id: string;
32
+ goal: string;
33
+ steps: LiaPlanStep[];
34
+ status: 'planning' | 'executing' | 'completed' | 'failed';
35
+ createdAt: Date;
36
+ completedAt?: Date;
37
+ }
38
+ export interface LiaPlanStep {
39
+ id: string;
40
+ description: string;
41
+ status: TaskStatus;
42
+ taskId?: string;
43
+ notes?: string;
44
+ }
45
+ export type TaskQueueCallback = (task: LiaTask) => void;
46
+ export type PlanCallback = (plan: LiaPlan) => void;
47
+ /**
48
+ * Lia's task queue - manages her internal todo list
49
+ */
50
+ export declare class LiaTaskQueue {
51
+ private tasks;
52
+ private plans;
53
+ private queue;
54
+ private currentTask;
55
+ private isProcessing;
56
+ private onTaskUpdate?;
57
+ private onPlanUpdate?;
58
+ private processTask?;
59
+ constructor(options?: {
60
+ onTaskUpdate?: TaskQueueCallback;
61
+ onPlanUpdate?: PlanCallback;
62
+ processTask?: (task: LiaTask) => Promise<string>;
63
+ });
64
+ /**
65
+ * Add a new task to the queue
66
+ */
67
+ addTask(task: Omit<LiaTask, 'id' | 'status' | 'createdAt'>): LiaTask;
68
+ /**
69
+ * Insert task ID into queue based on priority
70
+ */
71
+ private insertByPriority;
72
+ /**
73
+ * Start processing the queue
74
+ */
75
+ startProcessing(): Promise<void>;
76
+ /**
77
+ * Get the current task being processed
78
+ */
79
+ getCurrentTask(): LiaTask | null;
80
+ /**
81
+ * Get all pending tasks
82
+ */
83
+ getPendingTasks(): LiaTask[];
84
+ /**
85
+ * Get recent tasks (last N)
86
+ */
87
+ getRecentTasks(limit?: number): LiaTask[];
88
+ /**
89
+ * Get task by ID
90
+ */
91
+ getTask(taskId: string): LiaTask | null;
92
+ /**
93
+ * Cancel a pending task
94
+ */
95
+ cancelTask(taskId: string): boolean;
96
+ /**
97
+ * Create a plan (breaks down a goal into steps)
98
+ */
99
+ createPlan(goal: string, steps: string[]): LiaPlan;
100
+ /**
101
+ * Update a plan step
102
+ */
103
+ updatePlanStep(planId: string, stepId: string, update: Partial<LiaPlanStep>): boolean;
104
+ /**
105
+ * Get current plan
106
+ */
107
+ getCurrentPlan(): LiaPlan | null;
108
+ /**
109
+ * Get all plans
110
+ */
111
+ getPlans(): LiaPlan[];
112
+ /**
113
+ * Get queue status for display
114
+ */
115
+ getStatus(): {
116
+ isProcessing: boolean;
117
+ currentTask: LiaTask | null;
118
+ pendingCount: number;
119
+ queuePreview: string[];
120
+ };
121
+ /**
122
+ * Format tasks as readable todo list (for LLM context)
123
+ */
124
+ formatAsTodoList(): string;
125
+ }