@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/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 "chokidar";
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 "fs";
27
- import { join, basename } from "path";
28
- import { Cron } from "croner";
29
- import { spawn, execSync } from "child_process";
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 "./config.js";
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("./indexer.js");
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("../opencode/conversations.js"),
48
- import("./conversations.js"),
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: "utf-8" }).trim();
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(), ".daemon.pid");
75
+ return join(getDaemonDir(), '.daemon.pid');
76
76
  }
77
77
 
78
78
  export function getLogFile() {
79
- return join(getDaemonDir(), ".daemon.log");
79
+ return join(getDaemonDir(), '.daemon.log');
80
80
  }
81
81
 
82
82
  export function getPendingContext() {
83
- return join(getStateRoot(), ".pending-context");
83
+ return join(getStateRoot(), '.pending-context');
84
84
  }
85
85
 
86
86
  export function getHeartbeatFile() {
87
- return join(getDaemonDir(), ".daemon.heartbeat");
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: "cron" | "once";
104
+ type: 'cron' | 'once';
105
105
  expression: string; // cron expression or ISO datetime
106
106
  description: string;
107
107
  payload: string;
108
- agent?: "opencode" | "claude"; // Which agent to trigger
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 + "\n");
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: "opencode" | "claude" | undefined,
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("No agent specified in schedule, skipping trigger");
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 || "reminder"}]
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 === "opencode") {
155
+ if (agent === 'opencode') {
156
156
  // opencode run "message" --model provider/model
157
- const args = ["run", fullMessage];
157
+ const args = ['run', fullMessage];
158
158
  if (options.model) {
159
- args.push("--model", options.model);
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("opencode")) || "npx";
164
- const finalArgs = opencodePath === "npx" ? ["opencode", ...args] : args;
163
+ const opencodePath = (await findExecutable('opencode')) || 'npx';
164
+ const finalArgs = opencodePath === 'npx' ? ['opencode', ...args] : args;
165
165
 
166
- log(`Triggering OpenCode: ${opencodePath} ${finalArgs.join(" ").substring(0, 50)}...`);
166
+ log(`Triggering OpenCode: ${opencodePath} ${finalArgs.join(' ').substring(0, 50)}...`);
167
167
 
168
- spawnSupervisedChild(opencodePath, finalArgs, "opencode");
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 = ["--print", fullMessage];
173
+ const args = ['--print', fullMessage];
174
174
 
175
175
  log(`Triggering Claude Code: claude --print "..."`);
176
176
 
177
- spawnSupervisedChild("claude", args, "claude");
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: ["ignore", "pipe", "pipe"],
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, "SIGKILL");
207
+ process.kill(-proc.pid, 'SIGKILL');
208
208
  } catch {
209
209
  try {
210
- proc.kill("SIGKILL");
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("data", (data) => {
219
+ proc.stdout?.on('data', (data) => {
220
220
  log(`[${label} stdout] ${data.toString().trim()}`);
221
221
  });
222
- proc.stderr?.on("data", (data) => {
222
+ proc.stderr?.on('data', (data) => {
223
223
  log(`[${label} stderr] ${data.toString().trim()}`);
224
224
  });
225
- proc.on("error", (err) => {
225
+ proc.on('error', (err) => {
226
226
  clearTimeout(killTimer);
227
227
  logError(`[${label}] child process error: ${String(err)}`);
228
228
  });
229
- proc.on("exit", (code, signal) => {
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, "people"),
247
- join(entitiesDir, "projects"),
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(".json"));
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), "utf-8");
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("Embedding model preloaded");
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("Starting macrodata local daemon");
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, "utf-8").trim();
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("SIGTERM", () => this.shutdown("SIGTERM"));
395
- process.on("SIGINT", () => this.shutdown("SIGINT"));
396
- process.on("SIGHUP", () => this.reload());
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("unhandledRejection", (reason) => {
400
+ process.on('unhandledRejection', (reason) => {
401
401
  logError(`Unhandled rejection (daemon continues): ${String(reason)}`);
402
402
  });
403
- process.on("uncaughtException", (err) => {
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("Daemon running");
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("add", (path) => {
448
- if (!path.endsWith(".json")) return;
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, "utf-8")) as Schedule;
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("error", (err) => {
461
+ this.schedulesWatcher.on('error', (err) => {
462
462
  logError(`Reminders watcher error: ${String(err)}`);
463
463
  });
464
464
 
465
- this.schedulesWatcher.on("change", (path) => {
466
- if (!path.endsWith(".json")) return;
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, "utf-8")) as Schedule;
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("unlink", (path) => this.onReminderUnlinked(path));
479
+ this.schedulesWatcher.on('unlink', (path) => this.onReminderUnlinked(path));
480
480
  }
481
481
 
482
482
  private onReminderUnlinked(path: string) {
483
- if (!path.endsWith(".json")) return;
484
- const id = basename(path, ".json");
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 === "cron") {
494
+ if (schedule.type === 'cron') {
495
495
  this.startCronJob(schedule);
496
496
  return;
497
497
  }
498
- if (schedule.type === "once") {
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 === "cron") {
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, "state");
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("all", (event, path) => this.onWatchedFileEvent(event, path));
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(".md")) return;
621
- if (event !== "add" && event !== "change") return;
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(), "state");
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, "utf-8");
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("Reloading config (SIGHUP)");
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("Reload complete");
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, "utf-8").trim();
747
+ const pid = readFileSync(pidFile, 'utf-8').trim();
748
748
  if (pid === process.pid.toString()) {
749
749
  unlinkSync(pidFile);
750
750
  }
@@ -3,10 +3,10 @@
3
3
  * Returns JSON with system, git, github, and code directory info
4
4
  */
5
5
 
6
- import { execSync } from "child_process";
7
- import { existsSync } from "fs";
8
- import { homedir } from "os";
9
- import { join } from "path";
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: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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("whoami");
43
- const fullName = exec("id -F") || exec(`getent passwd ${username} | cut -d: -f5 | cut -d, -f1`);
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("/etc/timezone")) {
48
- timezone = exec("cat /etc/timezone");
49
- } else if (existsSync("/etc/localtime")) {
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("git config --global user.name");
55
- const gitEmail = exec("git config --global user.email");
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("command -v gh");
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
- "Repos",
75
- "repos",
76
- "Code",
77
- "code",
78
- "Projects",
79
- "projects",
80
- "Developer",
81
- "dev",
82
- "src",
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 "fs";
26
- import { homedir } from "os";
27
- import { join } from "path";
28
- import type { FeatureExtractionPipeline } from "@huggingface/transformers";
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: "openai-compatible";
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(), ".config", "macrodata", "config.json");
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, "utf-8"));
56
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
57
57
  const embedding = config.embedding;
58
58
  if (
59
59
  embedding &&
60
- embedding.provider === "openai-compatible" &&
61
- typeof embedding.endpoint === "string" &&
62
- typeof embedding.model === "string"
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: "passage" | "query",
95
+ kind: 'passage' | 'query',
96
96
  ): Promise<number[][]> {
97
- const url = `${config.endpoint.replace(/\/$/, "")}/embeddings`;
97
+ const url = `${config.endpoint.replace(/\/$/, '')}/embeddings`;
98
98
  const apiKey = resolveApiKey(config);
99
- const inputType = kind === "query" ? config.query_input_type : config.input_type;
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: "POST",
111
+ method: 'POST',
112
112
  headers: {
113
- "Content-Type": "application/json",
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: "passage" | "query",
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("@huggingface/transformers");
177
- return pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
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: "q8",
179
+ dtype: 'q8',
180
180
  });
181
181
  })();
182
182
 
183
183
  try {
184
184
  embeddingPipeline = await pipelineLoading;
185
- console.log("[Embeddings] Model loaded successfully");
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: "mean",
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, "query");
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, "passage");
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("[Embeddings] Remote embedding provider configured, skipping local model load");
258
+ console.log('[Embeddings] Remote embedding provider configured, skipping local model load');
259
259
  return;
260
260
  }
261
261
  await getEmbeddingPipeline();