@cortask/core 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/index.js CHANGED
@@ -934,6 +934,8 @@ function createProvider(id, apiKey) {
934
934
 
935
935
  // src/agent/runner.ts
936
936
  import crypto2 from "crypto";
937
+ import fs2 from "fs/promises";
938
+ import nodePath from "path";
937
939
 
938
940
  // src/agent/system-prompt.ts
939
941
  function buildSystemPrompt(ctx) {
@@ -960,7 +962,12 @@ Current date and time: ${(/* @__PURE__ */ new Date()).toLocaleString("en-US", {
960
962
  - If a task requires multiple steps, break it down and work through each step
961
963
  - After creating or generating a file the user would want to download (documents, images, exports, etc.), always call show_file so it appears as a downloadable card in the chat
962
964
  - When the user wants to view file contents inline (e.g. "show me the file here"), use the artifact tool with the path parameter to read from disk. NEVER re-output file contents as the content parameter \u2014 use path instead so it loads instantly
963
- - When the user shares personal info, preferences, or important context, proactively use memory_append to save it. Use global memory for user info (name, preferences), project memory for project-specific context. Don't ask \u2014 just save and briefly acknowledge.
965
+ - **Memory system**: You have two memory mechanisms:
966
+ - **Pinned notes** (memory.md): A concise, curated summary of the most important context. Read via memory_read, rewritten via memory_save. Keep this short \u2014 it's loaded into every conversation. Use memory_save to consolidate when it grows too long.
967
+ - **Searchable memory** (database): Individual facts indexed for search. Use memory_append to save a note (appends to pinned notes AND indexes in the database). Use memory_search to recall previously saved context.
968
+ - When the user shares personal info, preferences, or important context, use memory_append to save it. Use global scope for user info (name, preferences), project scope for project-specific context. Don't ask \u2014 just save and briefly acknowledge.
969
+ - When a topic might relate to prior conversations, use memory_search to check for relevant memories before answering.
970
+ - If memory.md gets long, use memory_read then memory_save to consolidate it into a concise summary. Don't let it become a log \u2014 keep it to key facts only.
964
971
 
965
972
  ## File Organization
966
973
  When creating or saving files, organize them into logical subdirectories rather than dumping everything in the workspace root. Choose a clear, descriptive directory structure based on the content. For example:
@@ -1188,12 +1195,13 @@ var AgentRunner = class {
1188
1195
  };
1189
1196
  return buildSystemPrompt(ctx);
1190
1197
  }
1191
- createToolContext(sessionId, runId) {
1198
+ createToolContext(sessionId, runId, workspaceId) {
1192
1199
  return {
1193
1200
  workspacePath: this.deps.getWorkspacePath(),
1194
1201
  dataDir: this.deps.getDataDir(),
1195
1202
  sessionId,
1196
1203
  runId,
1204
+ workspaceId,
1197
1205
  requestPermission: async (req) => {
1198
1206
  if (this.deps.onPermissionRequest) {
1199
1207
  return this.deps.onPermissionRequest(req);
@@ -1205,22 +1213,53 @@ var AgentRunner = class {
1205
1213
  return this.deps.onQuestionnaireRequest(req);
1206
1214
  }
1207
1215
  return {};
1208
- }
1216
+ },
1217
+ memoryManager: this.deps.memoryManager
1209
1218
  };
1210
1219
  }
1220
+ async readFileReferences(refs) {
1221
+ if (!refs?.length) return [];
1222
+ const workspacePath = this.deps.getWorkspacePath();
1223
+ const parts = [];
1224
+ const MAX_FILE_SIZE = 50 * 1024;
1225
+ for (const ref of refs) {
1226
+ const fullPath = nodePath.resolve(workspacePath, ref);
1227
+ if (!fullPath.startsWith(nodePath.resolve(workspacePath))) continue;
1228
+ try {
1229
+ const stat = await fs2.stat(fullPath);
1230
+ if (stat.size > MAX_FILE_SIZE) {
1231
+ parts.push({ type: "text", text: `[File: ${ref}]
1232
+ (Skipped \u2014 file exceeds 50KB limit)` });
1233
+ continue;
1234
+ }
1235
+ const content = await fs2.readFile(fullPath, "utf-8");
1236
+ parts.push({ type: "text", text: `[File: ${ref}]
1237
+ \`\`\`
1238
+ ${content}
1239
+ \`\`\`` });
1240
+ } catch {
1241
+ parts.push({ type: "text", text: `[File: ${ref}]
1242
+ (File not found or unreadable)` });
1243
+ }
1244
+ }
1245
+ return parts;
1246
+ }
1211
1247
  async run(params) {
1212
1248
  const runId = crypto2.randomUUID();
1213
1249
  const sessionId = params.sessionId ?? crypto2.randomUUID();
1214
1250
  const { config } = this.deps;
1215
1251
  const messages = await this.deps.getSessionMessages(sessionId);
1216
- if (params.attachments?.length) {
1252
+ const fileRefParts = await this.readFileReferences(params.fileReferences);
1253
+ const hasMultipart = (params.attachments?.length ?? 0) > 0 || fileRefParts.length > 0;
1254
+ if (hasMultipart) {
1217
1255
  const parts = [
1218
1256
  { type: "text", text: params.prompt },
1219
- ...params.attachments.map((a) => ({
1257
+ ...fileRefParts,
1258
+ ...params.attachments?.map((a) => ({
1220
1259
  type: "image",
1221
1260
  imageBase64: a.base64,
1222
1261
  mimeType: a.mimeType
1223
- }))
1262
+ })) ?? []
1224
1263
  ];
1225
1264
  messages.push({ role: "user", content: parts });
1226
1265
  } else {
@@ -1228,7 +1267,7 @@ var AgentRunner = class {
1228
1267
  }
1229
1268
  const systemPrompt = await this.buildSystemPromptText();
1230
1269
  const toolDefs = this.buildToolDefinitions();
1231
- const toolContext = this.createToolContext(sessionId, runId);
1270
+ const toolContext = this.createToolContext(sessionId, runId, params.workspaceId);
1232
1271
  let totalInputTokens = 0;
1233
1272
  let totalOutputTokens = 0;
1234
1273
  const allToolCalls = [];
@@ -1335,14 +1374,17 @@ var AgentRunner = class {
1335
1374
  const { config } = this.deps;
1336
1375
  const signal = params.signal;
1337
1376
  const messages = await this.deps.getSessionMessages(sessionId);
1338
- if (params.attachments?.length) {
1377
+ const streamFileRefParts = await this.readFileReferences(params.fileReferences);
1378
+ const streamHasMultipart = (params.attachments?.length ?? 0) > 0 || streamFileRefParts.length > 0;
1379
+ if (streamHasMultipart) {
1339
1380
  const parts = [
1340
1381
  { type: "text", text: params.prompt },
1341
- ...params.attachments.map((a) => ({
1382
+ ...streamFileRefParts,
1383
+ ...params.attachments?.map((a) => ({
1342
1384
  type: "image",
1343
1385
  imageBase64: a.base64,
1344
1386
  mimeType: a.mimeType
1345
- }))
1387
+ })) ?? []
1346
1388
  ];
1347
1389
  messages.push({ role: "user", content: parts });
1348
1390
  } else {
@@ -1350,7 +1392,7 @@ var AgentRunner = class {
1350
1392
  }
1351
1393
  const systemPrompt = await this.buildSystemPromptText();
1352
1394
  const toolDefs = this.buildToolDefinitions();
1353
- const toolContext = this.createToolContext(sessionId, runId);
1395
+ const toolContext = this.createToolContext(sessionId, runId, params.workspaceId);
1354
1396
  let totalInputTokens = 0;
1355
1397
  let totalOutputTokens = 0;
1356
1398
  let finalContent = "";
@@ -1595,7 +1637,7 @@ var AgentRunner = class {
1595
1637
  };
1596
1638
 
1597
1639
  // src/agent/tools/read-file.ts
1598
- import fs2 from "fs/promises";
1640
+ import fs3 from "fs/promises";
1599
1641
  import path2 from "path";
1600
1642
  var readFileTool = {
1601
1643
  definition: {
@@ -1622,7 +1664,7 @@ var readFileTool = {
1622
1664
  };
1623
1665
  }
1624
1666
  try {
1625
- const content = await fs2.readFile(filePath, "utf-8");
1667
+ const content = await fs3.readFile(filePath, "utf-8");
1626
1668
  return { toolCallId: "", content };
1627
1669
  } catch (err) {
1628
1670
  return {
@@ -1635,7 +1677,7 @@ var readFileTool = {
1635
1677
  };
1636
1678
 
1637
1679
  // src/agent/tools/write-file.ts
1638
- import fs3 from "fs/promises";
1680
+ import fs4 from "fs/promises";
1639
1681
  import path3 from "path";
1640
1682
  var writeFileTool = {
1641
1683
  definition: {
@@ -1679,8 +1721,8 @@ var writeFileTool = {
1679
1721
  };
1680
1722
  }
1681
1723
  try {
1682
- await fs3.mkdir(path3.dirname(filePath), { recursive: true });
1683
- await fs3.writeFile(filePath, args.content, "utf-8");
1724
+ await fs4.mkdir(path3.dirname(filePath), { recursive: true });
1725
+ await fs4.writeFile(filePath, args.content, "utf-8");
1684
1726
  return {
1685
1727
  toolCallId: "",
1686
1728
  content: `File written: ${args.path}`
@@ -1696,7 +1738,7 @@ var writeFileTool = {
1696
1738
  };
1697
1739
 
1698
1740
  // src/agent/tools/list-files.ts
1699
- import fs4 from "fs/promises";
1741
+ import fs5 from "fs/promises";
1700
1742
  import path4 from "path";
1701
1743
  var listFilesTool = {
1702
1744
  definition: {
@@ -1725,7 +1767,7 @@ var listFilesTool = {
1725
1767
  };
1726
1768
  }
1727
1769
  try {
1728
- const entries = await fs4.readdir(dirPath, { withFileTypes: true });
1770
+ const entries = await fs5.readdir(dirPath, { withFileTypes: true });
1729
1771
  const lines = entries.map((e) => {
1730
1772
  const suffix = e.isDirectory() ? "/" : "";
1731
1773
  return `${e.name}${suffix}`;
@@ -2073,9 +2115,11 @@ var questionnaireTool = {
2073
2115
  };
2074
2116
 
2075
2117
  // src/agent/tools/memory.ts
2076
- import fs5 from "fs/promises";
2118
+ import crypto3 from "crypto";
2119
+ import fs6 from "fs/promises";
2077
2120
  import path5 from "path";
2078
2121
  var CORTASK_DIR = ".cortask";
2122
+ var MEMORY_FILE_WARNING_LINES = 200;
2079
2123
  function getMemoryPath(scope, context) {
2080
2124
  if (scope === "global") {
2081
2125
  return path5.join(context.dataDir, "memory.md");
@@ -2101,7 +2145,7 @@ var memoryReadTool = {
2101
2145
  const scope = args.scope || "project";
2102
2146
  const memoryPath = getMemoryPath(scope, context);
2103
2147
  try {
2104
- const content = await fs5.readFile(memoryPath, "utf-8");
2148
+ const content = await fs6.readFile(memoryPath, "utf-8");
2105
2149
  return { toolCallId: "", content };
2106
2150
  } catch {
2107
2151
  return {
@@ -2142,8 +2186,8 @@ var memorySaveTool = {
2142
2186
  }
2143
2187
  const scope = args.scope || "project";
2144
2188
  const memoryPath = getMemoryPath(scope, context);
2145
- await fs5.mkdir(path5.dirname(memoryPath), { recursive: true });
2146
- await fs5.writeFile(memoryPath, content, "utf-8");
2189
+ await fs6.mkdir(path5.dirname(memoryPath), { recursive: true });
2190
+ await fs6.writeFile(memoryPath, content, "utf-8");
2147
2191
  return {
2148
2192
  toolCallId: "",
2149
2193
  content: `${scope === "global" ? "Global" : "Project"} memory saved (${content.length} chars)`
@@ -2153,7 +2197,7 @@ var memorySaveTool = {
2153
2197
  var memoryAppendTool = {
2154
2198
  definition: {
2155
2199
  name: "memory_append",
2156
- description: "Append a note to long-term memory. Use scope 'global' for user preferences that apply across all projects, or 'project' (default) for project-specific context.",
2200
+ description: "Append a note to long-term memory. Use scope 'global' for user preferences that apply across all projects, or 'project' (default) for project-specific context. The note is also indexed in the searchable memory database.",
2157
2201
  inputSchema: {
2158
2202
  type: "object",
2159
2203
  properties: {
@@ -2181,24 +2225,104 @@ var memoryAppendTool = {
2181
2225
  }
2182
2226
  const scope = args.scope || "project";
2183
2227
  const memoryPath = getMemoryPath(scope, context);
2184
- await fs5.mkdir(path5.dirname(memoryPath), { recursive: true });
2228
+ await fs6.mkdir(path5.dirname(memoryPath), { recursive: true });
2185
2229
  let existing = "";
2186
2230
  try {
2187
- existing = await fs5.readFile(memoryPath, "utf-8");
2231
+ existing = await fs6.readFile(memoryPath, "utf-8");
2188
2232
  } catch {
2189
2233
  existing = scope === "global" ? "# Global Memory\n\nUser preferences and context that apply across all projects.\n" : "# Project Memory\n\nThis file is used by Cortask to remember important context about this project.\n";
2190
2234
  }
2191
2235
  const updated = existing.trimEnd() + "\n\n" + note + "\n";
2192
- await fs5.writeFile(memoryPath, updated, "utf-8");
2236
+ await fs6.writeFile(memoryPath, updated, "utf-8");
2237
+ if (context.memoryManager && scope === "project") {
2238
+ try {
2239
+ await context.memoryManager.index([
2240
+ {
2241
+ id: crypto3.randomUUID(),
2242
+ content: note,
2243
+ source: "agent",
2244
+ sessionId: context.sessionId,
2245
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2246
+ metadata: { tool: "memory_append" }
2247
+ }
2248
+ ]);
2249
+ } catch {
2250
+ }
2251
+ }
2252
+ const lineCount = updated.split("\n").length;
2253
+ const warning = lineCount > MEMORY_FILE_WARNING_LINES ? ` Warning: memory.md is now ${lineCount} lines. Consider using memory_save to consolidate and rewrite it as a concise summary.` : "";
2193
2254
  return {
2194
2255
  toolCallId: "",
2195
- content: `Note appended to ${scope} memory.`
2256
+ content: `Note appended to ${scope} memory.${warning}`
2196
2257
  };
2197
2258
  }
2198
2259
  };
2260
+ var memorySearchTool = {
2261
+ definition: {
2262
+ name: "memory_search",
2263
+ description: "Search long-term memory using semantic and keyword search. Returns the most relevant memories matching the query. Use this to recall previously saved context, decisions, or notes.",
2264
+ inputSchema: {
2265
+ type: "object",
2266
+ properties: {
2267
+ query: {
2268
+ type: "string",
2269
+ description: "The search query to find relevant memories."
2270
+ },
2271
+ limit: {
2272
+ type: "number",
2273
+ description: "Maximum number of results to return (default: 5)."
2274
+ }
2275
+ },
2276
+ required: ["query"]
2277
+ }
2278
+ },
2279
+ async execute(args, context) {
2280
+ const query = args.query;
2281
+ if (!query) {
2282
+ return {
2283
+ toolCallId: "",
2284
+ content: "Error: query is required",
2285
+ isError: true
2286
+ };
2287
+ }
2288
+ if (!context.memoryManager) {
2289
+ return {
2290
+ toolCallId: "",
2291
+ content: "Memory search is not available for this workspace."
2292
+ };
2293
+ }
2294
+ const limit = typeof args.limit === "number" ? args.limit : 5;
2295
+ try {
2296
+ const results = await context.memoryManager.search(query, limit);
2297
+ if (results.length === 0) {
2298
+ return {
2299
+ toolCallId: "",
2300
+ content: "No memories found matching the query."
2301
+ };
2302
+ }
2303
+ const formatted = results.map((r, i) => {
2304
+ const date = r.entry.createdAt ? new Date(r.entry.createdAt).toLocaleDateString() : "unknown";
2305
+ return `${i + 1}. [${r.matchType}] (score: ${r.score.toFixed(3)}, ${date})
2306
+ ${r.entry.content}`;
2307
+ }).join("\n\n");
2308
+ return {
2309
+ toolCallId: "",
2310
+ content: `Found ${results.length} memories:
2311
+
2312
+ ${formatted}`
2313
+ };
2314
+ } catch (err) {
2315
+ return {
2316
+ toolCallId: "",
2317
+ content: `Memory search error: ${err instanceof Error ? err.message : String(err)}`,
2318
+ isError: true
2319
+ };
2320
+ }
2321
+ }
2322
+ };
2199
2323
 
2200
2324
  // src/agent/tools/show-file.ts
2201
- import fs6 from "fs/promises";
2325
+ import fs7 from "fs/promises";
2202
2326
  import path6 from "path";
2203
2327
  var showFileTool = {
2204
2328
  definition: {
@@ -2229,7 +2353,7 @@ var showFileTool = {
2229
2353
  };
2230
2354
  }
2231
2355
  try {
2232
- const stat = await fs6.stat(filePath);
2356
+ const stat = await fs7.stat(filePath);
2233
2357
  return {
2234
2358
  toolCallId: "",
2235
2359
  content: JSON.stringify({
@@ -2251,11 +2375,11 @@ var showFileTool = {
2251
2375
 
2252
2376
  // src/agent/tools/data-file.ts
2253
2377
  import path8 from "path";
2254
- import fs8 from "fs";
2378
+ import fs9 from "fs";
2255
2379
  import Database from "better-sqlite3";
2256
2380
 
2257
2381
  // src/data/parser.ts
2258
- import fs7 from "fs";
2382
+ import fs8 from "fs";
2259
2383
  import path7 from "path";
2260
2384
  var MAX_RAW_SIZE = 5e5;
2261
2385
  var MAX_ROWS = 5e4;
@@ -2286,7 +2410,7 @@ function detectColumnTypes(data) {
2286
2410
  });
2287
2411
  }
2288
2412
  function parseCsv(filePath, forceDelimiter) {
2289
- let raw = fs7.readFileSync(filePath, "utf-8");
2413
+ let raw = fs8.readFileSync(filePath, "utf-8");
2290
2414
  if (raw.length > MAX_RAW_SIZE) {
2291
2415
  raw = raw.slice(0, MAX_RAW_SIZE);
2292
2416
  const lastNewline = raw.lastIndexOf("\n");
@@ -2352,7 +2476,7 @@ function splitCsvLines(text, delimiter) {
2352
2476
  return result;
2353
2477
  }
2354
2478
  function parseJson(filePath) {
2355
- let raw = fs7.readFileSync(filePath, "utf-8");
2479
+ let raw = fs8.readFileSync(filePath, "utf-8");
2356
2480
  if (raw.length > MAX_RAW_SIZE) {
2357
2481
  throw new Error(
2358
2482
  `JSON file too large (${(raw.length / 1024).toFixed(0)}KB, max ${MAX_RAW_SIZE / 1024}KB). Consider converting to CSV first.`
@@ -2472,7 +2596,7 @@ var dataFileTool = {
2472
2596
  isError: true
2473
2597
  };
2474
2598
  }
2475
- if (!fs8.existsSync(filePath)) {
2599
+ if (!fs9.existsSync(filePath)) {
2476
2600
  return {
2477
2601
  toolCallId: "",
2478
2602
  content: `File not found: ${rawPath}`,
@@ -2717,12 +2841,12 @@ function createCronTool(cronService) {
2717
2841
  required: ["action"]
2718
2842
  }
2719
2843
  },
2720
- execute: async (args, _context) => {
2844
+ execute: async (args, context) => {
2721
2845
  const action = args.action;
2722
2846
  try {
2723
2847
  switch (action) {
2724
2848
  case "list": {
2725
- const jobs = cronService.list();
2849
+ const jobs = cronService.list(context.workspaceId);
2726
2850
  if (jobs.length === 0) {
2727
2851
  return { toolCallId: "", content: "No cron jobs found." };
2728
2852
  }
@@ -2756,7 +2880,8 @@ function createCronTool(cronService) {
2756
2880
  schedule,
2757
2881
  prompt,
2758
2882
  enabled: args.enabled ?? true,
2759
- delivery
2883
+ delivery,
2884
+ workspaceId: context.workspaceId
2760
2885
  });
2761
2886
  return {
2762
2887
  toolCallId: "",
@@ -2879,7 +3004,7 @@ function buildSchedule(args) {
2879
3004
  }
2880
3005
 
2881
3006
  // src/agent/tools/artifact.ts
2882
- import fs9 from "fs/promises";
3007
+ import fs10 from "fs/promises";
2883
3008
  import path9 from "path";
2884
3009
  function createArtifactTool(artifactStore) {
2885
3010
  return {
@@ -2932,7 +3057,7 @@ function createArtifactTool(artifactStore) {
2932
3057
  };
2933
3058
  }
2934
3059
  try {
2935
- content = await fs9.readFile(resolved, "utf-8");
3060
+ content = await fs10.readFile(resolved, "utf-8");
2936
3061
  } catch {
2937
3062
  return {
2938
3063
  toolCallId: "",
@@ -3077,9 +3202,9 @@ async function ensureBrowser() {
3077
3202
  async screenshot() {
3078
3203
  const tmpPath = `${process.env.TEMP || "/tmp"}/ab-screenshot-${Date.now()}.png`;
3079
3204
  await exec2(["screenshot", tmpPath]);
3080
- const fs20 = await import("fs/promises");
3081
- const buf = await fs20.readFile(tmpPath);
3082
- await fs20.unlink(tmpPath).catch(() => {
3205
+ const fs22 = await import("fs/promises");
3206
+ const buf = await fs22.readFile(tmpPath);
3207
+ await fs22.unlink(tmpPath).catch(() => {
3083
3208
  });
3084
3209
  return buf;
3085
3210
  },
@@ -3408,7 +3533,7 @@ ${result.content}`,
3408
3533
  }
3409
3534
 
3410
3535
  // src/agent/tools/subagent.ts
3411
- import crypto3 from "crypto";
3536
+ import crypto4 from "crypto";
3412
3537
 
3413
3538
  // src/agent/subagent/types.ts
3414
3539
  var SUBAGENT_DEFAULTS = {
@@ -3545,7 +3670,7 @@ async function executeSpawn(args, context) {
3545
3670
  isError: true
3546
3671
  };
3547
3672
  }
3548
- const childRunId = crypto3.randomUUID();
3673
+ const childRunId = crypto4.randomUUID();
3549
3674
  const childSessionId = `subagent:${childRunId}`;
3550
3675
  const abortController = new AbortController();
3551
3676
  const record = {
@@ -3675,24 +3800,24 @@ ${lines.join("\n")}` };
3675
3800
  }
3676
3801
 
3677
3802
  // src/agent/tools/skill.ts
3678
- import fs14 from "fs/promises";
3803
+ import fs15 from "fs/promises";
3679
3804
 
3680
3805
  // src/skills/writer.ts
3681
- import fs12 from "fs/promises";
3806
+ import fs13 from "fs/promises";
3682
3807
  import path12 from "path";
3683
3808
 
3684
3809
  // src/skills/loader.ts
3685
- import fs11 from "fs/promises";
3810
+ import fs12 from "fs/promises";
3686
3811
  import path11 from "path";
3687
3812
  import matter from "gray-matter";
3688
3813
 
3689
3814
  // src/skills/credential-schema.ts
3690
- import fs10 from "fs/promises";
3815
+ import fs11 from "fs/promises";
3691
3816
  import path10 from "path";
3692
3817
  async function loadCredentialSchema(skillDir) {
3693
3818
  const schemaPath = path10.join(skillDir, "credentials.json");
3694
3819
  try {
3695
- const raw = await fs10.readFile(schemaPath, "utf-8");
3820
+ const raw = await fs11.readFile(schemaPath, "utf-8");
3696
3821
  const parsed = JSON.parse(raw);
3697
3822
  if (!parsed.credentials || !Array.isArray(parsed.credentials)) {
3698
3823
  return void 0;
@@ -3775,13 +3900,13 @@ function getEligibleSkills(skills) {
3775
3900
  async function scanSkillDir(dir, source) {
3776
3901
  const skills = [];
3777
3902
  try {
3778
- const entries = await fs11.readdir(dir, { withFileTypes: true });
3903
+ const entries = await fs12.readdir(dir, { withFileTypes: true });
3779
3904
  for (const entry of entries) {
3780
3905
  if (!entry.isDirectory()) continue;
3781
3906
  const skillDir = path11.join(dir, entry.name);
3782
3907
  const skillMdPath = path11.join(skillDir, "SKILL.md");
3783
3908
  try {
3784
- const raw = await fs11.readFile(skillMdPath, "utf-8");
3909
+ const raw = await fs12.readFile(skillMdPath, "utf-8");
3785
3910
  const parsed = matter(raw);
3786
3911
  const manifest = parsed.data;
3787
3912
  if (!manifest.name) {
@@ -3792,13 +3917,13 @@ async function scanSkillDir(dir, source) {
3792
3917
  }
3793
3918
  let skillSource = source;
3794
3919
  try {
3795
- await fs11.access(path11.join(skillDir, ".origin"));
3920
+ await fs12.access(path11.join(skillDir, ".origin"));
3796
3921
  skillSource = "git";
3797
3922
  } catch {
3798
3923
  }
3799
3924
  let hasCodeTools = false;
3800
3925
  try {
3801
- await fs11.access(path11.join(skillDir, "index.js"));
3926
+ await fs12.access(path11.join(skillDir, "index.js"));
3802
3927
  hasCodeTools = true;
3803
3928
  } catch {
3804
3929
  }
@@ -3945,13 +4070,13 @@ async function createSkill(skillsDir, name, rawContent) {
3945
4070
  if (nameErr) throw new Error(nameErr);
3946
4071
  const skillDir = path12.join(skillsDir, name);
3947
4072
  try {
3948
- await fs12.access(skillDir);
4073
+ await fs13.access(skillDir);
3949
4074
  throw new Error(`Skill "${name}" already exists`);
3950
4075
  } catch (err) {
3951
4076
  if (err.message.includes("already exists")) throw err;
3952
4077
  }
3953
- await fs12.mkdir(skillDir, { recursive: true });
3954
- await fs12.writeFile(path12.join(skillDir, "SKILL.md"), rawContent, "utf-8");
4078
+ await fs13.mkdir(skillDir, { recursive: true });
4079
+ await fs13.writeFile(path12.join(skillDir, "SKILL.md"), rawContent, "utf-8");
3955
4080
  clearSkillCache();
3956
4081
  return { name, path: skillDir };
3957
4082
  }
@@ -3959,17 +4084,17 @@ async function updateSkill(skillsDir, name, rawContent) {
3959
4084
  const skillDir = path12.join(skillsDir, name);
3960
4085
  const skillMdPath = path12.join(skillDir, "SKILL.md");
3961
4086
  try {
3962
- await fs12.access(skillMdPath);
4087
+ await fs13.access(skillMdPath);
3963
4088
  } catch {
3964
4089
  throw new Error(`Skill "${name}" not found`);
3965
4090
  }
3966
- await fs12.writeFile(skillMdPath, rawContent, "utf-8");
4091
+ await fs13.writeFile(skillMdPath, rawContent, "utf-8");
3967
4092
  clearSkillCache();
3968
4093
  }
3969
4094
  async function readSkillFile(skillsDir, name) {
3970
4095
  const skillMdPath = path12.join(skillsDir, name, "SKILL.md");
3971
4096
  try {
3972
- return await fs12.readFile(skillMdPath, "utf-8");
4097
+ return await fs13.readFile(skillMdPath, "utf-8");
3973
4098
  } catch {
3974
4099
  throw new Error(`Skill "${name}" not found`);
3975
4100
  }
@@ -3977,10 +4102,10 @@ async function readSkillFile(skillsDir, name) {
3977
4102
 
3978
4103
  // src/skills/installer.ts
3979
4104
  import { execFile as execFile2 } from "child_process";
3980
- import fs13 from "fs/promises";
4105
+ import fs14 from "fs/promises";
3981
4106
  import path13 from "path";
3982
4107
  async function installSkillFromGit(gitUrl, skillsDir) {
3983
- await fs13.mkdir(skillsDir, { recursive: true });
4108
+ await fs14.mkdir(skillsDir, { recursive: true });
3984
4109
  const repoName = gitUrl.split("/").pop()?.replace(/\.git$/, "");
3985
4110
  if (!repoName) {
3986
4111
  throw new Error(`Invalid git URL: ${gitUrl}`);
@@ -3990,27 +4115,27 @@ async function installSkillFromGit(gitUrl, skillsDir) {
3990
4115
  }
3991
4116
  const targetDir = path13.join(skillsDir, repoName);
3992
4117
  try {
3993
- await fs13.access(targetDir);
4118
+ await fs14.access(targetDir);
3994
4119
  throw new Error(`Skill "${repoName}" is already installed`);
3995
4120
  } catch (err) {
3996
4121
  if (err.message.includes("already installed")) throw err;
3997
4122
  }
3998
4123
  await execFileAsync("git", ["clone", "--depth", "1", gitUrl, targetDir]);
3999
4124
  try {
4000
- await fs13.access(path13.join(targetDir, "SKILL.md"));
4125
+ await fs14.access(path13.join(targetDir, "SKILL.md"));
4001
4126
  } catch {
4002
- await fs13.rm(targetDir, { recursive: true, force: true });
4127
+ await fs14.rm(targetDir, { recursive: true, force: true });
4003
4128
  throw new Error(
4004
4129
  `Invalid skill: no SKILL.md found in ${repoName}`
4005
4130
  );
4006
4131
  }
4007
- await fs13.writeFile(
4132
+ await fs14.writeFile(
4008
4133
  path13.join(targetDir, ".origin"),
4009
4134
  gitUrl,
4010
4135
  "utf-8"
4011
4136
  );
4012
4137
  try {
4013
- await fs13.rm(path13.join(targetDir, ".git"), {
4138
+ await fs14.rm(path13.join(targetDir, ".git"), {
4014
4139
  recursive: true,
4015
4140
  force: true
4016
4141
  });
@@ -4023,11 +4148,11 @@ async function installSkillFromGit(gitUrl, skillsDir) {
4023
4148
  async function removeSkill(skillName, skillsDir) {
4024
4149
  const targetDir = path13.join(skillsDir, skillName);
4025
4150
  try {
4026
- await fs13.access(targetDir);
4151
+ await fs14.access(targetDir);
4027
4152
  } catch {
4028
4153
  throw new Error(`Skill "${skillName}" not found`);
4029
4154
  }
4030
- await fs13.rm(targetDir, { recursive: true, force: true });
4155
+ await fs14.rm(targetDir, { recursive: true, force: true });
4031
4156
  clearSkillCache();
4032
4157
  logger.info(`Removed skill "${skillName}"`, "skills");
4033
4158
  }
@@ -4119,7 +4244,7 @@ function createSkillTool(userSkillsDir, bundledSkillNames) {
4119
4244
  }
4120
4245
  case "list": {
4121
4246
  try {
4122
- const entries = await fs14.readdir(userSkillsDir, {
4247
+ const entries = await fs15.readdir(userSkillsDir, {
4123
4248
  withFileTypes: true
4124
4249
  });
4125
4250
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
@@ -4186,22 +4311,23 @@ var builtinTools = [
4186
4311
  memoryReadTool,
4187
4312
  memorySaveTool,
4188
4313
  memoryAppendTool,
4314
+ memorySearchTool,
4189
4315
  showFileTool,
4190
4316
  dataFileTool
4191
4317
  ];
4192
4318
 
4193
4319
  // src/credentials/store.ts
4194
- import crypto4 from "crypto";
4195
- import fs15 from "fs/promises";
4320
+ import crypto5 from "crypto";
4321
+ import fs16 from "fs/promises";
4196
4322
  import path14 from "path";
4197
4323
  function deriveKey(secret, salt) {
4198
- return crypto4.scryptSync(secret, salt, 32);
4324
+ return crypto5.scryptSync(secret, salt, 32);
4199
4325
  }
4200
4326
  function encrypt(text, secret) {
4201
- const salt = crypto4.randomBytes(16);
4327
+ const salt = crypto5.randomBytes(16);
4202
4328
  const key = deriveKey(secret, salt);
4203
- const iv = crypto4.randomBytes(12);
4204
- const cipher = crypto4.createCipheriv("aes-256-gcm", key, iv);
4329
+ const iv = crypto5.randomBytes(12);
4330
+ const cipher = crypto5.createCipheriv("aes-256-gcm", key, iv);
4205
4331
  const encrypted = Buffer.concat([
4206
4332
  cipher.update(text, "utf-8"),
4207
4333
  cipher.final()
@@ -4219,7 +4345,7 @@ function decrypt(enc, secret) {
4219
4345
  const key = deriveKey(secret, salt);
4220
4346
  const iv = Buffer.from(enc.iv, "hex");
4221
4347
  const tag = Buffer.from(enc.tag, "hex");
4222
- const decipher = crypto4.createDecipheriv("aes-256-gcm", key, iv);
4348
+ const decipher = crypto5.createDecipheriv("aes-256-gcm", key, iv);
4223
4349
  decipher.setAuthTag(tag);
4224
4350
  const decrypted = Buffer.concat([
4225
4351
  decipher.update(Buffer.from(enc.data, "hex")),
@@ -4238,7 +4364,7 @@ var EncryptedCredentialStore = class {
4238
4364
  async load() {
4239
4365
  if (this.store) return this.store;
4240
4366
  try {
4241
- const raw = await fs15.readFile(this.filePath, "utf-8");
4367
+ const raw = await fs16.readFile(this.filePath, "utf-8");
4242
4368
  this.store = JSON.parse(raw);
4243
4369
  } catch {
4244
4370
  this.store = { version: 1, entries: {} };
@@ -4247,13 +4373,13 @@ var EncryptedCredentialStore = class {
4247
4373
  }
4248
4374
  async save() {
4249
4375
  if (!this.store) return;
4250
- await fs15.mkdir(path14.dirname(this.filePath), { recursive: true });
4376
+ await fs16.mkdir(path14.dirname(this.filePath), { recursive: true });
4251
4377
  const tmp = `${this.filePath}.${process.pid}.tmp`;
4252
- await fs15.writeFile(tmp, JSON.stringify(this.store, null, 2), {
4378
+ await fs16.writeFile(tmp, JSON.stringify(this.store, null, 2), {
4253
4379
  encoding: "utf-8",
4254
4380
  mode: 384
4255
4381
  });
4256
- await fs15.rename(tmp, this.filePath);
4382
+ await fs16.rename(tmp, this.filePath);
4257
4383
  }
4258
4384
  async get(key) {
4259
4385
  const store = await this.load();
@@ -4289,11 +4415,11 @@ async function getOrCreateSecret(dataDir) {
4289
4415
  if (secretFromEnv) return secretFromEnv;
4290
4416
  const keyPath = path14.join(dataDir, "master.key");
4291
4417
  try {
4292
- return await fs15.readFile(keyPath, "utf-8");
4418
+ return await fs16.readFile(keyPath, "utf-8");
4293
4419
  } catch {
4294
- const key = crypto4.randomBytes(32).toString("hex");
4295
- await fs15.mkdir(path14.dirname(keyPath), { recursive: true });
4296
- await fs15.writeFile(keyPath, key, { mode: 384 });
4420
+ const key = crypto5.randomBytes(32).toString("hex");
4421
+ await fs16.mkdir(path14.dirname(keyPath), { recursive: true });
4422
+ await fs16.writeFile(keyPath, key, { mode: 384 });
4297
4423
  return key;
4298
4424
  }
4299
4425
  }
@@ -4303,7 +4429,7 @@ function credentialKey(category, ...parts) {
4303
4429
 
4304
4430
  // src/config/schema.ts
4305
4431
  import { z } from "zod";
4306
- import fs16 from "fs/promises";
4432
+ import fs17 from "fs/promises";
4307
4433
  import path15 from "path";
4308
4434
  var providerConfigSchema = z.object({
4309
4435
  model: z.string().optional()
@@ -4329,6 +4455,10 @@ var spendingLimitSchema = z.object({
4329
4455
  maxCostUsd: z.number().nonnegative().optional(),
4330
4456
  period: z.enum(["daily", "weekly", "monthly"]).default("monthly")
4331
4457
  });
4458
+ var memoryConfigSchema = z.object({
4459
+ embeddingProvider: z.enum(["local", "openai", "google", "ollama"]).default("local"),
4460
+ embeddingModel: z.string().optional()
4461
+ });
4332
4462
  var serverConfigSchema = z.object({
4333
4463
  port: z.number().int().min(1).max(65535).default(3777),
4334
4464
  host: z.string().default("127.0.0.1")
@@ -4353,11 +4483,12 @@ var cortaskConfigSchema = z.object({
4353
4483
  spending: spendingLimitSchema.default({}),
4354
4484
  channels: channelsConfigSchema.default({}),
4355
4485
  skills: skillsConfigSchema.default({}),
4486
+ memory: memoryConfigSchema.default({}),
4356
4487
  server: serverConfigSchema.default({})
4357
4488
  });
4358
4489
  async function loadConfig(configPath) {
4359
4490
  try {
4360
- const raw = await fs16.readFile(configPath, "utf-8");
4491
+ const raw = await fs17.readFile(configPath, "utf-8");
4361
4492
  const resolved = raw.replace(/\$\{(\w+)\}/g, (_, name) => {
4362
4493
  return process.env[name] ?? "";
4363
4494
  });
@@ -4377,14 +4508,14 @@ async function loadConfig(configPath) {
4377
4508
  }
4378
4509
  async function saveConfig(configPath, config) {
4379
4510
  const dir = path15.dirname(configPath);
4380
- await fs16.mkdir(dir, { recursive: true });
4511
+ await fs17.mkdir(dir, { recursive: true });
4381
4512
  if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
4382
4513
  const { stringify } = await import("yaml").catch(() => ({
4383
4514
  stringify: (obj) => JSON.stringify(obj, null, 2)
4384
4515
  }));
4385
- await fs16.writeFile(configPath, stringify(config), "utf-8");
4516
+ await fs17.writeFile(configPath, stringify(config), "utf-8");
4386
4517
  } else {
4387
- await fs16.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
4518
+ await fs17.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
4388
4519
  }
4389
4520
  }
4390
4521
  function getDataDir() {
@@ -4437,7 +4568,7 @@ function getDefaultModel(type) {
4437
4568
  }
4438
4569
 
4439
4570
  // src/workspace/manager.ts
4440
- import fs17 from "fs/promises";
4571
+ import fs18 from "fs/promises";
4441
4572
  import path16 from "path";
4442
4573
  import Database2 from "better-sqlite3";
4443
4574
  var CORTASK_DIR2 = ".cortask";
@@ -4506,12 +4637,12 @@ var WorkspaceManager = class {
4506
4637
  const now = (/* @__PURE__ */ new Date()).toISOString();
4507
4638
  const absPath = rootPath ? path16.resolve(rootPath) : path16.join(this.dataDir, "projects", id);
4508
4639
  const cortaskDir = path16.join(absPath, CORTASK_DIR2);
4509
- await fs17.mkdir(cortaskDir, { recursive: true });
4640
+ await fs18.mkdir(cortaskDir, { recursive: true });
4510
4641
  const memoryPath = path16.join(cortaskDir, "memory.md");
4511
4642
  try {
4512
- await fs17.access(memoryPath);
4643
+ await fs18.access(memoryPath);
4513
4644
  } catch {
4514
- await fs17.writeFile(
4645
+ await fs18.writeFile(
4515
4646
  memoryPath,
4516
4647
  "# Project Memory\n\nThis file is used by Cortask to remember important context about this project.\n",
4517
4648
  "utf-8"
@@ -4576,27 +4707,27 @@ var WorkspaceManager = class {
4576
4707
  async readMemory(workspacePath) {
4577
4708
  const memoryPath = path16.join(workspacePath, CORTASK_DIR2, "memory.md");
4578
4709
  try {
4579
- return await fs17.readFile(memoryPath, "utf-8");
4710
+ return await fs18.readFile(memoryPath, "utf-8");
4580
4711
  } catch {
4581
4712
  return void 0;
4582
4713
  }
4583
4714
  }
4584
4715
  async writeMemory(workspacePath, content) {
4585
4716
  const memoryPath = path16.join(workspacePath, CORTASK_DIR2, "memory.md");
4586
- await fs17.mkdir(path16.dirname(memoryPath), { recursive: true });
4587
- await fs17.writeFile(memoryPath, content, "utf-8");
4717
+ await fs18.mkdir(path16.dirname(memoryPath), { recursive: true });
4718
+ await fs18.writeFile(memoryPath, content, "utf-8");
4588
4719
  }
4589
4720
  async readGlobalMemory(dataDir) {
4590
4721
  const memoryPath = path16.join(dataDir, "memory.md");
4591
4722
  try {
4592
- return await fs17.readFile(memoryPath, "utf-8");
4723
+ return await fs18.readFile(memoryPath, "utf-8");
4593
4724
  } catch {
4594
4725
  return void 0;
4595
4726
  }
4596
4727
  }
4597
4728
  async writeGlobalMemory(dataDir, content) {
4598
- await fs17.mkdir(dataDir, { recursive: true });
4599
- await fs17.writeFile(path16.join(dataDir, "memory.md"), content, "utf-8");
4729
+ await fs18.mkdir(dataDir, { recursive: true });
4730
+ await fs18.writeFile(path16.join(dataDir, "memory.md"), content, "utf-8");
4600
4731
  }
4601
4732
  getSessionDbPath(workspacePath) {
4602
4733
  return path16.join(workspacePath, CORTASK_DIR2, "sessions.db");
@@ -4715,9 +4846,9 @@ var SessionStore = class {
4715
4846
 
4716
4847
  // src/session/migrate.ts
4717
4848
  import Database4 from "better-sqlite3";
4718
- import fs18 from "fs";
4849
+ import fs19 from "fs";
4719
4850
  function migrateSessionDatabase(dbPath) {
4720
- if (!fs18.existsSync(dbPath)) {
4851
+ if (!fs19.existsSync(dbPath)) {
4721
4852
  logger.debug(`Session database does not exist yet: ${dbPath}`, "migration");
4722
4853
  return;
4723
4854
  }
@@ -4799,7 +4930,7 @@ function migrateAllWorkspaces(workspaces) {
4799
4930
  }
4800
4931
 
4801
4932
  // src/skills/tools.ts
4802
- import fs19 from "fs/promises";
4933
+ import fs20 from "fs/promises";
4803
4934
  import path17 from "path";
4804
4935
  async function buildSkillTools(skills, credentialStore) {
4805
4936
  const toolDefs = [];
@@ -4930,12 +5061,12 @@ async function executeHttpTool(template, args, skill, credentialStore, workspace
4930
5061
  };
4931
5062
  }
4932
5063
  const tempDir = path17.join(workspacePath, "_temp");
4933
- await fs19.mkdir(tempDir, { recursive: true });
5064
+ await fs20.mkdir(tempDir, { recursive: true });
4934
5065
  const isJson = looksLikeJson(content);
4935
5066
  const ext = isJson ? "json" : "txt";
4936
5067
  const filename = `${template.name}_${Date.now()}.${ext}`;
4937
5068
  const filePath = path17.join(tempDir, filename);
4938
- await fs19.writeFile(filePath, content, "utf-8");
5069
+ await fs20.writeFile(filePath, content, "utf-8");
4939
5070
  const summary = buildResponseSummary(content, isJson);
4940
5071
  return {
4941
5072
  toolCallId: "",
@@ -5120,7 +5251,7 @@ async function revokeSkillOAuth2(skillName, credentialId, credentialStore) {
5120
5251
  }
5121
5252
 
5122
5253
  // src/cron/service.ts
5123
- import crypto5 from "crypto";
5254
+ import crypto6 from "crypto";
5124
5255
  import Database5 from "better-sqlite3";
5125
5256
  var MAX_TIMER_DELAY_MS = 6e4;
5126
5257
  var MIN_REFIRE_GAP_MS = 2e3;
@@ -5288,7 +5419,7 @@ var CronService = class {
5288
5419
  }
5289
5420
  // CRUD
5290
5421
  add(input) {
5291
- const id = crypto5.randomUUID();
5422
+ const id = crypto6.randomUUID();
5292
5423
  const now = (/* @__PURE__ */ new Date()).toISOString();
5293
5424
  const job = {
5294
5425
  id,
@@ -5460,13 +5591,13 @@ function rowToJob(row) {
5460
5591
  }
5461
5592
 
5462
5593
  // src/artifacts/store.ts
5463
- import crypto6 from "crypto";
5594
+ import crypto7 from "crypto";
5464
5595
  var TTL_MS = 24 * 60 * 60 * 1e3;
5465
5596
  var ArtifactStore = class {
5466
5597
  store = /* @__PURE__ */ new Map();
5467
5598
  create(type, title, content) {
5468
5599
  this.pruneExpired();
5469
- const id = crypto6.randomUUID();
5600
+ const id = crypto7.randomUUID();
5470
5601
  const mimeType = getMimeType(type);
5471
5602
  const artifact = {
5472
5603
  id,
@@ -5765,6 +5896,449 @@ var TemplateStore = class {
5765
5896
  this.db.prepare("DELETE FROM prompt_templates WHERE id = ?").run(id);
5766
5897
  }
5767
5898
  };
5899
+
5900
+ // src/memory/manager.ts
5901
+ import crypto9 from "crypto";
5902
+
5903
+ // src/memory/store.ts
5904
+ import fs21 from "fs";
5905
+ import path18 from "path";
5906
+ import Database9 from "better-sqlite3";
5907
+ var MemoryStore = class {
5908
+ db;
5909
+ ftsAvailable = false;
5910
+ constructor(dbPath) {
5911
+ const dir = path18.dirname(dbPath);
5912
+ if (!fs21.existsSync(dir)) {
5913
+ fs21.mkdirSync(dir, { recursive: true });
5914
+ }
5915
+ this.db = new Database9(dbPath);
5916
+ this.db.pragma("journal_mode = WAL");
5917
+ this.init();
5918
+ }
5919
+ init() {
5920
+ this.db.exec(`
5921
+ CREATE TABLE IF NOT EXISTS chunks (
5922
+ id TEXT PRIMARY KEY,
5923
+ content TEXT NOT NULL,
5924
+ source TEXT NOT NULL DEFAULT 'manual',
5925
+ session_id TEXT,
5926
+ created_at TEXT NOT NULL,
5927
+ metadata TEXT NOT NULL DEFAULT '{}',
5928
+ provider_model TEXT
5929
+ );
5930
+ `);
5931
+ this.db.exec(`
5932
+ CREATE TABLE IF NOT EXISTS embedding_cache (
5933
+ content_hash TEXT PRIMARY KEY,
5934
+ provider_model TEXT NOT NULL,
5935
+ embedding BLOB NOT NULL,
5936
+ created_at TEXT NOT NULL
5937
+ );
5938
+ `);
5939
+ try {
5940
+ this.db.exec(`
5941
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
5942
+ id UNINDEXED,
5943
+ content,
5944
+ source UNINDEXED,
5945
+ content='chunks',
5946
+ content_rowid='rowid'
5947
+ );
5948
+ `);
5949
+ this.ftsAvailable = true;
5950
+ } catch {
5951
+ this.ftsAvailable = false;
5952
+ }
5953
+ if (this.ftsAvailable) {
5954
+ this.db.exec(`
5955
+ CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
5956
+ INSERT INTO chunks_fts(rowid, id, content, source)
5957
+ VALUES (new.rowid, new.id, new.content, new.source);
5958
+ END;
5959
+ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
5960
+ INSERT INTO chunks_fts(chunks_fts, rowid, id, content, source)
5961
+ VALUES ('delete', old.rowid, old.id, old.content, old.source);
5962
+ END;
5963
+ CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
5964
+ INSERT INTO chunks_fts(chunks_fts, rowid, id, content, source)
5965
+ VALUES ('delete', old.rowid, old.id, old.content, old.source);
5966
+ INSERT INTO chunks_fts(rowid, id, content, source)
5967
+ VALUES (new.rowid, new.id, new.content, new.source);
5968
+ END;
5969
+ `);
5970
+ }
5971
+ }
5972
+ insertChunk(chunk) {
5973
+ this.db.prepare(
5974
+ `INSERT OR REPLACE INTO chunks (id, content, source, session_id, created_at, metadata, provider_model)
5975
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
5976
+ ).run(
5977
+ chunk.id,
5978
+ chunk.content,
5979
+ chunk.source,
5980
+ chunk.session_id,
5981
+ chunk.created_at,
5982
+ chunk.metadata,
5983
+ chunk.provider_model
5984
+ );
5985
+ }
5986
+ insertChunks(chunks) {
5987
+ const insert = this.db.prepare(
5988
+ `INSERT OR REPLACE INTO chunks (id, content, source, session_id, created_at, metadata, provider_model)
5989
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
5990
+ );
5991
+ const tx = this.db.transaction((items) => {
5992
+ for (const chunk of items) {
5993
+ insert.run(
5994
+ chunk.id,
5995
+ chunk.content,
5996
+ chunk.source,
5997
+ chunk.session_id,
5998
+ chunk.created_at,
5999
+ chunk.metadata,
6000
+ chunk.provider_model
6001
+ );
6002
+ }
6003
+ });
6004
+ tx(chunks);
6005
+ }
6006
+ searchFTS(query, limit) {
6007
+ if (!this.ftsAvailable) return [];
6008
+ const sanitized = query.replace(/[^\w\s]/g, " ").trim();
6009
+ if (!sanitized) return [];
6010
+ const ftsQuery = sanitized.split(/\s+/).filter(Boolean).map((w) => `"${w}"`).join(" OR ");
6011
+ try {
6012
+ return this.db.prepare(
6013
+ `SELECT id, content, source, rank
6014
+ FROM chunks_fts
6015
+ WHERE chunks_fts MATCH ?
6016
+ ORDER BY rank
6017
+ LIMIT ?`
6018
+ ).all(ftsQuery, limit);
6019
+ } catch {
6020
+ return [];
6021
+ }
6022
+ }
6023
+ getChunk(id) {
6024
+ return this.db.prepare("SELECT * FROM chunks WHERE id = ?").get(id) ?? null;
6025
+ }
6026
+ listRecent(limit) {
6027
+ return this.db.prepare("SELECT * FROM chunks ORDER BY created_at DESC LIMIT ?").all(limit);
6028
+ }
6029
+ getAllChunks() {
6030
+ return this.db.prepare("SELECT * FROM chunks").all();
6031
+ }
6032
+ deleteChunk(id) {
6033
+ this.db.prepare("DELETE FROM chunks WHERE id = ?").run(id);
6034
+ }
6035
+ clearAll() {
6036
+ this.db.exec("DELETE FROM chunks");
6037
+ if (this.ftsAvailable) {
6038
+ this.db.exec("DELETE FROM chunks_fts");
6039
+ }
6040
+ this.db.exec("DELETE FROM embedding_cache");
6041
+ }
6042
+ clearEmbeddings() {
6043
+ this.db.exec("DELETE FROM embedding_cache");
6044
+ }
6045
+ getCachedEmbedding(contentHash, providerModel) {
6046
+ const row = this.db.prepare(
6047
+ "SELECT embedding FROM embedding_cache WHERE content_hash = ? AND provider_model = ?"
6048
+ ).get(contentHash, providerModel);
6049
+ if (!row) return null;
6050
+ return Array.from(new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4));
6051
+ }
6052
+ setCachedEmbedding(contentHash, providerModel, embedding) {
6053
+ const buffer = Buffer.from(new Float32Array(embedding).buffer);
6054
+ this.db.prepare(
6055
+ `INSERT OR REPLACE INTO embedding_cache (content_hash, provider_model, embedding, created_at)
6056
+ VALUES (?, ?, ?, ?)`
6057
+ ).run(contentHash, providerModel, buffer, (/* @__PURE__ */ new Date()).toISOString());
6058
+ }
6059
+ get isFTSAvailable() {
6060
+ return this.ftsAvailable;
6061
+ }
6062
+ close() {
6063
+ this.db.close();
6064
+ }
6065
+ };
6066
+
6067
+ // src/memory/embeddings.ts
6068
+ import crypto8 from "crypto";
6069
+ var DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
6070
+ var EmbeddingService = class {
6071
+ apiProvider;
6072
+ localProvider;
6073
+ model;
6074
+ store;
6075
+ constructor(opts) {
6076
+ this.apiProvider = opts.apiProvider ?? null;
6077
+ this.localProvider = opts.localProvider ?? null;
6078
+ this.model = opts.model ?? DEFAULT_EMBEDDING_MODEL;
6079
+ this.store = opts.store;
6080
+ }
6081
+ get providerId() {
6082
+ if (this.localProvider) return "local";
6083
+ return this.apiProvider?.id ?? "unknown";
6084
+ }
6085
+ async embed(texts) {
6086
+ const results = new Array(texts.length).fill(null);
6087
+ const toEmbed = [];
6088
+ for (let i = 0; i < texts.length; i++) {
6089
+ const hash = this.hashContent(texts[i]);
6090
+ const cached = this.store.getCachedEmbedding(hash, this.providerModel);
6091
+ if (cached) {
6092
+ results[i] = cached;
6093
+ } else {
6094
+ toEmbed.push({ index: i, text: texts[i] });
6095
+ }
6096
+ }
6097
+ if (toEmbed.length > 0) {
6098
+ const embeddings = await this.generateEmbeddings(
6099
+ toEmbed.map((t) => t.text)
6100
+ );
6101
+ for (let i = 0; i < toEmbed.length; i++) {
6102
+ const embedding = embeddings[i];
6103
+ const entry = toEmbed[i];
6104
+ results[entry.index] = embedding;
6105
+ const hash = this.hashContent(entry.text);
6106
+ this.store.setCachedEmbedding(hash, this.providerModel, embedding);
6107
+ }
6108
+ }
6109
+ return results;
6110
+ }
6111
+ async embedSingle(text) {
6112
+ const [result] = await this.embed([text]);
6113
+ return result;
6114
+ }
6115
+ async generateEmbeddings(texts) {
6116
+ if (this.localProvider) {
6117
+ return this.localProvider.embedBatch(texts);
6118
+ }
6119
+ if (this.apiProvider) {
6120
+ const result = await this.apiProvider.embed({
6121
+ model: this.model,
6122
+ inputs: texts
6123
+ });
6124
+ return result.embeddings;
6125
+ }
6126
+ throw new Error("No embedding provider available");
6127
+ }
6128
+ get providerModel() {
6129
+ if (this.localProvider) {
6130
+ return `local:${this.localProvider.model}`;
6131
+ }
6132
+ return `${this.apiProvider?.id ?? "unknown"}:${this.model}`;
6133
+ }
6134
+ hashContent(content) {
6135
+ return crypto8.createHash("sha256").update(content).digest("hex");
6136
+ }
6137
+ };
6138
+
6139
+ // src/memory/search.ts
6140
+ function cosineSimilarity(a, b) {
6141
+ if (a.length !== b.length) return 0;
6142
+ let dot = 0;
6143
+ let normA = 0;
6144
+ let normB = 0;
6145
+ for (let i = 0; i < a.length; i++) {
6146
+ dot += a[i] * b[i];
6147
+ normA += a[i] * a[i];
6148
+ normB += b[i] * b[i];
6149
+ }
6150
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
6151
+ return denom === 0 ? 0 : dot / denom;
6152
+ }
6153
+ function bm25RankToScore(rank) {
6154
+ return 1 / (1 + Math.abs(rank));
6155
+ }
6156
+ async function hybridSearch(store, embeddingService, query, limit) {
6157
+ const resultMap = /* @__PURE__ */ new Map();
6158
+ const ftsResults = store.searchFTS(query, limit * 2);
6159
+ for (const fts of ftsResults) {
6160
+ const entry = chunkToEntry(store.getChunk(fts.id));
6161
+ if (!entry) continue;
6162
+ resultMap.set(fts.id, {
6163
+ entry,
6164
+ score: bm25RankToScore(fts.rank),
6165
+ matchType: "fts"
6166
+ });
6167
+ }
6168
+ if (embeddingService) {
6169
+ try {
6170
+ const queryEmbedding = await embeddingService.embedSingle(query);
6171
+ const allChunks = store.getAllChunks();
6172
+ if (allChunks.length > 0) {
6173
+ const chunkTexts = allChunks.map((c) => c.content);
6174
+ const embeddings = await embeddingService.embed(chunkTexts);
6175
+ const scored = [];
6176
+ for (let i = 0; i < allChunks.length; i++) {
6177
+ scored.push({
6178
+ id: allChunks[i].id,
6179
+ score: cosineSimilarity(queryEmbedding, embeddings[i])
6180
+ });
6181
+ }
6182
+ scored.sort((a, b) => b.score - a.score);
6183
+ for (const item of scored.slice(0, limit * 2)) {
6184
+ const existing = resultMap.get(item.id);
6185
+ if (existing) {
6186
+ existing.score = Math.max(existing.score, item.score);
6187
+ existing.matchType = "hybrid";
6188
+ } else {
6189
+ const entry = chunkToEntry(store.getChunk(item.id));
6190
+ if (entry) {
6191
+ resultMap.set(item.id, {
6192
+ entry,
6193
+ score: item.score,
6194
+ matchType: "vector"
6195
+ });
6196
+ }
6197
+ }
6198
+ }
6199
+ }
6200
+ } catch {
6201
+ }
6202
+ }
6203
+ return Array.from(resultMap.values()).sort((a, b) => b.score - a.score).slice(0, limit);
6204
+ }
6205
+ function chunkToEntry(chunk) {
6206
+ if (!chunk) return null;
6207
+ return {
6208
+ id: chunk.id,
6209
+ content: chunk.content,
6210
+ source: chunk.source,
6211
+ sessionId: chunk.session_id ?? void 0,
6212
+ createdAt: chunk.created_at,
6213
+ metadata: JSON.parse(chunk.metadata)
6214
+ };
6215
+ }
6216
+
6217
+ // src/memory/manager.ts
6218
+ var MemoryManager = class {
6219
+ store;
6220
+ embeddingService = null;
6221
+ constructor(opts) {
6222
+ this.store = new MemoryStore(opts.dbPath);
6223
+ if (opts.localProvider) {
6224
+ this.embeddingService = new EmbeddingService({
6225
+ localProvider: opts.localProvider,
6226
+ store: this.store
6227
+ });
6228
+ } else if (opts.apiProvider) {
6229
+ this.embeddingService = new EmbeddingService({
6230
+ apiProvider: opts.apiProvider,
6231
+ model: opts.embeddingModel,
6232
+ store: this.store
6233
+ });
6234
+ }
6235
+ }
6236
+ async index(entries) {
6237
+ const chunks = entries.map((e) => ({
6238
+ id: e.id || crypto9.randomUUID(),
6239
+ content: e.content,
6240
+ source: e.source,
6241
+ session_id: e.sessionId ?? null,
6242
+ created_at: e.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
6243
+ metadata: JSON.stringify(e.metadata || {}),
6244
+ provider_model: null
6245
+ }));
6246
+ this.store.insertChunks(chunks);
6247
+ if (this.embeddingService && chunks.length > 0) {
6248
+ try {
6249
+ await this.embeddingService.embed(chunks.map((c) => c.content));
6250
+ } catch {
6251
+ }
6252
+ }
6253
+ }
6254
+ async search(query, limit = 5) {
6255
+ return hybridSearch(this.store, this.embeddingService, query, limit);
6256
+ }
6257
+ async list(limit = 20) {
6258
+ const rows = this.store.listRecent(limit);
6259
+ return rows.map((row) => ({
6260
+ id: row.id,
6261
+ content: row.content,
6262
+ source: row.source,
6263
+ sessionId: row.session_id ?? void 0,
6264
+ createdAt: row.created_at,
6265
+ metadata: JSON.parse(row.metadata)
6266
+ }));
6267
+ }
6268
+ async delete(id) {
6269
+ this.store.deleteChunk(id);
6270
+ }
6271
+ async clearEmbeddings() {
6272
+ this.store.clearEmbeddings();
6273
+ }
6274
+ async clear() {
6275
+ this.store.clearAll();
6276
+ }
6277
+ close() {
6278
+ this.store.close();
6279
+ }
6280
+ };
6281
+
6282
+ // src/memory/embeddings-local.ts
6283
+ var DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf";
6284
+ function normalizeEmbedding(vec) {
6285
+ const sanitized = vec.map((v) => Number.isFinite(v) ? v : 0);
6286
+ const magnitude = Math.sqrt(
6287
+ sanitized.reduce((sum, v) => sum + v * v, 0)
6288
+ );
6289
+ if (magnitude < 1e-10) return sanitized;
6290
+ return sanitized.map((v) => v / magnitude);
6291
+ }
6292
+ async function createLocalEmbeddingProvider(modelPath) {
6293
+ const resolvedPath = modelPath?.trim() || DEFAULT_LOCAL_MODEL;
6294
+ const { getLlama, resolveModelFile, LlamaLogLevel } = await import("node-llama-cpp");
6295
+ let llama = null;
6296
+ let model = null;
6297
+ let context = null;
6298
+ const ensureContext = async () => {
6299
+ if (!llama) {
6300
+ llama = await getLlama({ logLevel: LlamaLogLevel.error });
6301
+ }
6302
+ if (!model) {
6303
+ const resolved = await resolveModelFile(resolvedPath);
6304
+ model = await llama.loadModel({ modelPath: resolved });
6305
+ }
6306
+ if (!context) {
6307
+ context = await model.createEmbeddingContext();
6308
+ }
6309
+ return context;
6310
+ };
6311
+ return {
6312
+ id: "local",
6313
+ model: resolvedPath,
6314
+ async embedQuery(text) {
6315
+ const ctx = await ensureContext();
6316
+ const embedding = await ctx.getEmbeddingFor(text);
6317
+ return normalizeEmbedding(Array.from(embedding.vector));
6318
+ },
6319
+ async embedBatch(texts) {
6320
+ const ctx = await ensureContext();
6321
+ const results = await Promise.all(
6322
+ texts.map(async (text) => {
6323
+ const embedding = await ctx.getEmbeddingFor(text);
6324
+ return normalizeEmbedding(Array.from(embedding.vector));
6325
+ })
6326
+ );
6327
+ return results;
6328
+ },
6329
+ dispose() {
6330
+ if (context) {
6331
+ context.dispose?.();
6332
+ context = null;
6333
+ }
6334
+ if (model) {
6335
+ model.dispose?.();
6336
+ model = null;
6337
+ }
6338
+ llama = null;
6339
+ }
6340
+ };
6341
+ }
5768
6342
  export {
5769
6343
  AVAILABLE_PROVIDERS,
5770
6344
  AgentRunner,
@@ -5775,6 +6349,7 @@ export {
5775
6349
  GoogleProvider,
5776
6350
  GrokProvider,
5777
6351
  MODEL_DEFINITIONS,
6352
+ MemoryManager,
5778
6353
  MiniMaxProvider,
5779
6354
  ModelStore,
5780
6355
  MoonshotProvider,
@@ -5802,6 +6377,7 @@ export {
5802
6377
  createArtifactTool,
5803
6378
  createBrowserTool,
5804
6379
  createCronTool,
6380
+ createLocalEmbeddingProvider,
5805
6381
  createProvider,
5806
6382
  createSkill,
5807
6383
  createSkillTool,