@wipcomputer/wip-ldm-os 0.4.73-alpha.12 → 0.4.73-alpha.14

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.
@@ -6,6 +6,27 @@ import { homedir } from "os";
6
6
  import { promisify } from "util";
7
7
  import { randomUUID } from "crypto";
8
8
  var execAsync = promisify(exec);
9
+ var GATEWAY_HOST = "127.0.0.1";
10
+ var DEFAULT_GATEWAY_PORT = 18789;
11
+ var DEFAULT_INBOX_PORT = 18790;
12
+ var GATEWAY_TIMEOUT_MS = 15e3;
13
+ var OP_CLI_TIMEOUT_MS = 1e4;
14
+ var EMBEDDING_API_URL = "https://api.openai.com/v1/embeddings";
15
+ var DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
16
+ var DEFAULT_EMBEDDING_DIMS = 1536;
17
+ var VECTOR_SEARCH_ROW_LIMIT = 1e3;
18
+ var RECENCY_DECAY_RATE = 0.01;
19
+ var RECENCY_FLOOR = 0.5;
20
+ var FRESHNESS_FRESH_DAYS = 3;
21
+ var FRESHNESS_RECENT_DAYS = 7;
22
+ var FRESHNESS_AGING_DAYS = 14;
23
+ var DEFAULT_SEARCH_LIMIT = 5;
24
+ var WORKSPACE_MAX_DEPTH = 4;
25
+ var WORKSPACE_MAX_EXCERPTS = 5;
26
+ var WORKSPACE_MAX_RESULTS = 10;
27
+ var SKILL_EXEC_TIMEOUT_MS = 12e4;
28
+ var SKILL_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
29
+ var MS_PER_DAY = 1e3 * 60 * 60 * 24;
9
30
  var HOME = process.env.HOME || homedir();
10
31
  var LDM_ROOT = process.env.LDM_ROOT || join(HOME, ".ldm");
11
32
  function resolveConfig(overrides) {
@@ -14,9 +35,9 @@ function resolveConfig(overrides) {
14
35
  openclawDir,
15
36
  workspaceDir: overrides?.workspaceDir || join(openclawDir, "workspace"),
16
37
  dbPath: overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
17
- inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
18
- embeddingModel: overrides?.embeddingModel || "text-embedding-3-small",
19
- embeddingDimensions: overrides?.embeddingDimensions || 1536
38
+ inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
39
+ embeddingModel: overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
40
+ embeddingDimensions: overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS
20
41
  };
21
42
  }
22
43
  function resolveConfigMulti(overrides) {
@@ -29,9 +50,9 @@ function resolveConfigMulti(overrides) {
29
50
  openclawDir,
30
51
  workspaceDir: raw.workspaceDir || overrides?.workspaceDir || join(openclawDir, "workspace"),
31
52
  dbPath: raw.dbPath || overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
32
- inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
33
- embeddingModel: raw.embeddingModel || overrides?.embeddingModel || "text-embedding-3-small",
34
- embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || 1536
53
+ inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
54
+ embeddingModel: raw.embeddingModel || overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
55
+ embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS
35
56
  };
36
57
  } catch {
37
58
  }
@@ -53,7 +74,7 @@ function resolveApiKey(openclawDir) {
53
74
  `op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null`,
54
75
  {
55
76
  env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
56
- timeout: 1e4,
77
+ timeout: OP_CLI_TIMEOUT_MS,
57
78
  encoding: "utf-8"
58
79
  }
59
80
  ).trim();
@@ -76,7 +97,7 @@ function resolveGatewayConfig(openclawDir) {
76
97
  }
77
98
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
78
99
  const token = config?.gateway?.auth?.token;
79
- const port = config?.gateway?.port || 18789;
100
+ const port = config?.gateway?.port || DEFAULT_GATEWAY_PORT;
80
101
  if (!token) {
81
102
  throw new Error("No gateway.auth.token found in openclaw.json");
82
103
  }
