doer-agent 0.4.9 → 0.5.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/agent-codex-cli.js +24 -4
- package/dist/agent-daemon-rpc.js +1 -7
- package/dist/agent-settings-rpc.js +82 -2
- package/dist/agent-settings.js +124 -0
- package/dist/agent.js +11 -5
- package/dist/daemon-log-runner.js +3 -39
- package/dist/db-mcp-server.js +377 -0
- package/package.json +4 -1
package/dist/agent-codex-cli.js
CHANGED
|
@@ -46,9 +46,29 @@ export function buildManagedCodexArgs(args) {
|
|
|
46
46
|
];
|
|
47
47
|
}
|
|
48
48
|
export function buildDaemonMcpConfigArgs(args) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
return buildWorkspaceMcpConfigArgs({
|
|
50
|
+
agentProjectDir: args.agentProjectDir,
|
|
51
|
+
workspaceRoot: args.workspaceRoot,
|
|
52
|
+
serverName: args.serverName?.trim() || "doer_daemon",
|
|
53
|
+
distEntryRelativePath: path.join("dist", "daemon-mcp-server.js"),
|
|
54
|
+
srcEntryRelativePath: path.join("src", "daemon-mcp-server.ts"),
|
|
55
|
+
workspaceRootEnvName: "DOER_DAEMON_WORKSPACE_ROOT",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function buildDatabaseMcpConfigArgs(args) {
|
|
59
|
+
return buildWorkspaceMcpConfigArgs({
|
|
60
|
+
agentProjectDir: args.agentProjectDir,
|
|
61
|
+
workspaceRoot: args.workspaceRoot,
|
|
62
|
+
serverName: args.serverName?.trim() || "doer_database",
|
|
63
|
+
distEntryRelativePath: path.join("dist", "db-mcp-server.js"),
|
|
64
|
+
srcEntryRelativePath: path.join("src", "db-mcp-server.ts"),
|
|
65
|
+
workspaceRootEnvName: "DOER_DB_WORKSPACE_ROOT",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function buildWorkspaceMcpConfigArgs(args) {
|
|
69
|
+
const serverName = args.serverName.trim();
|
|
70
|
+
const distEntry = path.join(args.agentProjectDir, args.distEntryRelativePath);
|
|
71
|
+
const srcEntry = path.join(args.agentProjectDir, args.srcEntryRelativePath);
|
|
52
72
|
const tsxLoaderPath = path.join(args.agentProjectDir, "node_modules", "tsx", "dist", "loader.mjs");
|
|
53
73
|
const command = process.execPath;
|
|
54
74
|
const commandArgs = existsSync(distEntry)
|
|
@@ -60,7 +80,7 @@ export function buildDaemonMcpConfigArgs(args) {
|
|
|
60
80
|
"--config",
|
|
61
81
|
`mcp_servers.${serverName}.args=${toTomlStringArray(commandArgs)}`,
|
|
62
82
|
"--config",
|
|
63
|
-
`mcp_servers.${serverName}.env.
|
|
83
|
+
`mcp_servers.${serverName}.env.${args.workspaceRootEnvName}=${toTomlStringLiteral(args.workspaceRoot)}`,
|
|
64
84
|
"--config",
|
|
65
85
|
`mcp_servers.${serverName}.enabled=true`,
|
|
66
86
|
];
|
package/dist/agent-daemon-rpc.js
CHANGED
|
@@ -105,13 +105,7 @@ function normalizeLogEvent(value) {
|
|
|
105
105
|
}
|
|
106
106
|
const row = value;
|
|
107
107
|
const ts = typeof row.ts === "string" ? row.ts.trim() : "";
|
|
108
|
-
const type = row.type === "start" ||
|
|
109
|
-
row.type === "stdout" ||
|
|
110
|
-
row.type === "stderr" ||
|
|
111
|
-
row.type === "heartbeat" ||
|
|
112
|
-
row.type === "exit" ||
|
|
113
|
-
row.type === "signal" ||
|
|
114
|
-
row.type === "error"
|
|
108
|
+
const type = row.type === "start" || row.type === "stdout" || row.type === "stderr" || row.type === "exit" || row.type === "signal" || row.type === "error"
|
|
115
109
|
? row.type
|
|
116
110
|
: null;
|
|
117
111
|
if (!ts || !type) {
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createConnection as createMysqlConnection } from "mysql2/promise";
|
|
2
3
|
import { StringCodec } from "nats";
|
|
3
|
-
import {
|
|
4
|
+
import { Client as PostgresClient } from "pg";
|
|
5
|
+
import { normalizeAgentDatabaseConnection, normalizeAgentSettingsConfig, normalizeAgentSettingsPatch, readAgentSettingsConfig, resolveAgentDatabaseConnectionUrl, resolveAgentSettingsFilePath, toAgentSettingsPublic, writeAgentModelInstructions, writeAgentSettingsConfig, } from "./agent-settings.js";
|
|
4
6
|
const settingsRpcCodec = StringCodec();
|
|
5
7
|
function normalizeSettingsRpcRequest(args) {
|
|
6
8
|
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
7
9
|
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
8
10
|
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
9
|
-
const action = args.request.action === "update"
|
|
11
|
+
const action = args.request.action === "update"
|
|
12
|
+
? "update"
|
|
13
|
+
: args.request.action === "test_database_connection"
|
|
14
|
+
? "test_database_connection"
|
|
15
|
+
: "get";
|
|
10
16
|
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
11
17
|
throw new Error("invalid settings rpc request");
|
|
12
18
|
}
|
|
19
|
+
const connection = args.request.connection === undefined ? null : normalizeAgentDatabaseConnection(args.request.connection);
|
|
20
|
+
if (action === "test_database_connection" && !connection) {
|
|
21
|
+
throw new Error("invalid database connection test payload");
|
|
22
|
+
}
|
|
13
23
|
return {
|
|
14
24
|
requestId,
|
|
15
25
|
responseSubject,
|
|
@@ -18,11 +28,69 @@ function normalizeSettingsRpcRequest(args) {
|
|
|
18
28
|
defaults: args.request.defaults && typeof args.request.defaults === "object" && !Array.isArray(args.request.defaults)
|
|
19
29
|
? normalizeAgentSettingsConfig(args.request.defaults)
|
|
20
30
|
: null,
|
|
31
|
+
connection,
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
34
|
function publishSettingsRpcResponse(args) {
|
|
24
35
|
args.nc.publish(args.responseSubject, settingsRpcCodec.encode(JSON.stringify(args.payload)));
|
|
25
36
|
}
|
|
37
|
+
async function testDatabaseConnection(connection) {
|
|
38
|
+
const connectionUrl = resolveAgentDatabaseConnectionUrl(connection);
|
|
39
|
+
if (!connectionUrl) {
|
|
40
|
+
if (connection.connection.mode === "env") {
|
|
41
|
+
throw new Error(`Database URL env is missing: ${connection.connection.urlEnv}`);
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Database URL is missing for connection: ${connection.id}`);
|
|
44
|
+
}
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
if (connection.provider === "mysql") {
|
|
47
|
+
const client = await createMysqlConnection({
|
|
48
|
+
uri: connectionUrl,
|
|
49
|
+
connectTimeout: 5_000,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const [rows] = await client.query("SELECT DATABASE() AS database_name, VERSION() AS version");
|
|
53
|
+
const firstRow = Array.isArray(rows)
|
|
54
|
+
? rows[0]
|
|
55
|
+
: undefined;
|
|
56
|
+
const latencyMs = Date.now() - startedAt;
|
|
57
|
+
const database = firstRow?.database_name ?? null;
|
|
58
|
+
const serverVersion = firstRow?.version ?? null;
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
provider: connection.provider,
|
|
62
|
+
message: `Connected to MySQL${database ? ` (${database})` : ""} in ${latencyMs}ms`,
|
|
63
|
+
latencyMs,
|
|
64
|
+
database,
|
|
65
|
+
serverVersion,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await client.end().catch(() => undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const client = new PostgresClient({
|
|
73
|
+
connectionString: connectionUrl,
|
|
74
|
+
connectionTimeoutMillis: 5_000,
|
|
75
|
+
});
|
|
76
|
+
await client.connect();
|
|
77
|
+
try {
|
|
78
|
+
const result = await client.query("SELECT current_database() AS database_name, version() AS version");
|
|
79
|
+
const firstRow = result.rows[0] ?? { database_name: null, version: null };
|
|
80
|
+
const latencyMs = Date.now() - startedAt;
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
provider: connection.provider,
|
|
84
|
+
message: `Connected to PostgreSQL${firstRow.database_name ? ` (${firstRow.database_name})` : ""} in ${latencyMs}ms`,
|
|
85
|
+
latencyMs,
|
|
86
|
+
database: firstRow.database_name,
|
|
87
|
+
serverVersion: firstRow.version,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await client.end().catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
26
94
|
export async function handleSettingsRpcMessage(args) {
|
|
27
95
|
let requestId = "unknown";
|
|
28
96
|
let responseSubject = "";
|
|
@@ -31,6 +99,18 @@ export async function handleSettingsRpcMessage(args) {
|
|
|
31
99
|
const request = normalizeSettingsRpcRequest({ request: payload, agentId: args.agentId });
|
|
32
100
|
requestId = request.requestId;
|
|
33
101
|
responseSubject = request.responseSubject;
|
|
102
|
+
if (request.action === "test_database_connection") {
|
|
103
|
+
publishSettingsRpcResponse({
|
|
104
|
+
nc: args.nc,
|
|
105
|
+
responseSubject,
|
|
106
|
+
payload: {
|
|
107
|
+
requestId,
|
|
108
|
+
ok: true,
|
|
109
|
+
testResult: await testDatabaseConnection(request.connection),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
34
114
|
const existing = await readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot, defaults: request.defaults });
|
|
35
115
|
const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
|
|
36
116
|
if (request.action === "update") {
|
package/dist/agent-settings.js
CHANGED
|
@@ -63,6 +63,10 @@ export function createDefaultAgentSettingsConfig() {
|
|
|
63
63
|
enabled: false,
|
|
64
64
|
apiToken: null,
|
|
65
65
|
},
|
|
66
|
+
databases: {
|
|
67
|
+
defaultConnectionId: null,
|
|
68
|
+
connections: [],
|
|
69
|
+
},
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
function normalizeNullableString(value) {
|
|
@@ -78,6 +82,105 @@ function normalizeNullableString(value) {
|
|
|
78
82
|
function normalizeCodexPersonality(value, fallback) {
|
|
79
83
|
return value === "friendly" || value === "pragmatic" ? value : fallback;
|
|
80
84
|
}
|
|
85
|
+
function normalizeDatabaseConnectionId(value) {
|
|
86
|
+
if (typeof value !== "string") {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const trimmed = value.trim().toLowerCase();
|
|
90
|
+
if (!trimmed || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(trimmed)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return trimmed;
|
|
94
|
+
}
|
|
95
|
+
function normalizeEnvVarName(value) {
|
|
96
|
+
if (typeof value !== "string") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (!trimmed || !/^[A-Z_][A-Z0-9_]*$/.test(trimmed)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
function normalizeAgentDatabaseProvider(value) {
|
|
106
|
+
return value === "mysql" ? "mysql" : "postgres";
|
|
107
|
+
}
|
|
108
|
+
function normalizeAgentDatabaseConnectionSecret(value) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const raw = value;
|
|
113
|
+
const mode = raw.mode === "env" ? "env" : raw.mode === "url" ? "url" : null;
|
|
114
|
+
if (mode === "env") {
|
|
115
|
+
const urlEnv = normalizeEnvVarName(raw.urlEnv);
|
|
116
|
+
return urlEnv ? { mode, urlEnv } : null;
|
|
117
|
+
}
|
|
118
|
+
if (mode === "url") {
|
|
119
|
+
const url = normalizeNullableString(raw.url);
|
|
120
|
+
return url ? { mode, url } : null;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
export function normalizeAgentDatabaseConnection(value) {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const raw = value;
|
|
129
|
+
const id = normalizeDatabaseConnectionId(raw.id);
|
|
130
|
+
const connection = normalizeAgentDatabaseConnectionSecret(raw.connection);
|
|
131
|
+
if (!id || !connection) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
description: raw.description === null ? null : normalizeNullableString(raw.description),
|
|
137
|
+
provider: normalizeAgentDatabaseProvider(raw.provider),
|
|
138
|
+
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
|
|
139
|
+
readOnly: typeof raw.readOnly === "boolean" ? raw.readOnly : true,
|
|
140
|
+
connection,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function normalizeAgentDatabaseSettings(value, fallback) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
const raw = value;
|
|
148
|
+
const connectionsRaw = Array.isArray(raw.connections) ? raw.connections : [];
|
|
149
|
+
const connections = [];
|
|
150
|
+
const seenIds = new Set();
|
|
151
|
+
for (const item of connectionsRaw) {
|
|
152
|
+
const normalized = normalizeAgentDatabaseConnection(item);
|
|
153
|
+
if (!normalized || seenIds.has(normalized.id)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
seenIds.add(normalized.id);
|
|
157
|
+
connections.push(normalized);
|
|
158
|
+
}
|
|
159
|
+
const requestedDefault = raw.defaultConnectionId === null ? null : normalizeDatabaseConnectionId(raw.defaultConnectionId);
|
|
160
|
+
const defaultConnectionId = requestedDefault && connections.some((connection) => connection.id === requestedDefault)
|
|
161
|
+
? requestedDefault
|
|
162
|
+
: connections.some((connection) => connection.id === fallback.defaultConnectionId)
|
|
163
|
+
? fallback.defaultConnectionId
|
|
164
|
+
: connections[0]?.id ?? null;
|
|
165
|
+
return {
|
|
166
|
+
defaultConnectionId,
|
|
167
|
+
connections,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function getAgentDatabaseConnectionById(config, connectionId) {
|
|
171
|
+
const normalizedId = normalizeDatabaseConnectionId(connectionId);
|
|
172
|
+
if (!normalizedId) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return config.databases.connections.find((connection) => connection.id === normalizedId) ?? null;
|
|
176
|
+
}
|
|
177
|
+
export function resolveAgentDatabaseConnectionUrl(connection) {
|
|
178
|
+
if (connection.connection.mode === "url") {
|
|
179
|
+
return connection.connection.url.trim() || null;
|
|
180
|
+
}
|
|
181
|
+
const envValue = process.env[connection.connection.urlEnv]?.trim();
|
|
182
|
+
return envValue || null;
|
|
183
|
+
}
|
|
81
184
|
export function normalizeAgentSettingsConfig(value, fallback) {
|
|
82
185
|
const base = fallback ?? createDefaultAgentSettingsConfig();
|
|
83
186
|
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -90,6 +193,7 @@ export function normalizeAgentSettingsConfig(value, fallback) {
|
|
|
90
193
|
const notion = raw.notion && typeof raw.notion === "object" ? raw.notion : {};
|
|
91
194
|
const slack = raw.slack && typeof raw.slack === "object" ? raw.slack : {};
|
|
92
195
|
const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
|
|
196
|
+
const databases = raw.databases && typeof raw.databases === "object" ? raw.databases : null;
|
|
93
197
|
return {
|
|
94
198
|
general: {
|
|
95
199
|
personality: normalizeCodexPersonality(general.personality, base.general.personality),
|
|
@@ -145,6 +249,7 @@ export function normalizeAgentSettingsConfig(value, fallback) {
|
|
|
145
249
|
enabled: typeof figma.enabled === "boolean" ? figma.enabled : base.figma.enabled,
|
|
146
250
|
apiToken: figma.apiToken === null ? null : normalizeNullableString(figma.apiToken) ?? base.figma.apiToken,
|
|
147
251
|
},
|
|
252
|
+
databases: normalizeAgentDatabaseSettings(databases, base.databases),
|
|
148
253
|
};
|
|
149
254
|
}
|
|
150
255
|
export async function readAgentSettingsConfig(args) {
|
|
@@ -275,6 +380,25 @@ export async function toAgentSettingsPublic(args) {
|
|
|
275
380
|
apiTokenMasked: figmaToken.masked,
|
|
276
381
|
apiTokenLength: figmaToken.length,
|
|
277
382
|
},
|
|
383
|
+
databases: {
|
|
384
|
+
defaultConnectionId: args.config.databases.defaultConnectionId,
|
|
385
|
+
connections: args.config.databases.connections.map((connection) => ({
|
|
386
|
+
id: connection.id,
|
|
387
|
+
description: connection.description,
|
|
388
|
+
provider: connection.provider,
|
|
389
|
+
enabled: connection.enabled,
|
|
390
|
+
readOnly: connection.readOnly,
|
|
391
|
+
connection: connection.connection.mode === "url"
|
|
392
|
+
? {
|
|
393
|
+
mode: "url",
|
|
394
|
+
url: connection.connection.url,
|
|
395
|
+
}
|
|
396
|
+
: {
|
|
397
|
+
mode: "env",
|
|
398
|
+
urlEnv: connection.connection.urlEnv,
|
|
399
|
+
},
|
|
400
|
+
})),
|
|
401
|
+
},
|
|
278
402
|
};
|
|
279
403
|
}
|
|
280
404
|
export function normalizeAgentSettingsPatch(value) {
|
package/dist/agent.js
CHANGED
|
@@ -7,7 +7,7 @@ import { handleFsRpcMessage } from "./agent-fs-rpc.js";
|
|
|
7
7
|
import { handleGitRpcMessage } from "./agent-git-rpc.js";
|
|
8
8
|
import { subscribeToCodexAuthRpc } from "./agent-codex-auth-rpc.js";
|
|
9
9
|
import { subscribeToDaemonRpc } from "./agent-daemon-rpc.js";
|
|
10
|
-
import { buildDaemonMcpConfigArgs, buildManagedCodexArgs, createLocalCodexCliTools, normalizeCodexModel, normalizeShellRpcCodexAuthBundle, spawnManagedCodexCommand, } from "./agent-codex-cli.js";
|
|
10
|
+
import { buildDaemonMcpConfigArgs, buildDatabaseMcpConfigArgs, buildManagedCodexArgs, createLocalCodexCliTools, normalizeCodexModel, normalizeShellRpcCodexAuthBundle, spawnManagedCodexCommand, } from "./agent-codex-cli.js";
|
|
11
11
|
import { connectBootstrapWithRetry } from "./agent-jetstream.js";
|
|
12
12
|
import { prepareCommandExecution } from "./agent-run-execution.js";
|
|
13
13
|
import { attachManagedRunProcessLifecycle, createPendingRunSessionTracker } from "./agent-run-lifecycle.js";
|
|
@@ -274,10 +274,16 @@ async function handleRunRpcMessage(args) {
|
|
|
274
274
|
model: request.model,
|
|
275
275
|
personality: localAgentSettings.general.personality,
|
|
276
276
|
modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath(workspaceRoot) : null,
|
|
277
|
-
configOverrides:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
configOverrides: [
|
|
278
|
+
...buildDaemonMcpConfigArgs({
|
|
279
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
280
|
+
workspaceRoot,
|
|
281
|
+
}),
|
|
282
|
+
...buildDatabaseMcpConfigArgs({
|
|
283
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
284
|
+
workspaceRoot,
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
281
287
|
}),
|
|
282
288
|
cwd: request.cwd,
|
|
283
289
|
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
const DEFAULT_HEARTBEAT_MS = 15_000;
|
|
4
3
|
function readRequiredEnv(name) {
|
|
5
4
|
const value = process.env[name]?.trim() || "";
|
|
6
5
|
if (!value) {
|
|
@@ -8,17 +7,6 @@ function readRequiredEnv(name) {
|
|
|
8
7
|
}
|
|
9
8
|
return value;
|
|
10
9
|
}
|
|
11
|
-
function readHeartbeatIntervalMs() {
|
|
12
|
-
const raw = process.env.DOER_DAEMON_HEARTBEAT_MS?.trim();
|
|
13
|
-
if (!raw) {
|
|
14
|
-
return DEFAULT_HEARTBEAT_MS;
|
|
15
|
-
}
|
|
16
|
-
const numeric = Number(raw);
|
|
17
|
-
if (!Number.isFinite(numeric) || numeric < 1_000) {
|
|
18
|
-
return DEFAULT_HEARTBEAT_MS;
|
|
19
|
-
}
|
|
20
|
-
return Math.floor(numeric);
|
|
21
|
-
}
|
|
22
10
|
async function readState(statePath) {
|
|
23
11
|
const raw = await readFile(statePath, "utf8");
|
|
24
12
|
return JSON.parse(raw);
|
|
@@ -37,14 +25,13 @@ async function appendEvent(eventsPath, event) {
|
|
|
37
25
|
};
|
|
38
26
|
await appendFile(eventsPath, `${JSON.stringify(row)}\n`, "utf8");
|
|
39
27
|
}
|
|
40
|
-
function attachLineLogger(stream, type, eventsPath, pid
|
|
28
|
+
function attachLineLogger(stream, type, eventsPath, pid) {
|
|
41
29
|
if (!stream) {
|
|
42
30
|
return;
|
|
43
31
|
}
|
|
44
32
|
stream.setEncoding("utf8");
|
|
45
33
|
let pending = "";
|
|
46
34
|
stream.on("data", (chunk) => {
|
|
47
|
-
onActivity?.();
|
|
48
35
|
pending += chunk;
|
|
49
36
|
const lines = pending.split(/\r\n|\n|\r/);
|
|
50
37
|
pending = lines.pop() ?? "";
|
|
@@ -74,7 +61,6 @@ async function main() {
|
|
|
74
61
|
const command = readRequiredEnv("DOER_DAEMON_COMMAND");
|
|
75
62
|
const cwd = readRequiredEnv("DOER_DAEMON_CWD");
|
|
76
63
|
const shellPath = readRequiredEnv("DOER_DAEMON_SHELL_PATH");
|
|
77
|
-
const heartbeatIntervalMs = readHeartbeatIntervalMs();
|
|
78
64
|
const childEnv = { ...process.env };
|
|
79
65
|
delete childEnv.DOER_DAEMON_STATE_PATH;
|
|
80
66
|
delete childEnv.DOER_DAEMON_EVENTS_PATH;
|
|
@@ -108,28 +94,8 @@ async function main() {
|
|
|
108
94
|
type: "start",
|
|
109
95
|
pid: child.pid,
|
|
110
96
|
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
lastActivityAt = Date.now();
|
|
114
|
-
};
|
|
115
|
-
attachLineLogger(child.stdout, "stdout", eventsPath, child.pid, markActivity);
|
|
116
|
-
attachLineLogger(child.stderr, "stderr", eventsPath, child.pid, markActivity);
|
|
117
|
-
const heartbeatTimer = setInterval(() => {
|
|
118
|
-
if (child.exitCode !== null || child.killed) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const idleMs = Date.now() - lastActivityAt;
|
|
122
|
-
if (idleMs < heartbeatIntervalMs) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
lastActivityAt = Date.now();
|
|
126
|
-
void appendEvent(eventsPath, {
|
|
127
|
-
type: "heartbeat",
|
|
128
|
-
pid: child.pid,
|
|
129
|
-
text: `[doer-daemon] heartbeat: process still running without new output for ${Math.max(1, Math.round(idleMs / 1000))}s`,
|
|
130
|
-
});
|
|
131
|
-
}, heartbeatIntervalMs);
|
|
132
|
-
heartbeatTimer.unref?.();
|
|
97
|
+
attachLineLogger(child.stdout, "stdout", eventsPath, child.pid);
|
|
98
|
+
attachLineLogger(child.stderr, "stderr", eventsPath, child.pid);
|
|
133
99
|
const forwardSignal = (signal) => {
|
|
134
100
|
if (child.exitCode !== null || child.killed) {
|
|
135
101
|
return;
|
|
@@ -156,7 +122,6 @@ async function main() {
|
|
|
156
122
|
child.once("error", reject);
|
|
157
123
|
child.once("exit", async (code, signal) => {
|
|
158
124
|
try {
|
|
159
|
-
clearInterval(heartbeatTimer);
|
|
160
125
|
const latest = await readState(statePath);
|
|
161
126
|
await writeState(statePath, {
|
|
162
127
|
...latest,
|
|
@@ -175,7 +140,6 @@ async function main() {
|
|
|
175
140
|
resolve();
|
|
176
141
|
}
|
|
177
142
|
catch (error) {
|
|
178
|
-
clearInterval(heartbeatTimer);
|
|
179
143
|
reject(error);
|
|
180
144
|
}
|
|
181
145
|
});
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { createPool } from "mysql2/promise";
|
|
5
|
+
import { Pool as PostgresPool } from "pg";
|
|
6
|
+
import * as z from "zod/v4";
|
|
7
|
+
import { getAgentDatabaseConnectionById, readAgentSettingsConfig, resolveAgentDatabaseConnectionUrl, } from "./agent-settings.js";
|
|
8
|
+
const DEFAULT_ROW_LIMIT = 200;
|
|
9
|
+
const MAX_ROW_LIMIT = 1000;
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
11
|
+
const MAX_TIMEOUT_MS = 30_000;
|
|
12
|
+
const postgresPoolByKey = new Map();
|
|
13
|
+
const mysqlPoolByKey = new Map();
|
|
14
|
+
function parseWorkspaceRoot(argv) {
|
|
15
|
+
const flagIndex = argv.findIndex((token) => token === "--workspace-root");
|
|
16
|
+
const flagValue = flagIndex >= 0 ? argv[flagIndex + 1] : "";
|
|
17
|
+
const envValue = process.env.DOER_DB_WORKSPACE_ROOT?.trim() || process.env.WORKSPACE?.trim() || process.cwd();
|
|
18
|
+
return path.resolve((flagValue || envValue || process.cwd()).trim());
|
|
19
|
+
}
|
|
20
|
+
function formatJson(value) {
|
|
21
|
+
return JSON.stringify(value, null, 2);
|
|
22
|
+
}
|
|
23
|
+
function clampNumber(value, fallback, max) {
|
|
24
|
+
if (!Number.isFinite(value)) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
return Math.max(1, Math.min(max, Math.trunc(value)));
|
|
28
|
+
}
|
|
29
|
+
function normalizeSql(sql) {
|
|
30
|
+
const trimmed = sql.trim().replace(/;+$/g, "").trim();
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
throw new Error("sql is required");
|
|
33
|
+
}
|
|
34
|
+
if (trimmed.includes(";")) {
|
|
35
|
+
throw new Error("Multiple SQL statements are not supported");
|
|
36
|
+
}
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
function requireConnection(args) {
|
|
40
|
+
const enabledConnections = args.settings.databases.connections.filter((connection) => connection.enabled);
|
|
41
|
+
const requestedId = args.connectionId?.trim() || args.settings.databases.defaultConnectionId || enabledConnections[0]?.id || "";
|
|
42
|
+
const connection = getAgentDatabaseConnectionById(args.settings, requestedId);
|
|
43
|
+
if (!connection || !connection.enabled) {
|
|
44
|
+
throw new Error(requestedId
|
|
45
|
+
? `Database connection not found or disabled: ${requestedId}`
|
|
46
|
+
: "No enabled database connections are configured");
|
|
47
|
+
}
|
|
48
|
+
return connection;
|
|
49
|
+
}
|
|
50
|
+
function requireConnectionUrl(connection) {
|
|
51
|
+
const url = resolveAgentDatabaseConnectionUrl(connection);
|
|
52
|
+
if (!url) {
|
|
53
|
+
if (connection.connection.mode === "env") {
|
|
54
|
+
throw new Error(`Database URL env is missing: ${connection.connection.urlEnv}`);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Database URL is missing for connection: ${connection.id}`);
|
|
57
|
+
}
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
function getPostgresPool(connection) {
|
|
61
|
+
const connectionUrl = requireConnectionUrl(connection);
|
|
62
|
+
const key = `${connection.provider}:${connection.id}:${connectionUrl}`;
|
|
63
|
+
const existing = postgresPoolByKey.get(key);
|
|
64
|
+
if (existing) {
|
|
65
|
+
return existing;
|
|
66
|
+
}
|
|
67
|
+
const pool = new PostgresPool({
|
|
68
|
+
connectionString: connectionUrl,
|
|
69
|
+
max: 4,
|
|
70
|
+
});
|
|
71
|
+
postgresPoolByKey.set(key, pool);
|
|
72
|
+
return pool;
|
|
73
|
+
}
|
|
74
|
+
function getMysqlPool(connection) {
|
|
75
|
+
const connectionUrl = requireConnectionUrl(connection);
|
|
76
|
+
const key = `${connection.provider}:${connection.id}:${connectionUrl}`;
|
|
77
|
+
const existing = mysqlPoolByKey.get(key);
|
|
78
|
+
if (existing) {
|
|
79
|
+
return existing;
|
|
80
|
+
}
|
|
81
|
+
const pool = createPool({
|
|
82
|
+
uri: connectionUrl,
|
|
83
|
+
connectionLimit: 4,
|
|
84
|
+
namedPlaceholders: false,
|
|
85
|
+
multipleStatements: false,
|
|
86
|
+
});
|
|
87
|
+
mysqlPoolByKey.set(key, pool);
|
|
88
|
+
return pool;
|
|
89
|
+
}
|
|
90
|
+
function serializeRow(row) {
|
|
91
|
+
return Object.fromEntries(Object.entries(row));
|
|
92
|
+
}
|
|
93
|
+
async function runPostgresSql(args) {
|
|
94
|
+
const pool = getPostgresPool(args.connection);
|
|
95
|
+
const client = await pool.connect();
|
|
96
|
+
const rowLimit = clampNumber(args.rowLimit, DEFAULT_ROW_LIMIT, MAX_ROW_LIMIT);
|
|
97
|
+
const timeoutMs = clampNumber(args.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
98
|
+
let inTransaction = false;
|
|
99
|
+
try {
|
|
100
|
+
await client.query("BEGIN");
|
|
101
|
+
inTransaction = true;
|
|
102
|
+
await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
|
|
103
|
+
if (args.connection.readOnly) {
|
|
104
|
+
await client.query("SET TRANSACTION READ ONLY");
|
|
105
|
+
}
|
|
106
|
+
const result = await client.query(normalizeSql(args.sql));
|
|
107
|
+
await client.query("COMMIT");
|
|
108
|
+
inTransaction = false;
|
|
109
|
+
return {
|
|
110
|
+
connectionId: args.connection.id,
|
|
111
|
+
rowCount: result.rowCount ?? result.rows.length,
|
|
112
|
+
rows: result.rows.slice(0, rowLimit).map((row) => serializeRow(row)),
|
|
113
|
+
fields: result.fields.map((field) => field.name),
|
|
114
|
+
truncated: result.rows.length > rowLimit,
|
|
115
|
+
readOnly: args.connection.readOnly,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (inTransaction) {
|
|
120
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
client.release();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function runMysqlSql(args) {
|
|
129
|
+
const pool = getMysqlPool(args.connection);
|
|
130
|
+
const client = await pool.getConnection();
|
|
131
|
+
const rowLimit = clampNumber(args.rowLimit, DEFAULT_ROW_LIMIT, MAX_ROW_LIMIT);
|
|
132
|
+
const timeoutMs = clampNumber(args.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
133
|
+
let inTransaction = false;
|
|
134
|
+
try {
|
|
135
|
+
await client.query(args.connection.readOnly ? "START TRANSACTION READ ONLY" : "START TRANSACTION");
|
|
136
|
+
inTransaction = true;
|
|
137
|
+
const [rows, fields] = await client.query({
|
|
138
|
+
sql: normalizeSql(args.sql),
|
|
139
|
+
timeout: timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
await client.query("COMMIT");
|
|
142
|
+
inTransaction = false;
|
|
143
|
+
if (Array.isArray(rows)) {
|
|
144
|
+
const rowPackets = rows;
|
|
145
|
+
const fieldPackets = Array.isArray(fields) ? fields : [];
|
|
146
|
+
return {
|
|
147
|
+
connectionId: args.connection.id,
|
|
148
|
+
rowCount: rowPackets.length,
|
|
149
|
+
rows: rowPackets.slice(0, rowLimit).map((row) => serializeRow(row)),
|
|
150
|
+
fields: fieldPackets.map((field) => field.name),
|
|
151
|
+
truncated: rowPackets.length > rowLimit,
|
|
152
|
+
readOnly: args.connection.readOnly,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const result = rows;
|
|
156
|
+
return {
|
|
157
|
+
connectionId: args.connection.id,
|
|
158
|
+
rowCount: typeof result.affectedRows === "number" ? result.affectedRows : 0,
|
|
159
|
+
rows: [],
|
|
160
|
+
fields: [],
|
|
161
|
+
truncated: false,
|
|
162
|
+
readOnly: args.connection.readOnly,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (inTransaction) {
|
|
167
|
+
await client.query("ROLLBACK").catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
client.release();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function runSql(args) {
|
|
176
|
+
if (args.connection.provider === "mysql") {
|
|
177
|
+
return runMysqlSql(args);
|
|
178
|
+
}
|
|
179
|
+
return runPostgresSql(args);
|
|
180
|
+
}
|
|
181
|
+
async function listTables(connection, schema) {
|
|
182
|
+
const normalizedSchema = schema?.trim();
|
|
183
|
+
if (connection.provider === "mysql") {
|
|
184
|
+
const pool = getMysqlPool(connection);
|
|
185
|
+
const [rows] = normalizedSchema
|
|
186
|
+
? await pool.query(`
|
|
187
|
+
SELECT table_schema, table_name
|
|
188
|
+
FROM information_schema.tables
|
|
189
|
+
WHERE table_schema = ? AND table_type = 'BASE TABLE'
|
|
190
|
+
ORDER BY table_schema, table_name
|
|
191
|
+
`, [normalizedSchema])
|
|
192
|
+
: await pool.query(`
|
|
193
|
+
SELECT table_schema, table_name
|
|
194
|
+
FROM information_schema.tables
|
|
195
|
+
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
|
|
196
|
+
ORDER BY table_schema, table_name
|
|
197
|
+
`);
|
|
198
|
+
return rows.map((row) => ({
|
|
199
|
+
schema: String(row.table_schema),
|
|
200
|
+
name: String(row.table_name),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
const pool = getPostgresPool(connection);
|
|
204
|
+
const result = normalizedSchema
|
|
205
|
+
? await pool.query(`
|
|
206
|
+
SELECT table_schema, table_name
|
|
207
|
+
FROM information_schema.tables
|
|
208
|
+
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
|
|
209
|
+
ORDER BY table_schema, table_name
|
|
210
|
+
`, [normalizedSchema])
|
|
211
|
+
: await pool.query(`
|
|
212
|
+
SELECT table_schema, table_name
|
|
213
|
+
FROM information_schema.tables
|
|
214
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
215
|
+
AND table_type = 'BASE TABLE'
|
|
216
|
+
ORDER BY table_schema, table_name
|
|
217
|
+
`);
|
|
218
|
+
return result.rows.map((row) => ({
|
|
219
|
+
schema: String(row.table_schema),
|
|
220
|
+
name: String(row.table_name),
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
async function describeTable(args) {
|
|
224
|
+
const schema = args.schema.trim();
|
|
225
|
+
const table = args.table.trim();
|
|
226
|
+
if (args.connection.provider === "mysql") {
|
|
227
|
+
const pool = getMysqlPool(args.connection);
|
|
228
|
+
const [rows] = await pool.query(`
|
|
229
|
+
SELECT column_name, column_type, is_nullable, column_default
|
|
230
|
+
FROM information_schema.columns
|
|
231
|
+
WHERE table_schema = ? AND table_name = ?
|
|
232
|
+
ORDER BY ordinal_position
|
|
233
|
+
`, [schema, table]);
|
|
234
|
+
return rows.map((row) => ({
|
|
235
|
+
name: String(row.column_name),
|
|
236
|
+
dataType: String(row.column_type),
|
|
237
|
+
nullable: String(row.is_nullable) === "YES",
|
|
238
|
+
defaultValue: row.column_default === null ? null : String(row.column_default),
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
const pool = getPostgresPool(args.connection);
|
|
242
|
+
const result = await pool.query(`
|
|
243
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
244
|
+
FROM information_schema.columns
|
|
245
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
246
|
+
ORDER BY ordinal_position
|
|
247
|
+
`, [schema, table]);
|
|
248
|
+
return result.rows.map((row) => ({
|
|
249
|
+
name: String(row.column_name),
|
|
250
|
+
dataType: String(row.data_type),
|
|
251
|
+
nullable: String(row.is_nullable) === "YES",
|
|
252
|
+
defaultValue: row.column_default === null ? null : String(row.column_default),
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
async function main() {
|
|
256
|
+
const workspaceRoot = parseWorkspaceRoot(process.argv.slice(2));
|
|
257
|
+
const server = new McpServer({
|
|
258
|
+
name: "doer-database",
|
|
259
|
+
version: "0.1.0",
|
|
260
|
+
}, {
|
|
261
|
+
capabilities: {
|
|
262
|
+
tools: {},
|
|
263
|
+
},
|
|
264
|
+
instructions: "Inspect configured PostgreSQL or MySQL databases and run SQL queries through named connections.",
|
|
265
|
+
});
|
|
266
|
+
server.registerTool("db_list_connections", {
|
|
267
|
+
description: "List configured database connections available to the local agent.",
|
|
268
|
+
inputSchema: {},
|
|
269
|
+
}, async () => {
|
|
270
|
+
const settings = await readAgentSettingsConfig({ workspaceRoot });
|
|
271
|
+
const connections = settings.databases.connections
|
|
272
|
+
.filter((connection) => connection.enabled)
|
|
273
|
+
.map((connection) => ({
|
|
274
|
+
id: connection.id,
|
|
275
|
+
description: connection.description,
|
|
276
|
+
provider: connection.provider,
|
|
277
|
+
readOnly: connection.readOnly,
|
|
278
|
+
default: settings.databases.defaultConnectionId === connection.id,
|
|
279
|
+
configuredVia: connection.connection.mode,
|
|
280
|
+
urlResolved: Boolean(resolveAgentDatabaseConnectionUrl(connection)),
|
|
281
|
+
}));
|
|
282
|
+
return {
|
|
283
|
+
content: [
|
|
284
|
+
{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: formatJson({ connections }),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
structuredContent: { connections },
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
server.registerTool("db_list_tables", {
|
|
293
|
+
description: "List tables for a configured database connection.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
connectionId: z.string().optional().describe("Database connection id. Defaults to the configured default connection."),
|
|
296
|
+
schema: z.string().optional().describe("Optional schema or database name to filter by."),
|
|
297
|
+
},
|
|
298
|
+
}, async ({ connectionId, schema }) => {
|
|
299
|
+
const settings = await readAgentSettingsConfig({ workspaceRoot });
|
|
300
|
+
const connection = requireConnection({ settings, connectionId });
|
|
301
|
+
const tables = await listTables(connection, schema);
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: formatJson({ connectionId: connection.id, provider: connection.provider, tables }),
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
structuredContent: { connectionId: connection.id, provider: connection.provider, tables },
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
server.registerTool("db_describe_table", {
|
|
313
|
+
description: "Describe the columns for a specific table.",
|
|
314
|
+
inputSchema: {
|
|
315
|
+
connectionId: z.string().optional().describe("Database connection id. Defaults to the configured default connection."),
|
|
316
|
+
schema: z.string().min(1).describe("Schema name for PostgreSQL, or database name for MySQL."),
|
|
317
|
+
table: z.string().min(1).describe("Table name."),
|
|
318
|
+
},
|
|
319
|
+
}, async ({ connectionId, schema, table }) => {
|
|
320
|
+
const settings = await readAgentSettingsConfig({ workspaceRoot });
|
|
321
|
+
const connection = requireConnection({ settings, connectionId });
|
|
322
|
+
const columns = await describeTable({
|
|
323
|
+
connection,
|
|
324
|
+
schema,
|
|
325
|
+
table,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: formatJson({ connectionId: connection.id, provider: connection.provider, schema: schema.trim(), table: table.trim(), columns }),
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
structuredContent: {
|
|
335
|
+
connectionId: connection.id,
|
|
336
|
+
provider: connection.provider,
|
|
337
|
+
schema: schema.trim(),
|
|
338
|
+
table: table.trim(),
|
|
339
|
+
columns,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
server.registerTool("db_query", {
|
|
344
|
+
description: "Run a SQL query through a configured database connection.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
connectionId: z.string().optional().describe("Database connection id. Defaults to the configured default connection."),
|
|
347
|
+
sql: z.string().min(1).describe("A single SQL statement to execute."),
|
|
348
|
+
rowLimit: z.number().int().min(1).max(MAX_ROW_LIMIT).optional().describe("Maximum number of rows to return."),
|
|
349
|
+
timeoutMs: z.number().int().min(1).max(MAX_TIMEOUT_MS).optional().describe("Statement timeout in milliseconds."),
|
|
350
|
+
},
|
|
351
|
+
}, async ({ connectionId, sql, rowLimit, timeoutMs }) => {
|
|
352
|
+
const settings = await readAgentSettingsConfig({ workspaceRoot });
|
|
353
|
+
const connection = requireConnection({ settings, connectionId });
|
|
354
|
+
const result = await runSql({
|
|
355
|
+
connection,
|
|
356
|
+
sql,
|
|
357
|
+
rowLimit,
|
|
358
|
+
timeoutMs,
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: formatJson({ ...result, provider: connection.provider }),
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
structuredContent: { ...result, provider: connection.provider },
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const transport = new StdioServerTransport();
|
|
371
|
+
await server.connect(transport);
|
|
372
|
+
}
|
|
373
|
+
main().catch((error) => {
|
|
374
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
375
|
+
process.stderr.write(`${message}\n`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Reverse-polling agent runtime for doer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/agent.js",
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
28
28
|
"@openai/codex-sdk": "^0.115.0",
|
|
29
|
+
"mysql2": "^3.22.2",
|
|
29
30
|
"nats": "^2.29.3",
|
|
31
|
+
"pg": "^8.16.3",
|
|
30
32
|
"tar": "^7.5.13"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@types/node": "^20",
|
|
36
|
+
"@types/pg": "^8.15.5",
|
|
34
37
|
"tsx": "^4.21.0",
|
|
35
38
|
"typescript": "^5"
|
|
36
39
|
}
|