@geoql/mdr 0.0.1 → 1.0.0
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 +3 -3
- package/bin/detect-user.sh +0 -0
- package/bin/index-conversations.ts +2 -2
- package/bin/macrodata-daemon.ts +1 -1
- package/bin/macrodata-hook.sh +1 -1
- package/dist/bin/index-conversations.js +0 -0
- package/dist/bin/macrodata-daemon.js +0 -0
- package/dist/opencode/skills/macrodata-distill/SKILL.md +1 -1
- package/opencode/context.ts +44 -44
- package/opencode/conversations.ts +20 -20
- package/opencode/index.ts +26 -26
- package/opencode/journal.ts +24 -24
- package/opencode/logger.ts +8 -8
- package/opencode/search.ts +31 -31
- package/opencode/skills/macrodata-distill/SKILL.md +1 -1
- package/opencode/tools.ts +62 -62
- package/package.json +17 -18
- package/src/config.ts +13 -13
- package/src/conversations.ts +61 -61
- package/src/daemon.ts +71 -71
- package/src/detect-user.ts +24 -24
- package/src/embeddings.ts +25 -25
- package/src/index.ts +129 -129
- package/src/indexer.ts +36 -36
package/src/daemon.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* MACRODATA_CHILD_TIMEOUT_MS=<ms> (default: 10 minutes)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { watch } from
|
|
17
|
+
import { watch } from 'chokidar';
|
|
18
18
|
import {
|
|
19
19
|
existsSync,
|
|
20
20
|
readFileSync,
|
|
@@ -23,29 +23,29 @@ import {
|
|
|
23
23
|
mkdirSync,
|
|
24
24
|
readdirSync,
|
|
25
25
|
unlinkSync,
|
|
26
|
-
} from
|
|
27
|
-
import { join, basename } from
|
|
28
|
-
import { Cron } from
|
|
29
|
-
import { spawn, execSync } from
|
|
26
|
+
} from 'fs';
|
|
27
|
+
import { join, basename } from 'path';
|
|
28
|
+
import { Cron } from 'croner';
|
|
29
|
+
import { spawn, execSync } from 'child_process';
|
|
30
30
|
import {
|
|
31
31
|
getStateRoot,
|
|
32
32
|
getEntitiesDir,
|
|
33
33
|
getJournalDir,
|
|
34
34
|
getIndexDir,
|
|
35
35
|
getRemindersDir,
|
|
36
|
-
} from
|
|
36
|
+
} from './config.js';
|
|
37
37
|
|
|
38
38
|
// The indexing modules pull in @huggingface/transformers + vectra (multi-second
|
|
39
39
|
// import). Load them lazily so the daemon writes its PID file and starts
|
|
40
40
|
// scheduling immediately instead of blocking on heavy imports.
|
|
41
41
|
export async function loadIndexer() {
|
|
42
|
-
return import(
|
|
42
|
+
return import('./indexer.js');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function loadConversationIndexers() {
|
|
46
46
|
const [oc, cc] = await Promise.all([
|
|
47
|
-
import(
|
|
48
|
-
import(
|
|
47
|
+
import('../opencode/conversations.js'),
|
|
48
|
+
import('./conversations.js'),
|
|
49
49
|
]);
|
|
50
50
|
return {
|
|
51
51
|
updateOpenCodeConversations: oc.updateConversationIndex,
|
|
@@ -58,7 +58,7 @@ export async function loadConversationIndexers() {
|
|
|
58
58
|
*/
|
|
59
59
|
export async function findExecutable(name: string): Promise<string | null> {
|
|
60
60
|
try {
|
|
61
|
-
const result = execSync(`which ${name}`, { encoding:
|
|
61
|
+
const result = execSync(`which ${name}`, { encoding: 'utf-8' }).trim();
|
|
62
62
|
return result || null;
|
|
63
63
|
} catch {
|
|
64
64
|
return null;
|
|
@@ -72,19 +72,19 @@ export function getDaemonDir() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function getPidFile() {
|
|
75
|
-
return join(getDaemonDir(),
|
|
75
|
+
return join(getDaemonDir(), '.daemon.pid');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export function getLogFile() {
|
|
79
|
-
return join(getDaemonDir(),
|
|
79
|
+
return join(getDaemonDir(), '.daemon.log');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function getPendingContext() {
|
|
83
|
-
return join(getStateRoot(),
|
|
83
|
+
return join(getStateRoot(), '.pending-context');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function getHeartbeatFile() {
|
|
87
|
-
return join(getDaemonDir(),
|
|
87
|
+
return join(getDaemonDir(), '.daemon.heartbeat');
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export const HEARTBEAT_INTERVAL_MS = 60_000;
|
|
@@ -101,11 +101,11 @@ export function getChildTimeoutMs(): number {
|
|
|
101
101
|
|
|
102
102
|
export interface Schedule {
|
|
103
103
|
id: string;
|
|
104
|
-
type:
|
|
104
|
+
type: 'cron' | 'once';
|
|
105
105
|
expression: string; // cron expression or ISO datetime
|
|
106
106
|
description: string;
|
|
107
107
|
payload: string;
|
|
108
|
-
agent?:
|
|
108
|
+
agent?: 'opencode' | 'claude'; // Which agent to trigger
|
|
109
109
|
model?: string; // Optional model override (e.g., "anthropic/claude-opus-4-6")
|
|
110
110
|
createdAt: string;
|
|
111
111
|
}
|
|
@@ -124,7 +124,7 @@ export function logError(message: string) {
|
|
|
124
124
|
|
|
125
125
|
export function writePendingContext(message: string) {
|
|
126
126
|
try {
|
|
127
|
-
appendFileSync(getPendingContext(), message +
|
|
127
|
+
appendFileSync(getPendingContext(), message + '\n');
|
|
128
128
|
} catch (err) {
|
|
129
129
|
logError(`Failed to write pending context: ${String(err)}`);
|
|
130
130
|
}
|
|
@@ -134,17 +134,17 @@ export function writePendingContext(message: string) {
|
|
|
134
134
|
* Trigger an agent with a message
|
|
135
135
|
*/
|
|
136
136
|
export async function triggerAgent(
|
|
137
|
-
agent:
|
|
137
|
+
agent: 'opencode' | 'claude' | undefined,
|
|
138
138
|
message: string,
|
|
139
139
|
options: { model?: string; description?: string } = {},
|
|
140
140
|
): Promise<boolean> {
|
|
141
141
|
if (!agent) {
|
|
142
|
-
log(
|
|
142
|
+
log('No agent specified in schedule, skipping trigger');
|
|
143
143
|
return false;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const timestamp = new Date().toLocaleString();
|
|
147
|
-
const fullMessage = `[Scheduled reminder: ${options.description ||
|
|
147
|
+
const fullMessage = `[Scheduled reminder: ${options.description || 'reminder'}]
|
|
148
148
|
Current time: ${timestamp}
|
|
149
149
|
|
|
150
150
|
IMPORTANT: Use the macrodata_* tools (e.g., macrodata_log_journal, macrodata_search_memory) for memory operations. You are running in a non-interactive scheduled context.
|
|
@@ -152,29 +152,29 @@ IMPORTANT: Use the macrodata_* tools (e.g., macrodata_log_journal, macrodata_sea
|
|
|
152
152
|
${message}`;
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
|
-
if (agent ===
|
|
155
|
+
if (agent === 'opencode') {
|
|
156
156
|
// opencode run "message" --model provider/model
|
|
157
|
-
const args = [
|
|
157
|
+
const args = ['run', fullMessage];
|
|
158
158
|
if (options.model) {
|
|
159
|
-
args.push(
|
|
159
|
+
args.push('--model', options.model);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Find opencode in PATH or use npx as fallback
|
|
163
|
-
const opencodePath = (await findExecutable(
|
|
164
|
-
const finalArgs = opencodePath ===
|
|
163
|
+
const opencodePath = (await findExecutable('opencode')) || 'npx';
|
|
164
|
+
const finalArgs = opencodePath === 'npx' ? ['opencode', ...args] : args;
|
|
165
165
|
|
|
166
|
-
log(`Triggering OpenCode: ${opencodePath} ${finalArgs.join(
|
|
166
|
+
log(`Triggering OpenCode: ${opencodePath} ${finalArgs.join(' ').substring(0, 50)}...`);
|
|
167
167
|
|
|
168
|
-
spawnSupervisedChild(opencodePath, finalArgs,
|
|
168
|
+
spawnSupervisedChild(opencodePath, finalArgs, 'opencode');
|
|
169
169
|
|
|
170
170
|
return true;
|
|
171
171
|
} else {
|
|
172
172
|
// claude --print "message" or claude -p "message"
|
|
173
|
-
const args = [
|
|
173
|
+
const args = ['--print', fullMessage];
|
|
174
174
|
|
|
175
175
|
log(`Triggering Claude Code: claude --print "..."`);
|
|
176
176
|
|
|
177
|
-
spawnSupervisedChild(
|
|
177
|
+
spawnSupervisedChild('claude', args, 'claude');
|
|
178
178
|
|
|
179
179
|
return true;
|
|
180
180
|
}
|
|
@@ -193,7 +193,7 @@ ${message}`;
|
|
|
193
193
|
export function spawnSupervisedChild(command: string, args: string[], label: string) {
|
|
194
194
|
const childTimeoutMs = getChildTimeoutMs();
|
|
195
195
|
const proc = spawn(command, args, {
|
|
196
|
-
stdio: [
|
|
196
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
197
197
|
detached: true,
|
|
198
198
|
env: { ...process.env, PATH: process.env.PATH },
|
|
199
199
|
});
|
|
@@ -204,10 +204,10 @@ export function spawnSupervisedChild(command: string, args: string[], label: str
|
|
|
204
204
|
log(`[${label}] child exceeded ${childTimeoutMs}ms timeout, killing process group ${proc.pid}`);
|
|
205
205
|
if (proc.pid) {
|
|
206
206
|
try {
|
|
207
|
-
process.kill(-proc.pid,
|
|
207
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
208
208
|
} catch {
|
|
209
209
|
try {
|
|
210
|
-
proc.kill(
|
|
210
|
+
proc.kill('SIGKILL');
|
|
211
211
|
} catch {
|
|
212
212
|
// Child already gone
|
|
213
213
|
}
|
|
@@ -216,17 +216,17 @@ export function spawnSupervisedChild(command: string, args: string[], label: str
|
|
|
216
216
|
}, childTimeoutMs);
|
|
217
217
|
killTimer.unref();
|
|
218
218
|
|
|
219
|
-
proc.stdout?.on(
|
|
219
|
+
proc.stdout?.on('data', (data) => {
|
|
220
220
|
log(`[${label} stdout] ${data.toString().trim()}`);
|
|
221
221
|
});
|
|
222
|
-
proc.stderr?.on(
|
|
222
|
+
proc.stderr?.on('data', (data) => {
|
|
223
223
|
log(`[${label} stderr] ${data.toString().trim()}`);
|
|
224
224
|
});
|
|
225
|
-
proc.on(
|
|
225
|
+
proc.on('error', (err) => {
|
|
226
226
|
clearTimeout(killTimer);
|
|
227
227
|
logError(`[${label}] child process error: ${String(err)}`);
|
|
228
228
|
});
|
|
229
|
-
proc.on(
|
|
229
|
+
proc.on('exit', (code, signal) => {
|
|
230
230
|
clearTimeout(killTimer);
|
|
231
231
|
log(`[${label}] child exited (code=${code}, signal=${signal})`);
|
|
232
232
|
});
|
|
@@ -243,8 +243,8 @@ export function ensureDirectories() {
|
|
|
243
243
|
entitiesDir,
|
|
244
244
|
getJournalDir(),
|
|
245
245
|
getRemindersDir(),
|
|
246
|
-
join(entitiesDir,
|
|
247
|
-
join(entitiesDir,
|
|
246
|
+
join(entitiesDir, 'people'),
|
|
247
|
+
join(entitiesDir, 'projects'),
|
|
248
248
|
];
|
|
249
249
|
for (const dir of dirs) {
|
|
250
250
|
if (!existsSync(dir)) {
|
|
@@ -292,10 +292,10 @@ export function loadAllSchedules(): Schedule[] {
|
|
|
292
292
|
try {
|
|
293
293
|
if (!existsSync(remindersDir)) return schedules;
|
|
294
294
|
|
|
295
|
-
const files = readdirSync(remindersDir).filter((f) => f.endsWith(
|
|
295
|
+
const files = readdirSync(remindersDir).filter((f) => f.endsWith('.json'));
|
|
296
296
|
for (const file of files) {
|
|
297
297
|
try {
|
|
298
|
-
const content = readFileSync(join(remindersDir, file),
|
|
298
|
+
const content = readFileSync(join(remindersDir, file), 'utf-8');
|
|
299
299
|
const schedule = JSON.parse(content) as Schedule;
|
|
300
300
|
schedules.push(schedule);
|
|
301
301
|
} catch (err) {
|
|
@@ -350,7 +350,7 @@ export async function defaultBackgroundIndexing(
|
|
|
350
350
|
const updateAll = deps.updateAll ?? updateAllConversationIndexes;
|
|
351
351
|
const indexer = await load();
|
|
352
352
|
await indexer.preloadModel();
|
|
353
|
-
log(
|
|
353
|
+
log('Embedding model preloaded');
|
|
354
354
|
await updateAll();
|
|
355
355
|
}
|
|
356
356
|
|
|
@@ -369,14 +369,14 @@ export class MacrodataLocalDaemon {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
async start() {
|
|
372
|
-
log(
|
|
372
|
+
log('Starting macrodata local daemon');
|
|
373
373
|
log(`State root: ${getStateRoot()}`);
|
|
374
374
|
|
|
375
375
|
// Check if already running
|
|
376
376
|
ensureDirectories();
|
|
377
377
|
const pidFile = getPidFile();
|
|
378
378
|
if (existsSync(pidFile)) {
|
|
379
|
-
const existingPid = readFileSync(pidFile,
|
|
379
|
+
const existingPid = readFileSync(pidFile, 'utf-8').trim();
|
|
380
380
|
try {
|
|
381
381
|
process.kill(parseInt(existingPid, 10), 0); // Check if process exists
|
|
382
382
|
log(`Daemon already running (PID ${existingPid}), exiting`);
|
|
@@ -391,16 +391,16 @@ export class MacrodataLocalDaemon {
|
|
|
391
391
|
writeFileSync(pidFile, process.pid.toString());
|
|
392
392
|
|
|
393
393
|
// Set up signal handlers
|
|
394
|
-
process.on(
|
|
395
|
-
process.on(
|
|
396
|
-
process.on(
|
|
394
|
+
process.on('SIGTERM', () => this.shutdown('SIGTERM'));
|
|
395
|
+
process.on('SIGINT', () => this.shutdown('SIGINT'));
|
|
396
|
+
process.on('SIGHUP', () => this.reload());
|
|
397
397
|
|
|
398
398
|
// The daemon must be hard to stop: a failed child, watcher error, or
|
|
399
399
|
// rejected background promise should be logged, never fatal (#25).
|
|
400
|
-
process.on(
|
|
400
|
+
process.on('unhandledRejection', (reason) => {
|
|
401
401
|
logError(`Unhandled rejection (daemon continues): ${String(reason)}`);
|
|
402
402
|
});
|
|
403
|
-
process.on(
|
|
403
|
+
process.on('uncaughtException', (err) => {
|
|
404
404
|
logError(`Uncaught exception (daemon continues): ${String(err?.stack || err)}`);
|
|
405
405
|
});
|
|
406
406
|
|
|
@@ -420,7 +420,7 @@ export class MacrodataLocalDaemon {
|
|
|
420
420
|
this.startHeartbeat();
|
|
421
421
|
|
|
422
422
|
// Keep process alive
|
|
423
|
-
log(
|
|
423
|
+
log('Daemon running');
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
private startHeartbeat() {
|
|
@@ -444,12 +444,12 @@ export class MacrodataLocalDaemon {
|
|
|
444
444
|
awaitWriteFinish: { stabilityThreshold: 100 },
|
|
445
445
|
});
|
|
446
446
|
|
|
447
|
-
this.schedulesWatcher.on(
|
|
448
|
-
if (!path.endsWith(
|
|
447
|
+
this.schedulesWatcher.on('add', (path) => {
|
|
448
|
+
if (!path.endsWith('.json')) return;
|
|
449
449
|
log(`Reminder added: ${basename(path)}`);
|
|
450
450
|
this.reloadSchedules();
|
|
451
451
|
try {
|
|
452
|
-
const schedule = JSON.parse(readFileSync(path,
|
|
452
|
+
const schedule = JSON.parse(readFileSync(path, 'utf-8')) as Schedule;
|
|
453
453
|
writePendingContext(
|
|
454
454
|
`<macrodata-update type="schedule-added" id="${schedule.id}">${schedule.description}</macrodata-update>`,
|
|
455
455
|
);
|
|
@@ -458,16 +458,16 @@ export class MacrodataLocalDaemon {
|
|
|
458
458
|
}
|
|
459
459
|
});
|
|
460
460
|
|
|
461
|
-
this.schedulesWatcher.on(
|
|
461
|
+
this.schedulesWatcher.on('error', (err) => {
|
|
462
462
|
logError(`Reminders watcher error: ${String(err)}`);
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
-
this.schedulesWatcher.on(
|
|
466
|
-
if (!path.endsWith(
|
|
465
|
+
this.schedulesWatcher.on('change', (path) => {
|
|
466
|
+
if (!path.endsWith('.json')) return;
|
|
467
467
|
log(`Reminder changed: ${basename(path)}`);
|
|
468
468
|
this.reloadSchedules();
|
|
469
469
|
try {
|
|
470
|
-
const schedule = JSON.parse(readFileSync(path,
|
|
470
|
+
const schedule = JSON.parse(readFileSync(path, 'utf-8')) as Schedule;
|
|
471
471
|
writePendingContext(
|
|
472
472
|
`<macrodata-update type="schedule-updated" id="${schedule.id}">${schedule.description}</macrodata-update>`,
|
|
473
473
|
);
|
|
@@ -476,12 +476,12 @@ export class MacrodataLocalDaemon {
|
|
|
476
476
|
}
|
|
477
477
|
});
|
|
478
478
|
|
|
479
|
-
this.schedulesWatcher.on(
|
|
479
|
+
this.schedulesWatcher.on('unlink', (path) => this.onReminderUnlinked(path));
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
private onReminderUnlinked(path: string) {
|
|
483
|
-
if (!path.endsWith(
|
|
484
|
-
const id = basename(path,
|
|
483
|
+
if (!path.endsWith('.json')) return;
|
|
484
|
+
const id = basename(path, '.json');
|
|
485
485
|
log(`Reminder removed: ${id}`);
|
|
486
486
|
writePendingContext(`<macrodata-update type="schedule-removed" id="${id}" />`);
|
|
487
487
|
if (this.cronJobs.has(id)) {
|
|
@@ -491,11 +491,11 @@ export class MacrodataLocalDaemon {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
private scheduleFor(schedule: Schedule) {
|
|
494
|
-
if (schedule.type ===
|
|
494
|
+
if (schedule.type === 'cron') {
|
|
495
495
|
this.startCronJob(schedule);
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
498
|
-
if (schedule.type ===
|
|
498
|
+
if (schedule.type === 'once') {
|
|
499
499
|
if (new Date(schedule.expression).getTime() > Date.now()) {
|
|
500
500
|
this.startOnceJob(schedule);
|
|
501
501
|
} else {
|
|
@@ -587,7 +587,7 @@ export class MacrodataLocalDaemon {
|
|
|
587
587
|
saveSchedule(schedule);
|
|
588
588
|
|
|
589
589
|
// Start the job
|
|
590
|
-
if (schedule.type ===
|
|
590
|
+
if (schedule.type === 'cron') {
|
|
591
591
|
this.startCronJob(schedule);
|
|
592
592
|
} else {
|
|
593
593
|
this.startOnceJob(schedule);
|
|
@@ -603,7 +603,7 @@ export class MacrodataLocalDaemon {
|
|
|
603
603
|
private startFileWatcher() {
|
|
604
604
|
const stateRoot = getStateRoot();
|
|
605
605
|
const entitiesDir = getEntitiesDir();
|
|
606
|
-
const stateDir = join(stateRoot,
|
|
606
|
+
const stateDir = join(stateRoot, 'state');
|
|
607
607
|
|
|
608
608
|
// Watch both state files and entities
|
|
609
609
|
this.watcher = watch([stateDir, entitiesDir], {
|
|
@@ -611,18 +611,18 @@ export class MacrodataLocalDaemon {
|
|
|
611
611
|
persistent: true,
|
|
612
612
|
});
|
|
613
613
|
|
|
614
|
-
this.watcher.on(
|
|
614
|
+
this.watcher.on('all', (event, path) => this.onWatchedFileEvent(event, path));
|
|
615
615
|
|
|
616
616
|
log(`Watching for state/entity changes in: ${stateRoot}`);
|
|
617
617
|
}
|
|
618
618
|
|
|
619
619
|
onWatchedFileEvent(event: string, path: string) {
|
|
620
|
-
if (!path.endsWith(
|
|
621
|
-
if (event !==
|
|
620
|
+
if (!path.endsWith('.md')) return;
|
|
621
|
+
if (event !== 'add' && event !== 'change') return;
|
|
622
622
|
|
|
623
623
|
log(`File ${event}: ${path}`);
|
|
624
624
|
|
|
625
|
-
const stateDir = join(getStateRoot(),
|
|
625
|
+
const stateDir = join(getStateRoot(), 'state');
|
|
626
626
|
const entitiesDir = getEntitiesDir();
|
|
627
627
|
|
|
628
628
|
if (path.startsWith(stateDir)) {
|
|
@@ -636,7 +636,7 @@ export class MacrodataLocalDaemon {
|
|
|
636
636
|
|
|
637
637
|
private injectStateFileDelta(path: string) {
|
|
638
638
|
try {
|
|
639
|
-
const raw = readFileSync(path,
|
|
639
|
+
const raw = readFileSync(path, 'utf-8');
|
|
640
640
|
const cap = 4000;
|
|
641
641
|
// Cap the injected delta so a mid-session write can't blow the budget.
|
|
642
642
|
const content =
|
|
@@ -685,7 +685,7 @@ export class MacrodataLocalDaemon {
|
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
reload() {
|
|
688
|
-
log(
|
|
688
|
+
log('Reloading config (SIGHUP)');
|
|
689
689
|
log(`New state root: ${getStateRoot()}`);
|
|
690
690
|
|
|
691
691
|
// Stop existing watchers
|
|
@@ -712,7 +712,7 @@ export class MacrodataLocalDaemon {
|
|
|
712
712
|
this.watchRemindersDir();
|
|
713
713
|
this.startFileWatcher();
|
|
714
714
|
|
|
715
|
-
log(
|
|
715
|
+
log('Reload complete');
|
|
716
716
|
}
|
|
717
717
|
|
|
718
718
|
shutdown(signal: string) {
|
|
@@ -744,7 +744,7 @@ export class MacrodataLocalDaemon {
|
|
|
744
744
|
try {
|
|
745
745
|
const pidFile = getPidFile();
|
|
746
746
|
if (existsSync(pidFile)) {
|
|
747
|
-
const pid = readFileSync(pidFile,
|
|
747
|
+
const pid = readFileSync(pidFile, 'utf-8').trim();
|
|
748
748
|
if (pid === process.pid.toString()) {
|
|
749
749
|
unlinkSync(pidFile);
|
|
750
750
|
}
|
package/src/detect-user.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Returns JSON with system, git, github, and code directory info
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { execSync } from
|
|
7
|
-
import { existsSync } from
|
|
8
|
-
import { homedir } from
|
|
9
|
-
import { join } from
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
10
|
|
|
11
11
|
interface GitInfo {
|
|
12
12
|
name: string;
|
|
@@ -31,32 +31,32 @@ export interface UserInfo {
|
|
|
31
31
|
|
|
32
32
|
function exec(cmd: string): string {
|
|
33
33
|
try {
|
|
34
|
-
return execSync(cmd, { encoding:
|
|
34
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
35
35
|
} catch {
|
|
36
|
-
return
|
|
36
|
+
return '';
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function detectUser(): UserInfo {
|
|
41
41
|
// System info
|
|
42
|
-
const username = exec(
|
|
43
|
-
const fullName = exec(
|
|
42
|
+
const username = exec('whoami');
|
|
43
|
+
const fullName = exec('id -F') || exec(`getent passwd ${username} | cut -d: -f5 | cut -d, -f1`);
|
|
44
44
|
|
|
45
45
|
// Timezone
|
|
46
|
-
let timezone =
|
|
47
|
-
if (existsSync(
|
|
48
|
-
timezone = exec(
|
|
49
|
-
} else if (existsSync(
|
|
46
|
+
let timezone = '';
|
|
47
|
+
if (existsSync('/etc/timezone')) {
|
|
48
|
+
timezone = exec('cat /etc/timezone');
|
|
49
|
+
} else if (existsSync('/etc/localtime')) {
|
|
50
50
|
timezone = exec("readlink /etc/localtime | sed 's|.*/zoneinfo/||'");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Git config
|
|
54
|
-
const gitName = exec(
|
|
55
|
-
const gitEmail = exec(
|
|
54
|
+
const gitName = exec('git config --global user.name');
|
|
55
|
+
const gitEmail = exec('git config --global user.email');
|
|
56
56
|
|
|
57
57
|
// GitHub CLI (if authenticated)
|
|
58
58
|
let github: GitHubInfo = {};
|
|
59
|
-
const ghCheck = exec(
|
|
59
|
+
const ghCheck = exec('command -v gh');
|
|
60
60
|
if (ghCheck) {
|
|
61
61
|
const ghJson = exec("gh api user --jq '{login: .login, name: .name, blog: .blog, bio: .bio}'");
|
|
62
62
|
if (ghJson) {
|
|
@@ -71,15 +71,15 @@ export function detectUser(): UserInfo {
|
|
|
71
71
|
// Code directories that exist
|
|
72
72
|
const home = homedir();
|
|
73
73
|
const possibleDirs = [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
'Repos',
|
|
75
|
+
'repos',
|
|
76
|
+
'Code',
|
|
77
|
+
'code',
|
|
78
|
+
'Projects',
|
|
79
|
+
'projects',
|
|
80
|
+
'Developer',
|
|
81
|
+
'dev',
|
|
82
|
+
'src',
|
|
83
83
|
];
|
|
84
84
|
const codeDirs = possibleDirs.map((dir) => join(home, dir)).filter((dir) => existsSync(dir));
|
|
85
85
|
|
package/src/embeddings.ts
CHANGED
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* }
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { existsSync, readFileSync } from
|
|
26
|
-
import { homedir } from
|
|
27
|
-
import { join } from
|
|
28
|
-
import type { FeatureExtractionPipeline } from
|
|
25
|
+
import { existsSync, readFileSync } from 'fs';
|
|
26
|
+
import { homedir } from 'os';
|
|
27
|
+
import { join } from 'path';
|
|
28
|
+
import type { FeatureExtractionPipeline } from '@huggingface/transformers';
|
|
29
29
|
|
|
30
30
|
export interface RemoteEmbeddingConfig {
|
|
31
|
-
provider:
|
|
31
|
+
provider: 'openai-compatible';
|
|
32
32
|
endpoint: string;
|
|
33
33
|
api_key?: string;
|
|
34
34
|
api_key_env?: string;
|
|
@@ -48,18 +48,18 @@ export function getRemoteEmbeddingConfig(): RemoteEmbeddingConfig | null {
|
|
|
48
48
|
if (cachedRemoteConfig !== undefined) return cachedRemoteConfig;
|
|
49
49
|
|
|
50
50
|
const configPath =
|
|
51
|
-
process.env.MACRODATA_CONFIG_PATH || join(homedir(),
|
|
51
|
+
process.env.MACRODATA_CONFIG_PATH || join(homedir(), '.config', 'macrodata', 'config.json');
|
|
52
52
|
|
|
53
53
|
cachedRemoteConfig = null;
|
|
54
54
|
if (existsSync(configPath)) {
|
|
55
55
|
try {
|
|
56
|
-
const config = JSON.parse(readFileSync(configPath,
|
|
56
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
57
57
|
const embedding = config.embedding;
|
|
58
58
|
if (
|
|
59
59
|
embedding &&
|
|
60
|
-
embedding.provider ===
|
|
61
|
-
typeof embedding.endpoint ===
|
|
62
|
-
typeof embedding.model ===
|
|
60
|
+
embedding.provider === 'openai-compatible' &&
|
|
61
|
+
typeof embedding.endpoint === 'string' &&
|
|
62
|
+
typeof embedding.model === 'string'
|
|
63
63
|
) {
|
|
64
64
|
cachedRemoteConfig = embedding as RemoteEmbeddingConfig;
|
|
65
65
|
}
|
|
@@ -92,11 +92,11 @@ function resolveApiKey(config: RemoteEmbeddingConfig): string | undefined {
|
|
|
92
92
|
async function embedRemote(
|
|
93
93
|
texts: string[],
|
|
94
94
|
config: RemoteEmbeddingConfig,
|
|
95
|
-
kind:
|
|
95
|
+
kind: 'passage' | 'query',
|
|
96
96
|
): Promise<number[][]> {
|
|
97
|
-
const url = `${config.endpoint.replace(/\/$/,
|
|
97
|
+
const url = `${config.endpoint.replace(/\/$/, '')}/embeddings`;
|
|
98
98
|
const apiKey = resolveApiKey(config);
|
|
99
|
-
const inputType = kind ===
|
|
99
|
+
const inputType = kind === 'query' ? config.query_input_type : config.input_type;
|
|
100
100
|
|
|
101
101
|
const body: Record<string, unknown> = {
|
|
102
102
|
...config.extra_body,
|
|
@@ -108,16 +108,16 @@ async function embedRemote(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const response = await fetch(url, {
|
|
111
|
-
method:
|
|
111
|
+
method: 'POST',
|
|
112
112
|
headers: {
|
|
113
|
-
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
114
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
115
115
|
},
|
|
116
116
|
body: JSON.stringify(body),
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
if (!response.ok) {
|
|
120
|
-
const errText = await response.text().catch(() =>
|
|
120
|
+
const errText = await response.text().catch(() => '');
|
|
121
121
|
throw new Error(
|
|
122
122
|
`Remote embedding request failed: ${response.status} ${response.statusText} ${errText.slice(0, 300)}`,
|
|
123
123
|
);
|
|
@@ -141,7 +141,7 @@ async function embedRemote(
|
|
|
141
141
|
async function embedRemoteBatched(
|
|
142
142
|
texts: string[],
|
|
143
143
|
config: RemoteEmbeddingConfig,
|
|
144
|
-
kind:
|
|
144
|
+
kind: 'passage' | 'query',
|
|
145
145
|
): Promise<number[][]> {
|
|
146
146
|
const batchSize = config.batch_size && config.batch_size > 0 ? config.batch_size : 64;
|
|
147
147
|
const results: number[][] = [];
|
|
@@ -173,16 +173,16 @@ async function getEmbeddingPipeline(): Promise<FeatureExtractionPipeline> {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
pipelineLoading = (async () => {
|
|
176
|
-
const { pipeline } = await import(
|
|
177
|
-
return pipeline(
|
|
176
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
177
|
+
return pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
|
|
178
178
|
// Use quantized model for faster loading
|
|
179
|
-
dtype:
|
|
179
|
+
dtype: 'q8',
|
|
180
180
|
});
|
|
181
181
|
})();
|
|
182
182
|
|
|
183
183
|
try {
|
|
184
184
|
embeddingPipeline = await pipelineLoading;
|
|
185
|
-
console.log(
|
|
185
|
+
console.log('[Embeddings] Model loaded successfully');
|
|
186
186
|
return embeddingPipeline;
|
|
187
187
|
} finally {
|
|
188
188
|
pipelineLoading = null;
|
|
@@ -199,7 +199,7 @@ async function embedLocal(texts: string[]): Promise<number[][]> {
|
|
|
199
199
|
for (let i = 0; i < texts.length; i += batchSize) {
|
|
200
200
|
const batch = texts.slice(i, i + batchSize);
|
|
201
201
|
const outputs = await pipe(batch, {
|
|
202
|
-
pooling:
|
|
202
|
+
pooling: 'mean',
|
|
203
203
|
normalize: true,
|
|
204
204
|
});
|
|
205
205
|
|
|
@@ -229,7 +229,7 @@ export async function embed(text: string): Promise<number[]> {
|
|
|
229
229
|
export async function embedQuery(text: string): Promise<number[]> {
|
|
230
230
|
const remote = getRemoteEmbeddingConfig();
|
|
231
231
|
if (remote) {
|
|
232
|
-
const [vector] = await embedRemoteBatched([text], remote,
|
|
232
|
+
const [vector] = await embedRemoteBatched([text], remote, 'query');
|
|
233
233
|
return vector;
|
|
234
234
|
}
|
|
235
235
|
const [vector] = await embedLocal([text]);
|
|
@@ -244,7 +244,7 @@ export async function embedBatch(texts: string[]): Promise<number[][]> {
|
|
|
244
244
|
|
|
245
245
|
const remote = getRemoteEmbeddingConfig();
|
|
246
246
|
if (remote) {
|
|
247
|
-
return embedRemoteBatched(texts, remote,
|
|
247
|
+
return embedRemoteBatched(texts, remote, 'passage');
|
|
248
248
|
}
|
|
249
249
|
return embedLocal(texts);
|
|
250
250
|
}
|
|
@@ -255,7 +255,7 @@ export async function embedBatch(texts: string[]): Promise<number[][]> {
|
|
|
255
255
|
*/
|
|
256
256
|
export async function preloadModel(): Promise<void> {
|
|
257
257
|
if (getRemoteEmbeddingConfig()) {
|
|
258
|
-
console.log(
|
|
258
|
+
console.log('[Embeddings] Remote embedding provider configured, skipping local model load');
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
261
|
await getEmbeddingPipeline();
|