opencode-gateway 0.2.10 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/dist/cli.js +12 -3
- package/dist/config/memory.d.ts +2 -1
- package/dist/gateway.d.ts +3 -1
- package/dist/index.js +365 -74
- package/dist/memory/files.d.ts +13 -0
- package/dist/memory/runtime.d.ts +28 -0
- package/dist/tools/memory-get.d.ts +3 -0
- package/dist/tools/memory-search.d.ts +3 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -98,8 +98,12 @@ inject_content = true
|
|
|
98
98
|
[[memory.entries]]
|
|
99
99
|
path = "memory/notes"
|
|
100
100
|
description = "Domain notes and operating docs"
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
search_only = true
|
|
102
|
+
|
|
103
|
+
[[memory.entries]]
|
|
104
|
+
path = "memory/snippets"
|
|
105
|
+
description = "Selected files are auto-injected; the rest stay searchable on demand"
|
|
106
|
+
globs = ["**/*.md", "notes/**/*.txt"]
|
|
103
107
|
```
|
|
104
108
|
|
|
105
109
|
When Telegram is enabled, export the bot token through the configured
|
|
@@ -122,13 +126,16 @@ Mailbox rules:
|
|
|
122
126
|
Memory rules:
|
|
123
127
|
|
|
124
128
|
- all entries inject their configured path and description
|
|
125
|
-
- file contents are injected only when `inject_content = true`
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
|
|
129
|
+
- file contents are auto-injected only when `inject_content = true`
|
|
130
|
+
- `search_only = true` keeps an entry available to `memory_search` and `memory_get`
|
|
131
|
+
without auto-injecting its content
|
|
132
|
+
- directory entries default to description-only plus on-demand search
|
|
133
|
+
- directory `globs` are relative to the configured directory and define which
|
|
134
|
+
files are auto-injected; other UTF-8 text files remain searchable on demand
|
|
130
135
|
- relative paths are resolved from `opencode-gateway-workspace`
|
|
131
136
|
- absolute paths are still allowed
|
|
132
137
|
- missing files and directories are created automatically on load
|
|
133
138
|
- the default template includes `USER.md` as persistent user-profile memory
|
|
134
139
|
- memory is injected only into gateway-managed sessions
|
|
140
|
+
- `memory_search` returns matching snippets and paths; `memory_get` reads a
|
|
141
|
+
specific configured memory file by path and optional line window
|
package/dist/cli.js
CHANGED
|
@@ -518,10 +518,19 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
518
518
|
"inject_content = true",
|
|
519
519
|
"#",
|
|
520
520
|
"# [[memory.entries]]",
|
|
521
|
+
'# path = "memory/project.md"',
|
|
522
|
+
'# description = "Project conventions and long-lived context"',
|
|
523
|
+
"# inject_content = true",
|
|
524
|
+
"#",
|
|
525
|
+
"# [[memory.entries]]",
|
|
521
526
|
'# path = "memory/notes"',
|
|
522
|
-
'# description = "Domain notes and operating docs"',
|
|
523
|
-
"#
|
|
524
|
-
|
|
527
|
+
'# description = "Domain notes and operating docs. Use memory_search and memory_get to inspect this directory on demand."',
|
|
528
|
+
"# search_only = true",
|
|
529
|
+
"#",
|
|
530
|
+
"# [[memory.entries]]",
|
|
531
|
+
'# path = "memory/snippets"',
|
|
532
|
+
'# description = "Selected files are auto-injected; the rest stay searchable on demand."',
|
|
533
|
+
'# globs = ["**/*.md", "notes/**/*.txt"]',
|
|
525
534
|
""
|
|
526
535
|
].join(`
|
|
527
536
|
`);
|
package/dist/config/memory.d.ts
CHANGED
|
@@ -7,12 +7,13 @@ export type GatewayMemoryEntryConfig = {
|
|
|
7
7
|
displayPath: string;
|
|
8
8
|
description: string;
|
|
9
9
|
injectContent: boolean;
|
|
10
|
+
searchOnly: boolean;
|
|
10
11
|
} | {
|
|
11
12
|
kind: "directory";
|
|
12
13
|
path: string;
|
|
13
14
|
displayPath: string;
|
|
14
15
|
description: string;
|
|
15
|
-
injectMarkdownContents: boolean;
|
|
16
16
|
globs: string[];
|
|
17
|
+
searchOnly: boolean;
|
|
17
18
|
};
|
|
18
19
|
export declare function parseMemoryConfig(value: unknown, workspaceDirPath: string): Promise<GatewayMemoryConfig>;
|
package/dist/gateway.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin";
|
|
|
2
2
|
import type { GatewayBindingModule, GatewayContract } from "./binding";
|
|
3
3
|
import { GatewayCronRuntime } from "./cron/runtime";
|
|
4
4
|
import { ChannelFileSender } from "./host/file-sender";
|
|
5
|
+
import { GatewayMemoryRuntime } from "./memory/runtime";
|
|
5
6
|
import { GatewayExecutor } from "./runtime/executor";
|
|
6
7
|
import { GatewaySessionContext } from "./session/context";
|
|
7
8
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
@@ -29,7 +30,8 @@ export declare class GatewayPluginRuntime {
|
|
|
29
30
|
readonly channelSessions: ChannelSessionSwitcher;
|
|
30
31
|
readonly sessionContext: GatewaySessionContext;
|
|
31
32
|
readonly systemPrompts: GatewaySystemPromptBuilder;
|
|
32
|
-
|
|
33
|
+
readonly memory: GatewayMemoryRuntime;
|
|
34
|
+
constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext, systemPrompts: GatewaySystemPromptBuilder, memory: GatewayMemoryRuntime);
|
|
33
35
|
status(): GatewayPluginStatus;
|
|
34
36
|
}
|
|
35
37
|
export declare function createGatewayRuntime(module: GatewayBindingModule, input: PluginInput): Promise<GatewayPluginRuntime>;
|
package/dist/index.js
CHANGED
|
@@ -16151,30 +16151,42 @@ async function readMemoryEntry(value, index, workspaceDirPath) {
|
|
|
16151
16151
|
throw new Error(`${field} must be a table`);
|
|
16152
16152
|
}
|
|
16153
16153
|
const entry = value;
|
|
16154
|
+
if (entry.inject_markdown_contents !== undefined) {
|
|
16155
|
+
throw new Error(`${field}.inject_markdown_contents has been removed; use globs for directory injection or search_only for on-demand access`);
|
|
16156
|
+
}
|
|
16154
16157
|
const displayPath = readRequiredString(entry.path, `${field}.path`);
|
|
16155
16158
|
const description = readRequiredString(entry.description, `${field}.description`);
|
|
16156
16159
|
const resolvedPath = resolve(workspaceDirPath, displayPath);
|
|
16157
16160
|
const metadata = await ensurePathMetadata(resolvedPath, displayPath, entry, `${field}.path`);
|
|
16161
|
+
const searchOnly = readBoolean2(entry.search_only, `${field}.search_only`, false);
|
|
16158
16162
|
if (metadata.isFile()) {
|
|
16159
|
-
ensureDirectoryOnlyFieldIsAbsent(entry.inject_markdown_contents, `${field}.inject_markdown_contents`);
|
|
16160
16163
|
ensureDirectoryOnlyFieldIsAbsent(entry.globs, `${field}.globs`);
|
|
16164
|
+
const injectContent = readBoolean2(entry.inject_content, `${field}.inject_content`, false);
|
|
16165
|
+
if (searchOnly && injectContent) {
|
|
16166
|
+
throw new Error(`${field} cannot enable both inject_content and search_only`);
|
|
16167
|
+
}
|
|
16161
16168
|
return {
|
|
16162
16169
|
kind: "file",
|
|
16163
16170
|
path: resolvedPath,
|
|
16164
16171
|
displayPath,
|
|
16165
16172
|
description,
|
|
16166
|
-
injectContent
|
|
16173
|
+
injectContent,
|
|
16174
|
+
searchOnly
|
|
16167
16175
|
};
|
|
16168
16176
|
}
|
|
16169
16177
|
if (metadata.isDirectory()) {
|
|
16170
16178
|
ensureFileOnlyFieldIsAbsent(entry.inject_content, `${field}.inject_content`);
|
|
16179
|
+
const globs = readGlobList(entry.globs, `${field}.globs`);
|
|
16180
|
+
if (searchOnly && globs.length > 0) {
|
|
16181
|
+
throw new Error(`${field} cannot enable both globs and search_only`);
|
|
16182
|
+
}
|
|
16171
16183
|
return {
|
|
16172
16184
|
kind: "directory",
|
|
16173
16185
|
path: resolvedPath,
|
|
16174
16186
|
displayPath,
|
|
16175
16187
|
description,
|
|
16176
|
-
|
|
16177
|
-
|
|
16188
|
+
globs,
|
|
16189
|
+
searchOnly
|
|
16178
16190
|
};
|
|
16179
16191
|
}
|
|
16180
16192
|
throw new Error(`${field}.path must point to a regular file or directory`);
|
|
@@ -16198,7 +16210,7 @@ function inferMissingEntryKind(displayPath, entry) {
|
|
|
16198
16210
|
if (entry.inject_content !== undefined) {
|
|
16199
16211
|
return "file";
|
|
16200
16212
|
}
|
|
16201
|
-
if (entry.
|
|
16213
|
+
if (entry.globs !== undefined) {
|
|
16202
16214
|
return "directory";
|
|
16203
16215
|
}
|
|
16204
16216
|
const trimmedPath = displayPath.trim();
|
|
@@ -17614,87 +17626,58 @@ class GatewayMailboxRouter {
|
|
|
17614
17626
|
}
|
|
17615
17627
|
}
|
|
17616
17628
|
|
|
17617
|
-
// src/memory/
|
|
17629
|
+
// src/memory/files.ts
|
|
17618
17630
|
var import_fast_glob = __toESM(require_out4(), 1);
|
|
17619
17631
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
17620
17632
|
import { extname as extname3, relative } from "node:path";
|
|
17621
|
-
var
|
|
17633
|
+
var ALL_FILES_GLOB = "**/*";
|
|
17622
17634
|
var UTF8_TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
17623
|
-
|
|
17624
|
-
class GatewayMemoryPromptProvider {
|
|
17625
|
-
config;
|
|
17626
|
-
logger;
|
|
17627
|
-
constructor(config, logger) {
|
|
17628
|
-
this.config = config;
|
|
17629
|
-
this.logger = logger;
|
|
17630
|
-
}
|
|
17631
|
-
async buildPrompt() {
|
|
17632
|
-
if (this.config.entries.length === 0) {
|
|
17633
|
-
return null;
|
|
17634
|
-
}
|
|
17635
|
-
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17636
|
-
return ["Gateway memory:", ...sections].join(`
|
|
17637
|
-
|
|
17638
|
-
`);
|
|
17639
|
-
}
|
|
17640
|
-
async buildEntrySection(entry) {
|
|
17641
|
-
const lines = [`Configured path: ${entry.displayPath}`, `Description: ${entry.description}`];
|
|
17642
|
-
const injectedFiles = await collectInjectedFiles(entry, this.logger);
|
|
17643
|
-
for (const file of injectedFiles) {
|
|
17644
|
-
lines.push("");
|
|
17645
|
-
lines.push(`File: ${file.displayPath}`);
|
|
17646
|
-
lines.push(codeFence(file.infoString, file.text));
|
|
17647
|
-
}
|
|
17648
|
-
return lines.join(`
|
|
17649
|
-
`);
|
|
17650
|
-
}
|
|
17651
|
-
}
|
|
17652
|
-
async function collectInjectedFiles(entry, logger) {
|
|
17635
|
+
async function collectInjectedMemoryFiles(entry, logger) {
|
|
17653
17636
|
if (entry.kind === "file") {
|
|
17654
|
-
if (!entry.injectContent) {
|
|
17637
|
+
if (entry.searchOnly || !entry.injectContent) {
|
|
17655
17638
|
return [];
|
|
17656
17639
|
}
|
|
17657
|
-
const text = await
|
|
17640
|
+
const text = await readMemoryTextFile(entry.path, logger);
|
|
17658
17641
|
if (text === null) {
|
|
17659
17642
|
return [];
|
|
17660
17643
|
}
|
|
17661
17644
|
return [
|
|
17662
17645
|
{
|
|
17646
|
+
path: entry.path,
|
|
17663
17647
|
displayPath: entry.displayPath,
|
|
17648
|
+
description: entry.description,
|
|
17664
17649
|
infoString: inferFenceInfoString(entry.path),
|
|
17665
17650
|
text
|
|
17666
17651
|
}
|
|
17667
17652
|
];
|
|
17668
17653
|
}
|
|
17669
|
-
|
|
17670
|
-
|
|
17671
|
-
for (const pattern of MARKDOWN_GLOBS) {
|
|
17672
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17673
|
-
}
|
|
17674
|
-
}
|
|
17675
|
-
for (const pattern of entry.globs) {
|
|
17676
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17654
|
+
if (entry.searchOnly || entry.globs.length === 0) {
|
|
17655
|
+
return [];
|
|
17677
17656
|
}
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17657
|
+
return await readDirectoryFiles(entry, entry.globs, logger);
|
|
17658
|
+
}
|
|
17659
|
+
async function collectSearchableMemoryFiles(config, logger) {
|
|
17660
|
+
const files = [];
|
|
17661
|
+
for (const entry of config.entries) {
|
|
17662
|
+
if (entry.kind === "file") {
|
|
17663
|
+
const text = await readMemoryTextFile(entry.path, logger);
|
|
17664
|
+
if (text === null) {
|
|
17665
|
+
continue;
|
|
17666
|
+
}
|
|
17667
|
+
files.push({
|
|
17668
|
+
path: entry.path,
|
|
17669
|
+
displayPath: entry.displayPath,
|
|
17670
|
+
description: entry.description,
|
|
17671
|
+
infoString: inferFenceInfoString(entry.path),
|
|
17672
|
+
text
|
|
17673
|
+
});
|
|
17682
17674
|
continue;
|
|
17683
17675
|
}
|
|
17684
|
-
|
|
17685
|
-
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
17686
|
-
infoString: inferFenceInfoString(filePath),
|
|
17687
|
-
text
|
|
17688
|
-
});
|
|
17689
|
-
}
|
|
17690
|
-
return injectedFiles;
|
|
17691
|
-
}
|
|
17692
|
-
function addMatchingFiles(result, cwd, pattern) {
|
|
17693
|
-
for (const match of import_fast_glob.globSync(pattern, { cwd, absolute: true, onlyFiles: true })) {
|
|
17694
|
-
result.add(match);
|
|
17676
|
+
files.push(...await readDirectoryFiles(entry, [ALL_FILES_GLOB], logger));
|
|
17695
17677
|
}
|
|
17678
|
+
return files;
|
|
17696
17679
|
}
|
|
17697
|
-
async function
|
|
17680
|
+
async function readMemoryTextFile(path, logger) {
|
|
17698
17681
|
let bytes;
|
|
17699
17682
|
try {
|
|
17700
17683
|
bytes = await readFile3(path);
|
|
@@ -17715,6 +17698,37 @@ async function readTextFile(path, logger) {
|
|
|
17715
17698
|
}
|
|
17716
17699
|
return text;
|
|
17717
17700
|
}
|
|
17701
|
+
function codeFence(infoString, text) {
|
|
17702
|
+
const language = infoString.length === 0 ? "" : infoString;
|
|
17703
|
+
return [`\`\`\`${language}`, text, "```"].join(`
|
|
17704
|
+
`);
|
|
17705
|
+
}
|
|
17706
|
+
function addMatchingFiles(result, cwd, pattern) {
|
|
17707
|
+
for (const match of import_fast_glob.globSync(pattern, { cwd, absolute: true, onlyFiles: true, followSymbolicLinks: false })) {
|
|
17708
|
+
result.add(match);
|
|
17709
|
+
}
|
|
17710
|
+
}
|
|
17711
|
+
async function readDirectoryFiles(entry, patterns, logger) {
|
|
17712
|
+
const filePaths = new Set;
|
|
17713
|
+
for (const pattern of patterns) {
|
|
17714
|
+
addMatchingFiles(filePaths, entry.path, pattern);
|
|
17715
|
+
}
|
|
17716
|
+
const files = [];
|
|
17717
|
+
for (const filePath of [...filePaths].sort((left, right) => left.localeCompare(right))) {
|
|
17718
|
+
const text = await readMemoryTextFile(filePath, logger);
|
|
17719
|
+
if (text === null) {
|
|
17720
|
+
continue;
|
|
17721
|
+
}
|
|
17722
|
+
files.push({
|
|
17723
|
+
path: filePath,
|
|
17724
|
+
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
17725
|
+
description: entry.description,
|
|
17726
|
+
infoString: inferFenceInfoString(filePath),
|
|
17727
|
+
text
|
|
17728
|
+
});
|
|
17729
|
+
}
|
|
17730
|
+
return files;
|
|
17731
|
+
}
|
|
17718
17732
|
function relativeDisplayPath(rootPath, rootDisplayPath, filePath) {
|
|
17719
17733
|
const suffix = relative(rootPath, filePath);
|
|
17720
17734
|
if (suffix.length === 0) {
|
|
@@ -17729,11 +17743,6 @@ function inferFenceInfoString(path) {
|
|
|
17729
17743
|
}
|
|
17730
17744
|
return extension2;
|
|
17731
17745
|
}
|
|
17732
|
-
function codeFence(infoString, text) {
|
|
17733
|
-
const language = infoString.length === 0 ? "" : infoString;
|
|
17734
|
-
return [`\`\`\`${language}`, text, "```"].join(`
|
|
17735
|
-
`);
|
|
17736
|
-
}
|
|
17737
17746
|
function formatError2(error) {
|
|
17738
17747
|
if (error instanceof Error && error.message.trim().length > 0) {
|
|
17739
17748
|
return error.message;
|
|
@@ -17741,6 +17750,218 @@ function formatError2(error) {
|
|
|
17741
17750
|
return String(error);
|
|
17742
17751
|
}
|
|
17743
17752
|
|
|
17753
|
+
// src/memory/prompt.ts
|
|
17754
|
+
class GatewayMemoryPromptProvider {
|
|
17755
|
+
config;
|
|
17756
|
+
logger;
|
|
17757
|
+
constructor(config, logger) {
|
|
17758
|
+
this.config = config;
|
|
17759
|
+
this.logger = logger;
|
|
17760
|
+
}
|
|
17761
|
+
async buildPrompt() {
|
|
17762
|
+
if (this.config.entries.length === 0) {
|
|
17763
|
+
return null;
|
|
17764
|
+
}
|
|
17765
|
+
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17766
|
+
return ["Gateway memory:", ...sections].join(`
|
|
17767
|
+
|
|
17768
|
+
`);
|
|
17769
|
+
}
|
|
17770
|
+
async buildEntrySection(entry) {
|
|
17771
|
+
const lines = [
|
|
17772
|
+
`Configured path: ${entry.displayPath}`,
|
|
17773
|
+
`Description: ${entry.description}`,
|
|
17774
|
+
`Access: ${describeMemoryAccess(entry)}`
|
|
17775
|
+
];
|
|
17776
|
+
const injectedFiles = await collectInjectedMemoryFiles(entry, this.logger);
|
|
17777
|
+
if (entry.kind === "directory" && entry.globs.length > 0 && !entry.searchOnly) {
|
|
17778
|
+
lines.push(`Auto-injected globs: ${entry.globs.join(", ")}`);
|
|
17779
|
+
}
|
|
17780
|
+
for (const file of injectedFiles) {
|
|
17781
|
+
lines.push("");
|
|
17782
|
+
lines.push(`File: ${file.displayPath}`);
|
|
17783
|
+
lines.push(codeFence(file.infoString, file.text));
|
|
17784
|
+
}
|
|
17785
|
+
return lines.join(`
|
|
17786
|
+
`);
|
|
17787
|
+
}
|
|
17788
|
+
}
|
|
17789
|
+
function describeMemoryAccess(entry) {
|
|
17790
|
+
if (entry.kind === "file") {
|
|
17791
|
+
if (entry.injectContent && !entry.searchOnly) {
|
|
17792
|
+
return "auto-injected; use memory_search or memory_get for targeted follow-up";
|
|
17793
|
+
}
|
|
17794
|
+
return "search-only; use memory_search or memory_get when this file is relevant";
|
|
17795
|
+
}
|
|
17796
|
+
if (entry.searchOnly) {
|
|
17797
|
+
return "search-only; all UTF-8 text files under this directory are available via memory_search or memory_get";
|
|
17798
|
+
}
|
|
17799
|
+
if (entry.globs.length > 0) {
|
|
17800
|
+
return "globs are auto-injected; all UTF-8 text files under this directory remain available via memory_search or memory_get";
|
|
17801
|
+
}
|
|
17802
|
+
return "search-only by default; use memory_search or memory_get when this directory is relevant";
|
|
17803
|
+
}
|
|
17804
|
+
|
|
17805
|
+
// src/memory/runtime.ts
|
|
17806
|
+
import { dirname as dirname3 } from "node:path";
|
|
17807
|
+
import { FileFinder } from "@ff-labs/fff-node";
|
|
17808
|
+
var DEFAULT_SEARCH_LIMIT = 5;
|
|
17809
|
+
var MAX_SEARCH_LIMIT = 20;
|
|
17810
|
+
var DEFAULT_GET_MAX_LINES = 200;
|
|
17811
|
+
var MAX_GET_MAX_LINES = 500;
|
|
17812
|
+
var SEARCH_CONTEXT_RADIUS = 1;
|
|
17813
|
+
var SEARCH_SCAN_TIMEOUT_MS = 5000;
|
|
17814
|
+
|
|
17815
|
+
class GatewayMemoryRuntime {
|
|
17816
|
+
config;
|
|
17817
|
+
logger;
|
|
17818
|
+
finderCache = new Map;
|
|
17819
|
+
constructor(config, logger) {
|
|
17820
|
+
this.config = config;
|
|
17821
|
+
this.logger = logger;
|
|
17822
|
+
}
|
|
17823
|
+
hasEntries() {
|
|
17824
|
+
return this.config.entries.length > 0;
|
|
17825
|
+
}
|
|
17826
|
+
async search(query, limit = DEFAULT_SEARCH_LIMIT) {
|
|
17827
|
+
const normalizedQuery = normalizeRequiredString(query, "query");
|
|
17828
|
+
const normalizedLimit = normalizePositiveInteger(limit, "limit", MAX_SEARCH_LIMIT);
|
|
17829
|
+
const searchableFiles = await collectSearchableMemoryFiles(this.config, this.logger);
|
|
17830
|
+
const searchableFilesByPath = new Map(searchableFiles.map((file) => [file.path, file]));
|
|
17831
|
+
const results = [];
|
|
17832
|
+
for (const entry of this.config.entries) {
|
|
17833
|
+
const finder = await this.getFinder(searchRootForEntry(entry));
|
|
17834
|
+
const grep = finder.grep(normalizedQuery, {
|
|
17835
|
+
mode: "plain",
|
|
17836
|
+
beforeContext: SEARCH_CONTEXT_RADIUS,
|
|
17837
|
+
afterContext: SEARCH_CONTEXT_RADIUS
|
|
17838
|
+
});
|
|
17839
|
+
if (!grep.ok) {
|
|
17840
|
+
throw new Error(`memory search failed for ${entry.displayPath}: ${grep.error}`);
|
|
17841
|
+
}
|
|
17842
|
+
for (const match of grep.value.items) {
|
|
17843
|
+
if (entry.kind === "file" && match.path !== entry.path) {
|
|
17844
|
+
continue;
|
|
17845
|
+
}
|
|
17846
|
+
const searchableFile = searchableFilesByPath.get(match.path);
|
|
17847
|
+
if (searchableFile === undefined) {
|
|
17848
|
+
continue;
|
|
17849
|
+
}
|
|
17850
|
+
const window2 = readSnippetWindow(searchableFile.text, match.lineNumber, SEARCH_CONTEXT_RADIUS);
|
|
17851
|
+
results.push({
|
|
17852
|
+
path: displayPathForMatch(entry, match.relativePath),
|
|
17853
|
+
description: entry.description,
|
|
17854
|
+
lineStart: window2.lineStart,
|
|
17855
|
+
lineEnd: window2.lineEnd,
|
|
17856
|
+
snippet: window2.text,
|
|
17857
|
+
infoString: searchableFile.infoString
|
|
17858
|
+
});
|
|
17859
|
+
if (results.length >= normalizedLimit) {
|
|
17860
|
+
return results;
|
|
17861
|
+
}
|
|
17862
|
+
}
|
|
17863
|
+
}
|
|
17864
|
+
return results;
|
|
17865
|
+
}
|
|
17866
|
+
async get(path, startLine = 1, maxLines = DEFAULT_GET_MAX_LINES) {
|
|
17867
|
+
const normalizedPath = normalizeRequiredString(path, "path");
|
|
17868
|
+
const normalizedStartLine = normalizePositiveInteger(startLine, "start_line");
|
|
17869
|
+
const normalizedMaxLines = normalizePositiveInteger(maxLines, "max_lines", MAX_GET_MAX_LINES);
|
|
17870
|
+
const files = await collectSearchableMemoryFiles(this.config, this.logger);
|
|
17871
|
+
const matches = files.filter((file2) => file2.displayPath === normalizedPath);
|
|
17872
|
+
if (matches.length === 0) {
|
|
17873
|
+
throw new Error(`memory path was not found: ${normalizedPath}`);
|
|
17874
|
+
}
|
|
17875
|
+
if (matches.length > 1) {
|
|
17876
|
+
throw new Error(`memory path is ambiguous: ${normalizedPath}`);
|
|
17877
|
+
}
|
|
17878
|
+
const file = matches[0];
|
|
17879
|
+
const lines = splitLines(file.text);
|
|
17880
|
+
if (normalizedStartLine > lines.length) {
|
|
17881
|
+
throw new Error(`start_line ${normalizedStartLine} exceeds the file length of ${lines.length} line(s) for ${normalizedPath}`);
|
|
17882
|
+
}
|
|
17883
|
+
const startIndex = normalizedStartLine - 1;
|
|
17884
|
+
const window2 = lines.slice(startIndex, startIndex + normalizedMaxLines);
|
|
17885
|
+
return {
|
|
17886
|
+
path: file.displayPath,
|
|
17887
|
+
description: file.description,
|
|
17888
|
+
lineStart: normalizedStartLine,
|
|
17889
|
+
lineEnd: startIndex + window2.length,
|
|
17890
|
+
text: window2.join(`
|
|
17891
|
+
`),
|
|
17892
|
+
infoString: file.infoString
|
|
17893
|
+
};
|
|
17894
|
+
}
|
|
17895
|
+
async getFinder(rootPath) {
|
|
17896
|
+
const cached = this.finderCache.get(rootPath);
|
|
17897
|
+
if (cached !== undefined && !cached.isDestroyed) {
|
|
17898
|
+
return cached;
|
|
17899
|
+
}
|
|
17900
|
+
const created = FileFinder.create({
|
|
17901
|
+
basePath: rootPath,
|
|
17902
|
+
aiMode: true
|
|
17903
|
+
});
|
|
17904
|
+
if (!created.ok) {
|
|
17905
|
+
throw new Error(`could not initialize memory search index for ${rootPath}: ${created.error}`);
|
|
17906
|
+
}
|
|
17907
|
+
const finder = created.value;
|
|
17908
|
+
const ready = await finder.waitForScan(SEARCH_SCAN_TIMEOUT_MS);
|
|
17909
|
+
if (!ready.ok) {
|
|
17910
|
+
finder.destroy();
|
|
17911
|
+
throw new Error(`memory search index failed while waiting for scan: ${ready.error}`);
|
|
17912
|
+
}
|
|
17913
|
+
if (!ready.value) {
|
|
17914
|
+
this.logger.log("warn", `memory search scan is still warming after ${SEARCH_SCAN_TIMEOUT_MS}ms: ${rootPath}`);
|
|
17915
|
+
}
|
|
17916
|
+
this.finderCache.set(rootPath, finder);
|
|
17917
|
+
return finder;
|
|
17918
|
+
}
|
|
17919
|
+
}
|
|
17920
|
+
function searchRootForEntry(entry) {
|
|
17921
|
+
return entry.kind === "file" ? dirname3(entry.path) : entry.path;
|
|
17922
|
+
}
|
|
17923
|
+
function displayPathForMatch(entry, relativePath) {
|
|
17924
|
+
if (entry.kind === "file") {
|
|
17925
|
+
return entry.displayPath;
|
|
17926
|
+
}
|
|
17927
|
+
const normalizedRelativePath = relativePath.replaceAll("\\", "/");
|
|
17928
|
+
return normalizedRelativePath.length === 0 ? entry.displayPath : `${entry.displayPath}/${normalizedRelativePath}`;
|
|
17929
|
+
}
|
|
17930
|
+
function splitLines(text) {
|
|
17931
|
+
return text.split(/\r?\n/);
|
|
17932
|
+
}
|
|
17933
|
+
function readSnippetWindow(text, lineNumber, contextRadius) {
|
|
17934
|
+
const lines = splitLines(text);
|
|
17935
|
+
const matchIndex = Math.min(Math.max(lineNumber - 1, 0), Math.max(lines.length - 1, 0));
|
|
17936
|
+
const startIndex = Math.max(0, matchIndex - contextRadius);
|
|
17937
|
+
const endIndex = Math.min(lines.length - 1, matchIndex + contextRadius);
|
|
17938
|
+
return {
|
|
17939
|
+
lineStart: startIndex + 1,
|
|
17940
|
+
lineEnd: endIndex + 1,
|
|
17941
|
+
text: lines.slice(startIndex, endIndex + 1).join(`
|
|
17942
|
+
`)
|
|
17943
|
+
};
|
|
17944
|
+
}
|
|
17945
|
+
function normalizePositiveInteger(value, field, maxValue) {
|
|
17946
|
+
if (!Number.isSafeInteger(value)) {
|
|
17947
|
+
throw new Error(`${field} must be an integer`);
|
|
17948
|
+
}
|
|
17949
|
+
if (value <= 0) {
|
|
17950
|
+
throw new Error(`${field} must be greater than 0`);
|
|
17951
|
+
}
|
|
17952
|
+
if (maxValue !== undefined && value > maxValue) {
|
|
17953
|
+
throw new Error(`${field} must be less than or equal to ${maxValue}`);
|
|
17954
|
+
}
|
|
17955
|
+
return value;
|
|
17956
|
+
}
|
|
17957
|
+
function normalizeRequiredString(value, field) {
|
|
17958
|
+
const trimmed = value.trim();
|
|
17959
|
+
if (trimmed.length === 0) {
|
|
17960
|
+
throw new Error(`${field} must not be empty`);
|
|
17961
|
+
}
|
|
17962
|
+
return trimmed;
|
|
17963
|
+
}
|
|
17964
|
+
|
|
17744
17965
|
// src/opencode/adapter.ts
|
|
17745
17966
|
import { basename as basename2 } from "node:path";
|
|
17746
17967
|
import { pathToFileURL } from "node:url";
|
|
@@ -18010,18 +18231,24 @@ function listAssistantResponses(messages, userMessageId) {
|
|
|
18010
18231
|
function selectAssistantResponse(assistantChildren) {
|
|
18011
18232
|
for (let index = assistantChildren.length - 1;index >= 0; index -= 1) {
|
|
18012
18233
|
const candidate = assistantChildren[index];
|
|
18013
|
-
if (hasVisibleText(candidate)) {
|
|
18234
|
+
if (isTerminalAssistantMessage(candidate) && hasVisibleText(candidate)) {
|
|
18014
18235
|
return candidate;
|
|
18015
18236
|
}
|
|
18016
18237
|
}
|
|
18017
18238
|
for (let index = assistantChildren.length - 1;index >= 0; index -= 1) {
|
|
18018
18239
|
const candidate = assistantChildren[index];
|
|
18019
|
-
if (candidate
|
|
18240
|
+
if (isTerminalAssistantMessage(candidate)) {
|
|
18020
18241
|
return candidate;
|
|
18021
18242
|
}
|
|
18022
18243
|
}
|
|
18023
18244
|
return null;
|
|
18024
18245
|
}
|
|
18246
|
+
function isTerminalAssistantMessage(message) {
|
|
18247
|
+
if (message.info.error !== undefined) {
|
|
18248
|
+
return true;
|
|
18249
|
+
}
|
|
18250
|
+
return typeof message.info.finish === "string" && message.info.finish !== "tool-calls";
|
|
18251
|
+
}
|
|
18025
18252
|
function createAssistantProgressKey(messages) {
|
|
18026
18253
|
return JSON.stringify(messages.map(createAssistantCandidateKey));
|
|
18027
18254
|
}
|
|
@@ -22492,7 +22719,7 @@ function buildGatewayContextPrompt(targets) {
|
|
|
22492
22719
|
|
|
22493
22720
|
// src/store/sqlite.ts
|
|
22494
22721
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
22495
|
-
import { dirname as
|
|
22722
|
+
import { dirname as dirname4 } from "node:path";
|
|
22496
22723
|
|
|
22497
22724
|
// src/store/migrations.ts
|
|
22498
22725
|
var LATEST_SCHEMA_VERSION = 7;
|
|
@@ -23406,7 +23633,7 @@ function readBooleanField(value, field, fallback) {
|
|
|
23406
23633
|
return raw;
|
|
23407
23634
|
}
|
|
23408
23635
|
async function openSqliteStore(path) {
|
|
23409
|
-
await mkdir2(
|
|
23636
|
+
await mkdir2(dirname4(path), { recursive: true });
|
|
23410
23637
|
const { openRuntimeSqliteDatabase: openRuntimeSqliteDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
23411
23638
|
const db2 = await openRuntimeSqliteDatabase2(path);
|
|
23412
23639
|
migrateGatewayDatabase(db2);
|
|
@@ -24136,7 +24363,8 @@ class GatewayPluginRuntime {
|
|
|
24136
24363
|
channelSessions;
|
|
24137
24364
|
sessionContext;
|
|
24138
24365
|
systemPrompts;
|
|
24139
|
-
|
|
24366
|
+
memory;
|
|
24367
|
+
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts, memory) {
|
|
24140
24368
|
this.contract = contract;
|
|
24141
24369
|
this.executor = executor;
|
|
24142
24370
|
this.cron = cron;
|
|
@@ -24145,6 +24373,7 @@ class GatewayPluginRuntime {
|
|
|
24145
24373
|
this.channelSessions = channelSessions;
|
|
24146
24374
|
this.sessionContext = sessionContext;
|
|
24147
24375
|
this.systemPrompts = systemPrompts;
|
|
24376
|
+
this.memory = memory;
|
|
24148
24377
|
}
|
|
24149
24378
|
status() {
|
|
24150
24379
|
const rustStatus = this.contract.gatewayStatus();
|
|
@@ -24175,6 +24404,7 @@ async function createGatewayRuntime(module, input) {
|
|
|
24175
24404
|
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
24176
24405
|
const store = await openSqliteStore(config.stateDbPath);
|
|
24177
24406
|
const sessionContext = new GatewaySessionContext(store);
|
|
24407
|
+
const memory = new GatewayMemoryRuntime(config.memory, logger);
|
|
24178
24408
|
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
24179
24409
|
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
24180
24410
|
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
@@ -24199,7 +24429,7 @@ async function createGatewayRuntime(module, input) {
|
|
|
24199
24429
|
cron.start();
|
|
24200
24430
|
mailbox.start();
|
|
24201
24431
|
telegram.start();
|
|
24202
|
-
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
24432
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts, memory);
|
|
24203
24433
|
});
|
|
24204
24434
|
}
|
|
24205
24435
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
@@ -36761,6 +36991,63 @@ function formatGatewayStatus(status) {
|
|
|
36761
36991
|
`);
|
|
36762
36992
|
}
|
|
36763
36993
|
|
|
36994
|
+
// src/tools/memory-get.ts
|
|
36995
|
+
function createMemoryGetTool(runtime) {
|
|
36996
|
+
return tool({
|
|
36997
|
+
description: "Read a configured gateway memory file by path. Use the path returned by memory_search or a configured file path.",
|
|
36998
|
+
args: {
|
|
36999
|
+
path: tool.schema.string().min(1),
|
|
37000
|
+
start_line: tool.schema.number().optional(),
|
|
37001
|
+
max_lines: tool.schema.number().optional()
|
|
37002
|
+
},
|
|
37003
|
+
async execute(args) {
|
|
37004
|
+
return formatMemoryGetResult(await runtime.get(args.path, args.start_line, args.max_lines));
|
|
37005
|
+
}
|
|
37006
|
+
});
|
|
37007
|
+
}
|
|
37008
|
+
function formatMemoryGetResult(result) {
|
|
37009
|
+
return [
|
|
37010
|
+
`path=${result.path}`,
|
|
37011
|
+
`description=${result.description}`,
|
|
37012
|
+
`line_start=${result.lineStart}`,
|
|
37013
|
+
`line_end=${result.lineEnd}`,
|
|
37014
|
+
"content:",
|
|
37015
|
+
codeFence(result.infoString, result.text)
|
|
37016
|
+
].join(`
|
|
37017
|
+
`);
|
|
37018
|
+
}
|
|
37019
|
+
|
|
37020
|
+
// src/tools/memory-search.ts
|
|
37021
|
+
function createMemorySearchTool(runtime) {
|
|
37022
|
+
return tool({
|
|
37023
|
+
description: "Search configured gateway memory files and directories. Returns matching snippets and paths that can be read in more detail with memory_get.",
|
|
37024
|
+
args: {
|
|
37025
|
+
query: tool.schema.string().min(1),
|
|
37026
|
+
limit: tool.schema.number().optional()
|
|
37027
|
+
},
|
|
37028
|
+
async execute(args) {
|
|
37029
|
+
const results = await runtime.search(args.query, args.limit);
|
|
37030
|
+
if (results.length === 0) {
|
|
37031
|
+
return "no memory matches";
|
|
37032
|
+
}
|
|
37033
|
+
return results.map((result, index) => formatMemorySearchResult(result, index + 1)).join(`
|
|
37034
|
+
|
|
37035
|
+
`);
|
|
37036
|
+
}
|
|
37037
|
+
});
|
|
37038
|
+
}
|
|
37039
|
+
function formatMemorySearchResult(result, ordinal) {
|
|
37040
|
+
return [
|
|
37041
|
+
`result[${ordinal}].path=${result.path}`,
|
|
37042
|
+
`result[${ordinal}].description=${result.description}`,
|
|
37043
|
+
`result[${ordinal}].line_start=${result.lineStart}`,
|
|
37044
|
+
`result[${ordinal}].line_end=${result.lineEnd}`,
|
|
37045
|
+
`result[${ordinal}].snippet:`,
|
|
37046
|
+
codeFence(result.infoString, result.snippet)
|
|
37047
|
+
].join(`
|
|
37048
|
+
`);
|
|
37049
|
+
}
|
|
37050
|
+
|
|
36764
37051
|
// src/tools/schedule-cancel.ts
|
|
36765
37052
|
function createScheduleCancelTool(runtime) {
|
|
36766
37053
|
return tool({
|
|
@@ -37001,6 +37288,10 @@ var OpencodeGatewayPlugin = async (input) => {
|
|
|
37001
37288
|
schedule_once: createScheduleOnceTool(runtime.cron, runtime.sessionContext),
|
|
37002
37289
|
schedule_status: createScheduleStatusTool(runtime.cron)
|
|
37003
37290
|
};
|
|
37291
|
+
if (runtime.memory.hasEntries()) {
|
|
37292
|
+
tools.memory_search = createMemorySearchTool(runtime.memory);
|
|
37293
|
+
tools.memory_get = createMemoryGetTool(runtime.memory);
|
|
37294
|
+
}
|
|
37004
37295
|
if (runtime.files.hasEnabledChannel()) {
|
|
37005
37296
|
tools.channel_send_file = createChannelSendFileTool(runtime.files, runtime.sessionContext);
|
|
37006
37297
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig, GatewayMemoryEntryConfig } from "../config/memory";
|
|
3
|
+
export type SearchableMemoryFile = {
|
|
4
|
+
path: string;
|
|
5
|
+
displayPath: string;
|
|
6
|
+
description: string;
|
|
7
|
+
infoString: string;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function collectInjectedMemoryFiles(entry: GatewayMemoryEntryConfig, logger: Pick<BindingLoggerHost, "log">): Promise<SearchableMemoryFile[]>;
|
|
11
|
+
export declare function collectSearchableMemoryFiles(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">): Promise<SearchableMemoryFile[]>;
|
|
12
|
+
export declare function readMemoryTextFile(path: string, logger: Pick<BindingLoggerHost, "log">): Promise<string | null>;
|
|
13
|
+
export declare function codeFence(infoString: string, text: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig } from "../config/memory";
|
|
3
|
+
export type MemorySearchResult = {
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
lineStart: number;
|
|
7
|
+
lineEnd: number;
|
|
8
|
+
snippet: string;
|
|
9
|
+
infoString: string;
|
|
10
|
+
};
|
|
11
|
+
export type MemoryGetResult = {
|
|
12
|
+
path: string;
|
|
13
|
+
description: string;
|
|
14
|
+
lineStart: number;
|
|
15
|
+
lineEnd: number;
|
|
16
|
+
text: string;
|
|
17
|
+
infoString: string;
|
|
18
|
+
};
|
|
19
|
+
export declare class GatewayMemoryRuntime {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
private readonly finderCache;
|
|
23
|
+
constructor(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">);
|
|
24
|
+
hasEntries(): boolean;
|
|
25
|
+
search(query: string, limit?: number): Promise<MemorySearchResult[]>;
|
|
26
|
+
get(path: string, startLine?: number, maxLines?: number): Promise<MemoryGetResult>;
|
|
27
|
+
private getFinder;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Gateway plugin for OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"@ff-labs/fff-node": "^0.4.2",
|
|
55
56
|
"@opencode-ai/plugin": "~1.3.0",
|
|
56
57
|
"@opencode-ai/sdk": "~1.3.0",
|
|
57
58
|
"better-sqlite3": "^12.8.0",
|