@@ -263,10 +284,10 @@ async function sendMessage(openclawDir, message, options) {
263
284
  const agentId = options?.agentId || "main";
264
285
  const senderLabel = options?.senderLabel || "Claude Code";
265
286
  const controller = new AbortController();
266
- const timeoutId = setTimeout(() => controller.abort(), 15e3);
287
+ const timeoutId = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
267
288
  try {
268
289
  const response = await fetch(
269
- `http://127.0.0.1:${port}/v1/chat/completions`,
290
+ `http://${GATEWAY_HOST}:${port}/v1/chat/completions`,
270
291
  {
271
292
  method: "POST",
272
293
  headers: {
@@ -308,8 +329,8 @@ async function sendMessage(openclawDir, message, options) {
308
329
  throw err;
309
330
  }
310
331
  }
311
- async function getQueryEmbedding(text, apiKey, model = "text-embedding-3-small", dimensions = 1536) {
312
- const response = await fetch("https://api.openai.com/v1/embeddings", {
332
+ async function getQueryEmbedding(text, apiKey, model = DEFAULT_EMBEDDING_MODEL, dimensions = DEFAULT_EMBEDDING_DIMS) {
333
+ const response = await fetch(EMBEDDING_API_URL, {
313
334
  method: "POST",
314
335
  headers: {
315
336
  Authorization: `Bearer ${apiKey}`,
@@ -348,15 +369,15 @@ function cosineSimilarity(a, b) {
348
369
  return denom === 0 ? 0 : dot / denom;
349
370
  }
350
371
  function recencyWeight(ageDays) {
351
- return Math.max(0.5, 1 - ageDays * 0.01);
372
+ return Math.max(RECENCY_FLOOR, 1 - ageDays * RECENCY_DECAY_RATE);
352
373
  }
353
374
  function freshnessLabel(ageDays) {
354
- if (ageDays < 3) return "fresh";
355
- if (ageDays < 7) return "recent";
356
- if (ageDays < 14) return "aging";
375
+ if (ageDays < FRESHNESS_FRESH_DAYS) return "fresh";
376
+ if (ageDays < FRESHNESS_RECENT_DAYS) return "recent";
377
+ if (ageDays < FRESHNESS_AGING_DAYS) return "aging";
357
378
  return "stale";
358
379
  }
359
- async function searchConversations(config, query, limit = 5) {
380
+ async function searchConversations(config, query, limit = DEFAULT_SEARCH_LIMIT) {
360
381
  const Database = (await import("better-sqlite3")).default;
361
382
  if (!existsSync(config.dbPath)) {
362
383
  throw new Error(`Database not found: ${config.dbPath}`);
@@ -377,12 +398,12 @@ async function searchConversations(config, query, limit = 5) {
377
398
  FROM conversation_chunks
378
399
  WHERE embedding IS NOT NULL
379
400
  ORDER BY timestamp DESC
380
- LIMIT 1000`
401
+ LIMIT ${VECTOR_SEARCH_ROW_LIMIT}`
381
402
  ).all();
382
403
  const now = Date.now();
383
404
  return rows.map((row) => {
384
405
  const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
385
- const ageDays = (now - row.timestamp) / (1e3 * 60 * 60 * 24);
406
+ const ageDays = (now - row.timestamp) / MS_PER_DAY;
386
407
  const weight = recencyWeight(ageDays);
387
408
  return {
388
409
  text: row.chunk_text,
@@ -413,7 +434,7 @@ async function searchConversations(config, query, limit = 5) {
413
434
  db.close();
414
435
  }
415
436
  }
416
- function findMarkdownFiles(dir, maxDepth = 4, depth = 0) {
437
+ function findMarkdownFiles(dir, maxDepth = WORKSPACE_MAX_DEPTH, depth = 0) {
417
438
  if (depth > maxDepth || !existsSync(dir)) return [];
418
439
  const files = [];
419
440
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -443,7 +464,7 @@ function searchWorkspace(workspaceDir, query) {
443
464
  if (score === 0) continue;
444
465
  const lines = content.split("\n");
445
466
  const excerpts = [];
446
- for (let i = 0; i < lines.length && excerpts.length < 5; i++) {
467
+ for (let i = 0; i < lines.length && excerpts.length < WORKSPACE_MAX_EXCERPTS; i++) {
447
468
  const lineLower = lines[i].toLowerCase();
448
469
  if (words.some((w) => lineLower.includes(w))) {
449
470
  const start = Math.max(0, i - 1);
@@ -455,7 +476,7 @@ function searchWorkspace(workspaceDir, query) {
455
476
  } catch {
456
477
  }
457
478
  }
458
- return results.sort((a, b) => b.score - a.score).slice(0, 10);
479
+ return results.sort((a, b) => b.score - a.score).slice(0, WORKSPACE_MAX_RESULTS);
459
480
  }
460
481
  function parseSkillFrontmatter(content) {
461
482
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -549,9 +570,8 @@ async function executeSkillScript(skillDir, scripts, scriptName, args) {
549
570
  `${interpreter} "${scriptPath}" ${args}`,
550
571
  {
551
572
  env: { ...process.env },
552
- timeout: 12e4,
553
- maxBuffer: 10 * 1024 * 1024
554
- // 10MB
573
+ timeout: SKILL_EXEC_TIMEOUT_MS,
574
+ maxBuffer: SKILL_EXEC_MAX_BUFFER
555
575
  }
556
576
  );
557
577
  return stdout || stderr || "(no output)";
@@ -8,7 +8,7 @@ import {
8
8
  searchConversations,
9
9
  searchWorkspace,
10
10
  sendMessage
11
- } from "./chunk-HFSMW37U.js";
11
+ } from "./chunk-RUQEH7GZ.js";
12
12
 
13
13
  // cli.ts
14
14
  import { existsSync, statSync } from "fs";
@@ -23,7 +23,7 @@ import {
23
23
  sendLdmMessage,
24
24
  sendMessage,
25
25
  setSessionIdentity
26
- } from "./chunk-HFSMW37U.js";
26
+ } from "./chunk-RUQEH7GZ.js";
27
27
  export {
28
28
  LDM_ROOT,
29
29
  blobToEmbedding,
@@ -15,7 +15,7 @@ import {
15
15
  sendLdmMessage,
16
16
  sendMessage,
17
17
  setSessionIdentity
18
- } from "./chunk-HFSMW37U.js";
18
+ } from "./chunk-RUQEH7GZ.js";
19
19
 
20
20
  // mcp-server.ts
21
21
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.73-alpha.12",
3
+ "version": "0.4.73-alpha.14",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -14,11 +14,11 @@ Always use a branch and PR.
14
14
 
15
15
  ## Co-authors on every commit
16
16
 
17
- List all contributors. Read co-author lines from `settings/config.json` in your workspace.
17
+ List all contributors. Read co-author lines from `~/.ldm/config.json` coAuthors field.
18
18
 
19
19
  ## Branch prefixes
20
20
 
21
- Each agent uses a prefix from `settings/config.json` agents section. Prevents collisions.
21
+ Each agent uses a prefix from `~/.ldm/config.json` agents section. Prevents collisions.
22
22
 
23
23
  ## Worktrees
24
24
 
@@ -30,4 +30,4 @@ For private/public repo pairs, all issues go on the public repo.
30
30
 
31
31
  ## On-demand reference
32
32
 
33
- Before doing repo work, read `~/wipcomputerinc/settings/docs/how-worktrees-work.md` for the full worktree workflow with commands.
33
+ Before doing repo work, read `~/wipcomputerinc/library/documentation/how-worktrees-work.md` for the full worktree workflow with commands.
@@ -39,4 +39,4 @@ Installed tools are for execution. Repo clones are for development. Use the inst
39
39
 
40
40
  ## On-demand reference
41
41
 
42
- Before releasing, read `~/wipcomputerinc/settings/docs/how-releases-work.md` for the full pipeline with commands.
42
+ Before releasing, read `~/wipcomputerinc/library/documentation/how-releases-work.md` for the full pipeline with commands.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Secret management
4
4
 
5
- Use your org's secret management tool (configured in settings/config.json). Never hardcode API keys, tokens, or credentials.
5
+ Use your org's secret management tool (configured in `~/.ldm/config.json`). Never hardcode API keys, tokens, or credentials.
6
6
 
7
7
  ## Security audit before installing anything
8
8
 
@@ -22,4 +22,4 @@ Installed tools are for execution. Repo clones are for development. Use the inst
22
22
 
23
23
  ## On-demand reference
24
24
 
25
- For the full directory map, read `~/wipcomputerinc/settings/docs/system-directories.md`.
25
+ For the full directory map, read `~/wipcomputerinc/library/documentation/system-directories.md`.
@@ -1,5 +1,5 @@
1
1
  # Writing Style
2
2
 
3
- Read writing conventions from your org's `settings/config.json` writingStyle section.
3
+ Read writing conventions from `~/.ldm/config.json` writingStyle section.
4
4
 
5
5
  **Full paths in documentation.** Never truncate paths. Always show the complete path so there's no ambiguity.
@@ -10,6 +10,31 @@ import { randomUUID } from "node:crypto";
10
10
 
11
11
  const execAsync = promisify(exec);
12
12
 
13
+ // ── Settings ─────────────────────────────────────────────────────────
14
+ // All tunable constants in one place. No magic numbers below this block.
15
+
16
+ const GATEWAY_HOST = "127.0.0.1";
17
+ const DEFAULT_GATEWAY_PORT = 18_789; // openclaw.json gateway.port fallback
18
+ const DEFAULT_INBOX_PORT = 18_790; // env LESA_BRIDGE_INBOX_PORT fallback
19
+ const GATEWAY_TIMEOUT_MS = 15_000; // max wait for gateway chat response
20
+ const OP_CLI_TIMEOUT_MS = 10_000; // max wait for 1Password CLI
21
+ const EMBEDDING_API_URL = "https://api.openai.com/v1/embeddings";
22
+ const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
23
+ const DEFAULT_EMBEDDING_DIMS = 1_536;
24
+ const VECTOR_SEARCH_ROW_LIMIT = 1_000; // max rows scanned for cosine ranking
25
+ const RECENCY_DECAY_RATE = 0.01; // per-day decay multiplier
26
+ const RECENCY_FLOOR = 0.5; // minimum recency weight
27
+ const FRESHNESS_FRESH_DAYS = 3;
28
+ const FRESHNESS_RECENT_DAYS = 7;
29
+ const FRESHNESS_AGING_DAYS = 14;
30
+ const DEFAULT_SEARCH_LIMIT = 5; // default results for searchConversations
31
+ const WORKSPACE_MAX_DEPTH = 4; // findMarkdownFiles recursion limit
32
+ const WORKSPACE_MAX_EXCERPTS = 5; // max excerpts per file in search
33
+ const WORKSPACE_MAX_RESULTS = 10; // max files returned from workspace search
34
+ const SKILL_EXEC_TIMEOUT_MS = 120_000; // max wait for skill script execution
35
+ const SKILL_EXEC_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB stdout/stderr cap
36
+ const MS_PER_DAY = 1_000 * 60 * 60 * 24;
37
+
13
38
  // ── Constants ─────────────────────────────────────────────────────────
14
39
 
15
40
  const HOME = process.env.HOME || homedir();
@@ -66,9 +91,9 @@ export function resolveConfig(overrides?: Partial<BridgeConfig>): BridgeConfig {
66
91
  openclawDir,
67
92
  workspaceDir: overrides?.workspaceDir || join(openclawDir, "workspace"),
68
93
  dbPath: overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
69
- inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
70
- embeddingModel: overrides?.embeddingModel || "text-embedding-3-small",
71
- embeddingDimensions: overrides?.embeddingDimensions || 1536,
94
+ inboxPort: overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
95
+ embeddingModel: overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
96
+ embeddingDimensions: overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS,
72
97
  };
73
98
  }
74
99
 
@@ -88,9 +113,9 @@ export function resolveConfigMulti(overrides?: Partial<BridgeConfig>): BridgeCon
88
113
  openclawDir,
89
114
  workspaceDir: raw.workspaceDir || overrides?.workspaceDir || join(openclawDir, "workspace"),
90
115
  dbPath: raw.dbPath || overrides?.dbPath || join(openclawDir, "memory", "context-embeddings.sqlite"),
91
- inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || "18790", 10),
92
- embeddingModel: raw.embeddingModel || overrides?.embeddingModel || "text-embedding-3-small",
93
- embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || 1536,
116
+ inboxPort: raw.inboxPort || overrides?.inboxPort || parseInt(process.env.LESA_BRIDGE_INBOX_PORT || String(DEFAULT_INBOX_PORT), 10),
117
+ embeddingModel: raw.embeddingModel || overrides?.embeddingModel || DEFAULT_EMBEDDING_MODEL,
118
+ embeddingDimensions: raw.embeddingDimensions || overrides?.embeddingDimensions || DEFAULT_EMBEDDING_DIMS,
94
119
  };
95
120
  } catch {
96
121
  // LDM config unreadable, fall through to legacy
@@ -123,7 +148,7 @@ export function resolveApiKey(openclawDir: string): string | null {
123
148
  `op read "op://Agent Secrets/OpenAI API/api key" 2>/dev/null`,
124
149
  {
125
150
  env: { ...process.env, OP_SERVICE_ACCOUNT_TOKEN: saToken },
126
- timeout: 10000,
151
+ timeout: OP_CLI_TIMEOUT_MS,
127
152
  encoding: "utf-8",
128
153
  }
129
154
  ).trim();
@@ -154,7 +179,7 @@ export function resolveGatewayConfig(openclawDir: string): GatewayConfig {
154
179
 
155
180
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
156
181
  const token = config?.gateway?.auth?.token;
157
- const port = config?.gateway?.port || 18789;
182
+ const port = config?.gateway?.port || DEFAULT_GATEWAY_PORT;
158
183
 
159
184
  if (!token) {
160
185
  throw new Error("No gateway.auth.token found in openclaw.json");
@@ -460,11 +485,11 @@ export async function sendMessage(
460
485
  // This ensures Parker sees CC's messages in the same stream as iMessage.
461
486
  // The OpenClaw gateway treats user: "main" as "use the default session."
462
487
  const controller = new AbortController();
463
- const timeoutId = setTimeout(() => controller.abort(), 15_000);
488
+ const timeoutId = setTimeout(() => controller.abort(), GATEWAY_TIMEOUT_MS);
464
489
 
465
490
  try {
466
491
  const response = await fetch(
467
- `http://127.0.0.1:${port}/v1/chat/completions`,
492
+ `http://${GATEWAY_HOST}:${port}/v1/chat/completions`,
468
493
  {
469
494
  method: "POST",
470
495
  headers: {
@@ -518,10 +543,10 @@ export async function sendMessage(
518
543
  export async function getQueryEmbedding(
519
544
  text: string,
520
545
  apiKey: string,
521
- model = "text-embedding-3-small",
522
- dimensions = 1536
546
+ model = DEFAULT_EMBEDDING_MODEL,
547
+ dimensions = DEFAULT_EMBEDDING_DIMS
523
548
  ): Promise<number[]> {
524
- const response = await fetch("https://api.openai.com/v1/embeddings", {
549
+ const response = await fetch(EMBEDDING_API_URL, {
525
550
  method: "POST",
526
551
  headers: {
527
552
  Authorization: `Bearer ${apiKey}`,
@@ -567,15 +592,15 @@ export function cosineSimilarity(a: number[], b: number[]): number {
567
592
  // ── Recency scoring ─────────────────────────────────────────────────
568
593
 
569
594
  function recencyWeight(ageDays: number): number {
570
- // Linear decay with floor at 0.5. Old stuff never fully disappears
595
+ // Linear decay with floor. Old stuff never fully disappears
571
596
  // but fresh context wins ties. ~50 days to hit the floor.
572
- return Math.max(0.5, 1.0 - ageDays * 0.01);
597
+ return Math.max(RECENCY_FLOOR, 1.0 - ageDays * RECENCY_DECAY_RATE);
573
598
  }
574
599
 
575
600
  function freshnessLabel(ageDays: number): "fresh" | "recent" | "aging" | "stale" {
576
- if (ageDays < 3) return "fresh";
577
- if (ageDays < 7) return "recent";
578
- if (ageDays < 14) return "aging";
601
+ if (ageDays < FRESHNESS_FRESH_DAYS) return "fresh";
602
+ if (ageDays < FRESHNESS_RECENT_DAYS) return "recent";
603
+ if (ageDays < FRESHNESS_AGING_DAYS) return "aging";
579
604
  return "stale";
580
605
  }
581
606
 
@@ -584,7 +609,7 @@ function freshnessLabel(ageDays: number): "fresh" | "recent" | "aging" | "stale"
584
609
  export async function searchConversations(
585
610
  config: BridgeConfig,
586
611
  query: string,
587
- limit = 5
612
+ limit = DEFAULT_SEARCH_LIMIT
588
613
  ): Promise<ConversationResult[]> {
589
614
  // Lazy import to avoid requiring better-sqlite3 if not needed
590
615
  const Database = (await import("better-sqlite3")).default;
@@ -611,7 +636,7 @@ export async function searchConversations(
611
636
  FROM conversation_chunks
612
637
  WHERE embedding IS NOT NULL
613
638
  ORDER BY timestamp DESC
614
- LIMIT 1000`
639
+ LIMIT ${VECTOR_SEARCH_ROW_LIMIT}`
615
640
  )
616
641
  .all() as Array<{
617
642
  chunk_text: string;
@@ -625,7 +650,7 @@ export async function searchConversations(
625
650
  return rows
626
651
  .map((row) => {
627
652
  const cosine = cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding));
628
- const ageDays = (now - row.timestamp) / (1000 * 60 * 60 * 24);
653
+ const ageDays = (now - row.timestamp) / MS_PER_DAY;
629
654
  const weight = recencyWeight(ageDays);
630
655
  return {
631
656
  text: row.chunk_text,
@@ -670,7 +695,7 @@ export async function searchConversations(
670
695
 
671
696
  // ── Workspace search ─────────────────────────────────────────────────
672
697
 
673
- export function findMarkdownFiles(dir: string, maxDepth = 4, depth = 0): string[] {
698
+ export function findMarkdownFiles(dir: string, maxDepth = WORKSPACE_MAX_DEPTH, depth = 0): string[] {
674
699
  if (depth > maxDepth || !existsSync(dir)) return [];
675
700
 
676
701
  const files: string[] = [];
@@ -705,7 +730,7 @@ export function searchWorkspace(workspaceDir: string, query: string): WorkspaceS
705
730
 
706
731
  const lines = content.split("\n");
707
732
  const excerpts: string[] = [];
708
- for (let i = 0; i < lines.length && excerpts.length < 5; i++) {
733
+ for (let i = 0; i < lines.length && excerpts.length < WORKSPACE_MAX_EXCERPTS; i++) {
709
734
  const lineLower = lines[i].toLowerCase();
710
735
  if (words.some((w) => lineLower.includes(w))) {
711
736
  const start = Math.max(0, i - 1);
@@ -720,7 +745,7 @@ export function searchWorkspace(workspaceDir: string, query: string): WorkspaceS
720
745
  }
721
746
  }
722
747
 
723
- return results.sort((a, b) => b.score - a.score).slice(0, 10);
748
+ return results.sort((a, b) => b.score - a.score).slice(0, WORKSPACE_MAX_RESULTS);
724
749
  }
725
750
 
726
751
  // ── Read workspace file ──────────────────────────────────────────────
@@ -876,8 +901,8 @@ export async function executeSkillScript(
876
901
  `${interpreter} "${scriptPath}" ${args}`,
877
902
  {
878
903
  env: { ...process.env },
879
- timeout: 120000,
880
- maxBuffer: 10 * 1024 * 1024, // 10MB
904
+ timeout: SKILL_EXEC_TIMEOUT_MS,
905
+ maxBuffer: SKILL_EXEC_MAX_BUFFER,
881
906
  }
882
907
  );
883
908
  return stdout || stderr || "(no output)";