opencode-gateway 0.2.2 → 0.2.4
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/cli.js +0 -0
- package/dist/index.js +20907 -52
- package/dist/telegram/client.d.ts +1 -1
- package/dist/telegram/poller.d.ts +16 -1
- package/dist/telegram/runtime.d.ts +3 -1
- package/dist/telegram/state.d.ts +7 -0
- package/package.json +1 -1
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -179
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -97
- package/dist/telegram/runtime.js +0 -133
- package/dist/telegram/state.js +0 -128
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/gateway.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { loadGatewayConfig } from "./config/gateway";
|
|
3
|
-
import { GatewayCronRuntime } from "./cron/runtime";
|
|
4
|
-
import { TelegramProgressiveSupport } from "./delivery/telegram";
|
|
5
|
-
import { GatewayTextDelivery } from "./delivery/text";
|
|
6
|
-
import { ChannelFileSender } from "./host/file-sender";
|
|
7
|
-
import { ConsoleLoggerHost } from "./host/logger";
|
|
8
|
-
import { GatewayTransportHost } from "./host/transport";
|
|
9
|
-
import { GatewayMailboxRouter } from "./mailbox/router";
|
|
10
|
-
import { GatewayMemoryPromptProvider } from "./memory/prompt";
|
|
11
|
-
import { OpencodeSdkAdapter } from "./opencode/adapter";
|
|
12
|
-
import { OpencodeEventStream } from "./opencode/event-stream";
|
|
13
|
-
import { OpencodeEventHub } from "./opencode/events";
|
|
14
|
-
import { createQuestionClient } from "./questions/client";
|
|
15
|
-
import { GatewayQuestionRuntime } from "./questions/runtime";
|
|
16
|
-
import { GatewayExecutor } from "./runtime/executor";
|
|
17
|
-
import { GatewayMailboxRuntime } from "./runtime/mailbox";
|
|
18
|
-
import { getOrCreateRuntimeSingleton } from "./runtime/runtime-singleton";
|
|
19
|
-
import { GatewaySessionContext } from "./session/context";
|
|
20
|
-
import { resolveConversationKeyForTarget } from "./session/conversation-key";
|
|
21
|
-
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
22
|
-
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
23
|
-
import { openSqliteStore } from "./store/sqlite";
|
|
24
|
-
import { TelegramBotClient } from "./telegram/client";
|
|
25
|
-
import { TelegramInboundMediaStore } from "./telegram/media";
|
|
26
|
-
import { TelegramPollingService } from "./telegram/poller";
|
|
27
|
-
import { GatewayTelegramRuntime } from "./telegram/runtime";
|
|
28
|
-
export class GatewayPluginRuntime {
|
|
29
|
-
contract;
|
|
30
|
-
executor;
|
|
31
|
-
cron;
|
|
32
|
-
telegram;
|
|
33
|
-
files;
|
|
34
|
-
channelSessions;
|
|
35
|
-
sessionContext;
|
|
36
|
-
systemPrompts;
|
|
37
|
-
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts) {
|
|
38
|
-
this.contract = contract;
|
|
39
|
-
this.executor = executor;
|
|
40
|
-
this.cron = cron;
|
|
41
|
-
this.telegram = telegram;
|
|
42
|
-
this.files = files;
|
|
43
|
-
this.channelSessions = channelSessions;
|
|
44
|
-
this.sessionContext = sessionContext;
|
|
45
|
-
this.systemPrompts = systemPrompts;
|
|
46
|
-
}
|
|
47
|
-
status() {
|
|
48
|
-
const rustStatus = this.contract.gatewayStatus();
|
|
49
|
-
return {
|
|
50
|
-
runtimeMode: rustStatus.runtimeMode,
|
|
51
|
-
supportsTelegram: rustStatus.supportsTelegram,
|
|
52
|
-
supportsCron: rustStatus.supportsCron,
|
|
53
|
-
hasWebUi: rustStatus.hasWebUi,
|
|
54
|
-
cronTimezone: this.cron.timeZone(),
|
|
55
|
-
cronEnabled: this.cron.isEnabled(),
|
|
56
|
-
cronPolling: this.cron.isRunning(),
|
|
57
|
-
cronRunningJobs: this.cron.runningJobs(),
|
|
58
|
-
telegramEnabled: this.telegram.isEnabled(),
|
|
59
|
-
telegramPolling: this.telegram.isPolling(),
|
|
60
|
-
telegramAllowlistMode: this.telegram.allowlistMode(),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export async function createGatewayRuntime(module, input) {
|
|
65
|
-
const config = await loadGatewayConfig();
|
|
66
|
-
return await getOrCreateRuntimeSingleton(config.configPath, async () => {
|
|
67
|
-
await mkdir(config.workspaceDirPath, { recursive: true });
|
|
68
|
-
const logger = new ConsoleLoggerHost(config.logLevel);
|
|
69
|
-
if (config.hasLegacyGatewayTimezone) {
|
|
70
|
-
const suffix = config.legacyGatewayTimezone === null ? "" : ` (${config.legacyGatewayTimezone})`;
|
|
71
|
-
logger.log("warn", `gateway.timezone${suffix} is ignored; use cron.timezone instead`);
|
|
72
|
-
}
|
|
73
|
-
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
74
|
-
const store = await openSqliteStore(config.stateDbPath);
|
|
75
|
-
const sessionContext = new GatewaySessionContext(store);
|
|
76
|
-
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
77
|
-
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
78
|
-
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
79
|
-
const telegramMediaStore = config.telegram.enabled && telegramClient !== null
|
|
80
|
-
? new TelegramInboundMediaStore(telegramClient, config.mediaRootPath)
|
|
81
|
-
: null;
|
|
82
|
-
const mailboxRouter = new GatewayMailboxRouter(config.mailbox.routes);
|
|
83
|
-
const opencodeEvents = new OpencodeEventHub();
|
|
84
|
-
const opencode = new OpencodeSdkAdapter(input.client, config.workspaceDirPath);
|
|
85
|
-
const questionClient = createQuestionClient(input.client, input.serverUrl, config.workspaceDirPath);
|
|
86
|
-
const transport = new GatewayTransportHost(telegramClient, store);
|
|
87
|
-
const files = new ChannelFileSender(telegramClient);
|
|
88
|
-
const channelSessions = new ChannelSessionSwitcher(store, sessionContext, mailboxRouter, module, opencode, config.telegram.enabled);
|
|
89
|
-
const questions = new GatewayQuestionRuntime(questionClient, config.workspaceDirPath, store, sessionContext, transport, telegramClient, logger);
|
|
90
|
-
const progressiveSupport = new TelegramProgressiveSupport(telegramClient, store, logger);
|
|
91
|
-
const delivery = new GatewayTextDelivery(transport, store, progressiveSupport);
|
|
92
|
-
const executor = new GatewayExecutor(module, store, opencode, opencodeEvents, delivery, logger);
|
|
93
|
-
const mailbox = new GatewayMailboxRuntime(executor, store, logger, config.mailbox, questions);
|
|
94
|
-
const cron = new GatewayCronRuntime(executor, module, store, logger, config.cron, effectiveCronTimeZone, (target) => resolveConversationKeyForTarget(target, mailboxRouter, module));
|
|
95
|
-
const eventStream = new OpencodeEventStream(input.client, config.workspaceDirPath, opencodeEvents, [questions], logger);
|
|
96
|
-
const telegramPolling = config.telegram.enabled && telegramClient !== null && telegramMediaStore !== null
|
|
97
|
-
? new TelegramPollingService(telegramClient, mailbox, store, logger, config.telegram, mailboxRouter, telegramMediaStore, questions)
|
|
98
|
-
: null;
|
|
99
|
-
const telegram = new GatewayTelegramRuntime(telegramClient, delivery, store, logger, config.telegram, telegramPolling, eventStream);
|
|
100
|
-
eventStream.start();
|
|
101
|
-
cron.start();
|
|
102
|
-
mailbox.start();
|
|
103
|
-
telegram.start();
|
|
104
|
-
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
function resolveEffectiveCronTimeZone(module, config) {
|
|
108
|
-
const candidate = config.cron.timezone ?? resolveRuntimeLocalTimeZone();
|
|
109
|
-
return module.normalizeCronTimeZone(candidate);
|
|
110
|
-
}
|
|
111
|
-
function resolveRuntimeLocalTimeZone() {
|
|
112
|
-
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
113
|
-
if (typeof timeZone !== "string" || timeZone.trim().length === 0) {
|
|
114
|
-
throw new Error("runtime local time zone could not be determined");
|
|
115
|
-
}
|
|
116
|
-
return timeZone;
|
|
117
|
-
}
|
package/dist/host/file-sender.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute } from "node:path";
|
|
3
|
-
import { inferLocalFileMimeType, isImageMimeType } from "../media/mime";
|
|
4
|
-
export class ChannelFileSender {
|
|
5
|
-
telegramClient;
|
|
6
|
-
constructor(telegramClient) {
|
|
7
|
-
this.telegramClient = telegramClient;
|
|
8
|
-
}
|
|
9
|
-
hasEnabledChannel() {
|
|
10
|
-
return this.telegramClient !== null;
|
|
11
|
-
}
|
|
12
|
-
async sendFile(target, filePath, caption) {
|
|
13
|
-
const normalizedPath = normalizeAbsoluteFilePath(filePath);
|
|
14
|
-
await assertRegularFile(normalizedPath);
|
|
15
|
-
const mimeType = await inferLocalFileMimeType(normalizedPath);
|
|
16
|
-
if (target.channel !== "telegram") {
|
|
17
|
-
throw new Error(`unsupported outbound channel: ${target.channel}`);
|
|
18
|
-
}
|
|
19
|
-
if (this.telegramClient === null) {
|
|
20
|
-
throw new Error("telegram transport is not configured");
|
|
21
|
-
}
|
|
22
|
-
if (isImageMimeType(mimeType)) {
|
|
23
|
-
await this.telegramClient.sendPhoto(target.target, normalizedPath, caption, target.topic, mimeType);
|
|
24
|
-
return {
|
|
25
|
-
channel: target.channel,
|
|
26
|
-
target: target.target,
|
|
27
|
-
topic: target.topic,
|
|
28
|
-
filePath: normalizedPath,
|
|
29
|
-
mimeType,
|
|
30
|
-
deliveryKind: "photo",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
await this.telegramClient.sendDocument(target.target, normalizedPath, caption, target.topic, mimeType);
|
|
34
|
-
return {
|
|
35
|
-
channel: target.channel,
|
|
36
|
-
target: target.target,
|
|
37
|
-
topic: target.topic,
|
|
38
|
-
filePath: normalizedPath,
|
|
39
|
-
mimeType,
|
|
40
|
-
deliveryKind: "document",
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function normalizeAbsoluteFilePath(filePath) {
|
|
45
|
-
const trimmed = filePath.trim();
|
|
46
|
-
if (trimmed.length === 0) {
|
|
47
|
-
throw new Error("file_path must not be empty");
|
|
48
|
-
}
|
|
49
|
-
if (!isAbsolute(trimmed)) {
|
|
50
|
-
throw new Error("file_path must be an absolute path");
|
|
51
|
-
}
|
|
52
|
-
return trimmed;
|
|
53
|
-
}
|
|
54
|
-
async function assertRegularFile(filePath) {
|
|
55
|
-
const metadata = await stat(filePath);
|
|
56
|
-
if (!metadata.isFile()) {
|
|
57
|
-
throw new Error(`file_path is not a regular file: ${filePath}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
package/dist/host/logger.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
const LOG_LEVEL_PRIORITY = {
|
|
2
|
-
debug: 10,
|
|
3
|
-
info: 20,
|
|
4
|
-
warn: 30,
|
|
5
|
-
error: 40,
|
|
6
|
-
};
|
|
7
|
-
export class ConsoleLoggerHost {
|
|
8
|
-
threshold;
|
|
9
|
-
constructor(threshold) {
|
|
10
|
-
this.threshold = threshold;
|
|
11
|
-
}
|
|
12
|
-
log(level, message) {
|
|
13
|
-
if (!shouldLog(level, this.threshold)) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
const line = `[gateway:${level}] ${message}`;
|
|
17
|
-
switch (level) {
|
|
18
|
-
case "error":
|
|
19
|
-
console.error(line);
|
|
20
|
-
return;
|
|
21
|
-
case "warn":
|
|
22
|
-
console.warn(line);
|
|
23
|
-
return;
|
|
24
|
-
default:
|
|
25
|
-
console.info(line);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export function parseGatewayLogLevel(value, field) {
|
|
30
|
-
if (value === undefined) {
|
|
31
|
-
return "off";
|
|
32
|
-
}
|
|
33
|
-
if (typeof value !== "string") {
|
|
34
|
-
throw new Error(`${field} must be a string when present`);
|
|
35
|
-
}
|
|
36
|
-
const normalized = value.trim().toLowerCase();
|
|
37
|
-
if (normalized === "off") {
|
|
38
|
-
return "off";
|
|
39
|
-
}
|
|
40
|
-
if (isBindingLogLevel(normalized)) {
|
|
41
|
-
return normalized;
|
|
42
|
-
}
|
|
43
|
-
throw new Error(`${field} must be one of: off, error, warn, info, debug`);
|
|
44
|
-
}
|
|
45
|
-
function shouldLog(level, threshold) {
|
|
46
|
-
if (threshold === "off") {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[threshold];
|
|
50
|
-
}
|
|
51
|
-
function isBindingLogLevel(value) {
|
|
52
|
-
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
53
|
-
}
|
package/dist/host/transport.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { recordTelegramSendFailure, recordTelegramSendSuccess } from "../telegram/state";
|
|
2
|
-
import { formatError } from "../utils/error";
|
|
3
|
-
export class GatewayTransportHost {
|
|
4
|
-
telegramClient;
|
|
5
|
-
store;
|
|
6
|
-
constructor(telegramClient, store) {
|
|
7
|
-
this.telegramClient = telegramClient;
|
|
8
|
-
this.store = store;
|
|
9
|
-
}
|
|
10
|
-
async sendMessage(message) {
|
|
11
|
-
try {
|
|
12
|
-
if (message.deliveryTarget.channel !== "telegram") {
|
|
13
|
-
throw new Error(`unsupported outbound channel: ${message.deliveryTarget.channel}`);
|
|
14
|
-
}
|
|
15
|
-
if (this.telegramClient === null) {
|
|
16
|
-
throw new Error("telegram transport is not configured");
|
|
17
|
-
}
|
|
18
|
-
const body = message.body.trim();
|
|
19
|
-
if (body.length === 0) {
|
|
20
|
-
throw new Error("telegram outbound message body must not be empty");
|
|
21
|
-
}
|
|
22
|
-
await this.telegramClient.sendMessage(message.deliveryTarget.target, body, message.deliveryTarget.topic);
|
|
23
|
-
recordTelegramSendSuccess(this.store, Date.now());
|
|
24
|
-
return {
|
|
25
|
-
errorMessage: null,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
recordTelegramSendFailure(this.store, formatError(error), Date.now());
|
|
30
|
-
return {
|
|
31
|
-
errorMessage: formatError(error),
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
package/dist/mailbox/router.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export class GatewayMailboxRouter {
|
|
2
|
-
routes;
|
|
3
|
-
constructor(routes) {
|
|
4
|
-
this.routes = routes;
|
|
5
|
-
}
|
|
6
|
-
resolve(target) {
|
|
7
|
-
for (const route of this.routes) {
|
|
8
|
-
if (route.channel === target.channel &&
|
|
9
|
-
route.target === target.target &&
|
|
10
|
-
route.topic === (target.topic ?? null)) {
|
|
11
|
-
return route.mailboxKey;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
package/dist/media/mime.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { open } from "node:fs/promises";
|
|
2
|
-
const FALLBACK_MIME_TYPE = "application/octet-stream";
|
|
3
|
-
export async function inferLocalFileMimeType(filePath) {
|
|
4
|
-
const bunMimeType = Bun.file(filePath).type.trim();
|
|
5
|
-
if (bunMimeType.length > 0 && bunMimeType !== FALLBACK_MIME_TYPE) {
|
|
6
|
-
return bunMimeType;
|
|
7
|
-
}
|
|
8
|
-
const header = await readFileHeader(filePath, 16);
|
|
9
|
-
return inferImageMimeTypeFromHeader(header) ?? (bunMimeType.length > 0 ? bunMimeType : FALLBACK_MIME_TYPE);
|
|
10
|
-
}
|
|
11
|
-
export function isImageMimeType(mimeType) {
|
|
12
|
-
return mimeType.startsWith("image/");
|
|
13
|
-
}
|
|
14
|
-
async function readFileHeader(filePath, length) {
|
|
15
|
-
const file = await open(filePath, "r");
|
|
16
|
-
try {
|
|
17
|
-
const buffer = new Uint8Array(length);
|
|
18
|
-
const result = await file.read(buffer, 0, length, 0);
|
|
19
|
-
return buffer.subarray(0, result.bytesRead);
|
|
20
|
-
}
|
|
21
|
-
finally {
|
|
22
|
-
await file.close();
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function inferImageMimeTypeFromHeader(header) {
|
|
26
|
-
if (matchesPrefix(header, [0xff, 0xd8, 0xff])) {
|
|
27
|
-
return "image/jpeg";
|
|
28
|
-
}
|
|
29
|
-
if (matchesPrefix(header, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
30
|
-
return "image/png";
|
|
31
|
-
}
|
|
32
|
-
if (matchesAsciiPrefix(header, "GIF87a") || matchesAsciiPrefix(header, "GIF89a")) {
|
|
33
|
-
return "image/gif";
|
|
34
|
-
}
|
|
35
|
-
if (matchesAsciiPrefix(header, "RIFF") && matchesAsciiPrefix(header.subarray(8), "WEBP")) {
|
|
36
|
-
return "image/webp";
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
function matchesPrefix(header, prefix) {
|
|
41
|
-
return prefix.every((byte, index) => header[index] === byte);
|
|
42
|
-
}
|
|
43
|
-
function matchesAsciiPrefix(header, prefix) {
|
|
44
|
-
return prefix.split("").every((char, index) => header[index] === char.charCodeAt(0));
|
|
45
|
-
}
|
package/dist/memory/prompt.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, relative } from "node:path";
|
|
3
|
-
const MARKDOWN_GLOBS = ["**/*.md", "**/*.markdown"];
|
|
4
|
-
const UTF8_TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
-
export class GatewayMemoryPromptProvider {
|
|
6
|
-
config;
|
|
7
|
-
logger;
|
|
8
|
-
constructor(config, logger) {
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.logger = logger;
|
|
11
|
-
}
|
|
12
|
-
async buildPrompt() {
|
|
13
|
-
if (this.config.entries.length === 0) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
const sections = await Promise.all(this.config.entries.map((entry) => this.buildEntrySection(entry)));
|
|
17
|
-
return ["Gateway memory:", ...sections].join("\n\n");
|
|
18
|
-
}
|
|
19
|
-
async buildEntrySection(entry) {
|
|
20
|
-
const lines = [`Configured path: ${entry.displayPath}`, `Description: ${entry.description}`];
|
|
21
|
-
const injectedFiles = await collectInjectedFiles(entry, this.logger);
|
|
22
|
-
for (const file of injectedFiles) {
|
|
23
|
-
lines.push("");
|
|
24
|
-
lines.push(`File: ${file.displayPath}`);
|
|
25
|
-
lines.push(codeFence(file.infoString, file.text));
|
|
26
|
-
}
|
|
27
|
-
return lines.join("\n");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async function collectInjectedFiles(entry, logger) {
|
|
31
|
-
if (entry.kind === "file") {
|
|
32
|
-
if (!entry.injectContent) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const text = await readTextFile(entry.path, logger);
|
|
36
|
-
if (text === null) {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
displayPath: entry.displayPath,
|
|
42
|
-
infoString: inferFenceInfoString(entry.path),
|
|
43
|
-
text,
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
const filePaths = new Set();
|
|
48
|
-
if (entry.injectMarkdownContents) {
|
|
49
|
-
for (const pattern of MARKDOWN_GLOBS) {
|
|
50
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
for (const pattern of entry.globs) {
|
|
54
|
-
addMatchingFiles(filePaths, entry.path, pattern);
|
|
55
|
-
}
|
|
56
|
-
const injectedFiles = [];
|
|
57
|
-
for (const filePath of [...filePaths].sort((left, right) => left.localeCompare(right))) {
|
|
58
|
-
const text = await readTextFile(filePath, logger);
|
|
59
|
-
if (text === null) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
injectedFiles.push({
|
|
63
|
-
displayPath: relativeDisplayPath(entry.path, entry.displayPath, filePath),
|
|
64
|
-
infoString: inferFenceInfoString(filePath),
|
|
65
|
-
text,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return injectedFiles;
|
|
69
|
-
}
|
|
70
|
-
function addMatchingFiles(result, cwd, pattern) {
|
|
71
|
-
const glob = new Bun.Glob(pattern);
|
|
72
|
-
for (const match of glob.scanSync({ cwd, absolute: true, onlyFiles: true })) {
|
|
73
|
-
result.add(match);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function readTextFile(path, logger) {
|
|
77
|
-
let bytes;
|
|
78
|
-
try {
|
|
79
|
-
bytes = await readFile(path);
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
logger.log("warn", `memory file could not be read and will be skipped: ${path}: ${formatError(error)}`);
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
let text;
|
|
86
|
-
try {
|
|
87
|
-
text = UTF8_TEXT_DECODER.decode(bytes);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
logger.log("warn", `memory file is not valid UTF-8 and will be skipped: ${path}`);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
if (text.includes("\u0000")) {
|
|
94
|
-
logger.log("warn", `memory file looks binary and will be skipped: ${path}`);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return text;
|
|
98
|
-
}
|
|
99
|
-
function relativeDisplayPath(rootPath, rootDisplayPath, filePath) {
|
|
100
|
-
const suffix = relative(rootPath, filePath);
|
|
101
|
-
if (suffix.length === 0) {
|
|
102
|
-
return rootDisplayPath;
|
|
103
|
-
}
|
|
104
|
-
return `${rootDisplayPath}/${suffix.replaceAll("\\", "/")}`;
|
|
105
|
-
}
|
|
106
|
-
function inferFenceInfoString(path) {
|
|
107
|
-
const extension = extname(path).slice(1).toLowerCase();
|
|
108
|
-
if (!/^[a-z0-9_+-]+$/.test(extension)) {
|
|
109
|
-
return "";
|
|
110
|
-
}
|
|
111
|
-
return extension;
|
|
112
|
-
}
|
|
113
|
-
function codeFence(infoString, text) {
|
|
114
|
-
const language = infoString.length === 0 ? "" : infoString;
|
|
115
|
-
return [`\`\`\`${language}`, text, "```"].join("\n");
|
|
116
|
-
}
|
|
117
|
-
function formatError(error) {
|
|
118
|
-
if (error instanceof Error && error.message.trim().length > 0) {
|
|
119
|
-
return error.message;
|
|
120
|
-
}
|
|
121
|
-
return String(error);
|
|
122
|
-
}
|