@geoql/mdr 0.0.1

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.
Files changed (46) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +152 -0
  3. package/USAGE.md +59 -0
  4. package/bin/detect-user.sh +53 -0
  5. package/bin/index-conversations.ts +34 -0
  6. package/bin/macrodata-daemon.ts +28 -0
  7. package/bin/macrodata-hook.sh +277 -0
  8. package/dist/bin/index-conversations.js +31 -0
  9. package/dist/bin/macrodata-daemon.js +30 -0
  10. package/dist/opencode/context.js +210 -0
  11. package/dist/opencode/conversations.js +367 -0
  12. package/dist/opencode/index.js +155 -0
  13. package/dist/opencode/journal.js +108 -0
  14. package/dist/opencode/logger.js +29 -0
  15. package/dist/opencode/search.js +210 -0
  16. package/dist/opencode/skills/macrodata-distill/SKILL.md +171 -0
  17. package/dist/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
  18. package/dist/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
  19. package/dist/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
  20. package/dist/opencode/tools.js +367 -0
  21. package/dist/src/config.js +55 -0
  22. package/dist/src/conversations.js +513 -0
  23. package/dist/src/daemon.js +582 -0
  24. package/dist/src/detect-user.js +73 -0
  25. package/dist/src/embeddings.js +190 -0
  26. package/dist/src/index.js +413 -0
  27. package/dist/src/indexer.js +286 -0
  28. package/opencode/context.ts +322 -0
  29. package/opencode/conversations.ts +467 -0
  30. package/opencode/index.ts +208 -0
  31. package/opencode/journal.ts +153 -0
  32. package/opencode/logger.ts +32 -0
  33. package/opencode/search.ts +288 -0
  34. package/opencode/skills/macrodata-distill/SKILL.md +171 -0
  35. package/opencode/skills/macrodata-dreamtime/SKILL.md +120 -0
  36. package/opencode/skills/macrodata-memory-maintenance/SKILL.md +96 -0
  37. package/opencode/skills/macrodata-onboarding/SKILL.md +346 -0
  38. package/opencode/tools.ts +453 -0
  39. package/package.json +87 -0
  40. package/src/config.ts +66 -0
  41. package/src/conversations.ts +709 -0
  42. package/src/daemon.ts +785 -0
  43. package/src/detect-user.ts +97 -0
  44. package/src/embeddings.ts +262 -0
  45. package/src/index.ts +726 -0
  46. package/src/indexer.ts +394 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Detect user information for onboarding
