@howaboua/opencode-chat 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/dist/config.d.ts +13 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +65 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +44 -0
  9. package/dist/script/download-model.d.ts +2 -0
  10. package/dist/script/download-model.d.ts.map +1 -0
  11. package/dist/script/download-model.js +39 -0
  12. package/dist/script/semantic-index.d.ts +2 -0
  13. package/dist/script/semantic-index.d.ts.map +1 -0
  14. package/dist/script/semantic-index.js +63 -0
  15. package/dist/semantic/chunker.d.ts +8 -0
  16. package/dist/semantic/chunker.d.ts.map +1 -0
  17. package/dist/semantic/chunker.js +163 -0
  18. package/dist/semantic/embedder.d.ts +12 -0
  19. package/dist/semantic/embedder.d.ts.map +1 -0
  20. package/dist/semantic/embedder.js +54 -0
  21. package/dist/semantic/index.d.ts +41 -0
  22. package/dist/semantic/index.d.ts.map +1 -0
  23. package/dist/semantic/index.js +178 -0
  24. package/dist/system.d.ts +5 -0
  25. package/dist/system.d.ts.map +1 -0
  26. package/dist/system.js +93 -0
  27. package/dist/tools/bash.d.ts +22 -0
  28. package/dist/tools/bash.d.ts.map +1 -0
  29. package/dist/tools/bash.js +59 -0
  30. package/dist/tools/batch.d.ts +25 -0
  31. package/dist/tools/batch.d.ts.map +1 -0
  32. package/dist/tools/batch.js +49 -0
  33. package/dist/tools/edit.d.ts +25 -0
  34. package/dist/tools/edit.d.ts.map +1 -0
  35. package/dist/tools/edit.js +44 -0
  36. package/dist/tools/glob.d.ts +19 -0
  37. package/dist/tools/glob.d.ts.map +1 -0
  38. package/dist/tools/glob.js +54 -0
  39. package/dist/tools/grep.d.ts +22 -0
  40. package/dist/tools/grep.d.ts.map +1 -0
  41. package/dist/tools/grep.js +92 -0
  42. package/dist/tools/index.d.ts +12 -0
  43. package/dist/tools/index.d.ts.map +1 -0
  44. package/dist/tools/index.js +51 -0
  45. package/dist/tools/patch.d.ts +16 -0
  46. package/dist/tools/patch.d.ts.map +1 -0
  47. package/dist/tools/patch.js +87 -0
  48. package/dist/tools/read.d.ts +22 -0
  49. package/dist/tools/read.d.ts.map +1 -0
  50. package/dist/tools/read.js +70 -0
  51. package/dist/tools/remember.d.ts +16 -0
  52. package/dist/tools/remember.d.ts.map +1 -0
  53. package/dist/tools/remember.js +58 -0
  54. package/dist/tools/semantic-search.d.ts +19 -0
  55. package/dist/tools/semantic-search.d.ts.map +1 -0
  56. package/dist/tools/semantic-search.js +54 -0
  57. package/dist/tools/skill.d.ts +17 -0
  58. package/dist/tools/skill.d.ts.map +1 -0
  59. package/dist/tools/skill.js +106 -0
  60. package/dist/tools/todo.d.ts +62 -0
  61. package/dist/tools/todo.d.ts.map +1 -0
  62. package/dist/tools/todo.js +62 -0
  63. package/dist/tools/write.d.ts +19 -0
  64. package/dist/tools/write.d.ts.map +1 -0
  65. package/dist/tools/write.js +37 -0
  66. package/dist/util/constants.d.ts +16 -0
  67. package/dist/util/constants.d.ts.map +1 -0
  68. package/dist/util/constants.js +39 -0
  69. package/dist/util/patch.d.ts +32 -0
  70. package/dist/util/patch.d.ts.map +1 -0
  71. package/dist/util/patch.js +240 -0
  72. package/dist/util/paths.d.ts +6 -0
  73. package/dist/util/paths.d.ts.map +1 -0
  74. package/dist/util/paths.js +76 -0
  75. package/dist/util/text.d.ts +4 -0
  76. package/dist/util/text.d.ts.map +1 -0
  77. package/dist/util/text.js +37 -0
  78. package/dist/util/todo.d.ts +5 -0
  79. package/dist/util/todo.d.ts.map +1 -0
  80. package/dist/util/todo.js +48 -0
  81. package/dist/util/types.d.ts +22 -0
  82. package/dist/util/types.d.ts.map +1 -0
  83. package/dist/util/types.js +1 -0
  84. package/package.json +53 -0
