doer-agent 0.5.0 → 0.5.2

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.
@@ -46,9 +46,29 @@ export function buildManagedCodexArgs(args) {
46
46
  ];
47
47
  }
48
48
  export function buildDaemonMcpConfigArgs(args) {
49
- const serverName = args.serverName?.trim() || "doer_daemon";
50
- const distEntry = path.join(args.agentProjectDir, "dist", "daemon-mcp-server.js");
51
- const srcEntry = path.join(args.agentProjectDir, "src", "daemon-mcp-server.ts");
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.DOER_DAEMON_WORKSPACE_ROOT=${toTomlStringLiteral(args.workspaceRoot)}`,
83
+ `mcp_servers.${serverName}.env.${args.workspaceRootEnvName}=${toTomlStringLiteral(args.workspaceRoot)}`,
64
84
  "--config",
65
85
  `mcp_servers.${serverName}.enabled=true`,
66
86
  ];
@@ -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 { normalizeAgentSettingsConfig, normalizeAgentSettingsPatch, readAgentSettingsConfig, resolveAgentSettingsFilePath, toAgentSettingsPublic, writeAgentModelInstructions, writeAgentSettingsConfig, } from "./agent-settings.js";
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" ? "update" : "get";
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") {
@@ -34,13 +34,6 @@ export function createDefaultAgentSettingsConfig() {
34
34
  oauthLogin: null,
35
35
  oauthScope: null,
36
36
  },
37
- aws: {
38
- enabled: true,
39
- accessKeyId: null,
40
- defaultRegion: null,
41
- secretAccessKey: null,
42
- sessionToken: null,
43
- },
44
37
  jira: {
45
38
  baseUrl: null,
46
39
  email: null,
@@ -63,6 +56,13 @@ export function createDefaultAgentSettingsConfig() {
63
56
  enabled: false,
64
57
  apiToken: null,
65
58
  },
59
+ env: {
60
+ variables: [],
61
+ },
62
+ databases: {
63
+ defaultConnectionId: null,
64
+ connections: [],
65
+ },
66
66
  };
67
67
  }
68
68
  function normalizeNullableString(value) {
@@ -78,6 +78,137 @@ function normalizeNullableString(value) {
78
78
  function normalizeCodexPersonality(value, fallback) {
79
79
  return value === "friendly" || value === "pragmatic" ? value : fallback;
80
80
  }
81
+ function normalizeDatabaseConnectionId(value) {
82
+ if (typeof value !== "string") {
83
+ return null;
84
+ }
85
+ const trimmed = value.trim().toLowerCase();
86
+ if (!trimmed || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(trimmed)) {
87
+ return null;
88
+ }
89
+ return trimmed;
90
+ }
91
+ function normalizeEnvVarName(value) {
92
+ if (typeof value !== "string") {
93
+ return null;
94
+ }
95
+ const trimmed = value.trim();
96
+ if (!trimmed || !/^[A-Z_][A-Z0-9_]*$/.test(trimmed)) {
97
+ return null;
98
+ }
99
+ return trimmed;
100
+ }
101
+ function normalizeAgentDatabaseProvider(value) {
102
+ return value === "mysql" ? "mysql" : "postgres";
103
+ }
104
+ function normalizeAgentDatabaseConnectionSecret(value) {
105
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
106
+ return null;
107
+ }
108
+ const raw = value;
109
+ const mode = raw.mode === "env" ? "env" : raw.mode === "url" ? "url" : null;
110
+ if (mode === "env") {
111
+ const urlEnv = normalizeEnvVarName(raw.urlEnv);
112
+ return urlEnv ? { mode, urlEnv } : null;
113
+ }
114
+ if (mode === "url") {
115
+ const url = normalizeNullableString(raw.url);
116
+ return url ? { mode, url } : null;
117
+ }
118
+ return null;
119
+ }
120
+ export function normalizeAgentDatabaseConnection(value) {
121
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
122
+ return null;
123
+ }
124
+ const raw = value;
125
+ const id = normalizeDatabaseConnectionId(raw.id);
126
+ const connection = normalizeAgentDatabaseConnectionSecret(raw.connection);
127
+ if (!id || !connection) {
128
+ return null;
129
+ }
130
+ return {
131
+ id,
132
+ description: raw.description === null ? null : normalizeNullableString(raw.description),
133
+ provider: normalizeAgentDatabaseProvider(raw.provider),
134
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
135
+ readOnly: typeof raw.readOnly === "boolean" ? raw.readOnly : true,
136
+ connection,
137
+ };
138
+ }
139
+ function normalizeAgentDatabaseSettings(value, fallback) {
140
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
141
+ return fallback;
142
+ }
143
+ const raw = value;
144
+ const connectionsRaw = Array.isArray(raw.connections) ? raw.connections : [];
145
+ const connections = [];
146
+ const seenIds = new Set();
147
+ for (const item of connectionsRaw) {
148
+ const normalized = normalizeAgentDatabaseConnection(item);
149
+ if (!normalized || seenIds.has(normalized.id)) {
150
+ continue;
151
+ }
152
+ seenIds.add(normalized.id);
153
+ connections.push(normalized);
154
+ }
155
+ const requestedDefault = raw.defaultConnectionId === null ? null : normalizeDatabaseConnectionId(raw.defaultConnectionId);
156
+ const defaultConnectionId = requestedDefault && connections.some((connection) => connection.id === requestedDefault)
157
+ ? requestedDefault
158
+ : connections.some((connection) => connection.id === fallback.defaultConnectionId)
159
+ ? fallback.defaultConnectionId
160
+ : connections[0]?.id ?? null;
161
+ return {
162
+ defaultConnectionId,
163
+ connections,
164
+ };
165
+ }
166
+ function normalizeAgentEnvironmentVariable(value) {
167
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
168
+ return null;
169
+ }
170
+ const raw = value;
171
+ const key = normalizeEnvVarName(raw.key);
172
+ if (!key || typeof raw.value !== "string") {
173
+ return null;
174
+ }
175
+ return {
176
+ key,
177
+ value: raw.value.replace(/\r/g, ""),
178
+ };
179
+ }
180
+ function normalizeAgentEnvironmentSettings(value, fallback) {
181
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
182
+ return fallback;
183
+ }
184
+ const raw = value;
185
+ const variablesRaw = Array.isArray(raw.variables) ? raw.variables : [];
186
+ const variables = [];
187
+ const seenKeys = new Set();
188
+ for (const item of variablesRaw) {
189
+ const normalized = normalizeAgentEnvironmentVariable(item);
190
+ if (!normalized || seenKeys.has(normalized.key)) {
191
+ continue;
192
+ }
193
+ seenKeys.add(normalized.key);
194
+ variables.push(normalized);
195
+ }
196
+ return { variables };
197
+ }
198
+ export function getAgentDatabaseConnectionById(config, connectionId) {
199
+ const normalizedId = normalizeDatabaseConnectionId(connectionId);
200
+ if (!normalizedId) {
201
+ return null;
202
+ }
203
+ return config.databases.connections.find((connection) => connection.id === normalizedId) ?? null;
204
+ }
205
+ export function resolveAgentDatabaseConnectionUrl(connection) {
206
+ if (connection.connection.mode === "url") {
207
+ return connection.connection.url.trim() || null;
208
+ }
209
+ const envValue = process.env[connection.connection.urlEnv]?.trim();
210
+ return envValue || null;
211
+ }
81
212
  export function normalizeAgentSettingsConfig(value, fallback) {
82
213
  const base = fallback ?? createDefaultAgentSettingsConfig();
83
214
  const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -85,11 +216,12 @@ export function normalizeAgentSettingsConfig(value, fallback) {
85
216
  const codex = raw.codex && typeof raw.codex === "object" ? raw.codex : {};
86
217
  const realtime = raw.realtime && typeof raw.realtime === "object" ? raw.realtime : {};
87
218
  const git = raw.git && typeof raw.git === "object" ? raw.git : {};
88
- const aws = raw.aws && typeof raw.aws === "object" ? raw.aws : {};
89
219
  const jira = raw.jira && typeof raw.jira === "object" ? raw.jira : {};
90
220
  const notion = raw.notion && typeof raw.notion === "object" ? raw.notion : {};
91
221
  const slack = raw.slack && typeof raw.slack === "object" ? raw.slack : {};
92
222
  const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
223
+ const env = raw.env && typeof raw.env === "object" ? raw.env : null;
224
+ const databases = raw.databases && typeof raw.databases === "object" ? raw.databases : null;
93
225
  return {
94
226
  general: {
95
227
  personality: normalizeCodexPersonality(general.personality, base.general.personality),
@@ -114,15 +246,6 @@ export function normalizeAgentSettingsConfig(value, fallback) {
114
246
  oauthLogin: git.oauthLogin === null ? null : normalizeNullableString(git.oauthLogin) ?? base.git.oauthLogin,
115
247
  oauthScope: git.oauthScope === null ? null : normalizeNullableString(git.oauthScope) ?? base.git.oauthScope,
116
248
  },
117
- aws: {
118
- enabled: typeof aws.enabled === "boolean" ? aws.enabled : base.aws.enabled,
119
- accessKeyId: aws.accessKeyId === null ? null : normalizeNullableString(aws.accessKeyId) ?? base.aws.accessKeyId,
120
- defaultRegion: aws.defaultRegion === null ? null : normalizeNullableString(aws.defaultRegion) ?? base.aws.defaultRegion,
121
- secretAccessKey: aws.secretAccessKey === null
122
- ? null
123
- : normalizeNullableString(aws.secretAccessKey) ?? base.aws.secretAccessKey,
124
- sessionToken: aws.sessionToken === null ? null : normalizeNullableString(aws.sessionToken) ?? base.aws.sessionToken,
125
- },
126
249
  jira: {
127
250
  baseUrl: jira.baseUrl === null ? null : normalizeNullableString(jira.baseUrl) ?? base.jira.baseUrl,
128
251
  email: jira.email === null ? null : normalizeNullableString(jira.email) ?? base.jira.email,
@@ -145,6 +268,8 @@ export function normalizeAgentSettingsConfig(value, fallback) {
145
268
  enabled: typeof figma.enabled === "boolean" ? figma.enabled : base.figma.enabled,
146
269
  apiToken: figma.apiToken === null ? null : normalizeNullableString(figma.apiToken) ?? base.figma.apiToken,
147
270
  },
271
+ env: normalizeAgentEnvironmentSettings(env, base.env),
272
+ databases: normalizeAgentDatabaseSettings(databases, base.databases),
148
273
  };
149
274
  }
150
275
  export async function readAgentSettingsConfig(args) {
@@ -195,8 +320,6 @@ function toMaskedSecret(value) {
195
320
  export async function toAgentSettingsPublic(args) {
196
321
  const realtimeKey = toMaskedSecret(args.config.realtime.apiKey);
197
322
  const gitOauth = toMaskedSecret(args.config.git.oauthToken);
198
- const awsSecret = toMaskedSecret(args.config.aws.secretAccessKey);
199
- const awsSession = toMaskedSecret(args.config.aws.sessionToken);
200
323
  const jiraToken = toMaskedSecret(args.config.jira.apiToken);
201
324
  const notionToken = toMaskedSecret(args.config.notion.apiToken);
202
325
  const slackToken = toMaskedSecret(args.config.slack.botToken);
@@ -234,17 +357,6 @@ export async function toAgentSettingsPublic(args) {
234
357
  oauthLogin: args.config.git.oauthLogin,
235
358
  oauthScope: args.config.git.oauthScope,
236
359
  },
237
- aws: {
238
- enabled: args.config.aws.enabled,
239
- accessKeyId: args.config.aws.accessKeyId,
240
- defaultRegion: args.config.aws.defaultRegion,
241
- hasSecretAccessKey: awsSecret.has,
242
- secretAccessKeyMasked: awsSecret.masked,
243
- secretAccessKeyLength: awsSecret.length,
244
- hasSessionToken: awsSession.has,
245
- sessionTokenMasked: awsSession.masked,
246
- sessionTokenLength: awsSession.length,
247
- },
248
360
  jira: {
249
361
  baseUrl: args.config.jira.baseUrl,
250
362
  email: args.config.jira.email,
@@ -275,6 +387,31 @@ export async function toAgentSettingsPublic(args) {
275
387
  apiTokenMasked: figmaToken.masked,
276
388
  apiTokenLength: figmaToken.length,
277
389
  },
390
+ env: {
391
+ variables: args.config.env.variables.map((variable) => ({
392
+ key: variable.key,
393
+ value: variable.value,
394
+ })),
395
+ },
396
+ databases: {
397
+ defaultConnectionId: args.config.databases.defaultConnectionId,
398
+ connections: args.config.databases.connections.map((connection) => ({
399
+ id: connection.id,
400
+ description: connection.description,
401
+ provider: connection.provider,
402
+ enabled: connection.enabled,
403
+ readOnly: connection.readOnly,
404
+ connection: connection.connection.mode === "url"
405
+ ? {
406
+ mode: "url",
407
+ url: connection.connection.url,
408
+ }
409
+ : {
410
+ mode: "env",
411
+ urlEnv: connection.connection.urlEnv,
412
+ },
413
+ })),
414
+ },
278
415
  };
279
416
  }
280
417
  export function normalizeAgentSettingsPatch(value) {
@@ -312,11 +449,6 @@ export function normalizeAgentSettingsPatch(value) {
312
449
  move("gitOauthToken", "git", "oauthToken");
313
450
  move("gitOauthLogin", "git", "oauthLogin");
314
451
  move("gitOauthScope", "git", "oauthScope");
315
- move("awsEnabled", "aws", "enabled");
316
- move("awsAccessKeyId", "aws", "accessKeyId");
317
- move("awsDefaultRegion", "aws", "defaultRegion");
318
- move("awsSecretAccessKey", "aws", "secretAccessKey");
319
- move("awsSessionToken", "aws", "sessionToken");
320
452
  move("jiraBaseUrl", "jira", "baseUrl");
321
453
  move("jiraEmail", "jira", "email");
322
454
  move("jiraEnabled", "jira", "enabled");
@@ -331,6 +463,7 @@ export function normalizeAgentSettingsPatch(value) {
331
463
  move("figmaBaseUrl", "figma", "baseUrl");
332
464
  move("figmaEnabled", "figma", "enabled");
333
465
  move("figmaApiToken", "figma", "apiToken");
466
+ move("environmentVariables", "env", "variables");
334
467
  return patch;
335
468
  }
336
469
  export function buildAgentSettingsEnvPatch(config) {
@@ -353,18 +486,6 @@ export function buildAgentSettingsEnvPatch(config) {
353
486
  if (config.git.oauthScope)
354
487
  envPatch.DOER_GIT_OAUTH_SCOPE = config.git.oauthScope;
355
488
  }
356
- if (config.aws.enabled) {
357
- if (config.aws.accessKeyId)
358
- envPatch.AWS_ACCESS_KEY_ID = config.aws.accessKeyId;
359
- if (config.aws.defaultRegion)
360
- envPatch.AWS_DEFAULT_REGION = config.aws.defaultRegion;
361
- if (config.aws.defaultRegion)
362
- envPatch.AWS_REGION = config.aws.defaultRegion;
363
- if (config.aws.secretAccessKey)
364
- envPatch.AWS_SECRET_ACCESS_KEY = config.aws.secretAccessKey;
365
- if (config.aws.sessionToken)
366
- envPatch.AWS_SESSION_TOKEN = config.aws.sessionToken;
367
- }
368
489
  if (config.jira.enabled) {
369
490
  if (config.jira.baseUrl)
370
491
  envPatch.JIRA_BASE_URL = config.jira.baseUrl;
@@ -393,5 +514,8 @@ export function buildAgentSettingsEnvPatch(config) {
393
514
  if (config.figma.apiToken)
394
515
  envPatch.FIGMA_API_TOKEN = config.figma.apiToken;
395
516
  }
517
+ for (const variable of config.env.variables) {
518
+ envPatch[variable.key] = variable.value;
519
+ }
396
520
  return envPatch;
397
521
  }
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: buildDaemonMcpConfigArgs({
278
- agentProjectDir: AGENT_PROJECT_DIR,
279
- workspaceRoot,
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,
@@ -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.5.0",
3
+ "version": "0.5.2",
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
  }