opencode-gateway 0.1.1 → 0.2.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.
- package/README.md +26 -0
- package/dist/binding/gateway.d.ts +2 -1
- package/dist/binding/index.d.ts +1 -1
- package/dist/cli/templates.js +15 -0
- package/dist/cli.js +15 -0
- package/dist/config/gateway.d.ts +4 -0
- package/dist/config/gateway.js +4 -0
- package/dist/config/memory.d.ts +18 -0
- package/dist/config/memory.js +105 -0
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +10 -4
- package/dist/host/logger.d.ts +8 -0
- package/dist/host/logger.js +53 -0
- package/dist/index.js +2 -2
- package/dist/memory/prompt.d.ts +9 -0
- package/dist/memory/prompt.js +122 -0
- package/dist/runtime/executor.d.ts +1 -0
- package/dist/runtime/executor.js +14 -2
- package/dist/session/context.d.ts +1 -1
- package/dist/session/context.js +2 -29
- package/dist/session/system-prompt.d.ts +8 -0
- package/dist/session/system-prompt.js +52 -0
- package/dist/store/sqlite.d.ts +1 -0
- package/dist/store/sqlite.js +22 -0
- package/package.json +1 -1
- package/dist/host/noop.d.ts +0 -4
- package/dist/host/noop.js +0 -14
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ opencode serve
|
|
|
48
48
|
```toml
|
|
49
49
|
[gateway]
|
|
50
50
|
state_db = "/home/you/.local/share/opencode-gateway/state.db"
|
|
51
|
+
# log_level = "warn"
|
|
51
52
|
|
|
52
53
|
[cron]
|
|
53
54
|
enabled = true
|
|
@@ -60,6 +61,17 @@ bot_token_env = "TELEGRAM_BOT_TOKEN"
|
|
|
60
61
|
poll_timeout_seconds = 25
|
|
61
62
|
allowed_chats = []
|
|
62
63
|
allowed_users = []
|
|
64
|
+
|
|
65
|
+
[[memory.entries]]
|
|
66
|
+
path = "memory/project.md"
|
|
67
|
+
description = "Project conventions and long-lived context"
|
|
68
|
+
inject_content = true
|
|
69
|
+
|
|
70
|
+
[[memory.entries]]
|
|
71
|
+
path = "memory/notes"
|
|
72
|
+
description = "Domain notes and operating docs"
|
|
73
|
+
inject_markdown_contents = true
|
|
74
|
+
globs = ["**/*.rs", "notes/**/*.txt"]
|
|
63
75
|
```
|
|
64
76
|
|
|
65
77
|
When Telegram is enabled, export the bot token through the configured
|
|
@@ -68,3 +80,17 @@ environment variable, for example:
|
|
|
68
80
|
```bash
|
|
69
81
|
export TELEGRAM_BOT_TOKEN="..."
|
|
70
82
|
```
|
|
83
|
+
|
|
84
|
+
Gateway plugin logs are off by default. Set `gateway.log_level` to `error`,
|
|
85
|
+
`warn`, `info`, or `debug` to emit that level and anything above it.
|
|
86
|
+
|
|
87
|
+
Memory rules:
|
|
88
|
+
|
|
89
|
+
- all entries inject their configured path and description
|
|
90
|
+
- file contents are injected only when `inject_content = true`
|
|
91
|
+
- directory entries default to description-only
|
|
92
|
+
- `inject_markdown_contents = true` recursively injects `*.md` and `*.markdown`
|
|
93
|
+
- `globs` are relative to the configured directory and may match other UTF-8
|
|
94
|
+
text files
|
|
95
|
+
- relative paths are resolved from `opencode-gateway.toml`
|
|
96
|
+
- memory is injected only into gateway-managed sessions
|
|
@@ -60,8 +60,9 @@ export type BindingOutboundMessage = {
|
|
|
60
60
|
export type BindingTransportHost = {
|
|
61
61
|
sendMessage(message: BindingOutboundMessage): Promise<BindingHostAck>;
|
|
62
62
|
};
|
|
63
|
+
export type BindingLogLevel = "debug" | "info" | "warn" | "error";
|
|
63
64
|
export type BindingLoggerHost = {
|
|
64
|
-
log(level:
|
|
65
|
+
log(level: BindingLogLevel, message: string): void;
|
|
65
66
|
};
|
|
66
67
|
export type GatewayContract = {
|
|
67
68
|
gatewayStatus(): GatewayStatusSnapshot;
|
package/dist/binding/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BindingCronJobSpec, BindingInboundMessage, BindingPreparedExecution, GatewayContract } from "./gateway";
|
|
2
2
|
import type { BindingOpencodeExecutionInput, OpencodeExecutionDriver } from "./opencode";
|
|
3
3
|
export type { BindingExecutionObservation, BindingProgressiveDirective } from "./execution";
|
|
4
|
-
export type { BindingCronJobSpec, BindingDeliveryTarget, BindingHostAck, BindingInboundAttachment, BindingInboundMessage, BindingLoggerHost, BindingOutboundMessage, BindingPreparedExecution, BindingPromptPart, BindingRuntimeReport, BindingTransportHost, GatewayContract, GatewayStatusSnapshot, } from "./gateway";
|
|
4
|
+
export type { BindingCronJobSpec, BindingDeliveryTarget, BindingHostAck, BindingInboundAttachment, BindingInboundMessage, BindingLoggerHost, BindingLogLevel, BindingOutboundMessage, BindingPreparedExecution, BindingPromptPart, BindingRuntimeReport, BindingTransportHost, GatewayContract, GatewayStatusSnapshot, } from "./gateway";
|
|
5
5
|
export type { BindingOpencodeCommand, BindingOpencodeCommandPart, BindingOpencodeCommandResult, BindingOpencodeDriverStep, BindingOpencodeExecutionInput, BindingOpencodeMessage, BindingOpencodeMessagePart, BindingOpencodePrompt, OpencodeExecutionDriver, } from "./opencode";
|
|
6
6
|
export type GatewayBindingModule = GatewayContract & {
|
|
7
7
|
prepareInboundExecution: (message: BindingInboundMessage) => BindingPreparedExecution;
|
package/dist/cli/templates.js
CHANGED
|
@@ -5,6 +5,7 @@ export function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
5
5
|
"",
|
|
6
6
|
"[gateway]",
|
|
7
7
|
`state_db = "${escapeTomlString(stateDbPath)}"`,
|
|
8
|
+
'# log_level = "warn"',
|
|
8
9
|
"",
|
|
9
10
|
"[cron]",
|
|
10
11
|
"enabled = true",
|
|
@@ -19,6 +20,20 @@ export function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
19
20
|
"allowed_chats = []",
|
|
20
21
|
"allowed_users = []",
|
|
21
22
|
"",
|
|
23
|
+
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
24
|
+
"# Relative paths are resolved from this config file.",
|
|
25
|
+
"#",
|
|
26
|
+
"# [[memory.entries]]",
|
|
27
|
+
'# path = "memory/project.md"',
|
|
28
|
+
'# description = "Project conventions and long-lived context"',
|
|
29
|
+
"# inject_content = true",
|
|
30
|
+
"#",
|
|
31
|
+
"# [[memory.entries]]",
|
|
32
|
+
'# path = "memory/notes"',
|
|
33
|
+
'# description = "Domain notes and operating docs"',
|
|
34
|
+
"# inject_markdown_contents = true",
|
|
35
|
+
'# globs = ["**/*.rs", "notes/**/*.txt"]',
|
|
36
|
+
"",
|
|
22
37
|
].join("\n");
|
|
23
38
|
}
|
|
24
39
|
function escapeTomlString(value) {
|
package/dist/cli.js
CHANGED
|
@@ -247,6 +247,7 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
247
247
|
"",
|
|
248
248
|
"[gateway]",
|
|
249
249
|
`state_db = "${escapeTomlString(stateDbPath)}"`,
|
|
250
|
+
'# log_level = "warn"',
|
|
250
251
|
"",
|
|
251
252
|
"[cron]",
|
|
252
253
|
"enabled = true",
|
|
@@ -260,6 +261,20 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
260
261
|
"poll_timeout_seconds = 25",
|
|
261
262
|
"allowed_chats = []",
|
|
262
263
|
"allowed_users = []",
|
|
264
|
+
"",
|
|
265
|
+
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
266
|
+
"# Relative paths are resolved from this config file.",
|
|
267
|
+
"#",
|
|
268
|
+
"# [[memory.entries]]",
|
|
269
|
+
'# path = "memory/project.md"',
|
|
270
|
+
'# description = "Project conventions and long-lived context"',
|
|
271
|
+
"# inject_content = true",
|
|
272
|
+
"#",
|
|
273
|
+
"# [[memory.entries]]",
|
|
274
|
+
'# path = "memory/notes"',
|
|
275
|
+
'# description = "Domain notes and operating docs"',
|
|
276
|
+
"# inject_markdown_contents = true",
|
|
277
|
+
'# globs = ["**/*.rs", "notes/**/*.txt"]',
|
|
263
278
|
""
|
|
264
279
|
].join(`
|
|
265
280
|
`);
|
package/dist/config/gateway.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type GatewayLogLevel } from "../host/logger";
|
|
1
2
|
import { type CronConfig } from "./cron";
|
|
3
|
+
import { type GatewayMemoryConfig } from "./memory";
|
|
2
4
|
import { type TelegramConfig } from "./telegram";
|
|
3
5
|
export type GatewayMailboxRouteConfig = {
|
|
4
6
|
channel: string;
|
|
@@ -16,9 +18,11 @@ export type GatewayConfig = {
|
|
|
16
18
|
stateDbPath: string;
|
|
17
19
|
mediaRootPath: string;
|
|
18
20
|
workspaceDirPath: string;
|
|
21
|
+
logLevel: GatewayLogLevel;
|
|
19
22
|
hasLegacyGatewayTimezone: boolean;
|
|
20
23
|
legacyGatewayTimezone: string | null;
|
|
21
24
|
mailbox: GatewayMailboxConfig;
|
|
25
|
+
memory: GatewayMemoryConfig;
|
|
22
26
|
cron: CronConfig;
|
|
23
27
|
telegram: TelegramConfig;
|
|
24
28
|
};
|
package/dist/config/gateway.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { parseGatewayLogLevel } from "../host/logger";
|
|
3
4
|
import { parseCronConfig } from "./cron";
|
|
5
|
+
import { parseMemoryConfig } from "./memory";
|
|
4
6
|
import { defaultGatewayStateDbPath, resolveGatewayConfigPath, resolveGatewayWorkspacePath } from "./paths";
|
|
5
7
|
import { parseTelegramConfig } from "./telegram";
|
|
6
8
|
export async function loadGatewayConfig(env = process.env) {
|
|
@@ -16,9 +18,11 @@ export async function loadGatewayConfig(env = process.env) {
|
|
|
16
18
|
stateDbPath,
|
|
17
19
|
mediaRootPath: resolveMediaRootPath(stateDbPath),
|
|
18
20
|
workspaceDirPath: resolveGatewayWorkspacePath(configPath),
|
|
21
|
+
logLevel: parseGatewayLogLevel(rawConfig?.gateway?.log_level, "gateway.log_level"),
|
|
19
22
|
hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
|
|
20
23
|
legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
|
|
21
24
|
mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
|
|
25
|
+
memory: await parseMemoryConfig(rawConfig?.memory, configPath),
|
|
22
26
|
cron: parseCronConfig(rawConfig?.cron),
|
|
23
27
|
telegram: parseTelegramConfig(rawConfig?.channels?.telegram, env),
|
|
24
28
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type GatewayMemoryConfig = {
|
|
2
|
+
entries: GatewayMemoryEntryConfig[];
|
|
3
|
+
};
|
|
4
|
+
export type GatewayMemoryEntryConfig = {
|
|
5
|
+
kind: "file";
|
|
6
|
+
path: string;
|
|
7
|
+
displayPath: string;
|
|
8
|
+
description: string;
|
|
9
|
+
injectContent: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "directory";
|
|
12
|
+
path: string;
|
|
13
|
+
displayPath: string;
|
|
14
|
+
description: string;
|
|
15
|
+
injectMarkdownContents: boolean;
|
|
16
|
+
globs: string[];
|
|
17
|
+
};
|
|
18
|
+
export declare function parseMemoryConfig(value: unknown, configPath: string): Promise<GatewayMemoryConfig>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
export async function parseMemoryConfig(value, configPath) {
|
|
4
|
+
const table = readMemoryTable(value);
|
|
5
|
+
const entries = await readMemoryEntries(table.entries, configPath);
|
|
6
|
+
return { entries };
|
|
7
|
+
}
|
|
8
|
+
function readMemoryTable(value) {
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
throw new Error("memory must be a table when present");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function readMemoryEntries(value, configPath) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
throw new Error("memory.entries must be an array when present");
|
|
23
|
+
}
|
|
24
|
+
return await Promise.all(value.map((entry, index) => readMemoryEntry(entry, index, configPath)));
|
|
25
|
+
}
|
|
26
|
+
async function readMemoryEntry(value, index, configPath) {
|
|
27
|
+
const field = `memory.entries[${index}]`;
|
|
28
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
29
|
+
throw new Error(`${field} must be a table`);
|
|
30
|
+
}
|
|
31
|
+
const entry = value;
|
|
32
|
+
const displayPath = readRequiredString(entry.path, `${field}.path`);
|
|
33
|
+
const description = readRequiredString(entry.description, `${field}.description`);
|
|
34
|
+
const resolvedPath = resolve(dirname(configPath), displayPath);
|
|
35
|
+
const metadata = await statPath(resolvedPath, `${field}.path`);
|
|
36
|
+
if (metadata.isFile()) {
|
|
37
|
+
ensureDirectoryOnlyFieldIsAbsent(entry.inject_markdown_contents, `${field}.inject_markdown_contents`);
|
|
38
|
+
ensureDirectoryOnlyFieldIsAbsent(entry.globs, `${field}.globs`);
|
|
39
|
+
return {
|
|
40
|
+
kind: "file",
|
|
41
|
+
path: resolvedPath,
|
|
42
|
+
displayPath,
|
|
43
|
+
description,
|
|
44
|
+
injectContent: readBoolean(entry.inject_content, `${field}.inject_content`, false),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (metadata.isDirectory()) {
|
|
48
|
+
ensureFileOnlyFieldIsAbsent(entry.inject_content, `${field}.inject_content`);
|
|
49
|
+
return {
|
|
50
|
+
kind: "directory",
|
|
51
|
+
path: resolvedPath,
|
|
52
|
+
displayPath,
|
|
53
|
+
description,
|
|
54
|
+
injectMarkdownContents: readBoolean(entry.inject_markdown_contents, `${field}.inject_markdown_contents`, false),
|
|
55
|
+
globs: readGlobList(entry.globs, `${field}.globs`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`${field}.path must point to a regular file or directory`);
|
|
59
|
+
}
|
|
60
|
+
async function statPath(path, field) {
|
|
61
|
+
try {
|
|
62
|
+
return await stat(path);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`${field} does not exist: ${path}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function ensureDirectoryOnlyFieldIsAbsent(value, field) {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
throw new Error(`${field} is only valid for directory entries`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function ensureFileOnlyFieldIsAbsent(value, field) {
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
throw new Error(`${field} is only valid for file entries`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function readBoolean(value, field, fallback) {
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "boolean") {
|
|
83
|
+
throw new Error(`${field} must be a boolean when present`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function readGlobList(value, field) {
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(value)) {
|
|
92
|
+
throw new Error(`${field} must be an array when present`);
|
|
93
|
+
}
|
|
94
|
+
return value.map((entry, index) => readRequiredString(entry, `${field}[${index}]`));
|
|
95
|
+
}
|
|
96
|
+
function readRequiredString(value, field) {
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
throw new Error(`${field} must be a string`);
|
|
99
|
+
}
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
if (trimmed.length === 0) {
|
|
102
|
+
throw new Error(`${field} must not be empty`);
|
|
103
|
+
}
|
|
104
|
+
return trimmed;
|
|
105
|
+
}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ChannelFileSender } from "./host/file-sender";
|
|
|
5
5
|
import { GatewayExecutor } from "./runtime/executor";
|
|
6
6
|
import { GatewaySessionContext } from "./session/context";
|
|
7
7
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
8
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
8
9
|
import { GatewayTelegramRuntime } from "./telegram/runtime";
|
|
9
10
|
export type GatewayPluginStatus = {
|
|
10
11
|
runtimeMode: string;
|
|
@@ -27,7 +28,8 @@ export declare class GatewayPluginRuntime {
|
|
|
27
28
|
readonly files: ChannelFileSender;
|
|
28
29
|
readonly channelSessions: ChannelSessionSwitcher;
|
|
29
30
|
readonly sessionContext: GatewaySessionContext;
|
|
30
|
-
|
|
31
|
+
readonly systemPrompts: GatewaySystemPromptBuilder;
|
|
32
|
+
constructor(contract: GatewayContract, executor: GatewayExecutor, cron: GatewayCronRuntime, telegram: GatewayTelegramRuntime, files: ChannelFileSender, channelSessions: ChannelSessionSwitcher, sessionContext: GatewaySessionContext, systemPrompts: GatewaySystemPromptBuilder);
|
|
31
33
|
status(): GatewayPluginStatus;
|
|
32
34
|
}
|
|
33
35
|
export declare function createGatewayRuntime(module: GatewayBindingModule, input: PluginInput): Promise<GatewayPluginRuntime>;
|
package/dist/gateway.js
CHANGED
|
@@ -4,9 +4,10 @@ import { GatewayCronRuntime } from "./cron/runtime";
|
|
|
4
4
|
import { TelegramProgressiveSupport } from "./delivery/telegram";
|
|
5
5
|
import { GatewayTextDelivery } from "./delivery/text";
|
|
6
6
|
import { ChannelFileSender } from "./host/file-sender";
|
|
7
|
-
import { ConsoleLoggerHost } from "./host/
|
|
7
|
+
import { ConsoleLoggerHost } from "./host/logger";
|
|
8
8
|
import { GatewayTransportHost } from "./host/transport";
|
|
9
9
|
import { GatewayMailboxRouter } from "./mailbox/router";
|
|
10
|
+
import { GatewayMemoryPromptProvider } from "./memory/prompt";
|
|
10
11
|
import { OpencodeSdkAdapter } from "./opencode/adapter";
|
|
11
12
|
import { OpencodeEventStream } from "./opencode/event-stream";
|
|
12
13
|
import { OpencodeEventHub } from "./opencode/events";
|
|
@@ -18,6 +19,7 @@ import { getOrCreateRuntimeSingleton } from "./runtime/runtime-singleton";
|
|
|
18
19
|
import { GatewaySessionContext } from "./session/context";
|
|
19
20
|
import { resolveConversationKeyForTarget } from "./session/conversation-key";
|
|
20
21
|
import { ChannelSessionSwitcher } from "./session/switcher";
|
|
22
|
+
import { GatewaySystemPromptBuilder } from "./session/system-prompt";
|
|
21
23
|
import { openSqliteStore } from "./store/sqlite";
|
|
22
24
|
import { TelegramBotClient } from "./telegram/client";
|
|
23
25
|
import { TelegramInboundMediaStore } from "./telegram/media";
|
|
@@ -31,7 +33,8 @@ export class GatewayPluginRuntime {
|
|
|
31
33
|
files;
|
|
32
34
|
channelSessions;
|
|
33
35
|
sessionContext;
|
|
34
|
-
|
|
36
|
+
systemPrompts;
|
|
37
|
+
constructor(contract, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts) {
|
|
35
38
|
this.contract = contract;
|
|
36
39
|
this.executor = executor;
|
|
37
40
|
this.cron = cron;
|
|
@@ -39,6 +42,7 @@ export class GatewayPluginRuntime {
|
|
|
39
42
|
this.files = files;
|
|
40
43
|
this.channelSessions = channelSessions;
|
|
41
44
|
this.sessionContext = sessionContext;
|
|
45
|
+
this.systemPrompts = systemPrompts;
|
|
42
46
|
}
|
|
43
47
|
status() {
|
|
44
48
|
const rustStatus = this.contract.gatewayStatus();
|
|
@@ -61,7 +65,7 @@ export async function createGatewayRuntime(module, input) {
|
|
|
61
65
|
const config = await loadGatewayConfig();
|
|
62
66
|
return await getOrCreateRuntimeSingleton(config.configPath, async () => {
|
|
63
67
|
await mkdir(config.workspaceDirPath, { recursive: true });
|
|
64
|
-
const logger = new ConsoleLoggerHost();
|
|
68
|
+
const logger = new ConsoleLoggerHost(config.logLevel);
|
|
65
69
|
if (config.hasLegacyGatewayTimezone) {
|
|
66
70
|
const suffix = config.legacyGatewayTimezone === null ? "" : ` (${config.legacyGatewayTimezone})`;
|
|
67
71
|
logger.log("warn", `gateway.timezone${suffix} is ignored; use cron.timezone instead`);
|
|
@@ -69,6 +73,8 @@ export async function createGatewayRuntime(module, input) {
|
|
|
69
73
|
const effectiveCronTimeZone = resolveEffectiveCronTimeZone(module, config);
|
|
70
74
|
const store = await openSqliteStore(config.stateDbPath);
|
|
71
75
|
const sessionContext = new GatewaySessionContext(store);
|
|
76
|
+
const memoryPrompts = new GatewayMemoryPromptProvider(config.memory, logger);
|
|
77
|
+
const systemPrompts = new GatewaySystemPromptBuilder(sessionContext, memoryPrompts);
|
|
72
78
|
const telegramClient = config.telegram.enabled ? new TelegramBotClient(config.telegram.botToken) : null;
|
|
73
79
|
const telegramMediaStore = config.telegram.enabled && telegramClient !== null
|
|
74
80
|
? new TelegramInboundMediaStore(telegramClient, config.mediaRootPath)
|
|
@@ -95,7 +101,7 @@ export async function createGatewayRuntime(module, input) {
|
|
|
95
101
|
cron.start();
|
|
96
102
|
mailbox.start();
|
|
97
103
|
telegram.start();
|
|
98
|
-
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext);
|
|
104
|
+
return new GatewayPluginRuntime(module, executor, cron, telegram, files, channelSessions, sessionContext, systemPrompts);
|
|
99
105
|
});
|
|
100
106
|
}
|
|
101
107
|
function resolveEffectiveCronTimeZone(module, config) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BindingLoggerHost, BindingLogLevel } from "../binding";
|
|
2
|
+
export type GatewayLogLevel = BindingLogLevel | "off";
|
|
3
|
+
export declare class ConsoleLoggerHost implements BindingLoggerHost {
|
|
4
|
+
private readonly threshold;
|
|
5
|
+
constructor(threshold: GatewayLogLevel);
|
|
6
|
+
log(level: BindingLogLevel, message: string): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseGatewayLogLevel(value: unknown, field: string): GatewayLogLevel;
|
|
@@ -0,0 +1,53 @@
|
|
|
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/index.js
CHANGED
|
@@ -46,8 +46,8 @@ export const OpencodeGatewayPlugin = async (input) => {
|
|
|
46
46
|
if (!sessionId) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
49
|
+
const systemPrompts = await runtime.systemPrompts.buildPrompts(sessionId);
|
|
50
|
+
for (const systemPrompt of systemPrompts) {
|
|
51
51
|
output.system.push(systemPrompt);
|
|
52
52
|
}
|
|
53
53
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BindingLoggerHost } from "../binding";
|
|
2
|
+
import type { GatewayMemoryConfig } from "../config/memory";
|
|
3
|
+
export declare class GatewayMemoryPromptProvider {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(config: GatewayMemoryConfig, logger: Pick<BindingLoggerHost, "log">);
|
|
7
|
+
buildPrompt(): Promise<string | null>;
|
|
8
|
+
private buildEntrySection;
|
|
9
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
}
|
|
@@ -29,6 +29,7 @@ export declare class GatewayExecutor {
|
|
|
29
29
|
private waitUntilIdle;
|
|
30
30
|
private appendPrompt;
|
|
31
31
|
private cleanupResidualBusySession;
|
|
32
|
+
private waitForSessionToSettle;
|
|
32
33
|
private abortSessionAndWaitForSettle;
|
|
33
34
|
private createInternalPromptIdentity;
|
|
34
35
|
private executeDriver;
|
package/dist/runtime/executor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ConversationCoordinator } from "./conversation-coordinator";
|
|
|
2
2
|
import { runOpencodeDriver } from "./opencode-runner";
|
|
3
3
|
const SESSION_ABORT_SETTLE_TIMEOUT_MS = 5_000;
|
|
4
4
|
const SESSION_ABORT_POLL_MS = 250;
|
|
5
|
+
const SESSION_RESIDUAL_BUSY_GRACE_POLLS = 3;
|
|
5
6
|
export class GatewayExecutor {
|
|
6
7
|
module;
|
|
7
8
|
store;
|
|
@@ -250,10 +251,10 @@ export class GatewayExecutor {
|
|
|
250
251
|
expectCommandResult(result, "appendPrompt");
|
|
251
252
|
}
|
|
252
253
|
async cleanupResidualBusySession(sessionId) {
|
|
253
|
-
if (
|
|
254
|
+
if (await this.waitForSessionToSettle(sessionId, SESSION_RESIDUAL_BUSY_GRACE_POLLS)) {
|
|
254
255
|
return;
|
|
255
256
|
}
|
|
256
|
-
this.logger.log("
|
|
257
|
+
this.logger.log("debug", `aborting residual busy gateway session after prompt completion: ${sessionId}`);
|
|
257
258
|
try {
|
|
258
259
|
await this.abortSessionAndWaitForSettle(sessionId);
|
|
259
260
|
}
|
|
@@ -261,6 +262,17 @@ export class GatewayExecutor {
|
|
|
261
262
|
this.logger.log("warn", `residual busy gateway session did not settle after abort: ${sessionId}: ${extractErrorMessage(error)}`);
|
|
262
263
|
}
|
|
263
264
|
}
|
|
265
|
+
async waitForSessionToSettle(sessionId, extraPolls) {
|
|
266
|
+
for (let attempt = 0; attempt <= extraPolls; attempt += 1) {
|
|
267
|
+
if (!(await this.opencode.isSessionBusy(sessionId))) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (attempt < extraPolls) {
|
|
271
|
+
await Bun.sleep(SESSION_ABORT_POLL_MS);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
264
276
|
async abortSessionAndWaitForSettle(sessionId) {
|
|
265
277
|
await this.opencode.abortSession(sessionId);
|
|
266
278
|
const deadline = Date.now() + SESSION_ABORT_SETTLE_TIMEOUT_MS;
|
|
@@ -6,5 +6,5 @@ export declare class GatewaySessionContext {
|
|
|
6
6
|
replaceReplyTargets(sessionId: string, conversationKey: string, targets: BindingDeliveryTarget[], recordedAtMs: number): void;
|
|
7
7
|
listReplyTargets(sessionId: string): BindingDeliveryTarget[];
|
|
8
8
|
getDefaultReplyTarget(sessionId: string): BindingDeliveryTarget | null;
|
|
9
|
-
|
|
9
|
+
isGatewaySession(sessionId: string): boolean;
|
|
10
10
|
}
|
package/dist/session/context.js
CHANGED
|
@@ -17,34 +17,7 @@ export class GatewaySessionContext {
|
|
|
17
17
|
getDefaultReplyTarget(sessionId) {
|
|
18
18
|
return this.store.getDefaultSessionReplyTarget(sessionId);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (targets.length === 0) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
if (targets.length === 1) {
|
|
26
|
-
const target = targets[0];
|
|
27
|
-
return [
|
|
28
|
-
"Gateway context:",
|
|
29
|
-
`- Current message source channel: ${target.channel}`,
|
|
30
|
-
`- Current reply target id: ${target.target}`,
|
|
31
|
-
`- Current reply topic: ${target.topic ?? "none"}`,
|
|
32
|
-
"- Unless the user explicitly asks otherwise, channel-aware actions should default to this target.",
|
|
33
|
-
"- If the user asks to start a fresh channel session, use channel_new_session.",
|
|
34
|
-
"- If the user asks for a one-shot reminder or relative-time follow-up, prefer schedule_once.",
|
|
35
|
-
"- If the user asks for a recurring schedule, prefer cron_upsert.",
|
|
36
|
-
"- Use schedule_list and schedule_status to inspect existing scheduled jobs and recent run results.",
|
|
37
|
-
"- Scheduled results delivered to this channel are automatically appended to this session as context.",
|
|
38
|
-
].join("\n");
|
|
39
|
-
}
|
|
40
|
-
return [
|
|
41
|
-
"Gateway context:",
|
|
42
|
-
`- This session currently fans out to ${targets.length} reply targets.`,
|
|
43
|
-
...targets.map((target, index) => `- Target ${index + 1}: channel=${target.channel}, id=${target.target}, topic=${target.topic ?? "none"}`),
|
|
44
|
-
"- If a tool needs a single explicit target, do not guess; ask the user or use explicit tool arguments.",
|
|
45
|
-
"- If the user asks to start a fresh channel session for this route, use channel_new_session.",
|
|
46
|
-
"- Prefer schedule_once for one-shot reminders and cron_upsert for recurring schedules.",
|
|
47
|
-
"- Use schedule_list and schedule_status to inspect scheduled jobs and recent run results.",
|
|
48
|
-
].join("\n");
|
|
20
|
+
isGatewaySession(sessionId) {
|
|
21
|
+
return this.store.hasGatewaySession(sessionId);
|
|
49
22
|
}
|
|
50
23
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GatewayMemoryPromptProvider } from "../memory/prompt";
|
|
2
|
+
import type { GatewaySessionContext } from "./context";
|
|
3
|
+
export declare class GatewaySystemPromptBuilder {
|
|
4
|
+
private readonly sessions;
|
|
5
|
+
private readonly memory;
|
|
6
|
+
constructor(sessions: GatewaySessionContext, memory: GatewayMemoryPromptProvider);
|
|
7
|
+
buildPrompts(sessionId: string): Promise<string[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class GatewaySystemPromptBuilder {
|
|
2
|
+
sessions;
|
|
3
|
+
memory;
|
|
4
|
+
constructor(sessions, memory) {
|
|
5
|
+
this.sessions = sessions;
|
|
6
|
+
this.memory = memory;
|
|
7
|
+
}
|
|
8
|
+
async buildPrompts(sessionId) {
|
|
9
|
+
if (!this.sessions.isGatewaySession(sessionId)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const prompts = [];
|
|
13
|
+
const gatewayPrompt = buildGatewayContextPrompt(this.sessions.listReplyTargets(sessionId));
|
|
14
|
+
if (gatewayPrompt !== null) {
|
|
15
|
+
prompts.push(gatewayPrompt);
|
|
16
|
+
}
|
|
17
|
+
const memoryPrompt = await this.memory.buildPrompt();
|
|
18
|
+
if (memoryPrompt !== null) {
|
|
19
|
+
prompts.push(memoryPrompt);
|
|
20
|
+
}
|
|
21
|
+
return prompts;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function buildGatewayContextPrompt(targets) {
|
|
25
|
+
if (targets.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (targets.length === 1) {
|
|
29
|
+
const target = targets[0];
|
|
30
|
+
return [
|
|
31
|
+
"Gateway context:",
|
|
32
|
+
`- Current message source channel: ${target.channel}`,
|
|
33
|
+
`- Current reply target id: ${target.target}`,
|
|
34
|
+
`- Current reply topic: ${target.topic ?? "none"}`,
|
|
35
|
+
"- Unless the user explicitly asks otherwise, channel-aware actions should default to this target.",
|
|
36
|
+
"- If the user asks to start a fresh channel session, use channel_new_session.",
|
|
37
|
+
"- If the user asks for a one-shot reminder or relative-time follow-up, prefer schedule_once.",
|
|
38
|
+
"- If the user asks for a recurring schedule, prefer cron_upsert.",
|
|
39
|
+
"- Use schedule_list and schedule_status to inspect existing scheduled jobs and recent run results.",
|
|
40
|
+
"- Scheduled results delivered to this channel are automatically appended to this session as context.",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
return [
|
|
44
|
+
"Gateway context:",
|
|
45
|
+
`- This session currently fans out to ${targets.length} reply targets.`,
|
|
46
|
+
...targets.map((target, index) => `- Target ${index + 1}: channel=${target.channel}, id=${target.target}, topic=${target.topic ?? "none"}`),
|
|
47
|
+
"- If a tool needs a single explicit target, do not guess; ask the user or use explicit tool arguments.",
|
|
48
|
+
"- If the user asks to start a fresh channel session for this route, use channel_new_session.",
|
|
49
|
+
"- Prefer schedule_once for one-shot reminders and cron_upsert for recurring schedules.",
|
|
50
|
+
"- Use schedule_list and schedule_status to inspect scheduled jobs and recent run results.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export declare class SqliteStore {
|
|
|
112
112
|
replaceSessionReplyTargets(input: PersistSessionReplyTargetsInput): void;
|
|
113
113
|
listSessionReplyTargets(sessionId: string): BindingDeliveryTarget[];
|
|
114
114
|
getDefaultSessionReplyTarget(sessionId: string): BindingDeliveryTarget | null;
|
|
115
|
+
hasGatewaySession(sessionId: string): boolean;
|
|
115
116
|
appendJournal(entry: RuntimeJournalEntry): void;
|
|
116
117
|
replacePendingQuestion(input: PersistPendingQuestionInput): void;
|
|
117
118
|
deletePendingQuestion(requestId: string): void;
|
package/dist/store/sqlite.js
CHANGED
|
@@ -109,6 +109,28 @@ export class SqliteStore {
|
|
|
109
109
|
.get(sessionId);
|
|
110
110
|
return row ? mapSessionReplyTargetRow(row) : null;
|
|
111
111
|
}
|
|
112
|
+
hasGatewaySession(sessionId) {
|
|
113
|
+
const binding = this.db
|
|
114
|
+
.query(`
|
|
115
|
+
SELECT 1 AS present
|
|
116
|
+
FROM session_bindings
|
|
117
|
+
WHERE session_id = ?1
|
|
118
|
+
LIMIT 1;
|
|
119
|
+
`)
|
|
120
|
+
.get(sessionId);
|
|
121
|
+
if (binding?.present === 1) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
const replyTarget = this.db
|
|
125
|
+
.query(`
|
|
126
|
+
SELECT 1 AS present
|
|
127
|
+
FROM session_reply_targets
|
|
128
|
+
WHERE session_id = ?1
|
|
129
|
+
LIMIT 1;
|
|
130
|
+
`)
|
|
131
|
+
.get(sessionId);
|
|
132
|
+
return replyTarget?.present === 1;
|
|
133
|
+
}
|
|
112
134
|
appendJournal(entry) {
|
|
113
135
|
this.db
|
|
114
136
|
.query(`
|
package/package.json
CHANGED
package/dist/host/noop.d.ts
DELETED
package/dist/host/noop.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export class ConsoleLoggerHost {
|
|
2
|
-
log(level, message) {
|
|
3
|
-
const line = `[gateway:${level}] ${message}`;
|
|
4
|
-
if (level === "error") {
|
|
5
|
-
console.error(line);
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
if (level === "warn") {
|
|
9
|
-
console.warn(line);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
console.info(line);
|
|
13
|
-
}
|
|
14
|
-
}
|