doer-agent 0.7.6 → 0.7.7

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.
@@ -1,12 +1,16 @@
1
- import { readFile, writeFile, mkdir } from "node:fs/promises";
1
+ import { appendFile, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { StringCodec } from "nats";
4
4
  const proxyRpcCodec = StringCodec();
5
5
  const PROXY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
6
6
  const MAX_PROXY_BODY_BYTES = 5 * 1024 * 1024;
7
+ const MAX_PROXY_LOGS = 100;
7
8
  function getProxyRegistryPath(workspaceRoot) {
8
9
  return path.join(workspaceRoot, ".doer-agent", "http-proxies.json");
9
10
  }
11
+ function getProxyLogsPath(workspaceRoot, proxyId) {
12
+ return path.join(workspaceRoot, ".doer-agent", "http-proxy-logs", `${proxyId}.jsonl`);
13
+ }
10
14
  function slugify(value) {
11
15
  const slug = value
12
16
  .trim()
@@ -91,10 +95,39 @@ function normalizeProxyRecord(value) {
91
95
  name: normalizeName(row.name),
92
96
  host,
93
97
  port,
98
+ enabled: row.enabled !== false,
94
99
  createdAt,
95
100
  updatedAt,
96
101
  };
97
102
  }
103
+ function normalizeProxyLogRecord(value) {
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
105
+ return null;
106
+ }
107
+ const row = value;
108
+ const id = typeof row.id === "string" && row.id.trim() ? row.id.trim() : "";
109
+ const at = typeof row.at === "string" && row.at.trim() ? row.at.trim() : "";
110
+ const method = typeof row.method === "string" && row.method.trim() ? row.method.trim().toUpperCase() : "";
111
+ const requestPath = typeof row.path === "string" && row.path.trim() ? row.path.trim() : "/";
112
+ const status = typeof row.status === "number" && Number.isInteger(row.status) ? row.status : null;
113
+ const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) ? Math.max(0, Math.round(row.durationMs)) : 0;
114
+ const requestBytes = typeof row.requestBytes === "number" && Number.isFinite(row.requestBytes) ? Math.max(0, Math.round(row.requestBytes)) : 0;
115
+ const responseBytes = typeof row.responseBytes === "number" && Number.isFinite(row.responseBytes) ? Math.max(0, Math.round(row.responseBytes)) : 0;
116
+ if (!id || !at || !method) {
117
+ return null;
118
+ }
119
+ return {
120
+ id,
121
+ at,
122
+ method,
123
+ path: requestPath,
124
+ status,
125
+ durationMs,
126
+ requestBytes,
127
+ responseBytes,
128
+ error: typeof row.error === "string" && row.error.trim() ? row.error.trim().slice(0, 500) : null,
129
+ };
130
+ }
98
131
  async function readProxyRegistry(workspaceRoot) {
99
132
  const raw = await readFile(getProxyRegistryPath(workspaceRoot), "utf8").catch(() => "");
100
133
  if (!raw) {
@@ -118,6 +151,33 @@ async function writeProxyRegistry(workspaceRoot, proxies) {
118
151
  await mkdir(path.dirname(registryPath), { recursive: true });
119
152
  await writeFile(registryPath, `${JSON.stringify({ proxies }, null, 2)}\n`, "utf8");
120
153
  }
154
+ function normalizeLimit(value, fallback) {
155
+ const numeric = Number(value);
156
+ if (!Number.isFinite(numeric)) {
157
+ return fallback;
158
+ }
159
+ return Math.max(1, Math.min(Math.floor(numeric), 1000));
160
+ }
161
+ async function readProxyLogs(workspaceRoot, proxyId, limit) {
162
+ const raw = await readFile(getProxyLogsPath(workspaceRoot, proxyId), "utf8").catch(() => "");
163
+ if (!raw) {
164
+ return [];
165
+ }
166
+ return raw
167
+ .split("\n")
168
+ .map((line) => line.trim())
169
+ .filter(Boolean)
170
+ .slice(-limit)
171
+ .map((line) => {
172
+ try {
173
+ return normalizeProxyLogRecord(JSON.parse(line));
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ })
179
+ .filter((log) => Boolean(log));
180
+ }
121
181
  async function createProxy(workspaceRoot, request) {
122
182
  const proxies = await readProxyRegistry(workspaceRoot);
123
183
  const name = normalizeName(request.name);
@@ -136,54 +196,132 @@ async function createProxy(workspaceRoot, request) {
136
196
  name,
137
197
  host,
138
198
  port,
199
+ enabled: request.enabled !== false,
139
200
  createdAt: now,
140
201
  updatedAt: now,
141
202
  };
142
203
  await writeProxyRegistry(workspaceRoot, [...proxies, proxy].sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
143
204
  return proxy;
144
205
  }
206
+ async function updateProxy(workspaceRoot, request) {
207
+ const proxyId = normalizeProxyId(request.proxyId);
208
+ const proxies = await readProxyRegistry(workspaceRoot);
209
+ const proxy = proxies.find((item) => item.id === proxyId);
210
+ if (!proxy) {
211
+ throw new Error("proxy not found");
212
+ }
213
+ const updated = {
214
+ ...proxy,
215
+ name: request.name === undefined ? proxy.name : normalizeName(request.name),
216
+ host: request.host === undefined ? proxy.host : normalizeHost(request.host),
217
+ port: request.port === undefined ? proxy.port : normalizePort(request.port),
218
+ enabled: request.enabled === undefined ? proxy.enabled : request.enabled !== false,
219
+ updatedAt: new Date().toISOString(),
220
+ };
221
+ await writeProxyRegistry(workspaceRoot, proxies.map((item) => (item.id === proxyId ? updated : item)));
222
+ return updated;
223
+ }
145
224
  async function deleteProxy(workspaceRoot, proxyId) {
146
225
  const proxies = await readProxyRegistry(workspaceRoot);
147
226
  await writeProxyRegistry(workspaceRoot, proxies.filter((proxy) => proxy.id !== proxyId));
227
+ await unlink(getProxyLogsPath(workspaceRoot, proxyId)).catch(() => undefined);
148
228
  }
149
- async function handleProxyFetch(workspaceRoot, request) {
229
+ async function appendProxyLog(workspaceRoot, proxyId, log) {
230
+ const entry = {
231
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
232
+ at: new Date().toISOString(),
233
+ ...log,
234
+ };
235
+ const logsPath = getProxyLogsPath(workspaceRoot, proxyId);
236
+ await mkdir(path.dirname(logsPath), { recursive: true });
237
+ await appendFile(logsPath, `${JSON.stringify(entry)}\n`, "utf8");
238
+ }
239
+ async function readProxyLogsForRequest(workspaceRoot, request) {
150
240
  const proxyId = normalizeProxyId(request.proxyId);
151
241
  const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
152
242
  if (!proxy) {
153
243
  throw new Error("proxy not found");
154
244
  }
245
+ return {
246
+ proxy,
247
+ events: await readProxyLogs(workspaceRoot, proxyId, normalizeLimit(request.limit, MAX_PROXY_LOGS)),
248
+ };
249
+ }
250
+ async function handleProxyFetch(workspaceRoot, request) {
251
+ const proxyId = normalizeProxyId(request.proxyId);
252
+ const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
253
+ const startedAt = Date.now();
155
254
  const method = normalizeMethod(request.method);
156
255
  const requestPath = normalizePath(request.path);
157
- const headers = normalizeHeaders(request.headers);
158
256
  const bodyBase64 = typeof request.bodyBase64 === "string" ? request.bodyBase64 : "";
159
257
  const body = bodyBase64 ? Buffer.from(bodyBase64, "base64") : undefined;
258
+ const requestBytes = body?.byteLength ?? 0;
259
+ let status = null;
260
+ let responseBytes = 0;
261
+ let errorMessage = null;
262
+ const finishLog = async () => {
263
+ await appendProxyLog(workspaceRoot, proxyId, {
264
+ method,
265
+ path: requestPath,
266
+ status,
267
+ durationMs: Date.now() - startedAt,
268
+ requestBytes,
269
+ responseBytes,
270
+ error: errorMessage,
271
+ }).catch(() => undefined);
272
+ };
273
+ if (!proxy) {
274
+ throw new Error("proxy not found");
275
+ }
276
+ if (!proxy.enabled) {
277
+ errorMessage = "proxy disabled";
278
+ await finishLog();
279
+ throw new Error("proxy disabled");
280
+ }
281
+ const headers = normalizeHeaders(request.headers);
160
282
  if ((body?.byteLength ?? 0) > MAX_PROXY_BODY_BYTES) {
283
+ errorMessage = "proxy request body too large";
284
+ await finishLog();
161
285
  throw new Error("proxy request body too large");
162
286
  }
163
- const url = new URL(requestPath, `http://${proxy.host}:${proxy.port}`);
164
- const response = await fetch(url, {
165
- method,
166
- headers,
167
- body: method === "GET" || method === "HEAD" ? undefined : body,
168
- redirect: "manual",
169
- });
170
- const responseBuffer = Buffer.from(await response.arrayBuffer());
171
- if (responseBuffer.byteLength > MAX_PROXY_BODY_BYTES) {
172
- throw new Error("proxy response body too large");
287
+ try {
288
+ const url = new URL(requestPath, `http://${proxy.host}:${proxy.port}`);
289
+ const response = await fetch(url, {
290
+ method,
291
+ headers,
292
+ body: method === "GET" || method === "HEAD" ? undefined : body,
293
+ redirect: "manual",
294
+ });
295
+ status = response.status;
296
+ const responseBuffer = Buffer.from(await response.arrayBuffer());
297
+ responseBytes = responseBuffer.byteLength;
298
+ if (responseBuffer.byteLength > MAX_PROXY_BODY_BYTES) {
299
+ errorMessage = "proxy response body too large";
300
+ await finishLog();
301
+ throw new Error("proxy response body too large");
302
+ }
303
+ const responseHeaders = {};
304
+ response.headers.forEach((value, key) => {
305
+ responseHeaders[key] = value;
306
+ });
307
+ await finishLog();
308
+ return {
309
+ status: response.status,
310
+ statusText: response.statusText,
311
+ headers: responseHeaders,
312
+ bodyBase64: responseBuffer.toString("base64"),
313
+ };
314
+ }
315
+ catch (error) {
316
+ if (!errorMessage) {
317
+ errorMessage = error instanceof Error ? error.message : String(error);
318
+ await finishLog();
319
+ }
320
+ throw error;
173
321
  }
174
- const responseHeaders = {};
175
- response.headers.forEach((value, key) => {
176
- responseHeaders[key] = value;
177
- });
178
- return {
179
- status: response.status,
180
- statusText: response.statusText,
181
- headers: responseHeaders,
182
- bodyBase64: responseBuffer.toString("base64"),
183
- };
184
322
  }
185
323
  async function executeProxyRpc(args) {
186
- const action = args.request.action === "create" || args.request.action === "delete" || args.request.action === "handle"
324
+ const action = args.request.action === "create" || args.request.action === "update" || args.request.action === "delete" || args.request.action === "logs" || args.request.action === "handle"
187
325
  ? args.request.action
188
326
  : "list";
189
327
  if (action === "list") {
@@ -192,10 +330,16 @@ async function executeProxyRpc(args) {
192
330
  if (action === "create") {
193
331
  return { ok: true, action, proxy: await createProxy(args.workspaceRoot, args.request) };
194
332
  }
333
+ if (action === "update") {
334
+ return { ok: true, action, proxy: await updateProxy(args.workspaceRoot, args.request) };
335
+ }
195
336
  if (action === "delete") {
196
337
  await deleteProxy(args.workspaceRoot, normalizeProxyId(args.request.proxyId));
197
338
  return { ok: true, action };
198
339
  }
340
+ if (action === "logs") {
341
+ return { ok: true, action, ...await readProxyLogsForRequest(args.workspaceRoot, args.request) };
342
+ }
199
343
  return { ok: true, action, response: await handleProxyFetch(args.workspaceRoot, args.request) };
200
344
  }
201
345
  export async function handleHttpProxyRpcMessage(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",