@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 +173 -13
- package/dist/api.d.ts +2 -0
- package/dist/cli.js +4 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +41 -0
- package/dist/core/assets.js +12 -1
- package/dist/core/context-graph.d.ts +94 -0
- package/dist/core/context-graph.js +247 -0
- package/dist/core/file-tracker.js +3 -1
- package/dist/core/hub.js +36 -1
- package/dist/core/memory-advanced.d.ts +5 -0
- package/dist/core/memory-advanced.js +27 -0
- package/dist/core/memory-postgres.d.ts +9 -0
- package/dist/core/memory-postgres.js +48 -0
- package/dist/core/preview-worker.js +3 -0
- package/dist/orchestrator.d.ts +46 -0
- package/dist/orchestrator.js +874 -26
- package/dist/tools/bash.js +4 -1
- package/dist/tools/calendar.d.ts +50 -0
- package/dist/tools/calendar.js +233 -0
- package/dist/tools/worker.d.ts +6 -1
- package/dist/tools/worker.js +6 -0
- package/dist/workers/manager.d.ts +7 -0
- package/dist/workers/manager.js +35 -5
- package/package.json +1 -1
package/dist/tools/bash.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
7
8
|
const execAsync = promisify(exec);
|
|
9
|
+
// Detect available shell - prefer bash for cross-platform compatibility
|
|
10
|
+
const SHELL = existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh';
|
|
8
11
|
const MAX_OUTPUT_LENGTH = 10000;
|
|
9
12
|
const DEFAULT_TIMEOUT = 30000;
|
|
10
13
|
const MAX_TIMEOUT = 120000;
|
|
@@ -28,7 +31,7 @@ export class BashTool {
|
|
|
28
31
|
cwd: this.workspaceDir,
|
|
29
32
|
timeout: effectiveTimeout,
|
|
30
33
|
maxBuffer: 1024 * 1024 * 10, // 10MB
|
|
31
|
-
shell:
|
|
34
|
+
shell: SHELL,
|
|
32
35
|
env: { ...process.env }
|
|
33
36
|
});
|
|
34
37
|
let output = '';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar + Todo Tools
|
|
3
|
+
*
|
|
4
|
+
* Safe, dedicated writers for Lia-managed JSON data used by /cal and /todo.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolResult } from './bash.js';
|
|
7
|
+
type CalendarDef = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
};
|
|
12
|
+
type CalendarEventInput = {
|
|
13
|
+
id?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
date?: string;
|
|
16
|
+
startTime?: string;
|
|
17
|
+
endTime?: string;
|
|
18
|
+
calendarId?: string;
|
|
19
|
+
notes?: string;
|
|
20
|
+
};
|
|
21
|
+
type TodoPriority = 'low' | 'medium' | 'high';
|
|
22
|
+
type TodoSubtaskInput = {
|
|
23
|
+
id?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
done?: boolean;
|
|
26
|
+
notes?: string;
|
|
27
|
+
};
|
|
28
|
+
type TodoItemInput = {
|
|
29
|
+
id?: string;
|
|
30
|
+
title: string;
|
|
31
|
+
done?: boolean;
|
|
32
|
+
due?: string;
|
|
33
|
+
priority?: TodoPriority;
|
|
34
|
+
notes?: string;
|
|
35
|
+
subtasks?: TodoSubtaskInput[];
|
|
36
|
+
};
|
|
37
|
+
export declare class CalendarTools {
|
|
38
|
+
private calendarPath;
|
|
39
|
+
private todoPath;
|
|
40
|
+
constructor();
|
|
41
|
+
upsertCalendarEvents(inputEvents: CalendarEventInput[], inputCalendars?: CalendarDef[]): ToolResult;
|
|
42
|
+
upsertTodoItems(inputItems: TodoItemInput[]): ToolResult;
|
|
43
|
+
private readCalendarPayload;
|
|
44
|
+
private normalizeCalendars;
|
|
45
|
+
private normalizeEvents;
|
|
46
|
+
private eventDedupeKey;
|
|
47
|
+
private readTodoPayload;
|
|
48
|
+
private normalizeTodoItems;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar + Todo Tools
|
|
3
|
+
*
|
|
4
|
+
* Safe, dedicated writers for Lia-managed JSON data used by /cal and /todo.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { ensureLiaHub, getLiaHubPaths } from '../core/hub.js';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
const DEFAULT_CALENDARS = [
|
|
11
|
+
{ id: 'primary', name: 'Primary', color: '#8b5cf6' },
|
|
12
|
+
{ id: 'work', name: 'Work', color: '#38bdf8' },
|
|
13
|
+
{ id: 'research', name: 'Research', color: '#22c55e' },
|
|
14
|
+
];
|
|
15
|
+
function isDateKey(value) {
|
|
16
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
17
|
+
}
|
|
18
|
+
function isTimeKey(value) {
|
|
19
|
+
return /^\d{2}:\d{2}$/.test(value);
|
|
20
|
+
}
|
|
21
|
+
function parseIsoDate(value) {
|
|
22
|
+
const parsed = new Date(value);
|
|
23
|
+
if (Number.isNaN(parsed.getTime()))
|
|
24
|
+
return null;
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
function toDateKey(date) {
|
|
28
|
+
const year = date.getFullYear();
|
|
29
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
30
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
31
|
+
return `${year}-${month}-${day}`;
|
|
32
|
+
}
|
|
33
|
+
function toTimeKey(date) {
|
|
34
|
+
const hour = `${date.getHours()}`.padStart(2, '0');
|
|
35
|
+
const minute = `${date.getMinutes()}`.padStart(2, '0');
|
|
36
|
+
return `${hour}:${minute}`;
|
|
37
|
+
}
|
|
38
|
+
function normalizeTime(value) {
|
|
39
|
+
if (!value || typeof value !== 'string')
|
|
40
|
+
return {};
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed)
|
|
43
|
+
return {};
|
|
44
|
+
if (isTimeKey(trimmed))
|
|
45
|
+
return { time: trimmed };
|
|
46
|
+
if (trimmed.includes('T')) {
|
|
47
|
+
const parsed = parseIsoDate(trimmed);
|
|
48
|
+
if (!parsed)
|
|
49
|
+
return {};
|
|
50
|
+
return { date: toDateKey(parsed), time: toTimeKey(parsed) };
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
function stableId(namespace, parts) {
|
|
55
|
+
const key = parts.filter(Boolean).join('|');
|
|
56
|
+
const hash = createHash('sha1').update(`${namespace}|${key}`).digest('hex').slice(0, 12);
|
|
57
|
+
return `${namespace}-${hash}`;
|
|
58
|
+
}
|
|
59
|
+
function safeParseJson(raw) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeFileAtomic(path, content) {
|
|
68
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
69
|
+
writeFileSync(tmp, content, 'utf-8');
|
|
70
|
+
renameSync(tmp, path);
|
|
71
|
+
}
|
|
72
|
+
export class CalendarTools {
|
|
73
|
+
calendarPath;
|
|
74
|
+
todoPath;
|
|
75
|
+
constructor() {
|
|
76
|
+
const outputs = getLiaHubPaths().outputs;
|
|
77
|
+
this.calendarPath = join(outputs, '.lia', 'calendar.json');
|
|
78
|
+
this.todoPath = join(outputs, '.lia', 'todos.json');
|
|
79
|
+
}
|
|
80
|
+
upsertCalendarEvents(inputEvents, inputCalendars) {
|
|
81
|
+
try {
|
|
82
|
+
ensureLiaHub();
|
|
83
|
+
const stateDir = join(getLiaHubPaths().outputs, '.lia');
|
|
84
|
+
if (!existsSync(stateDir))
|
|
85
|
+
mkdirSync(stateDir, { recursive: true });
|
|
86
|
+
const existing = this.readCalendarPayload();
|
|
87
|
+
const calendars = this.normalizeCalendars(inputCalendars || existing.calendars);
|
|
88
|
+
const events = this.normalizeEvents(inputEvents, calendars, existing.events);
|
|
89
|
+
const payload = {
|
|
90
|
+
version: 1,
|
|
91
|
+
calendars,
|
|
92
|
+
events,
|
|
93
|
+
};
|
|
94
|
+
writeFileAtomic(this.calendarPath, JSON.stringify(payload, null, 2) + '\n');
|
|
95
|
+
return { success: true, output: `Calendar updated: ${inputEvents.length} upsert request(s), ${events.length} total event(s).` };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
upsertTodoItems(inputItems) {
|
|
102
|
+
try {
|
|
103
|
+
ensureLiaHub();
|
|
104
|
+
const stateDir = join(getLiaHubPaths().outputs, '.lia');
|
|
105
|
+
if (!existsSync(stateDir))
|
|
106
|
+
mkdirSync(stateDir, { recursive: true });
|
|
107
|
+
const existing = this.readTodoPayload();
|
|
108
|
+
const items = this.normalizeTodoItems(inputItems, existing.items);
|
|
109
|
+
const payload = { version: 1, items };
|
|
110
|
+
writeFileAtomic(this.todoPath, JSON.stringify(payload, null, 2) + '\n');
|
|
111
|
+
return { success: true, output: `Todo updated: ${inputItems.length} upsert request(s), ${items.length} total item(s).` };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return { success: false, output: '', error: error instanceof Error ? error.message : String(error) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
readCalendarPayload() {
|
|
118
|
+
if (!existsSync(this.calendarPath)) {
|
|
119
|
+
return { version: 1, calendars: DEFAULT_CALENDARS, events: [] };
|
|
120
|
+
}
|
|
121
|
+
const raw = readFileSync(this.calendarPath, 'utf-8');
|
|
122
|
+
const parsed = safeParseJson(raw);
|
|
123
|
+
const calendars = this.normalizeCalendars(parsed?.calendars);
|
|
124
|
+
const events = Array.isArray(parsed?.events) ? parsed?.events : [];
|
|
125
|
+
return { version: 1, calendars, events };
|
|
126
|
+
}
|
|
127
|
+
normalizeCalendars(calendars) {
|
|
128
|
+
if (!Array.isArray(calendars) || calendars.length === 0)
|
|
129
|
+
return DEFAULT_CALENDARS;
|
|
130
|
+
const normalized = calendars
|
|
131
|
+
.filter((cal) => Boolean(cal) && typeof cal.id === 'string' && typeof cal.name === 'string')
|
|
132
|
+
.map((cal, index) => ({
|
|
133
|
+
id: cal.id,
|
|
134
|
+
name: cal.name,
|
|
135
|
+
color: typeof cal.color === 'string' && cal.color ? cal.color : DEFAULT_CALENDARS[index % DEFAULT_CALENDARS.length].color,
|
|
136
|
+
}));
|
|
137
|
+
return normalized.length > 0 ? normalized : DEFAULT_CALENDARS;
|
|
138
|
+
}
|
|
139
|
+
normalizeEvents(inputEvents, calendars, existingEvents) {
|
|
140
|
+
const existingById = new Map(existingEvents.map((event) => [event.id, event]));
|
|
141
|
+
const seenKey = new Set(existingEvents.map((event) => this.eventDedupeKey(event)));
|
|
142
|
+
const fallbackCalendarId = calendars[0]?.id || 'primary';
|
|
143
|
+
for (const rawEvent of inputEvents) {
|
|
144
|
+
if (!rawEvent || typeof rawEvent.title !== 'string' || !rawEvent.title.trim())
|
|
145
|
+
continue;
|
|
146
|
+
const title = rawEvent.title.trim();
|
|
147
|
+
let date = typeof rawEvent.date === 'string' && isDateKey(rawEvent.date.trim()) ? rawEvent.date.trim() : undefined;
|
|
148
|
+
const startNorm = normalizeTime(rawEvent.startTime);
|
|
149
|
+
const endNorm = normalizeTime(rawEvent.endTime);
|
|
150
|
+
if (!date)
|
|
151
|
+
date = startNorm.date || endNorm.date;
|
|
152
|
+
if (!date)
|
|
153
|
+
date = toDateKey(new Date());
|
|
154
|
+
const startTime = startNorm.time || (typeof rawEvent.startTime === 'string' && isTimeKey(rawEvent.startTime.trim()) ? rawEvent.startTime.trim() : undefined);
|
|
155
|
+
const endTime = endNorm.time || (typeof rawEvent.endTime === 'string' && isTimeKey(rawEvent.endTime.trim()) ? rawEvent.endTime.trim() : undefined);
|
|
156
|
+
const calendarId = typeof rawEvent.calendarId === 'string' && rawEvent.calendarId.trim()
|
|
157
|
+
? rawEvent.calendarId.trim()
|
|
158
|
+
: fallbackCalendarId;
|
|
159
|
+
const notes = typeof rawEvent.notes === 'string' && rawEvent.notes.trim() ? rawEvent.notes.trim() : undefined;
|
|
160
|
+
const id = typeof rawEvent.id === 'string' && rawEvent.id.trim()
|
|
161
|
+
? rawEvent.id.trim()
|
|
162
|
+
: stableId('event', [title, date, startTime, endTime, calendarId]);
|
|
163
|
+
const event = { id, title, date, startTime, endTime, calendarId, notes };
|
|
164
|
+
const key = this.eventDedupeKey(event);
|
|
165
|
+
if (seenKey.has(key) && !existingById.has(id))
|
|
166
|
+
continue;
|
|
167
|
+
seenKey.add(key);
|
|
168
|
+
existingById.set(id, event);
|
|
169
|
+
}
|
|
170
|
+
return Array.from(existingById.values()).sort((a, b) => {
|
|
171
|
+
const dateCmp = a.date.localeCompare(b.date);
|
|
172
|
+
if (dateCmp !== 0)
|
|
173
|
+
return dateCmp;
|
|
174
|
+
return (a.startTime || '').localeCompare(b.startTime || '');
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
eventDedupeKey(event) {
|
|
178
|
+
return [event.title, event.date, event.startTime || '', event.endTime || '', event.calendarId].join('|').toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
readTodoPayload() {
|
|
181
|
+
if (!existsSync(this.todoPath)) {
|
|
182
|
+
return { version: 1, items: [] };
|
|
183
|
+
}
|
|
184
|
+
const raw = readFileSync(this.todoPath, 'utf-8');
|
|
185
|
+
const parsed = safeParseJson(raw);
|
|
186
|
+
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
187
|
+
return { version: 1, items };
|
|
188
|
+
}
|
|
189
|
+
normalizeTodoItems(inputItems, existingItems) {
|
|
190
|
+
const existingById = new Map(existingItems.map((item) => [item.id, item]));
|
|
191
|
+
const seenKey = new Set(existingItems.map((item) => item.title.trim().toLowerCase()));
|
|
192
|
+
for (const rawItem of inputItems) {
|
|
193
|
+
if (!rawItem || typeof rawItem.title !== 'string' || !rawItem.title.trim())
|
|
194
|
+
continue;
|
|
195
|
+
const title = rawItem.title.trim();
|
|
196
|
+
const id = typeof rawItem.id === 'string' && rawItem.id.trim()
|
|
197
|
+
? rawItem.id.trim()
|
|
198
|
+
: stableId('todo', [title, rawItem.due]);
|
|
199
|
+
const priority = rawItem.priority === 'low' || rawItem.priority === 'high' ? rawItem.priority : 'medium';
|
|
200
|
+
const due = typeof rawItem.due === 'string' && rawItem.due.trim() ? rawItem.due.trim() : undefined;
|
|
201
|
+
const notes = typeof rawItem.notes === 'string' && rawItem.notes.trim() ? rawItem.notes.trim() : undefined;
|
|
202
|
+
const incomingSubtasks = Array.isArray(rawItem.subtasks)
|
|
203
|
+
? rawItem.subtasks
|
|
204
|
+
.filter((subtask) => subtask && typeof subtask.title === 'string' && subtask.title.trim())
|
|
205
|
+
.map((subtask, index) => ({
|
|
206
|
+
id: typeof subtask.id === 'string' && subtask.id.trim()
|
|
207
|
+
? subtask.id.trim()
|
|
208
|
+
: stableId('subtask', [id, subtask.title.trim(), String(index)]),
|
|
209
|
+
title: subtask.title.trim(),
|
|
210
|
+
done: Boolean(subtask.done),
|
|
211
|
+
notes: typeof subtask.notes === 'string' && subtask.notes.trim() ? subtask.notes.trim() : undefined,
|
|
212
|
+
}))
|
|
213
|
+
: undefined;
|
|
214
|
+
const previous = existingById.get(id);
|
|
215
|
+
const subtasks = incomingSubtasks || previous?.subtasks;
|
|
216
|
+
let done = Boolean(rawItem.done);
|
|
217
|
+
if (incomingSubtasks && incomingSubtasks.length > 0) {
|
|
218
|
+
done = incomingSubtasks.every((subtask) => subtask.done);
|
|
219
|
+
}
|
|
220
|
+
else if (done && Array.isArray(previous?.subtasks) && previous.subtasks.length > 0) {
|
|
221
|
+
// Marking a parent done should also complete subtasks.
|
|
222
|
+
for (const subtask of previous.subtasks)
|
|
223
|
+
subtask.done = true;
|
|
224
|
+
}
|
|
225
|
+
const key = title.toLowerCase();
|
|
226
|
+
if (seenKey.has(key) && !existingById.has(id))
|
|
227
|
+
continue;
|
|
228
|
+
seenKey.add(key);
|
|
229
|
+
existingById.set(id, { id, title, done, due, priority, notes, subtasks });
|
|
230
|
+
}
|
|
231
|
+
return Array.from(existingById.values());
|
|
232
|
+
}
|
|
233
|
+
}
|
package/dist/tools/worker.d.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Worker Tools
|
|
3
3
|
* Tools for spawning and managing Claude Code workers
|
|
4
4
|
*/
|
|
5
|
-
import { GalleryCallback, GalleryWorker } from '../workers/manager.js';
|
|
5
|
+
import { GalleryCallback, GalleryWorker, WorkerLogCallback } from '../workers/manager.js';
|
|
6
6
|
import type { ToolResult } from './bash.js';
|
|
7
7
|
export { GalleryCallback, GalleryWorker };
|
|
8
|
+
export type { WorkerLogCallback };
|
|
8
9
|
export declare class WorkerTools {
|
|
9
10
|
private manager;
|
|
10
11
|
constructor(workspaceDir: string);
|
|
@@ -12,6 +13,10 @@ export declare class WorkerTools {
|
|
|
12
13
|
* Set callback for gallery updates (worker assets for UI)
|
|
13
14
|
*/
|
|
14
15
|
setGalleryCallback(callback: GalleryCallback | null): void;
|
|
16
|
+
/**
|
|
17
|
+
* Set callback for worker log streaming (stdout/stderr)
|
|
18
|
+
*/
|
|
19
|
+
setLogCallback(callback: WorkerLogCallback | null): void;
|
|
15
20
|
/**
|
|
16
21
|
* Get gallery workers with assets
|
|
17
22
|
*/
|
package/dist/tools/worker.js
CHANGED
|
@@ -14,6 +14,12 @@ export class WorkerTools {
|
|
|
14
14
|
setGalleryCallback(callback) {
|
|
15
15
|
this.manager.setGalleryCallback(callback);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Set callback for worker log streaming (stdout/stderr)
|
|
19
|
+
*/
|
|
20
|
+
setLogCallback(callback) {
|
|
21
|
+
this.manager.setLogCallback(callback);
|
|
22
|
+
}
|
|
17
23
|
/**
|
|
18
24
|
* Get gallery workers with assets
|
|
19
25
|
*/
|
|
@@ -9,16 +9,23 @@ export interface GalleryWorker {
|
|
|
9
9
|
status: 'running' | 'completed' | 'failed';
|
|
10
10
|
assets: WorkerAsset[];
|
|
11
11
|
}
|
|
12
|
+
export type WorkerLogCallback = (workerId: string, line: string, stream: 'stdout' | 'stderr') => void;
|
|
12
13
|
export type GalleryCallback = (workers: GalleryWorker[]) => void;
|
|
13
14
|
export declare class WorkerManager {
|
|
14
15
|
private config;
|
|
15
16
|
private activeWorkers;
|
|
16
17
|
private galleryCallback;
|
|
18
|
+
private workerLogCallback;
|
|
17
19
|
constructor(workspaceDir: string, configOverrides?: Partial<WorkerConfig>);
|
|
18
20
|
/**
|
|
19
21
|
* Set callback for gallery updates (worker assets for UI)
|
|
20
22
|
*/
|
|
21
23
|
setGalleryCallback(callback: GalleryCallback | null): void;
|
|
24
|
+
/**
|
|
25
|
+
* Set callback for worker log streaming (stdout/stderr)
|
|
26
|
+
*/
|
|
27
|
+
setLogCallback(callback: WorkerLogCallback | null): void;
|
|
28
|
+
private emitWorkerLog;
|
|
22
29
|
/**
|
|
23
30
|
* Get gallery workers with assets for UI
|
|
24
31
|
*/
|
package/dist/workers/manager.js
CHANGED
|
@@ -52,6 +52,7 @@ export class WorkerManager {
|
|
|
52
52
|
config;
|
|
53
53
|
activeWorkers = new Map();
|
|
54
54
|
galleryCallback = null;
|
|
55
|
+
workerLogCallback = null;
|
|
55
56
|
constructor(workspaceDir, configOverrides) {
|
|
56
57
|
this.config = {
|
|
57
58
|
...DEFAULT_WORKER_CONFIG,
|
|
@@ -69,6 +70,23 @@ export class WorkerManager {
|
|
|
69
70
|
setGalleryCallback(callback) {
|
|
70
71
|
this.galleryCallback = callback;
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Set callback for worker log streaming (stdout/stderr)
|
|
75
|
+
*/
|
|
76
|
+
setLogCallback(callback) {
|
|
77
|
+
this.workerLogCallback = callback;
|
|
78
|
+
}
|
|
79
|
+
emitWorkerLog(workerId, chunk, stream) {
|
|
80
|
+
if (!this.workerLogCallback)
|
|
81
|
+
return;
|
|
82
|
+
const lines = chunk.split(/\r?\n/);
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
continue;
|
|
87
|
+
this.workerLogCallback(workerId, trimmed.slice(0, 500), stream);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
72
90
|
/**
|
|
73
91
|
* Get gallery workers with assets for UI
|
|
74
92
|
*/
|
|
@@ -174,10 +192,18 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
174
192
|
GH_TOKEN: process.env.GH_TOKEN,
|
|
175
193
|
GITHUB_TOKEN: process.env.GITHUB_TOKEN
|
|
176
194
|
};
|
|
195
|
+
// Ensure worker uses the user's workspace as HOME so ~/Lia-Hub maps correctly
|
|
196
|
+
spawnEnv = {
|
|
197
|
+
...spawnEnv,
|
|
198
|
+
HOME: workspace,
|
|
199
|
+
LIA_WORKSPACE: workspace,
|
|
200
|
+
USER: spawnEnv.USER || 'lia',
|
|
201
|
+
LOGNAME: spawnEnv.LOGNAME || 'lia'
|
|
202
|
+
};
|
|
177
203
|
if (isRoot) {
|
|
178
|
-
// When running as root, use runuser to switch to 'lia' user while preserving env
|
|
179
|
-
// runuser -
|
|
180
|
-
shellCmd = `runuser -u lia -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`;
|
|
204
|
+
// When running as root, use runuser to switch to 'lia' user while preserving env (HOME)
|
|
205
|
+
// runuser -m preserves env; -l ensures PATH resolution for nvm/claude
|
|
206
|
+
shellCmd = `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`;
|
|
181
207
|
}
|
|
182
208
|
else {
|
|
183
209
|
shellCmd = claudeCmd;
|
|
@@ -198,10 +224,14 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
198
224
|
let stdout = '';
|
|
199
225
|
let stderr = '';
|
|
200
226
|
child.stdout?.on('data', (data) => {
|
|
201
|
-
|
|
227
|
+
const text = data.toString();
|
|
228
|
+
stdout += text;
|
|
229
|
+
this.emitWorkerLog(jobId, text, 'stdout');
|
|
202
230
|
});
|
|
203
231
|
child.stderr?.on('data', (data) => {
|
|
204
|
-
|
|
232
|
+
const text = data.toString();
|
|
233
|
+
stderr += text;
|
|
234
|
+
this.emitWorkerLog(jobId, text, 'stderr');
|
|
205
235
|
});
|
|
206
236
|
// Set up timeout
|
|
207
237
|
const timeoutHandle = setTimeout(() => {
|