@botcord/daemon 0.1.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/dist/activity-tracker.d.ts +43 -0
- package/dist/activity-tracker.js +110 -0
- package/dist/adapters/runtimes.d.ts +14 -0
- package/dist/adapters/runtimes.js +18 -0
- package/dist/agent-discovery.d.ts +81 -0
- package/dist/agent-discovery.js +181 -0
- package/dist/agent-workspace.d.ts +31 -0
- package/dist/agent-workspace.js +221 -0
- package/dist/config.d.ts +116 -0
- package/dist/config.js +180 -0
- package/dist/control-channel.d.ts +99 -0
- package/dist/control-channel.js +388 -0
- package/dist/cross-room.d.ts +23 -0
- package/dist/cross-room.js +55 -0
- package/dist/daemon-config-map.d.ts +61 -0
- package/dist/daemon-config-map.js +153 -0
- package/dist/daemon.d.ts +123 -0
- package/dist/daemon.js +349 -0
- package/dist/doctor.d.ts +89 -0
- package/dist/doctor.js +191 -0
- package/dist/gateway/channel-manager.d.ts +54 -0
- package/dist/gateway/channel-manager.js +292 -0
- package/dist/gateway/channels/botcord.d.ts +93 -0
- package/dist/gateway/channels/botcord.js +510 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +1 -0
- package/dist/gateway/channels/sanitize.d.ts +20 -0
- package/dist/gateway/channels/sanitize.js +56 -0
- package/dist/gateway/dispatcher.d.ts +73 -0
- package/dist/gateway/dispatcher.js +431 -0
- package/dist/gateway/gateway.d.ts +87 -0
- package/dist/gateway/gateway.js +158 -0
- package/dist/gateway/index.d.ts +15 -0
- package/dist/gateway/index.js +15 -0
- package/dist/gateway/log.d.ts +9 -0
- package/dist/gateway/log.js +20 -0
- package/dist/gateway/router.d.ts +10 -0
- package/dist/gateway/router.js +48 -0
- package/dist/gateway/runtimes/claude-code.d.ts +30 -0
- package/dist/gateway/runtimes/claude-code.js +162 -0
- package/dist/gateway/runtimes/codex.d.ts +83 -0
- package/dist/gateway/runtimes/codex.js +272 -0
- package/dist/gateway/runtimes/gemini.d.ts +15 -0
- package/dist/gateway/runtimes/gemini.js +29 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +43 -0
- package/dist/gateway/runtimes/ndjson-stream.js +169 -0
- package/dist/gateway/runtimes/probe.d.ts +17 -0
- package/dist/gateway/runtimes/probe.js +54 -0
- package/dist/gateway/runtimes/registry.d.ts +59 -0
- package/dist/gateway/runtimes/registry.js +94 -0
- package/dist/gateway/session-store.d.ts +39 -0
- package/dist/gateway/session-store.js +133 -0
- package/dist/gateway/types.d.ts +265 -0
- package/dist/gateway/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +854 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +44 -0
- package/dist/provision.d.ts +88 -0
- package/dist/provision.js +749 -0
- package/dist/room-context-fetcher.d.ts +18 -0
- package/dist/room-context-fetcher.js +101 -0
- package/dist/room-context.d.ts +53 -0
- package/dist/room-context.js +112 -0
- package/dist/sender-classify.d.ts +30 -0
- package/dist/sender-classify.js +32 -0
- package/dist/snapshot-writer.d.ts +37 -0
- package/dist/snapshot-writer.js +84 -0
- package/dist/status-render.d.ts +28 -0
- package/dist/status-render.js +97 -0
- package/dist/system-context.d.ts +57 -0
- package/dist/system-context.js +91 -0
- package/dist/turn-text.d.ts +36 -0
- package/dist/turn-text.js +57 -0
- package/dist/user-auth.d.ts +75 -0
- package/dist/user-auth.js +245 -0
- package/dist/working-memory.d.ts +46 -0
- package/dist/working-memory.js +274 -0
- package/package.json +39 -0
- package/src/__tests__/activity-tracker.test.ts +130 -0
- package/src/__tests__/agent-discovery.test.ts +191 -0
- package/src/__tests__/agent-workspace.test.ts +147 -0
- package/src/__tests__/control-channel.test.ts +327 -0
- package/src/__tests__/cross-room.test.ts +116 -0
- package/src/__tests__/daemon-config-map.test.ts +416 -0
- package/src/__tests__/daemon.test.ts +300 -0
- package/src/__tests__/device-code.test.ts +152 -0
- package/src/__tests__/doctor.test.ts +218 -0
- package/src/__tests__/protocol-core-reexport.test.ts +24 -0
- package/src/__tests__/provision.test.ts +922 -0
- package/src/__tests__/room-context.test.ts +233 -0
- package/src/__tests__/runtime-discovery.test.ts +173 -0
- package/src/__tests__/snapshot-writer.test.ts +141 -0
- package/src/__tests__/status-render.test.ts +137 -0
- package/src/__tests__/system-context.test.ts +315 -0
- package/src/__tests__/turn-text.test.ts +116 -0
- package/src/__tests__/user-auth.test.ts +125 -0
- package/src/__tests__/working-memory.test.ts +240 -0
- package/src/activity-tracker.ts +140 -0
- package/src/adapters/runtimes.ts +30 -0
- package/src/agent-discovery.ts +262 -0
- package/src/agent-workspace.ts +247 -0
- package/src/config.ts +290 -0
- package/src/control-channel.ts +455 -0
- package/src/cross-room.ts +89 -0
- package/src/daemon-config-map.ts +200 -0
- package/src/daemon.ts +478 -0
- package/src/doctor.ts +282 -0
- package/src/gateway/__tests__/.gitkeep +0 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +480 -0
- package/src/gateway/__tests__/channel-manager.test.ts +475 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +318 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +350 -0
- package/src/gateway/__tests__/dispatcher.test.ts +1159 -0
- package/src/gateway/__tests__/gateway-add-channel.test.ts +180 -0
- package/src/gateway/__tests__/gateway-managed-routes.test.ts +181 -0
- package/src/gateway/__tests__/gateway.test.ts +222 -0
- package/src/gateway/__tests__/router.test.ts +247 -0
- package/src/gateway/__tests__/sanitize.test.ts +193 -0
- package/src/gateway/__tests__/session-store.test.ts +235 -0
- package/src/gateway/channel-manager.ts +349 -0
- package/src/gateway/channels/botcord.ts +605 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/sanitize.ts +68 -0
- package/src/gateway/dispatcher.ts +554 -0
- package/src/gateway/gateway.ts +211 -0
- package/src/gateway/index.ts +29 -0
- package/src/gateway/log.ts +30 -0
- package/src/gateway/router.ts +60 -0
- package/src/gateway/runtimes/claude-code.ts +180 -0
- package/src/gateway/runtimes/codex.ts +312 -0
- package/src/gateway/runtimes/gemini.ts +43 -0
- package/src/gateway/runtimes/ndjson-stream.ts +225 -0
- package/src/gateway/runtimes/probe.ts +73 -0
- package/src/gateway/runtimes/registry.ts +143 -0
- package/src/gateway/session-store.ts +157 -0
- package/src/gateway/types.ts +325 -0
- package/src/index.ts +961 -0
- package/src/log.ts +47 -0
- package/src/provision.ts +879 -0
- package/src/room-context-fetcher.ts +124 -0
- package/src/room-context.ts +167 -0
- package/src/sender-classify.ts +46 -0
- package/src/snapshot-writer.ts +103 -0
- package/src/status-render.ts +132 -0
- package/src/system-context.ts +162 -0
- package/src/turn-text.ts +93 -0
- package/src/user-auth.ts +295 -0
- package/src/working-memory.ts +352 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ClaudeCodeAdapter, probeClaude } from "./claude-code.js";
|
|
2
|
+
import { CodexAdapter, probeCodex } from "./codex.js";
|
|
3
|
+
import { GeminiAdapter, probeGemini } from "./gemini.js";
|
|
4
|
+
import type { RuntimeAdapter, RuntimeProbeResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Metadata + factory for a single runtime adapter, used by the registry.
|
|
8
|
+
* Add a new runtime by exporting one of these from the adapter file and
|
|
9
|
+
* registering it in `REGISTRY` below.
|
|
10
|
+
*/
|
|
11
|
+
export interface RuntimeModule {
|
|
12
|
+
id: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
/** Canonical PATH binary name — shown in `doctor` output. */
|
|
15
|
+
binary: string;
|
|
16
|
+
/**
|
|
17
|
+
* Env var that overrides the resolved CLI path for this adapter.
|
|
18
|
+
* Defaults to `BOTCORD_<ID>_BIN` with dashes → underscores, uppercased.
|
|
19
|
+
*/
|
|
20
|
+
envVar?: string;
|
|
21
|
+
/** Module-level probe so callers don't have to instantiate the adapter. */
|
|
22
|
+
probe(): RuntimeProbeResult;
|
|
23
|
+
create(): RuntimeAdapter;
|
|
24
|
+
/**
|
|
25
|
+
* Whether `create().run()` is implemented. Defaults to true. Stubs
|
|
26
|
+
* (e.g. gemini until we wire its driver) should set `false` so the
|
|
27
|
+
* config loader rejects routing turns to this adapter.
|
|
28
|
+
*/
|
|
29
|
+
supportsRun?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Built-in runtime module entry for Claude Code. */
|
|
33
|
+
export const claudeCodeModule: RuntimeModule = {
|
|
34
|
+
id: "claude-code",
|
|
35
|
+
displayName: "Claude Code",
|
|
36
|
+
binary: "claude",
|
|
37
|
+
envVar: "BOTCORD_CLAUDE_BIN",
|
|
38
|
+
probe: () => probeClaude(),
|
|
39
|
+
create: () => new ClaudeCodeAdapter(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Built-in runtime module entry for Codex. */
|
|
43
|
+
export const codexModule: RuntimeModule = {
|
|
44
|
+
id: "codex",
|
|
45
|
+
displayName: "Codex CLI",
|
|
46
|
+
binary: "codex",
|
|
47
|
+
probe: () => probeCodex(),
|
|
48
|
+
create: () => new CodexAdapter(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Built-in runtime module entry for Gemini (probe-only stub). */
|
|
52
|
+
export const geminiModule: RuntimeModule = {
|
|
53
|
+
id: "gemini",
|
|
54
|
+
displayName: "Gemini CLI",
|
|
55
|
+
binary: "gemini",
|
|
56
|
+
probe: () => probeGemini(),
|
|
57
|
+
create: () => new GeminiAdapter(),
|
|
58
|
+
supportsRun: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Built-in runtime modules. To add a new runtime:
|
|
63
|
+
* 1. Create `runtimes/<name>.ts` extending `NdjsonStreamAdapter` (or
|
|
64
|
+
* implementing `RuntimeAdapter` directly).
|
|
65
|
+
* 2. Add a `RuntimeModule` entry + register it here.
|
|
66
|
+
*/
|
|
67
|
+
export const RUNTIME_MODULES: readonly RuntimeModule[] = [
|
|
68
|
+
claudeCodeModule,
|
|
69
|
+
codexModule,
|
|
70
|
+
geminiModule,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const BY_ID = new Map<string, RuntimeModule>(
|
|
74
|
+
RUNTIME_MODULES.map((m) => [m.id, m]),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/** Lookup a runtime module by id, or null when the id is unknown. */
|
|
78
|
+
export function getRuntimeModule(id: string): RuntimeModule | null {
|
|
79
|
+
return BY_ID.get(id) ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** All registered runtime ids in registration order. */
|
|
83
|
+
export function listRuntimeIds(): string[] {
|
|
84
|
+
return RUNTIME_MODULES.map((m) => m.id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Env var name used to override the binary path for a given runtime id. */
|
|
88
|
+
export function envVarForRuntime(id: string): string {
|
|
89
|
+
const mod = getRuntimeModule(id);
|
|
90
|
+
if (mod?.envVar) return mod.envVar;
|
|
91
|
+
const token = id.replace(/-/g, "_").toUpperCase();
|
|
92
|
+
return `BOTCORD_${token}_BIN`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Instantiate a single runtime adapter by id; throws if unknown. */
|
|
96
|
+
export function createRuntime(id: string): RuntimeAdapter {
|
|
97
|
+
const mod = getRuntimeModule(id);
|
|
98
|
+
if (!mod) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Unknown runtime "${id}". Registered runtimes: ${listRuntimeIds().join(", ")}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return mod.create();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Instantiate every registered runtime — used as the dispatcher default. */
|
|
107
|
+
export function createAllRuntimes(): Record<string, RuntimeAdapter> {
|
|
108
|
+
const map: Record<string, RuntimeAdapter> = {};
|
|
109
|
+
for (const m of RUNTIME_MODULES) {
|
|
110
|
+
map[m.id] = m.create();
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** One probe result per registered runtime, for `doctor`-style listings. */
|
|
116
|
+
export interface RuntimeProbeEntry {
|
|
117
|
+
id: string;
|
|
118
|
+
displayName: string;
|
|
119
|
+
binary: string;
|
|
120
|
+
supportsRun: boolean;
|
|
121
|
+
result: RuntimeProbeResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Probe every registered runtime and report installation status. */
|
|
125
|
+
export function detectRuntimes(): RuntimeProbeEntry[] {
|
|
126
|
+
const out: RuntimeProbeEntry[] = [];
|
|
127
|
+
for (const m of RUNTIME_MODULES) {
|
|
128
|
+
let result: RuntimeProbeResult = { available: false };
|
|
129
|
+
try {
|
|
130
|
+
result = m.probe();
|
|
131
|
+
} catch {
|
|
132
|
+
result = { available: false };
|
|
133
|
+
}
|
|
134
|
+
out.push({
|
|
135
|
+
id: m.id,
|
|
136
|
+
displayName: m.displayName,
|
|
137
|
+
binary: m.binary,
|
|
138
|
+
supportsRun: m.supportsRun !== false,
|
|
139
|
+
result,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { GatewayLogger } from "./log.js";
|
|
4
|
+
import type { GatewaySessionEntry, SessionKeyInput } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_SESSION_STORE_MAX_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
/** Derive the canonical session-store key for a runtime + channel + conversation. */
|
|
9
|
+
export function sessionKey(input: SessionKeyInput): string {
|
|
10
|
+
const base = `${input.runtime}:${input.channel}:${input.accountId}:${input.conversationKind}:${input.conversationId}`;
|
|
11
|
+
const thread = input.threadId;
|
|
12
|
+
if (typeof thread === "string" && thread.length > 0) {
|
|
13
|
+
return `${base}:${thread}`;
|
|
14
|
+
}
|
|
15
|
+
return base;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Options for constructing a `SessionStore`. */
|
|
19
|
+
export interface SessionStoreOptions {
|
|
20
|
+
path: string;
|
|
21
|
+
log?: GatewayLogger;
|
|
22
|
+
/** Optional TTL for persisted entries. Omit to disable automatic pruning. */
|
|
23
|
+
maxEntryAgeMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StoreFile {
|
|
27
|
+
version: 1;
|
|
28
|
+
entries: Record<string, GatewaySessionEntry>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function emptyFile(): StoreFile {
|
|
32
|
+
return { version: 1, entries: {} };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isValidShape(x: unknown): x is StoreFile {
|
|
36
|
+
if (!x || typeof x !== "object") return false;
|
|
37
|
+
const o = x as Record<string, unknown>;
|
|
38
|
+
return o.version === 1 && !!o.entries && typeof o.entries === "object";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** JSON-backed session store for runtime resume ids, keyed by `sessionKey()`. */
|
|
42
|
+
export class SessionStore {
|
|
43
|
+
private readonly filePath: string;
|
|
44
|
+
private readonly log?: GatewayLogger;
|
|
45
|
+
private readonly maxEntryAgeMs?: number;
|
|
46
|
+
private data: StoreFile = emptyFile();
|
|
47
|
+
private loaded = false;
|
|
48
|
+
private writeQueue: Promise<void> = Promise.resolve();
|
|
49
|
+
|
|
50
|
+
constructor(opts: SessionStoreOptions) {
|
|
51
|
+
this.filePath = opts.path;
|
|
52
|
+
this.log = opts.log;
|
|
53
|
+
if (Number.isFinite(opts.maxEntryAgeMs) && opts.maxEntryAgeMs! > 0) {
|
|
54
|
+
this.maxEntryAgeMs = opts.maxEntryAgeMs;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Load entries from disk. Tolerates missing or corrupt files. */
|
|
59
|
+
async load(): Promise<void> {
|
|
60
|
+
let raw: string;
|
|
61
|
+
try {
|
|
62
|
+
raw = await readFile(this.filePath, "utf8");
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
65
|
+
if (code === "ENOENT") {
|
|
66
|
+
this.data = emptyFile();
|
|
67
|
+
this.loaded = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
74
|
+
if (isValidShape(parsed)) {
|
|
75
|
+
this.data = { version: 1, entries: parsed.entries };
|
|
76
|
+
} else {
|
|
77
|
+
this.log?.warn("gateway.session-store.invalid-shape", { path: this.filePath });
|
|
78
|
+
this.data = emptyFile();
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
this.log?.warn("gateway.session-store.parse-error", {
|
|
82
|
+
path: this.filePath,
|
|
83
|
+
error: (err as Error).message,
|
|
84
|
+
});
|
|
85
|
+
this.data = emptyFile();
|
|
86
|
+
}
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
if (this.maxEntryAgeMs !== undefined) {
|
|
89
|
+
await this.pruneExpired({ maxAgeMs: this.maxEntryAgeMs });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Look up an entry by its full session key. */
|
|
94
|
+
get(key: string): GatewaySessionEntry | undefined {
|
|
95
|
+
return this.data.entries[key];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Upsert an entry and persist the store atomically. */
|
|
99
|
+
async set(entry: GatewaySessionEntry): Promise<void> {
|
|
100
|
+
const callerTs = entry.updatedAt;
|
|
101
|
+
const updatedAt = Number.isFinite(callerTs) && callerTs > 0 ? callerTs : Date.now();
|
|
102
|
+
this.data.entries[entry.key] = { ...entry, updatedAt };
|
|
103
|
+
await this.persist();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Remove an entry and persist the store atomically. */
|
|
107
|
+
async delete(key: string): Promise<void> {
|
|
108
|
+
if (this.data.entries[key] !== undefined) {
|
|
109
|
+
delete this.data.entries[key];
|
|
110
|
+
}
|
|
111
|
+
await this.persist();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Snapshot of all entries (for status/debugging). */
|
|
115
|
+
all(): GatewaySessionEntry[] {
|
|
116
|
+
return Object.values(this.data.entries);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Remove entries whose `updatedAt` is older than `maxAgeMs`; returns count removed. */
|
|
120
|
+
async pruneExpired(opts: { maxAgeMs: number; now?: number }): Promise<number> {
|
|
121
|
+
const maxAgeMs = opts.maxAgeMs;
|
|
122
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) return 0;
|
|
123
|
+
const now = Number.isFinite(opts.now) ? opts.now! : Date.now();
|
|
124
|
+
const cutoff = now - maxAgeMs;
|
|
125
|
+
let removed = 0;
|
|
126
|
+
for (const [key, entry] of Object.entries(this.data.entries)) {
|
|
127
|
+
if (!Number.isFinite(entry.updatedAt) || entry.updatedAt < cutoff) {
|
|
128
|
+
delete this.data.entries[key];
|
|
129
|
+
removed += 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (removed > 0) {
|
|
133
|
+
this.log?.info("gateway.session-store.pruned-expired", {
|
|
134
|
+
path: this.filePath,
|
|
135
|
+
removed,
|
|
136
|
+
maxAgeMs,
|
|
137
|
+
});
|
|
138
|
+
await this.persist();
|
|
139
|
+
}
|
|
140
|
+
return removed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private persist(): Promise<void> {
|
|
144
|
+
const next = this.writeQueue.then(() => this.flushOnce());
|
|
145
|
+
this.writeQueue = next.catch(() => undefined);
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async flushOnce(): Promise<void> {
|
|
150
|
+
const dir = path.dirname(this.filePath);
|
|
151
|
+
await mkdir(dir, { recursive: true });
|
|
152
|
+
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
153
|
+
const payload = JSON.stringify(this.data, null, 2);
|
|
154
|
+
await writeFile(tmp, payload, { mode: 0o600 });
|
|
155
|
+
await rename(tmp, this.filePath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { GatewayLogger } from "./log.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Routing (§9)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/** Set of predicates matched against a normalized inbound message to pick a route. */
|
|
8
|
+
export interface RouteMatch {
|
|
9
|
+
channel?: string;
|
|
10
|
+
accountId?: string;
|
|
11
|
+
conversationId?: string;
|
|
12
|
+
conversationPrefix?: string;
|
|
13
|
+
conversationKind?: "direct" | "group";
|
|
14
|
+
senderId?: string;
|
|
15
|
+
mentioned?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Concurrency model for turns sharing the same queue key. */
|
|
19
|
+
export type QueueMode = "serial" | "cancel-previous";
|
|
20
|
+
|
|
21
|
+
/** Source-based trust tier used by runtimes to pick default permission flags. */
|
|
22
|
+
export type TrustLevel = "owner" | "trusted" | "public";
|
|
23
|
+
|
|
24
|
+
/** Declarative route entry selecting the runtime and execution flags for matched messages. */
|
|
25
|
+
export interface GatewayRoute {
|
|
26
|
+
match?: RouteMatch;
|
|
27
|
+
runtime: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
extraArgs?: string[];
|
|
30
|
+
queueMode?: QueueMode;
|
|
31
|
+
trustLevel?: TrustLevel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Config (§8)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-channel configuration entry. Channel-specific extras (e.g. BotCord
|
|
40
|
+
* `agentId`) are accepted via the index signature so adapters can read them
|
|
41
|
+
* without introducing a tagged-union everywhere.
|
|
42
|
+
*/
|
|
43
|
+
export interface GatewayChannelConfig {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
accountId: string;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Root gateway configuration document loaded from disk or assembled in memory. */
|
|
51
|
+
export interface GatewayConfig {
|
|
52
|
+
channels: GatewayChannelConfig[];
|
|
53
|
+
defaultRoute: GatewayRoute;
|
|
54
|
+
routes?: GatewayRoute[];
|
|
55
|
+
/**
|
|
56
|
+
* Daemon-synthesized per-agent routes. Snapshot/debug-only surface —
|
|
57
|
+
* `resolveRoute` reads the live map on the Gateway, not this array.
|
|
58
|
+
* Matched after `routes[]` and before `defaultRoute`.
|
|
59
|
+
*/
|
|
60
|
+
managedRoutes?: GatewayRoute[];
|
|
61
|
+
streamBlocks?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Inbound / outbound message shape (§7.3, §7.4, §7.5)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** Normalized inbound message produced by a channel adapter for the dispatcher. */
|
|
69
|
+
export interface GatewayInboundMessage {
|
|
70
|
+
id: string;
|
|
71
|
+
/** Channel adapter id (`ChannelAdapter.id`), not channel type. */
|
|
72
|
+
channel: string;
|
|
73
|
+
accountId: string;
|
|
74
|
+
conversation: {
|
|
75
|
+
id: string;
|
|
76
|
+
kind: "direct" | "group";
|
|
77
|
+
title?: string;
|
|
78
|
+
threadId?: string | null;
|
|
79
|
+
};
|
|
80
|
+
sender: {
|
|
81
|
+
id: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
kind: "user" | "agent" | "system";
|
|
84
|
+
};
|
|
85
|
+
text?: string;
|
|
86
|
+
raw: unknown;
|
|
87
|
+
replyTo?: string | null;
|
|
88
|
+
mentioned?: boolean;
|
|
89
|
+
receivedAt: number;
|
|
90
|
+
trace?: {
|
|
91
|
+
id: string;
|
|
92
|
+
streamable?: boolean;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Inbound envelope wrapping a normalized message with optional upstream ack callbacks. */
|
|
97
|
+
export interface GatewayInboundEnvelope {
|
|
98
|
+
message: GatewayInboundMessage;
|
|
99
|
+
ack?: {
|
|
100
|
+
accept(): Promise<void>;
|
|
101
|
+
reject?(reason: string): Promise<void>;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Channel-agnostic hook that produces a system-context string for a turn.
|
|
107
|
+
* Called before every `runtime.run(...)`; returned value is passed through
|
|
108
|
+
* as `RuntimeRunOptions.systemContext`. Runtimes surface it via
|
|
109
|
+
* `--append-system-prompt` (Claude Code) or an equivalent prefix.
|
|
110
|
+
*
|
|
111
|
+
* Returning `undefined` or an empty string means "no context for this turn".
|
|
112
|
+
* Builders must be resilient — if this throws, the dispatcher logs a warning
|
|
113
|
+
* and continues the turn without systemContext rather than dropping the turn.
|
|
114
|
+
*/
|
|
115
|
+
export type SystemContextBuilder = (
|
|
116
|
+
message: GatewayInboundMessage,
|
|
117
|
+
) => Promise<string | undefined> | string | undefined;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Optional side-effect hook invoked right after the dispatcher acks an
|
|
121
|
+
* envelope, before the turn executes. Intended for bookkeeping such as
|
|
122
|
+
* activity tracking; errors are caught and logged so they do not break the
|
|
123
|
+
* turn. Kept synchronous-or-async to match `SystemContextBuilder` ergonomics.
|
|
124
|
+
*/
|
|
125
|
+
export type InboundObserver = (
|
|
126
|
+
message: GatewayInboundMessage,
|
|
127
|
+
) => Promise<void> | void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Channel-agnostic hook that composes the user-turn text passed to the
|
|
131
|
+
* runtime. When omitted, the dispatcher passes `message.text.trim()` through
|
|
132
|
+
* as-is. Builders can wrap the content with sender metadata, room headers,
|
|
133
|
+
* reply hints, etc. — anything that should land in the session transcript.
|
|
134
|
+
*
|
|
135
|
+
* Must be synchronous + cheap (runs on the turn's critical path). Throws are
|
|
136
|
+
* caught by the dispatcher and the raw trimmed text is used as a fallback so
|
|
137
|
+
* a buggy composer never drops turns.
|
|
138
|
+
*/
|
|
139
|
+
export type UserTurnBuilder = (message: GatewayInboundMessage) => string;
|
|
140
|
+
|
|
141
|
+
/** Outbound reply payload passed to `ChannelAdapter.send()`. */
|
|
142
|
+
export interface GatewayOutboundMessage {
|
|
143
|
+
channel: string;
|
|
144
|
+
accountId: string;
|
|
145
|
+
conversationId: string;
|
|
146
|
+
threadId?: string | null;
|
|
147
|
+
text: string;
|
|
148
|
+
replyTo?: string | null;
|
|
149
|
+
traceId?: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Status (§14)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
/** Per-channel status snapshot exposed for `status`/`doctor` style output. */
|
|
157
|
+
export interface ChannelStatusSnapshot {
|
|
158
|
+
channel: string;
|
|
159
|
+
accountId: string;
|
|
160
|
+
running: boolean;
|
|
161
|
+
connected?: boolean;
|
|
162
|
+
restartPending?: boolean;
|
|
163
|
+
reconnectAttempts?: number;
|
|
164
|
+
lastStartAt?: number;
|
|
165
|
+
lastStopAt?: number;
|
|
166
|
+
lastError?: string | null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Per-turn status snapshot describing a currently-executing runtime invocation. */
|
|
170
|
+
export interface TurnStatusSnapshot {
|
|
171
|
+
key: string;
|
|
172
|
+
channel: string;
|
|
173
|
+
accountId: string;
|
|
174
|
+
conversationId: string;
|
|
175
|
+
runtime: string;
|
|
176
|
+
cwd: string;
|
|
177
|
+
startedAt: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Aggregate gateway state combining channel and turn snapshots. */
|
|
181
|
+
export interface GatewayRuntimeSnapshot {
|
|
182
|
+
channels: Record<string, ChannelStatusSnapshot>;
|
|
183
|
+
turns: Record<string, TurnStatusSnapshot>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Channel adapter (§7.1–7.3, §13, §14)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** Context passed to `ChannelAdapter.start()` for its lifetime. */
|
|
191
|
+
export interface ChannelStartContext {
|
|
192
|
+
config: GatewayConfig;
|
|
193
|
+
accountId: string;
|
|
194
|
+
abortSignal: AbortSignal;
|
|
195
|
+
log: GatewayLogger;
|
|
196
|
+
emit: (event: GatewayInboundEnvelope) => Promise<void>;
|
|
197
|
+
setStatus: (patch: Partial<ChannelStatusSnapshot>) => void;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Context passed to `ChannelAdapter.stop()` when the manager is tearing the channel down. */
|
|
201
|
+
export interface ChannelStopContext {
|
|
202
|
+
reason?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Context passed to `ChannelAdapter.send()` when delivering a reply. */
|
|
206
|
+
export interface ChannelSendContext {
|
|
207
|
+
message: GatewayOutboundMessage;
|
|
208
|
+
log: GatewayLogger;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Result returned by `ChannelAdapter.send()` — the upstream message id if known. */
|
|
212
|
+
export interface ChannelSendResult {
|
|
213
|
+
providerMessageId?: string | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Context passed to `ChannelAdapter.streamBlock()` for progressive output forwarding. */
|
|
217
|
+
export interface ChannelStreamBlockContext {
|
|
218
|
+
traceId: string;
|
|
219
|
+
accountId: string;
|
|
220
|
+
conversationId: string;
|
|
221
|
+
block: unknown;
|
|
222
|
+
log: GatewayLogger;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Upstream messaging surface such as BotCord, Telegram, or WeChat. */
|
|
226
|
+
export interface ChannelAdapter {
|
|
227
|
+
readonly id: string;
|
|
228
|
+
readonly type: string;
|
|
229
|
+
start(ctx: ChannelStartContext): Promise<unknown>;
|
|
230
|
+
stop?(ctx: ChannelStopContext): Promise<void>;
|
|
231
|
+
send(ctx: ChannelSendContext): Promise<ChannelSendResult>;
|
|
232
|
+
status?(): ChannelStatusSnapshot;
|
|
233
|
+
streamBlock?(ctx: ChannelStreamBlockContext): Promise<void>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Runtime adapter (§7.6)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** One parsed block from a runtime's streaming output, forwarded via `onBlock`. */
|
|
241
|
+
export interface StreamBlock {
|
|
242
|
+
/** Raw JSON object as emitted by the underlying CLI (e.g. claude-code stream-json). */
|
|
243
|
+
raw: unknown;
|
|
244
|
+
/** Normalized kind, used by channels to decide whether to forward progressive output. */
|
|
245
|
+
kind: "assistant_text" | "tool_use" | "tool_result" | "system" | "other";
|
|
246
|
+
/** 1-based sequence number within this turn. */
|
|
247
|
+
seq: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Options passed to a runtime adapter for a single turn. */
|
|
251
|
+
export interface RuntimeRunOptions {
|
|
252
|
+
text: string;
|
|
253
|
+
/** Runtime-native session id for resume; null/empty for a new session. */
|
|
254
|
+
sessionId: string | null;
|
|
255
|
+
cwd: string;
|
|
256
|
+
/**
|
|
257
|
+
* Owning agent id (the daemon's `accountId` for this route). Lets adapters
|
|
258
|
+
* resolve per-agent state — e.g. the codex adapter uses it to locate the
|
|
259
|
+
* per-agent `CODEX_HOME` carrying the AGENTS.md that injects systemContext.
|
|
260
|
+
*/
|
|
261
|
+
accountId: string;
|
|
262
|
+
signal: AbortSignal;
|
|
263
|
+
extraArgs?: string[];
|
|
264
|
+
trustLevel: TrustLevel;
|
|
265
|
+
/** System-level context injected alongside the user turn (memory, digest, room info). */
|
|
266
|
+
systemContext?: string;
|
|
267
|
+
/** Channel-agnostic bag for dispatch-time data (traceId, channel, conversation, etc.). */
|
|
268
|
+
context?: Record<string, unknown>;
|
|
269
|
+
/** Called for every parsed block while the turn is in progress. */
|
|
270
|
+
onBlock?: (block: StreamBlock) => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Result returned by a runtime adapter after a turn completes. */
|
|
274
|
+
export interface RuntimeRunResult {
|
|
275
|
+
/** Final assistant text for this turn (concatenated if streamed). */
|
|
276
|
+
text: string;
|
|
277
|
+
/** New runtime session id to persist so the next turn can resume. */
|
|
278
|
+
newSessionId: string;
|
|
279
|
+
/** Optional cost in USD, if the runtime reports it. */
|
|
280
|
+
costUsd?: number;
|
|
281
|
+
/** Populated when the runtime reported a hard error. */
|
|
282
|
+
error?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Detection result for whether a runtime binary/SDK is usable on this machine. */
|
|
286
|
+
export interface RuntimeProbeResult {
|
|
287
|
+
available: boolean;
|
|
288
|
+
path?: string;
|
|
289
|
+
version?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Downstream agent executor such as Claude Code, Codex, Gemini, or OpenClaw. */
|
|
293
|
+
export interface RuntimeAdapter {
|
|
294
|
+
readonly id: string;
|
|
295
|
+
run(opts: RuntimeRunOptions): Promise<RuntimeRunResult>;
|
|
296
|
+
probe?(): RuntimeProbeResult;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Session store (§10)
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/** Minimal fields needed to derive a session key for the JSON session store. */
|
|
304
|
+
export interface SessionKeyInput {
|
|
305
|
+
runtime: string;
|
|
306
|
+
channel: string;
|
|
307
|
+
accountId: string;
|
|
308
|
+
conversationKind: "direct" | "group";
|
|
309
|
+
conversationId: string;
|
|
310
|
+
threadId?: string | null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Persisted runtime-session record keyed by the derived session key. */
|
|
314
|
+
export interface GatewaySessionEntry {
|
|
315
|
+
key: string;
|
|
316
|
+
runtime: string;
|
|
317
|
+
runtimeSessionId: string;
|
|
318
|
+
channel: string;
|
|
319
|
+
accountId: string;
|
|
320
|
+
conversationKind: "direct" | "group";
|
|
321
|
+
conversationId: string;
|
|
322
|
+
threadId?: string | null;
|
|
323
|
+
cwd: string;
|
|
324
|
+
updatedAt: number;
|
|
325
|
+
}
|