@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,15 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./log.js";
|
|
3
|
+
export * from "./runtimes/registry.js";
|
|
4
|
+
export * from "./channels/index.js";
|
|
5
|
+
export { sanitizeUntrustedContent, sanitizeSenderName } from "./channels/sanitize.js";
|
|
6
|
+
export { sessionKey, SessionStore } from "./session-store.js";
|
|
7
|
+
export { resolveRoute, matchesRoute } from "./router.js";
|
|
8
|
+
export { ChannelManager } from "./channel-manager.js";
|
|
9
|
+
export { Dispatcher } from "./dispatcher.js";
|
|
10
|
+
export { Gateway } from "./gateway.js";
|
|
11
|
+
export { ClaudeCodeAdapter, probeClaude, resolveClaudeCommand, } from "./runtimes/claude-code.js";
|
|
12
|
+
export { CodexAdapter, probeCodex, resolveCodexCommand } from "./runtimes/codex.js";
|
|
13
|
+
export { GeminiAdapter, probeGemini, resolveGeminiCommand } from "./runtimes/gemini.js";
|
|
14
|
+
export { NdjsonStreamAdapter, } from "./runtimes/ndjson-stream.js";
|
|
15
|
+
export { firstExistingPath, readCommandVersion, resolveCommandOnPath, resolveHomePath, } from "./runtimes/probe.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Structured logger interface used across the gateway core and adapters. */
|
|
2
|
+
export interface GatewayLogger {
|
|
3
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
4
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
5
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
6
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
/** Default logger that writes JSON lines to stderr; debug lines gated by BOTCORD_GATEWAY_DEBUG. */
|
|
9
|
+
export declare const consoleLogger: GatewayLogger;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function write(level, msg, meta) {
|
|
2
|
+
const line = JSON.stringify({
|
|
3
|
+
ts: new Date().toISOString(),
|
|
4
|
+
level,
|
|
5
|
+
msg,
|
|
6
|
+
...(meta ?? {}),
|
|
7
|
+
});
|
|
8
|
+
// Always write to stderr so stdout stays clean for NDJSON-style channel output.
|
|
9
|
+
process.stderr.write(line + "\n");
|
|
10
|
+
}
|
|
11
|
+
/** Default logger that writes JSON lines to stderr; debug lines gated by BOTCORD_GATEWAY_DEBUG. */
|
|
12
|
+
export const consoleLogger = {
|
|
13
|
+
info: (msg, meta) => write("info", msg, meta),
|
|
14
|
+
warn: (msg, meta) => write("warn", msg, meta),
|
|
15
|
+
error: (msg, meta) => write("error", msg, meta),
|
|
16
|
+
debug: (msg, meta) => {
|
|
17
|
+
if (process.env.BOTCORD_GATEWAY_DEBUG)
|
|
18
|
+
write("debug", msg, meta);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GatewayConfig, GatewayInboundMessage, GatewayRoute, RouteMatch } from "./types.js";
|
|
2
|
+
/** Returns true if every provided field of `match` matches `message`; undefined match matches all. */
|
|
3
|
+
export declare function matchesRoute(message: GatewayInboundMessage, match: RouteMatch | undefined): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Picks the first matching route in priority order:
|
|
6
|
+
* 1. `config.routes[]` (user-authored)
|
|
7
|
+
* 2. `managedRoutes` (daemon-synthesized per-agent)
|
|
8
|
+
* 3. `config.defaultRoute`
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRoute(message: GatewayInboundMessage, config: Pick<GatewayConfig, "defaultRoute" | "routes">, managedRoutes?: readonly GatewayRoute[]): GatewayRoute;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Returns true if every provided field of `match` matches `message`; undefined match matches all. */
|
|
2
|
+
export function matchesRoute(message, match) {
|
|
3
|
+
if (!match)
|
|
4
|
+
return true;
|
|
5
|
+
if (match.channel !== undefined && match.channel !== message.channel)
|
|
6
|
+
return false;
|
|
7
|
+
if (match.accountId !== undefined && match.accountId !== message.accountId)
|
|
8
|
+
return false;
|
|
9
|
+
if (match.conversationId !== undefined && match.conversationId !== message.conversation.id) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (match.conversationPrefix !== undefined &&
|
|
13
|
+
!message.conversation.id.startsWith(match.conversationPrefix)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (match.conversationKind !== undefined &&
|
|
17
|
+
match.conversationKind !== message.conversation.kind) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (match.senderId !== undefined && match.senderId !== message.sender.id)
|
|
21
|
+
return false;
|
|
22
|
+
if (match.mentioned !== undefined) {
|
|
23
|
+
const actual = message.mentioned ?? false;
|
|
24
|
+
if (match.mentioned !== actual)
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Picks the first matching route in priority order:
|
|
31
|
+
* 1. `config.routes[]` (user-authored)
|
|
32
|
+
* 2. `managedRoutes` (daemon-synthesized per-agent)
|
|
33
|
+
* 3. `config.defaultRoute`
|
|
34
|
+
*/
|
|
35
|
+
export function resolveRoute(message, config, managedRoutes) {
|
|
36
|
+
const routes = config.routes ?? [];
|
|
37
|
+
for (const route of routes) {
|
|
38
|
+
if (matchesRoute(message, route.match))
|
|
39
|
+
return route;
|
|
40
|
+
}
|
|
41
|
+
if (managedRoutes) {
|
|
42
|
+
for (const route of managedRoutes) {
|
|
43
|
+
if (matchesRoute(message, route.match))
|
|
44
|
+
return route;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return config.defaultRoute;
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
|
|
2
|
+
import { type ProbeDeps } from "./probe.js";
|
|
3
|
+
import type { RuntimeProbeResult, RuntimeRunOptions } from "../types.js";
|
|
4
|
+
/** Resolve the Claude Code CLI path on PATH or the macOS desktop bundle fallback. */
|
|
5
|
+
export declare function resolveClaudeCommand(deps?: ProbeDeps): string | null;
|
|
6
|
+
/** Probe whether the Claude Code CLI is installed and report its version. */
|
|
7
|
+
export declare function probeClaude(deps?: ProbeDeps): RuntimeProbeResult;
|
|
8
|
+
/**
|
|
9
|
+
* Claude Code adapter — spawns `claude -p "<text>" --output-format stream-json`
|
|
10
|
+
* (with `--resume <sid>` when available) and parses the ndjson stream.
|
|
11
|
+
*
|
|
12
|
+
* stream-json shape (abridged):
|
|
13
|
+
* {type:"system", subtype:"init", session_id:"...", ...}
|
|
14
|
+
* {type:"assistant", message:{content:[{type:"text", text:"..."} | {type:"tool_use", ...}]}}
|
|
15
|
+
* {type:"user", message:{content:[{type:"tool_result", ...}]}}
|
|
16
|
+
* {type:"result", subtype:"success", session_id:"...", total_cost_usd: 0.01, result:"final text"}
|
|
17
|
+
*/
|
|
18
|
+
export declare class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
19
|
+
readonly id: "claude-code";
|
|
20
|
+
private readonly explicitBinary;
|
|
21
|
+
private resolvedBinary;
|
|
22
|
+
constructor(opts?: {
|
|
23
|
+
binary?: string;
|
|
24
|
+
});
|
|
25
|
+
probe(): RuntimeProbeResult;
|
|
26
|
+
run(opts: RuntimeRunOptions): Promise<import("../types.js").RuntimeRunResult>;
|
|
27
|
+
protected resolveBinary(): string;
|
|
28
|
+
protected buildArgs(opts: RuntimeRunOptions): string[];
|
|
29
|
+
protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { NdjsonStreamAdapter } from "./ndjson-stream.js";
|
|
3
|
+
import { firstExistingPath, readCommandVersion, resolveCommandOnPath, resolveHomePath, } from "./probe.js";
|
|
4
|
+
const CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
|
|
5
|
+
const CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
|
|
6
|
+
function isValidClaudeSessionId(sessionId) {
|
|
7
|
+
if (sessionId.length === 0 || sessionId.length > 512)
|
|
8
|
+
return false;
|
|
9
|
+
if (sessionId.startsWith("-"))
|
|
10
|
+
return false;
|
|
11
|
+
for (const ch of sessionId) {
|
|
12
|
+
const code = ch.codePointAt(0);
|
|
13
|
+
if (code === undefined || code < 0x20 || code === 0x7f)
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function invalidClaudeSessionIdError() {
|
|
19
|
+
return "claude-code: invalid sessionId (expected non-control text not starting with '-')";
|
|
20
|
+
}
|
|
21
|
+
/** Resolve the Claude Code CLI path on PATH or the macOS desktop bundle fallback. */
|
|
22
|
+
export function resolveClaudeCommand(deps = {}) {
|
|
23
|
+
const onPath = resolveCommandOnPath("claude", deps);
|
|
24
|
+
if (onPath)
|
|
25
|
+
return onPath;
|
|
26
|
+
if ((deps.platform ?? process.platform) !== "darwin")
|
|
27
|
+
return null;
|
|
28
|
+
return firstExistingPath([resolveHomePath(CLAUDE_DESKTOP_CLI_RELATIVE_PATH, deps), CLAUDE_DESKTOP_CLI_SYSTEM_PATH], deps);
|
|
29
|
+
}
|
|
30
|
+
/** Probe whether the Claude Code CLI is installed and report its version. */
|
|
31
|
+
export function probeClaude(deps = {}) {
|
|
32
|
+
const command = resolveClaudeCommand(deps);
|
|
33
|
+
if (!command)
|
|
34
|
+
return { available: false };
|
|
35
|
+
return {
|
|
36
|
+
available: true,
|
|
37
|
+
path: command,
|
|
38
|
+
version: readCommandVersion(command, [], deps) ?? undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Claude Code adapter — spawns `claude -p "<text>" --output-format stream-json`
|
|
43
|
+
* (with `--resume <sid>` when available) and parses the ndjson stream.
|
|
44
|
+
*
|
|
45
|
+
* stream-json shape (abridged):
|
|
46
|
+
* {type:"system", subtype:"init", session_id:"...", ...}
|
|
47
|
+
* {type:"assistant", message:{content:[{type:"text", text:"..."} | {type:"tool_use", ...}]}}
|
|
48
|
+
* {type:"user", message:{content:[{type:"tool_result", ...}]}}
|
|
49
|
+
* {type:"result", subtype:"success", session_id:"...", total_cost_usd: 0.01, result:"final text"}
|
|
50
|
+
*/
|
|
51
|
+
export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
52
|
+
id = "claude-code";
|
|
53
|
+
explicitBinary;
|
|
54
|
+
resolvedBinary = null;
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
super();
|
|
57
|
+
this.explicitBinary = opts?.binary ?? process.env.BOTCORD_CLAUDE_BIN;
|
|
58
|
+
}
|
|
59
|
+
probe() {
|
|
60
|
+
return probeClaude();
|
|
61
|
+
}
|
|
62
|
+
async run(opts) {
|
|
63
|
+
if (opts.sessionId && !isValidClaudeSessionId(opts.sessionId)) {
|
|
64
|
+
return { text: "", newSessionId: "", error: invalidClaudeSessionIdError() };
|
|
65
|
+
}
|
|
66
|
+
return super.run(opts);
|
|
67
|
+
}
|
|
68
|
+
resolveBinary() {
|
|
69
|
+
if (this.explicitBinary)
|
|
70
|
+
return this.explicitBinary;
|
|
71
|
+
if (this.resolvedBinary)
|
|
72
|
+
return this.resolvedBinary;
|
|
73
|
+
// Falls back to the macOS Claude Code URL Handler bundle when not on PATH.
|
|
74
|
+
this.resolvedBinary = resolveClaudeCommand() ?? "claude";
|
|
75
|
+
return this.resolvedBinary;
|
|
76
|
+
}
|
|
77
|
+
buildArgs(opts) {
|
|
78
|
+
const args = ["-p", opts.text, "--output-format", "stream-json", "--verbose"];
|
|
79
|
+
if (opts.sessionId) {
|
|
80
|
+
if (!isValidClaudeSessionId(opts.sessionId))
|
|
81
|
+
throw new Error(invalidClaudeSessionIdError());
|
|
82
|
+
args.push("--resume", opts.sessionId);
|
|
83
|
+
}
|
|
84
|
+
// Permission-mode policy:
|
|
85
|
+
// - owner: acceptEdits (owner trusts their own agent).
|
|
86
|
+
// - non-owner (trusted/public): default (let Claude Code prompt / reject edits per its own rules).
|
|
87
|
+
// `extraArgs` still wins — operators who know what they're doing can override either.
|
|
88
|
+
if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
|
|
89
|
+
if (opts.trustLevel === "owner") {
|
|
90
|
+
args.push("--permission-mode", "acceptEdits");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
args.push("--permission-mode", "default");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Claude Code's `--append-system-prompt` is applied per invocation and NOT
|
|
97
|
+
// persisted in the resumed session transcript — ideal for memory / digest
|
|
98
|
+
// content that should re-evaluate every turn.
|
|
99
|
+
if (opts.systemContext && !opts.extraArgs?.includes("--append-system-prompt")) {
|
|
100
|
+
args.push("--append-system-prompt", opts.systemContext);
|
|
101
|
+
}
|
|
102
|
+
if (opts.extraArgs?.length)
|
|
103
|
+
args.push(...opts.extraArgs);
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
handleEvent(raw, ctx) {
|
|
107
|
+
const obj = raw;
|
|
108
|
+
ctx.emitBlock(normalizeBlock(obj, ctx.seq));
|
|
109
|
+
if (obj.type === "system" && obj.session_id) {
|
|
110
|
+
ctx.state.newSessionId = String(obj.session_id);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
114
|
+
for (const c of obj.message.content) {
|
|
115
|
+
if (c?.type === "text" && typeof c.text === "string") {
|
|
116
|
+
ctx.appendAssistantText(c.text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (obj.type === "result") {
|
|
122
|
+
if (typeof obj.total_cost_usd === "number")
|
|
123
|
+
ctx.state.costUsd = obj.total_cost_usd;
|
|
124
|
+
if (obj.subtype === "success") {
|
|
125
|
+
if (typeof obj.session_id === "string")
|
|
126
|
+
ctx.state.newSessionId = obj.session_id;
|
|
127
|
+
if (typeof obj.result === "string")
|
|
128
|
+
ctx.state.finalText = obj.result;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Non-success result (e.g. resume targeted a missing UUID). Claude Code
|
|
132
|
+
// still emits a fresh `session_id` for the just-spawned empty session —
|
|
133
|
+
// persisting it would trap us into resuming a useless UUID forever.
|
|
134
|
+
// Wipe newSessionId so the dispatcher deletes the stale entry instead.
|
|
135
|
+
// The CLI also exits non-zero, so the base adapter synthesizes errorText
|
|
136
|
+
// from stderr if `obj.result` is missing.
|
|
137
|
+
ctx.state.newSessionId = "";
|
|
138
|
+
if (typeof obj.result === "string")
|
|
139
|
+
ctx.state.errorText = obj.result;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function normalizeBlock(obj, seq) {
|
|
145
|
+
let kind = "other";
|
|
146
|
+
if (obj?.type === "assistant") {
|
|
147
|
+
const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
|
|
148
|
+
if (contents.some((c) => c?.type === "tool_use"))
|
|
149
|
+
kind = "tool_use";
|
|
150
|
+
else if (contents.some((c) => c?.type === "text"))
|
|
151
|
+
kind = "assistant_text";
|
|
152
|
+
}
|
|
153
|
+
else if (obj?.type === "user") {
|
|
154
|
+
const contents = Array.isArray(obj.message?.content) ? obj.message.content : [];
|
|
155
|
+
if (contents.some((c) => c?.type === "tool_result"))
|
|
156
|
+
kind = "tool_result";
|
|
157
|
+
}
|
|
158
|
+
else if (obj?.type === "system") {
|
|
159
|
+
kind = "system";
|
|
160
|
+
}
|
|
161
|
+
return { raw: obj, kind, seq };
|
|
162
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
|
|
2
|
+
import { type ProbeDeps } from "./probe.js";
|
|
3
|
+
import type { RuntimeProbeResult, RuntimeRunOptions } from "../types.js";
|
|
4
|
+
/** Resolve the Codex CLI executable via PATH or macOS desktop bundle. */
|
|
5
|
+
export declare function resolveCodexCommand(deps?: ProbeDeps): string | null;
|
|
6
|
+
/** Probe whether the Codex CLI is installed and report its version. */
|
|
7
|
+
export declare function probeCodex(deps?: ProbeDeps): RuntimeProbeResult;
|
|
8
|
+
/**
|
|
9
|
+
* Codex adapter — spawns `codex exec [resume <sid>] --json ...` and parses the
|
|
10
|
+
* JSONL event stream.
|
|
11
|
+
*
|
|
12
|
+
* Event shape (abridged):
|
|
13
|
+
* {"type":"thread.started","thread_id":"<uuid>"}
|
|
14
|
+
* {"type":"turn.started"}
|
|
15
|
+
* {"type":"item.started","item":{"type":"command_execution", ...}}
|
|
16
|
+
* {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
|
|
17
|
+
* {"type":"turn.completed","usage":{...}}
|
|
18
|
+
*
|
|
19
|
+
* `codex exec` does not report USD cost — only token usage — so `costUsd` is
|
|
20
|
+
* not populated from this adapter.
|
|
21
|
+
*
|
|
22
|
+
* ## systemContext injection: per-agent CODEX_HOME + AGENTS.md
|
|
23
|
+
*
|
|
24
|
+
* Codex has no `--append-system-prompt` equivalent. Its documented way to
|
|
25
|
+
* inject instructions that do NOT land in the stored transcript is the
|
|
26
|
+
* `AGENTS.md` loaded from `<CODEX_HOME>/AGENTS.md` (alongside the user-global
|
|
27
|
+
* `~/.codex/AGENTS.md` and the cwd's `<cwd>/AGENTS.md`).
|
|
28
|
+
*
|
|
29
|
+
* This adapter therefore:
|
|
30
|
+
* 1. Points `CODEX_HOME` at a per-agent directory:
|
|
31
|
+
* `~/.botcord/agents/<accountId>/codex-home/`
|
|
32
|
+
* 2. Writes `opts.systemContext` to `<CODEX_HOME>/AGENTS.md` atomically
|
|
33
|
+
* (tmp + rename) before spawning the child.
|
|
34
|
+
* 3. Leaves the positional prompt as just `opts.text` — no more prepending
|
|
35
|
+
* systemContext to the transcript.
|
|
36
|
+
*
|
|
37
|
+
* With the transcript no longer accumulating systemContext, resume is safe to
|
|
38
|
+
* turn back on: `thread.started.thread_id` is persisted as `newSessionId`, and
|
|
39
|
+
* when the next turn arrives with a sessionId the adapter runs `exec resume
|
|
40
|
+
* <sid>` instead of `exec`. The per-agent CODEX_HOME also isolates codex's
|
|
41
|
+
* `sessions/` directory from `~/.codex/sessions/`, so daemon-owned sessions
|
|
42
|
+
* don't pollute the user's interactive session picker.
|
|
43
|
+
*
|
|
44
|
+
* ## `exec resume` flag quirk
|
|
45
|
+
*
|
|
46
|
+
* `codex exec resume` accepts a smaller flag set than `codex exec` — notably
|
|
47
|
+
* `-s / --sandbox` is NOT accepted on `resume`. We therefore express sandbox
|
|
48
|
+
* policy as `-c sandbox_mode="..."` (a `-c` override works on both
|
|
49
|
+
* subcommands) and the same tail of flags applies to both paths.
|
|
50
|
+
*/
|
|
51
|
+
export declare class CodexAdapter extends NdjsonStreamAdapter {
|
|
52
|
+
readonly id: "codex";
|
|
53
|
+
private readonly explicitBinary;
|
|
54
|
+
private resolvedBinary;
|
|
55
|
+
constructor(opts?: {
|
|
56
|
+
binary?: string;
|
|
57
|
+
});
|
|
58
|
+
probe(): RuntimeProbeResult;
|
|
59
|
+
/**
|
|
60
|
+
* Validate the sessionId shape and materialize the per-agent CODEX_HOME +
|
|
61
|
+
* AGENTS.md before handing off to the base adapter's spawn loop. Both steps
|
|
62
|
+
* must run BEFORE `super.run()` because `spawnEnv()` and `buildArgs()` are
|
|
63
|
+
* called synchronously from inside it and read the filesystem state we set
|
|
64
|
+
* up here.
|
|
65
|
+
*/
|
|
66
|
+
run(opts: RuntimeRunOptions): Promise<import("../types.js").RuntimeRunResult>;
|
|
67
|
+
protected resolveBinary(): string;
|
|
68
|
+
/**
|
|
69
|
+
* `extraArgs` are passed as Codex CLI flags (inserted before `--`), not
|
|
70
|
+
* prompt text. Use the route config's `extraArgs` for flags like
|
|
71
|
+
* `-c model="..."`, not for extra prompt content.
|
|
72
|
+
*
|
|
73
|
+
* Layout for fresh session: `exec <tail> -- <prompt>`
|
|
74
|
+
* Layout for resume: `exec resume <sid> <tail> -- <prompt>`
|
|
75
|
+
*
|
|
76
|
+
* Both paths share the same `<tail>`: sandbox/approval policy (as `-c`
|
|
77
|
+
* overrides so `resume` accepts them), `--skip-git-repo-check`, `--json`,
|
|
78
|
+
* and operator `extraArgs`.
|
|
79
|
+
*/
|
|
80
|
+
protected buildArgs(opts: RuntimeRunOptions): string[];
|
|
81
|
+
protected spawnEnv(opts: RuntimeRunOptions): NodeJS.ProcessEnv;
|
|
82
|
+
protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
|
|
83
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { agentCodexHomeDir, ensureAgentCodexHome } from "../../agent-workspace.js";
|
|
5
|
+
import { NdjsonStreamAdapter } from "./ndjson-stream.js";
|
|
6
|
+
import { firstExistingPath, readCommandVersion, resolveCommandOnPath, } from "./probe.js";
|
|
7
|
+
const CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
|
|
8
|
+
/** Codex UUIDv7 / v4 session ids are 36-char dashed hex; reject anything else to keep argv safe. */
|
|
9
|
+
const CODEX_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
10
|
+
/** Resolve the Codex CLI executable via PATH or macOS desktop bundle. */
|
|
11
|
+
export function resolveCodexCommand(deps = {}) {
|
|
12
|
+
const onPath = resolveCommandOnPath("codex", deps);
|
|
13
|
+
if (onPath)
|
|
14
|
+
return onPath;
|
|
15
|
+
return firstExistingPath([CODEX_DESKTOP_BUNDLE_PATH], deps);
|
|
16
|
+
}
|
|
17
|
+
function resolveCodexGlobalNpmEntry() {
|
|
18
|
+
try {
|
|
19
|
+
const globalRoot = execFileSync("npm", ["root", "-g"], {
|
|
20
|
+
encoding: "utf8",
|
|
21
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
}).trim();
|
|
24
|
+
if (!globalRoot)
|
|
25
|
+
return null;
|
|
26
|
+
const candidate = path.join(globalRoot, "@openai", "codex", "bin", "codex.js");
|
|
27
|
+
return existsSync(candidate) ? candidate : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Probe whether the Codex CLI is installed and report its version. */
|
|
34
|
+
export function probeCodex(deps = {}) {
|
|
35
|
+
const command = resolveCodexCommand(deps);
|
|
36
|
+
if (command) {
|
|
37
|
+
return {
|
|
38
|
+
available: true,
|
|
39
|
+
path: command,
|
|
40
|
+
version: readCommandVersion(command, [], deps) ?? undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const npmEntry = resolveCodexGlobalNpmEntry();
|
|
44
|
+
if (npmEntry) {
|
|
45
|
+
return {
|
|
46
|
+
available: true,
|
|
47
|
+
path: npmEntry,
|
|
48
|
+
version: readCommandVersion(process.execPath, [npmEntry], deps) ?? undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { available: false };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Codex adapter — spawns `codex exec [resume <sid>] --json ...` and parses the
|
|
55
|
+
* JSONL event stream.
|
|
56
|
+
*
|
|
57
|
+
* Event shape (abridged):
|
|
58
|
+
* {"type":"thread.started","thread_id":"<uuid>"}
|
|
59
|
+
* {"type":"turn.started"}
|
|
60
|
+
* {"type":"item.started","item":{"type":"command_execution", ...}}
|
|
61
|
+
* {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
|
|
62
|
+
* {"type":"turn.completed","usage":{...}}
|
|
63
|
+
*
|
|
64
|
+
* `codex exec` does not report USD cost — only token usage — so `costUsd` is
|
|
65
|
+
* not populated from this adapter.
|
|
66
|
+
*
|
|
67
|
+
* ## systemContext injection: per-agent CODEX_HOME + AGENTS.md
|
|
68
|
+
*
|
|
69
|
+
* Codex has no `--append-system-prompt` equivalent. Its documented way to
|
|
70
|
+
* inject instructions that do NOT land in the stored transcript is the
|
|
71
|
+
* `AGENTS.md` loaded from `<CODEX_HOME>/AGENTS.md` (alongside the user-global
|
|
72
|
+
* `~/.codex/AGENTS.md` and the cwd's `<cwd>/AGENTS.md`).
|
|
73
|
+
*
|
|
74
|
+
* This adapter therefore:
|
|
75
|
+
* 1. Points `CODEX_HOME` at a per-agent directory:
|
|
76
|
+
* `~/.botcord/agents/<accountId>/codex-home/`
|
|
77
|
+
* 2. Writes `opts.systemContext` to `<CODEX_HOME>/AGENTS.md` atomically
|
|
78
|
+
* (tmp + rename) before spawning the child.
|
|
79
|
+
* 3. Leaves the positional prompt as just `opts.text` — no more prepending
|
|
80
|
+
* systemContext to the transcript.
|
|
81
|
+
*
|
|
82
|
+
* With the transcript no longer accumulating systemContext, resume is safe to
|
|
83
|
+
* turn back on: `thread.started.thread_id` is persisted as `newSessionId`, and
|
|
84
|
+
* when the next turn arrives with a sessionId the adapter runs `exec resume
|
|
85
|
+
* <sid>` instead of `exec`. The per-agent CODEX_HOME also isolates codex's
|
|
86
|
+
* `sessions/` directory from `~/.codex/sessions/`, so daemon-owned sessions
|
|
87
|
+
* don't pollute the user's interactive session picker.
|
|
88
|
+
*
|
|
89
|
+
* ## `exec resume` flag quirk
|
|
90
|
+
*
|
|
91
|
+
* `codex exec resume` accepts a smaller flag set than `codex exec` — notably
|
|
92
|
+
* `-s / --sandbox` is NOT accepted on `resume`. We therefore express sandbox
|
|
93
|
+
* policy as `-c sandbox_mode="..."` (a `-c` override works on both
|
|
94
|
+
* subcommands) and the same tail of flags applies to both paths.
|
|
95
|
+
*/
|
|
96
|
+
export class CodexAdapter extends NdjsonStreamAdapter {
|
|
97
|
+
id = "codex";
|
|
98
|
+
explicitBinary;
|
|
99
|
+
resolvedBinary = null;
|
|
100
|
+
constructor(opts) {
|
|
101
|
+
super();
|
|
102
|
+
this.explicitBinary = opts?.binary ?? process.env.BOTCORD_CODEX_BIN;
|
|
103
|
+
}
|
|
104
|
+
probe() {
|
|
105
|
+
return probeCodex();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate the sessionId shape and materialize the per-agent CODEX_HOME +
|
|
109
|
+
* AGENTS.md before handing off to the base adapter's spawn loop. Both steps
|
|
110
|
+
* must run BEFORE `super.run()` because `spawnEnv()` and `buildArgs()` are
|
|
111
|
+
* called synchronously from inside it and read the filesystem state we set
|
|
112
|
+
* up here.
|
|
113
|
+
*/
|
|
114
|
+
async run(opts) {
|
|
115
|
+
if (opts.sessionId && !CODEX_SESSION_ID_RE.test(opts.sessionId)) {
|
|
116
|
+
throw new Error(`codex: invalid sessionId "${opts.sessionId}" (expected UUID)`);
|
|
117
|
+
}
|
|
118
|
+
if (opts.accountId) {
|
|
119
|
+
try {
|
|
120
|
+
ensureAgentCodexHome(opts.accountId);
|
|
121
|
+
writeCodexAgentsMd(opts.accountId, opts.systemContext);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// Writing AGENTS.md should never abort the turn — log and fall
|
|
125
|
+
// through. The child will spawn without the dynamic systemContext,
|
|
126
|
+
// which degrades to "codex replies without this turn's memory
|
|
127
|
+
// snapshot" rather than silence.
|
|
128
|
+
// eslint-disable-next-line no-console
|
|
129
|
+
console.warn("codex: failed to prepare CODEX_HOME/AGENTS.md", err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return super.run(opts);
|
|
133
|
+
}
|
|
134
|
+
resolveBinary() {
|
|
135
|
+
if (this.explicitBinary)
|
|
136
|
+
return this.explicitBinary;
|
|
137
|
+
if (this.resolvedBinary)
|
|
138
|
+
return this.resolvedBinary;
|
|
139
|
+
// Use the executable resolver only — probeCodex's npm-global fallback
|
|
140
|
+
// yields a `.js` path that can't be spawned directly.
|
|
141
|
+
this.resolvedBinary = resolveCodexCommand() ?? "codex";
|
|
142
|
+
return this.resolvedBinary;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `extraArgs` are passed as Codex CLI flags (inserted before `--`), not
|
|
146
|
+
* prompt text. Use the route config's `extraArgs` for flags like
|
|
147
|
+
* `-c model="..."`, not for extra prompt content.
|
|
148
|
+
*
|
|
149
|
+
* Layout for fresh session: `exec <tail> -- <prompt>`
|
|
150
|
+
* Layout for resume: `exec resume <sid> <tail> -- <prompt>`
|
|
151
|
+
*
|
|
152
|
+
* Both paths share the same `<tail>`: sandbox/approval policy (as `-c`
|
|
153
|
+
* overrides so `resume` accepts them), `--skip-git-repo-check`, `--json`,
|
|
154
|
+
* and operator `extraArgs`.
|
|
155
|
+
*/
|
|
156
|
+
buildArgs(opts) {
|
|
157
|
+
const tail = [];
|
|
158
|
+
// Sandbox / approval policy. Expressed as `-c` overrides because
|
|
159
|
+
// `codex exec resume` rejects `-s` / `--full-auto`. `-c` works on both
|
|
160
|
+
// the fresh `exec` and `exec resume` paths.
|
|
161
|
+
// - owner turn: bypass approvals + sandbox (owner trusts their agent)
|
|
162
|
+
// - non-owner turn: `workspace-write` sandbox + on-request approvals
|
|
163
|
+
const hasSandboxOverride = opts.extraArgs?.some((a) => a === "-s" ||
|
|
164
|
+
a.startsWith("--sandbox") ||
|
|
165
|
+
a === "--full-auto" ||
|
|
166
|
+
a === "--dangerously-bypass-approvals-and-sandbox" ||
|
|
167
|
+
a.startsWith("-c sandbox_mode=") ||
|
|
168
|
+
a.startsWith("-csandbox_mode=")) ?? false;
|
|
169
|
+
if (!hasSandboxOverride) {
|
|
170
|
+
if (opts.trustLevel === "owner") {
|
|
171
|
+
tail.push("-c", 'sandbox_mode="danger-full-access"', "-c", 'approval_policy="never"');
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
tail.push("-c", 'sandbox_mode="workspace-write"', "-c", 'approval_policy="on-request"');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
tail.push("--skip-git-repo-check", "--json");
|
|
178
|
+
if (opts.extraArgs?.length)
|
|
179
|
+
tail.push(...opts.extraArgs);
|
|
180
|
+
// `--` separates flags from positionals so a prompt starting with `-`
|
|
181
|
+
// can never be parsed as an option. `systemContext` is NOT prepended to
|
|
182
|
+
// the prompt any more — it lives in `<CODEX_HOME>/AGENTS.md` written by
|
|
183
|
+
// `run()` — so the transcript stays clean across resumes.
|
|
184
|
+
const prompt = opts.text;
|
|
185
|
+
if (opts.sessionId) {
|
|
186
|
+
return ["exec", "resume", opts.sessionId, ...tail, "--", prompt];
|
|
187
|
+
}
|
|
188
|
+
return ["exec", ...tail, "--", prompt];
|
|
189
|
+
}
|
|
190
|
+
spawnEnv(opts) {
|
|
191
|
+
const env = {
|
|
192
|
+
...process.env,
|
|
193
|
+
// Keep JSONL free of ANSI codes regardless of user terminal settings.
|
|
194
|
+
FORCE_COLOR: "0",
|
|
195
|
+
NO_COLOR: "1",
|
|
196
|
+
};
|
|
197
|
+
if (opts.accountId) {
|
|
198
|
+
env.CODEX_HOME = agentCodexHomeDir(opts.accountId);
|
|
199
|
+
}
|
|
200
|
+
return env;
|
|
201
|
+
}
|
|
202
|
+
handleEvent(raw, ctx) {
|
|
203
|
+
const obj = raw;
|
|
204
|
+
ctx.emitBlock(normalizeBlock(obj, ctx.seq));
|
|
205
|
+
// Persist the thread_id so the next turn on this session key resumes
|
|
206
|
+
// instead of spawning fresh. Safe now that systemContext lives in
|
|
207
|
+
// AGENTS.md rather than the transcript.
|
|
208
|
+
if (obj.type === "thread.started") {
|
|
209
|
+
if (typeof obj.thread_id === "string") {
|
|
210
|
+
ctx.state.newSessionId = obj.thread_id;
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (obj.type === "item.completed" && obj.item?.type === "agent_message") {
|
|
215
|
+
if (typeof obj.item.text === "string") {
|
|
216
|
+
ctx.appendAssistantText(obj.item.text);
|
|
217
|
+
// The last agent_message is the final reply.
|
|
218
|
+
ctx.state.finalText = obj.item.text;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (obj.type === "turn.completed" && obj.turn?.status === "failed") {
|
|
223
|
+
const msg = obj.turn.error?.message;
|
|
224
|
+
if (typeof msg === "string" && msg)
|
|
225
|
+
ctx.state.errorText = msg;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (obj.type === "error") {
|
|
229
|
+
ctx.state.errorText =
|
|
230
|
+
typeof obj.error === "string"
|
|
231
|
+
? obj.error
|
|
232
|
+
: obj.error?.message ?? "codex error";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Atomically overwrite `<CODEX_HOME>/AGENTS.md` with `systemContext`. codex
|
|
238
|
+
* reads this file at process start, so the write must complete before spawn.
|
|
239
|
+
* An empty or missing systemContext writes an empty file — deleting would
|
|
240
|
+
* race with a prior turn's file still being readable; empty is simpler and
|
|
241
|
+
* codex treats it as "no user-global AGENTS.md".
|
|
242
|
+
*/
|
|
243
|
+
function writeCodexAgentsMd(accountId, systemContext) {
|
|
244
|
+
const dir = agentCodexHomeDir(accountId);
|
|
245
|
+
// ensureAgentCodexHome already mkdir's dir; defensive mkdir here too for
|
|
246
|
+
// code paths that invoke this helper directly (tests, future callers).
|
|
247
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
248
|
+
const target = path.join(dir, "AGENTS.md");
|
|
249
|
+
const tmp = path.join(dir, `.AGENTS.md.${process.pid}.tmp`);
|
|
250
|
+
writeFileSync(tmp, systemContext ?? "", { mode: 0o600 });
|
|
251
|
+
renameSync(tmp, target);
|
|
252
|
+
}
|
|
253
|
+
function normalizeBlock(obj, seq) {
|
|
254
|
+
let kind = "other";
|
|
255
|
+
const type = obj?.type;
|
|
256
|
+
const itemType = obj?.item?.type;
|
|
257
|
+
if (type === "thread.started" || type === "turn.started" || type === "turn.completed") {
|
|
258
|
+
kind = "system";
|
|
259
|
+
}
|
|
260
|
+
else if (type === "item.completed" && itemType === "agent_message") {
|
|
261
|
+
kind = "assistant_text";
|
|
262
|
+
}
|
|
263
|
+
else if (type === "item.started" || type === "item.completed") {
|
|
264
|
+
if (itemType === "command_execution" ||
|
|
265
|
+
itemType === "file_change" ||
|
|
266
|
+
itemType === "mcp_tool_call" ||
|
|
267
|
+
itemType === "web_search") {
|
|
268
|
+
kind = "tool_use";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { raw: obj, kind, seq };
|
|
272
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ProbeDeps } from "./probe.js";
|
|
2
|
+
import type { RuntimeAdapter, RuntimeProbeResult, RuntimeRunOptions, RuntimeRunResult } from "../types.js";
|
|
3
|
+
/** Resolve the Gemini CLI executable on PATH. */
|
|
4
|
+
export declare function resolveGeminiCommand(deps?: ProbeDeps): string | null;
|
|
5
|
+
/** Probe whether the Gemini CLI is installed and report its version. */
|
|
6
|
+
export declare function probeGemini(deps?: ProbeDeps): RuntimeProbeResult;
|
|
7
|
+
/**
|
|
8
|
+
* Gemini adapter stub — probe() is wired up so `botcord-daemon doctor` can report it.
|
|
9
|
+
* run() is not implemented yet; routing a turn here will surface the error upstream.
|
|
10
|
+
*/
|
|
11
|
+
export declare class GeminiAdapter implements RuntimeAdapter {
|
|
12
|
+
readonly id: "gemini";
|
|
13
|
+
probe(): RuntimeProbeResult;
|
|
14
|
+
run(_opts: RuntimeRunOptions): Promise<RuntimeRunResult>;
|
|
15
|
+
}
|