@@ -0,0 +1,22 @@
1
+ export declare function createChatGrep(baseDir: string): {
2
+ id: string;
3
+ run: (args: {
4
+ pattern: string;
5
+ path?: string;
6
+ include?: string;
7
+ }) => Promise<string>;
8
+ tool: {
9
+ description: string;
10
+ args: {
11
+ pattern: import("zod").ZodString;
12
+ path: import("zod").ZodOptional<import("zod").ZodString>;
13
+ include: import("zod").ZodOptional<import("zod").ZodString>;
14
+ };
15
+ execute(args: {
16
+ pattern: string;
17
+ path?: string | undefined;
18
+ include?: string | undefined;
19
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
20
+ };
21
+ };
22
+ //# sourceMappingURL=grep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../tools/grep.ts"],"names":[],"mappings":"AAgCA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM;;gBACnB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;;;EAyD9E"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * chat_grep tool implementation.
3
+ * Searches file content using regex and formats results by file.
4
+ * Limits output to a manageable number of matches.
5
+ */
6
+ import * as fs from "fs/promises";
7
+ import { tool } from "@opencode-ai/plugin";
8
+ import { MAX_GREP_MATCHES } from "../util/constants";
9
+ import { resolvePath, isBinaryFile } from "../util/paths";
10
+ import { trimLine } from "../util/text";
11
+ function formatMatches(matches, truncated) {
12
+ const outputLines = [`Found ${matches.length} matches`];
13
+ let currentFile = "";
14
+ for (const match of matches) {
15
+ if (currentFile !== match.path) {
16
+ if (currentFile !== "") {
17
+ outputLines.push("");
18
+ }
19
+ currentFile = match.path;
20
+ outputLines.push(`${match.path}:`);
21
+ }
22
+ outputLines.push(` Line ${match.lineNum}: ${trimLine(match.lineText)}`);
23
+ }
24
+ if (truncated) {
25
+ outputLines.push("");
26
+ outputLines.push("(Results are truncated. Consider using a more specific path or pattern.)");
27
+ }
28
+ return outputLines.join("\n");
29
+ }
30
+ export function createChatGrep(baseDir) {
31
+ const run = async (args) => {
32
+ const searchRoot = resolvePath(baseDir, args.path ?? baseDir);
33
+ let matcher;
34
+ try {
35
+ matcher = new RegExp(args.pattern);
36
+ }
37
+ catch (_error) {
38
+ throw new Error(`Invalid regex pattern: ${args.pattern}`);
39
+ }
40
+ const globPattern = args.include ?? "**/*";
41
+ const glob = new Bun.Glob(globPattern);
42
+ const matches = [];
43
+ for await (const file of glob.scan({ cwd: searchRoot, absolute: true, onlyFiles: true })) {
44
+ if (matches.length >= MAX_GREP_MATCHES)
45
+ break;
46
+ if (await isBinaryFile(file))
47
+ continue;
48
+ const content = await fs.readFile(file, "utf-8").catch(() => "");
49
+ if (!content)
50
+ continue;
51
+ const lines = content.split("\n");
52
+ for (let i = 0; i < lines.length; i++) {
53
+ if (!matcher.test(lines[i]))
54
+ continue;
55
+ const modTime = await Bun.file(file)
56
+ .stat()
57
+ .then((stat) => stat.mtime.getTime())
58
+ .catch(() => 0);
59
+ matches.push({ path: file, modTime, lineNum: i + 1, lineText: lines[i] });
60
+ if (matches.length >= MAX_GREP_MATCHES)
61
+ break;
62
+ }
63
+ }
64
+ if (matches.length === 0)
65
+ return "No files found";
66
+ matches.sort((a, b) => b.modTime - a.modTime);
67
+ const truncated = matches.length >= MAX_GREP_MATCHES;
68
+ const finalMatches = truncated ? matches.slice(0, MAX_GREP_MATCHES) : matches;
69
+ return formatMatches(finalMatches, truncated);
70
+ };
71
+ return {
72
+ id: "chat_grep",
73
+ run,
74
+ tool: tool({
75
+ description: `Search file contents using regex.
76
+
77
+ Usage:
78
+ - Supports full regex syntax
79
+ - Filter files by pattern with include parameter
80
+ - Returns files sorted by modification time
81
+ - Results truncated at 100 matches`,
82
+ args: {
83
+ pattern: tool.schema.string().describe("The regex pattern to search for in file contents"),
84
+ path: tool.schema.string().optional().describe("The directory to search in"),
85
+ include: tool.schema.string().optional().describe("File pattern to include in the search"),
86
+ },
87
+ async execute(args) {
88
+ return await run(args);
89
+ },
90
+ }),
91
+ };
92
+ }
@@ -0,0 +1,12 @@
1
+ export declare function createChatTools(baseDir: string, repoRoot: string, todoPath: string): {
2
+ tools: Record<string, {
3
+ description: string;
4
+ args: Readonly<{
5
+ [k: string]: import("zod/v4/core").$ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
6
+ }>;
7
+ execute(args: Record<string, unknown>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
8
+ }>;
9
+ runners: Record<string, (p: Record<string, unknown>) => Promise<string>>;
10
+ toolIds: string[];
11
+ };
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../tools/index.ts"],"names":[],"mappings":"AAgBA,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;gCAW/C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC;;EA6B9E"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tool registry for the chatifier plugin.
3
+ * Builds chat_* tools and exposes runners for batch execution.
4
+ */
5
+ import { createChatBash } from "./bash";
6
+ import { createChatRead } from "./read";
7
+ import { createChatPatch } from "./patch";
8
+ import { createChatGlob } from "./glob";
9
+ import { createChatGrep } from "./grep";
10
+ import { createChatTodo } from "./todo";
11
+ import { createChatBatch } from "./batch";
12
+ import { createChatSkill } from "./skill";
13
+ import { createChatRemember } from "./remember";
14
+ import { createChatSemanticSearch } from "./semantic-search";
15
+ export function createChatTools(baseDir, repoRoot, todoPath) {
16
+ const read = createChatRead(baseDir);
17
+ const patch = createChatPatch(baseDir);
18
+ const glob = createChatGlob(baseDir);
19
+ const grep = createChatGrep(baseDir);
20
+ const bash = createChatBash(baseDir);
21
+ const todo = createChatTodo(todoPath);
22
+ const skill = createChatSkill(baseDir);
23
+ const remember = createChatRemember(baseDir);
24
+ const semantic = createChatSemanticSearch(repoRoot);
25
+ const runners = {
26
+ [read.id]: (p) => read.run(p),
27
+ [patch.id]: (p) => patch.run(p),
28
+ [glob.id]: (p) => glob.run(p),
29
+ [grep.id]: (p) => grep.run(p),
30
+ [bash.id]: (p) => bash.run(p),
31
+ [todo.write.id]: (p) => todo.write.run(p),
32
+ [skill.id]: (p) => skill.run(p),
33
+ [remember.id]: (p) => remember.run(p),
34
+ [semantic.id]: (p) => semantic.run(p),
35
+ };
36
+ const batch = createChatBatch(runners, todo.read.run);
37
+ const tools = {
38
+ [read.id]: read.tool,
39
+ [patch.id]: patch.tool,
40
+ [glob.id]: glob.tool,
41
+ [grep.id]: grep.tool,
42
+ [bash.id]: bash.tool,
43
+ [todo.write.id]: todo.write.tool,
44
+ [todo.read.id]: todo.read.tool,
45
+ [skill.id]: skill.tool,
46
+ [remember.id]: remember.tool,
47
+ [semantic.id]: semantic.tool,
48
+ [batch.id]: batch.tool,
49
+ };
50
+ return { tools, runners, toolIds: Object.keys(tools) };
51
+ }
@@ -0,0 +1,16 @@
1
+ export declare function createChatPatch(baseDir: string): {
2
+ id: string;
3
+ run: (args: {
4
+ patchText: string;
5
+ }) => Promise<string>;
6
+ tool: {
7
+ description: string;
8
+ args: {
9
+ patchText: import("zod").ZodString;
10
+ };
11
+ execute(args: {
12
+ patchText: string;
13
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
14
+ };
15
+ };
16
+ //# sourceMappingURL=patch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../tools/patch.ts"],"names":[],"mappings":"AAQA,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM;;gBACpB;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;EAkF/C"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * chat_patch tool implementation.
3
+ * Apply patches to create, update, delete, or move files.
4
+ */
5
+ import { tool } from "@opencode-ai/plugin";
6
+ import * as path from "path";
7
+ import { parsePatch, applyHunksToFiles } from "../util/patch";
8
+ export function createChatPatch(baseDir) {
9
+ const run = async (args) => {
10
+ const { hunks } = parsePatch(args.patchText);
11
+ const resolved = hunks.map((hunk) => {
12
+ if (hunk.type === "update" && hunk.move_path) {
13
+ return { ...hunk, path: path.resolve(baseDir, hunk.path), move_path: path.resolve(baseDir, hunk.move_path) };
14
+ }
15
+ return { ...hunk, path: path.resolve(baseDir, hunk.path) };
16
+ });
17
+ const result = await applyHunksToFiles(resolved);
18
+ const summary = [];
19
+ if (result.added.length)
20
+ summary.push(`Added: ${result.added.join(", ")}`);
21
+ if (result.modified.length)
22
+ summary.push(`Modified: ${result.modified.join(", ")}`);
23
+ if (result.deleted.length)
24
+ summary.push(`Deleted: ${result.deleted.join(", ")}`);
25
+ return summary.length ? summary.join("\n") : "No changes applied";
26
+ };
27
+ return {
28
+ id: "chat_patch",
29
+ run,
30
+ tool: tool({
31
+ description: `Apply a patch to create, update, or delete files.
32
+
33
+ FORMAT RULES:
34
+ - Start with: *** Begin Patch
35
+ - End with: *** End Patch
36
+ - Lines starting with "-" are REMOVED from the file
37
+ - Lines starting with "+" are ADDED to the file
38
+ - Lines starting with " " (space) are kept unchanged (context)
39
+ - @@ marks a context line to LOCATE where changes happen
40
+
41
+ CREATE A NEW FILE:
42
+ *** Begin Patch
43
+ *** Add File: path/to/new.txt
44
+ +first line of new file
45
+ +second line of new file
46
+ *** End Patch
47
+
48
+ REPLACE A LINE (must include both - and +):
49
+ *** Begin Patch
50
+ *** Update File: path/to/file.txt
51
+ @@ function hello() {
52
+ - return "old"
53
+ + return "new"
54
+ *** End Patch
55
+
56
+ DELETE A LINE (use - with no +):
57
+ *** Begin Patch
58
+ *** Update File: path/to/file.txt
59
+ @@ const config = {
60
+ - debug: true,
61
+ *** End Patch
62
+
63
+ INSERT A NEW LINE (use space prefix for context, then +):
64
+ *** Begin Patch
65
+ *** Update File: path/to/file.txt
66
+ @@ import React from "react"
67
+ import React from "react"
68
+ +import { useState } from "react"
69
+ *** End Patch
70
+
71
+ DELETE A FILE:
72
+ *** Begin Patch
73
+ *** Delete File: path/to/remove.txt
74
+ *** End Patch
75
+
76
+ IMPORTANT: To replace a line, you MUST use "-oldline" then "+newline". The @@ line only locates WHERE to make changes.`,
77
+ args: {
78
+ patchText: tool.schema
79
+ .string()
80
+ .describe("The patch text in the format shown above. Must include *** Begin Patch and *** End Patch markers."),
81
+ },
82
+ async execute(args) {
83
+ return await run(args);
84
+ },
85
+ }),
86
+ };
87
+ }
@@ -0,0 +1,22 @@
1
+ export declare function createChatRead(baseDir: string): {
2
+ id: string;
3
+ run: (args: {
4
+ filePath: string;
5
+ offset?: number;
6
+ limit?: number;
7
+ }) => Promise<string>;
8
+ tool: {
9
+ description: string;
10
+ args: {
11
+ filePath: import("zod").ZodString;
12
+ offset: import("zod").ZodOptional<import("zod").ZodNumber>;
13
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
14
+ };
15
+ execute(args: {
16
+ filePath: string;
17
+ offset?: number | undefined;
18
+ limit?: number | undefined;
19
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
20
+ };
21
+ };
22
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../tools/read.ts"],"names":[],"mappings":"AAWA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM;;gBACnB;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;;;EA6D/E"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * chat_read tool implementation.
3
+ * Reads text files with line numbers and paging hints.
4
+ * Blocks .env access and binary or image reads for safety.
5
+ */
6
+ import * as fs from "fs/promises";
7
+ import { tool } from "@opencode-ai/plugin";
8
+ import { DEFAULT_READ_LIMIT } from "../util/constants";
9
+ import { resolvePath, isBlockedEnvPath, isImageExtension, isBinaryFile } from "../util/paths";
10
+ import { trimLine } from "../util/text";
11
+ export function createChatRead(baseDir) {
12
+ const run = async (args) => {
13
+ const filePath = resolvePath(baseDir, args.filePath);
14
+ if (isBlockedEnvPath(filePath)) {
15
+ throw new Error(`The user has blocked you from reading ${filePath}`);
16
+ }
17
+ if (isImageExtension(filePath)) {
18
+ throw new Error(`Image reading is not supported: ${filePath}`);
19
+ }
20
+ if (await isBinaryFile(filePath)) {
21
+ throw new Error(`Cannot read binary file: ${filePath}`);
22
+ }
23
+ const stats = await fs.stat(filePath).catch(() => undefined);
24
+ if (!stats) {
25
+ throw new Error(`File not found: ${filePath}`);
26
+ }
27
+ if (stats.isDirectory()) {
28
+ throw new Error(`Path is a directory, not a file: ${filePath}`);
29
+ }
30
+ const offset = args.offset ?? 0;
31
+ const limit = args.limit ?? DEFAULT_READ_LIMIT;
32
+ const lines = await fs.readFile(filePath, "utf-8").then((content) => content.split("\n"));
33
+ const raw = lines.slice(offset, offset + limit).map(trimLine);
34
+ const content = raw.map((line, index) => `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`);
35
+ let output = "<file>\n";
36
+ output += content.join("\n");
37
+ const totalLines = lines.length;
38
+ const lastReadLine = offset + content.length;
39
+ const hasMoreLines = totalLines > lastReadLine;
40
+ if (hasMoreLines) {
41
+ output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})`;
42
+ }
43
+ if (!hasMoreLines) {
44
+ output += `\n\n(End of file - total ${totalLines} lines)`;
45
+ }
46
+ output += "\n</file>";
47
+ return output;
48
+ };
49
+ return {
50
+ id: "chat_read",
51
+ run,
52
+ tool: tool({
53
+ description: `Read file contents.
54
+
55
+ Usage:
56
+ - Returns line-numbered output
57
+ - Default limit is 2000 lines
58
+ - Use offset and limit for large files
59
+ - Long lines (>2000 chars) are truncated`,
60
+ args: {
61
+ filePath: tool.schema.string().describe("The path to the file to read"),
62
+ offset: tool.schema.number().optional().describe("The line number to start reading from (0-based)"),
63
+ limit: tool.schema.number().optional().describe("The number of lines to read (defaults to 2000)"),
64
+ },
65
+ async execute(args) {
66
+ return await run(args);
67
+ },
68
+ }),
69
+ };
70
+ }
@@ -0,0 +1,16 @@
1
+ export declare function createChatRemember(baseDir: string): {
2
+ id: string;
3
+ run: (args: {
4
+ memory: string;
5
+ }) => Promise<string>;
6
+ tool: {
7
+ description: string;
8
+ args: {
9
+ memory: import("zod").ZodString;
10
+ };
11
+ execute(args: {
12
+ memory: string;
13
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
14
+ };
15
+ };
16
+ //# sourceMappingURL=remember.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remember.d.ts","sourceRoot":"","sources":["../../tools/remember.ts"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM;;gBAGvB;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;EAqD5C"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * chat_remember tool implementation.
3
+ * Append a memory to AGENTS.md for long-term persistence.
4
+ * Since AGENTS.md is re-read on every turn, memories take effect immediately.
5
+ */
6
+ import * as fs from "fs/promises";
7
+ import * as path from "path";
8
+ import { tool } from "@opencode-ai/plugin";
9
+ export function createChatRemember(baseDir) {
10
+ const agentsPath = path.join(baseDir, "AGENTS.md");
11
+ const run = async (args) => {
12
+ const memory = args.memory.trim();
13
+ if (!memory) {
14
+ throw new Error("Memory cannot be empty");
15
+ }
16
+ // Check if file exists
17
+ let content = "";
18
+ try {
19
+ content = await fs.readFile(agentsPath, "utf-8");
20
+ }
21
+ catch {
22
+ // File doesn't exist, create with header
23
+ content = "# User Preferences & Memories\n\n";
24
+ }
25
+ // Ensure content ends with newline
26
+ if (!content.endsWith("\n")) {
27
+ content += "\n";
28
+ }
29
+ // Check for memories section, add if missing
30
+ if (!content.includes("## Memories")) {
31
+ content += "\n## Memories\n\n";
32
+ }
33
+ // Append the new memory with timestamp
34
+ const timestamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
35
+ const memoryLine = `- ${memory} (${timestamp})\n`;
36
+ content += memoryLine;
37
+ await fs.writeFile(agentsPath, content, "utf-8");
38
+ return `Remembered: "${memory}"`;
39
+ };
40
+ return {
41
+ id: "chat_remember",
42
+ run,
43
+ tool: tool({
44
+ description: `Save something to long-term memory. Use this to remember:
45
+ - User preferences (e.g., "User prefers concise responses")
46
+ - Important facts (e.g., "User's project uses React 19")
47
+ - Style preferences (e.g., "User likes bullet points over paragraphs")
48
+
49
+ Memories persist across conversations. Keep each memory to ONE short sentence.`,
50
+ args: {
51
+ memory: tool.schema.string().describe("A single short sentence to remember. Be specific and concise."),
52
+ },
53
+ async execute(args) {
54
+ return await run(args);
55
+ },
56
+ }),
57
+ };
58
+ }
@@ -0,0 +1,19 @@
1
+ export declare function createChatSemanticSearch(worktree: string): {
2
+ id: string;
3
+ run: (args: {
4
+ query: string;
5
+ limit?: number;
6
+ }) => Promise<string>;
7
+ tool: {
8
+ description: string;
9
+ args: {
10
+ query: import("zod").ZodString;
11
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
12
+ };
13
+ execute(args: {
14
+ query: string;
15
+ limit?: number | undefined;
16
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
17
+ };
18
+ };
19
+ //# sourceMappingURL=semantic-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-search.d.ts","sourceRoot":"","sources":["../../tools/semantic-search.ts"],"names":[],"mappings":"AAUA,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM;;gBAC9B;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;EA+C3D"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * chat_semantic_search tool implementation.
3
+ * Searches repo content using local embeddings stored in SQLite.
4
+ */
5
+ import * as path from "path";
6
+ import { tool } from "@opencode-ai/plugin";
7
+ import { semanticSearch } from "../semantic";
8
+ const DEFAULT_LIMIT = 5;
9
+ export function createChatSemanticSearch(worktree) {
10
+ const run = async (args) => {
11
+ const query = args.query.trim();
12
+ if (!query)
13
+ throw new Error("Query cannot be empty");
14
+ const dbPath = path.join(worktree, ".opencode", "chat", "semantic.sqlite");
15
+ const exists = await Bun.file(dbPath).exists();
16
+ if (!exists) {
17
+ return "Semantic index not found. Run chat_semantic_index first.";
18
+ }
19
+ const limit = Math.max(1, Math.min(args.limit ?? DEFAULT_LIMIT, 20));
20
+ const results = await semanticSearch(worktree, query, limit);
21
+ if (results.length === 0)
22
+ return "No semantic matches found.";
23
+ return results
24
+ .map((item, index) => {
25
+ const relPath = path.relative(worktree, item.path);
26
+ const snippet = item.content.trim().slice(0, 400);
27
+ return [
28
+ `${index + 1}. ${relPath}:${item.start_line}-${item.end_line}`,
29
+ `score: ${item.score.toFixed(3)}`,
30
+ snippet,
31
+ ].join("\n");
32
+ })
33
+ .join("\n\n");
34
+ };
35
+ return {
36
+ id: "chat_semantic_search",
37
+ run,
38
+ tool: tool({
39
+ description: `Semantic search over repo files using local embeddings.
40
+
41
+ Usage:
42
+ - Best for natural language queries ("where is auth handled")
43
+ - Returns file + line ranges + snippet
44
+ - Indexing is incremental based on file mtime`,
45
+ args: {
46
+ query: tool.schema.string().describe("Natural language search query"),
47
+ limit: tool.schema.number().optional().describe("Number of results (default 5, max 20)"),
48
+ },
49
+ async execute(args) {
50
+ return await run(args);
51
+ },
52
+ }),
53
+ };
54
+ }
@@ -0,0 +1,17 @@
1
+ export declare function createChatSkill(baseDir: string): {
2
+ id: string;
3
+ run: (args: {
4
+ name: string;
5
+ }) => Promise<string>;
6
+ buildDescription: () => Promise<string>;
7
+ tool: {
8
+ description: string;
9
+ args: {
10
+ name: import("zod").ZodString;
11
+ };
12
+ execute(args: {
13
+ name: string;
14
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
15
+ };
16
+ };
17
+ //# sourceMappingURL=skill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../tools/skill.ts"],"names":[],"mappings":"AAmCA,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM;;gBA0CpB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;EAiD1C"}