3
+ * Returns JSON with system, git, github, and code directory info
4
+ */
5
+
6
+ import { execSync } from "child_process";
7
+ import { existsSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+
11
+ interface GitInfo {
12
+ name: string;
13
+ email: string;
14
+ }
15
+
16
+ interface GitHubInfo {
17
+ login?: string;
18
+ name?: string;
19
+ blog?: string;
20
+ bio?: string;
21
+ }
22
+
23
+ export interface UserInfo {
24
+ username: string;
25
+ fullName: string;
26
+ timezone: string;
27
+ git: GitInfo;
28
+ github: GitHubInfo;
29
+ codeDirs: string[];
30
+ }
31
+
32
+ function exec(cmd: string): string {
33
+ try {
34
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
39
+
40
+ export function detectUser(): UserInfo {
41
+ // System info
42
+ const username = exec("whoami");
43
+ const fullName = exec("id -F") || exec(`getent passwd ${username} | cut -d: -f5 | cut -d, -f1`);
44
+
45
+ // Timezone
46
+ let timezone = "";
47
+ if (existsSync("/etc/timezone")) {
48
+ timezone = exec("cat /etc/timezone");
49
+ } else if (existsSync("/etc/localtime")) {
50
+ timezone = exec("readlink /etc/localtime | sed 's|.*/zoneinfo/||'");
51
+ }
52
+
53
+ // Git config
54
+ const gitName = exec("git config --global user.name");
55
+ const gitEmail = exec("git config --global user.email");
56
+
57
+ // GitHub CLI (if authenticated)
58
+ let github: GitHubInfo = {};
59
+ const ghCheck = exec("command -v gh");
60
+ if (ghCheck) {
61
+ const ghJson = exec("gh api user --jq '{login: .login, name: .name, blog: .blog, bio: .bio}'");
62
+ if (ghJson) {
63
+ try {
64
+ github = JSON.parse(ghJson);
65
+ } catch {
66
+ // Ignore parse errors
67
+ }
68
+ }
69
+ }
70
+
71
+ // Code directories that exist
72
+ const home = homedir();
73
+ const possibleDirs = [
74
+ "Repos",
75
+ "repos",
76
+ "Code",
77
+ "code",
78
+ "Projects",
79
+ "projects",
80
+ "Developer",
81
+ "dev",
82
+ "src",
83
+ ];
84
+ const codeDirs = possibleDirs.map((dir) => join(home, dir)).filter((dir) => existsSync(dir));
85
+
86
+ return {
87
+ username,
88
+ fullName,
89
+ timezone,
90
+ git: {
91
+ name: gitName,
92
+ email: gitEmail,
93
+ },
94
+ github,
95
+ codeDirs,
96
+ };
97
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Embeddings module
3
+ *
4
+ * Default: local embedding generation via Transformers.js
5
+ * (all-MiniLM-L6-v2, 384-dim), no API calls, fully offline.
6
+ *
7
+ * Optional: a remote OpenAI-compatible embeddings endpoint configured in
8
+ * ~/.config/macrodata/config.json, which offloads embedding to an API and
9
+ * avoids loading the local model entirely:
10
+ *
11
+ * {
12
+ * "embedding": {
13
+ * "provider": "openai-compatible",
14
+ * "endpoint": "https://api.example.com/v1",
15
+ * "api_key": "sk-...", // or "api_key_env": "MY_KEY_VAR"
16
+ * "model": "baai/bge-m3",
17
+ * "input_type": "passage", // optional, for models that need it
18
+ * "query_input_type": "query", // optional
19
+ * "batch_size": 64, // optional, default 64
20
+ * "extra_body": {} // optional, merged into the request
21
+ * }
22
+ * }
23
+ */
24
+
25
+ import { existsSync, readFileSync } from "fs";
26
+ import { homedir } from "os";
27
+ import { join } from "path";
28
+ import type { FeatureExtractionPipeline } from "@huggingface/transformers";
29
+
30
+ export interface RemoteEmbeddingConfig {
31
+ provider: "openai-compatible";
32
+ endpoint: string;
33
+ api_key?: string;
34
+ api_key_env?: string;
35
+ model: string;
36
+ input_type?: string;
37
+ query_input_type?: string;
38
+ batch_size?: number;
39
+ extra_body?: Record<string, unknown>;
40
+ }
41
+
42
+ // Local model produces 384-dimensional embeddings
43
+ export const EMBEDDING_DIMENSIONS = 384;
44
+
45
+ let cachedRemoteConfig: RemoteEmbeddingConfig | null | undefined;
46
+
47
+ export function getRemoteEmbeddingConfig(): RemoteEmbeddingConfig | null {
48
+ if (cachedRemoteConfig !== undefined) return cachedRemoteConfig;
49
+
50
+ const configPath =
51
+ process.env.MACRODATA_CONFIG_PATH || join(homedir(), ".config", "macrodata", "config.json");
52
+
53
+ cachedRemoteConfig = null;
54
+ if (existsSync(configPath)) {
55
+ try {
56
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
57
+ const embedding = config.embedding;
58
+ if (
59
+ embedding &&
60
+ embedding.provider === "openai-compatible" &&
61
+ typeof embedding.endpoint === "string" &&
62
+ typeof embedding.model === "string"
63
+ ) {
64
+ cachedRemoteConfig = embedding as RemoteEmbeddingConfig;
65
+ }
66
+ } catch (err) {
67
+ console.error(`[Embeddings] Failed to parse config.json, using local model: ${String(err)}`);
68
+ }
69
+ }
70
+
71
+ return cachedRemoteConfig;
72
+ }
73
+
74
+ export function resetEmbeddingConfigCache(): void {
75
+ cachedRemoteConfig = undefined;
76
+ }
77
+
78
+ // Test seam: drop the cached local pipeline so the load path can be re-exercised.
79
+ export function resetLocalPipelineForTests(): void {
80
+ embeddingPipeline = null;
81
+ pipelineLoading = null;
82
+ }
83
+
84
+ function resolveApiKey(config: RemoteEmbeddingConfig): string | undefined {
85
+ if (config.api_key_env) {
86
+ const fromEnv = process.env[config.api_key_env];
87
+ if (fromEnv) return fromEnv;
88
+ }
89
+ return config.api_key;
90
+ }
91
+
92
+ async function embedRemote(
93
+ texts: string[],
94
+ config: RemoteEmbeddingConfig,
95
+ kind: "passage" | "query",
96
+ ): Promise<number[][]> {
97
+ const url = `${config.endpoint.replace(/\/$/, "")}/embeddings`;
98
+ const apiKey = resolveApiKey(config);
99
+ const inputType = kind === "query" ? config.query_input_type : config.input_type;
100
+
101
+ const body: Record<string, unknown> = {
102
+ ...config.extra_body,
103
+ model: config.model,
104
+ input: texts,
105
+ };
106
+ if (inputType) {
107
+ body.input_type = inputType;
108
+ }
109
+
110
+ const response = await fetch(url, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
115
+ },
116
+ body: JSON.stringify(body),
117
+ });
118
+
119
+ if (!response.ok) {
120
+ const errText = await response.text().catch(() => "");
121
+ throw new Error(
122
+ `Remote embedding request failed: ${response.status} ${response.statusText} ${errText.slice(0, 300)}`,
123
+ );
124
+ }
125
+
126
+ const result = (await response.json()) as {
127
+ data?: { index?: number; embedding: number[] }[];
128
+ };
129
+
130
+ if (!result.data || result.data.length !== texts.length) {
131
+ throw new Error(
132
+ `Remote embedding response mismatch: expected ${texts.length} embeddings, got ${result.data?.length ?? 0}`,
133
+ );
134
+ }
135
+
136
+ // OpenAI-compatible APIs may return out of order; sort by index when present
137
+ const sorted = [...result.data].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
138
+ return sorted.map((d) => d.embedding);
139
+ }
140
+
141
+ async function embedRemoteBatched(
142
+ texts: string[],
143
+ config: RemoteEmbeddingConfig,
144
+ kind: "passage" | "query",
145
+ ): Promise<number[][]> {
146
+ const batchSize = config.batch_size && config.batch_size > 0 ? config.batch_size : 64;
147
+ const results: number[][] = [];
148
+
149
+ for (let i = 0; i < texts.length; i += batchSize) {
150
+ const batch = texts.slice(i, i + batchSize);
151
+ results.push(...(await embedRemote(batch, config, kind)));
152
+ }
153
+
154
+ return results;
155
+ }
156
+
157
+ // Singleton pipeline instance (expensive to create)
158
+ let embeddingPipeline: FeatureExtractionPipeline | null = null;
159
+ let pipelineLoading: Promise<FeatureExtractionPipeline> | null = null;
160
+
161
+ /**
162
+ * Get or create the local embedding pipeline
163
+ * Uses all-MiniLM-L6-v2 – good balance of quality and speed
164
+ */
165
+ async function getEmbeddingPipeline(): Promise<FeatureExtractionPipeline> {
166
+ if (embeddingPipeline) {
167
+ return embeddingPipeline;
168
+ }
169
+
170
+ // Prevent multiple concurrent pipeline creations
171
+ if (pipelineLoading) {
172
+ return pipelineLoading;
173
+ }
174
+
175
+ pipelineLoading = (async () => {
176
+ const { pipeline } = await import("@huggingface/transformers");
177
+ return pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
178
+ // Use quantized model for faster loading
179
+ dtype: "q8",
180
+ });
181
+ })();
182
+
183
+ try {
184
+ embeddingPipeline = await pipelineLoading;
185
+ console.log("[Embeddings] Model loaded successfully");
186
+ return embeddingPipeline;
187
+ } finally {
188
+ pipelineLoading = null;
189
+ }
190
+ }
191
+
192
+ async function embedLocal(texts: string[]): Promise<number[][]> {
193
+ const pipe = await getEmbeddingPipeline();
194
+
195
+ // Process in batches to avoid memory issues
196
+ const batchSize = 32;
197
+ const results: number[][] = [];
198
+
199
+ for (let i = 0; i < texts.length; i += batchSize) {
200
+ const batch = texts.slice(i, i + batchSize);
201
+ const outputs = await pipe(batch, {
202
+ pooling: "mean",
203
+ normalize: true,
204
+ });
205
+
206
+ const dims = (outputs.dims?.at(-1) as number) || EMBEDDING_DIMENSIONS;
207
+ for (let j = 0; j < batch.length; j++) {
208
+ const start = j * dims;
209
+ const end = start + dims;
210
+ results.push(Array.from((outputs.data as Float32Array).slice(start, end)));
211
+ }
212
+ }
213
+
214
+ return results;
215
+ }
216
+
217
+ /**
218
+ * Generate an embedding for a single text (indexed content)
219
+ */
220
+ export async function embed(text: string): Promise<number[]> {
221
+ const [vector] = await embedBatch([text]);
222
+ return vector;
223
+ }
224
+
225
+ /**
226
+ * Generate an embedding for a search query.
227
+ * Remote providers may use a different input_type for queries (e.g. BGE-M3).
228
+ */
229
+ export async function embedQuery(text: string): Promise<number[]> {
230
+ const remote = getRemoteEmbeddingConfig();
231
+ if (remote) {
232
+ const [vector] = await embedRemoteBatched([text], remote, "query");
233
+ return vector;
234
+ }
235
+ const [vector] = await embedLocal([text]);
236
+ return vector;
237
+ }
238
+
239
+ /**
240
+ * Generate embeddings for multiple texts (batched, indexed content)
241
+ */
242
+ export async function embedBatch(texts: string[]): Promise<number[][]> {
243
+ if (texts.length === 0) return [];
244
+
245
+ const remote = getRemoteEmbeddingConfig();
246
+ if (remote) {
247
+ return embedRemoteBatched(texts, remote, "passage");
248
+ }
249
+ return embedLocal(texts);
250
+ }
251
+
252
+ /**
253
+ * Preload the model (call during startup to avoid first-query delay).
254
+ * No-op when a remote embedding provider is configured.
255
+ */
256
+ export async function preloadModel(): Promise<void> {
257
+ if (getRemoteEmbeddingConfig()) {
258
+ console.log("[Embeddings] Remote embedding provider configured, skipping local model load");
259
+ return;
260
+ }
261
+ await getEmbeddingPipeline();
262
+ }