doer-agent 0.5.8 → 0.5.9
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-session-rpc.js +105 -8
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
-
import { open, readdir, realpath, rm, rmdir, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { open, mkdir, readdir, realpath, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { StringCodec } from "nats";
|
|
5
6
|
const sessionRpcCodec = StringCodec();
|
|
@@ -89,6 +90,84 @@ function buildInlineBlobMarker(value) {
|
|
|
89
90
|
}
|
|
90
91
|
return "[inline blob omitted]";
|
|
91
92
|
}
|
|
93
|
+
function extensionForImageMimeType(mimeType) {
|
|
94
|
+
const normalized = mimeType.trim().toLowerCase();
|
|
95
|
+
if (normalized === "image/jpeg" || normalized === "image/jpg")
|
|
96
|
+
return ".jpg";
|
|
97
|
+
if (normalized === "image/webp")
|
|
98
|
+
return ".webp";
|
|
99
|
+
if (normalized === "image/gif")
|
|
100
|
+
return ".gif";
|
|
101
|
+
if (normalized === "image/svg+xml")
|
|
102
|
+
return ".svg";
|
|
103
|
+
if (normalized === "image/bmp")
|
|
104
|
+
return ".bmp";
|
|
105
|
+
if (normalized === "image/avif")
|
|
106
|
+
return ".avif";
|
|
107
|
+
return ".png";
|
|
108
|
+
}
|
|
109
|
+
function decodeInlineImageDataUrl(value) {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
const match = /^data:([^;,]+)(?:;[^,]*)?;base64,([\s\S]+)$/i.exec(trimmed);
|
|
112
|
+
if (!match) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const mimeType = match[1]?.trim().toLowerCase() || "";
|
|
116
|
+
if (!mimeType.startsWith("image/")) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const base64 = match[2]?.replace(/\s/g, "") || "";
|
|
120
|
+
if (!base64) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return {
|
|
125
|
+
bytes: Buffer.from(base64, "base64"),
|
|
126
|
+
mimeType,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function sanitizeSessionPathSegment(value, fallback) {
|
|
134
|
+
return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || fallback;
|
|
135
|
+
}
|
|
136
|
+
function inferSessionIdFromSessionFilePath(filePath) {
|
|
137
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
138
|
+
const match = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)$/.exec(base);
|
|
139
|
+
const inferred = match?.[1]?.trim() || "";
|
|
140
|
+
return inferred || null;
|
|
141
|
+
}
|
|
142
|
+
async function materializeInlineSessionImage(args) {
|
|
143
|
+
const decoded = decodeInlineImageDataUrl(args.value);
|
|
144
|
+
if (!decoded || decoded.bytes.byteLength === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const inferredSessionId = inferSessionIdFromSessionFilePath(args.filePath);
|
|
148
|
+
const sessionBase = (args.sessionId ? sanitizeSessionPathSegment(args.sessionId, "") : "")
|
|
149
|
+
|| (inferredSessionId ? sanitizeSessionPathSegment(inferredSessionId, "") : "")
|
|
150
|
+
|| sanitizeSessionPathSegment(path.basename(args.filePath, path.extname(args.filePath)), "session");
|
|
151
|
+
const hash = crypto.createHash("sha256").update(decoded.bytes).digest("hex").slice(0, 16);
|
|
152
|
+
const ext = extensionForImageMimeType(decoded.mimeType);
|
|
153
|
+
const name = `line-${args.lineNumber}-${hash}${ext}`;
|
|
154
|
+
const relPath = path.posix.join(".doer-agent", "sessions", sessionBase, "outputs", name);
|
|
155
|
+
const absPath = path.resolve(args.workspaceRoot, relPath);
|
|
156
|
+
if (!(absPath === args.workspaceRoot || absPath.startsWith(args.workspaceRoot + path.sep))) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
160
|
+
const existing = await stat(absPath).catch(() => null);
|
|
161
|
+
if (!existing || !existing.isFile() || existing.size !== decoded.bytes.byteLength) {
|
|
162
|
+
await writeFile(absPath, decoded.bytes);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
relPath,
|
|
166
|
+
name,
|
|
167
|
+
size: decoded.bytes.byteLength,
|
|
168
|
+
mimeType: decoded.mimeType,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
92
171
|
function getSessionRpcPayloadByteLength(value) {
|
|
93
172
|
try {
|
|
94
173
|
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
@@ -104,12 +183,12 @@ function buildSessionRpcTruncatedMarker(label, value) {
|
|
|
104
183
|
? `[${label} truncated for session RPC pagination]`
|
|
105
184
|
: `[${label} truncated for session RPC pagination: ${byteLength} bytes omitted]`;
|
|
106
185
|
}
|
|
107
|
-
function sanitizeSessionRpcPayload(value) {
|
|
186
|
+
async function sanitizeSessionRpcPayload(value, args) {
|
|
108
187
|
if (typeof value === "string") {
|
|
109
188
|
return value;
|
|
110
189
|
}
|
|
111
190
|
if (Array.isArray(value)) {
|
|
112
|
-
return value.map((entry) => sanitizeSessionRpcPayload(entry));
|
|
191
|
+
return Promise.all(value.map((entry) => sanitizeSessionRpcPayload(entry, args)));
|
|
113
192
|
}
|
|
114
193
|
if (!isObjectRecord(value)) {
|
|
115
194
|
return value;
|
|
@@ -117,14 +196,25 @@ function sanitizeSessionRpcPayload(value) {
|
|
|
117
196
|
const sanitized = {};
|
|
118
197
|
for (const [key, entry] of Object.entries(value)) {
|
|
119
198
|
if (SESSION_RPC_BLOB_KEYS.has(key) && typeof entry === "string" && isInlineBlobString(entry)) {
|
|
199
|
+
if (key === "image_url") {
|
|
200
|
+
const materialized = await materializeInlineSessionImage({ ...args, value: entry });
|
|
201
|
+
if (materialized) {
|
|
202
|
+
sanitized[key] = materialized.relPath;
|
|
203
|
+
sanitized.name = typeof value.name === "string" && value.name.trim() ? value.name : materialized.name;
|
|
204
|
+
sanitized.size = typeof value.size === "number" && Number.isFinite(value.size) ? value.size : materialized.size;
|
|
205
|
+
sanitized.mimeType =
|
|
206
|
+
typeof value.mimeType === "string" && value.mimeType.trim() ? value.mimeType : materialized.mimeType;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
120
210
|
sanitized[key] = buildInlineBlobMarker(entry);
|
|
121
211
|
continue;
|
|
122
212
|
}
|
|
123
|
-
sanitized[key] = sanitizeSessionRpcPayload(entry);
|
|
213
|
+
sanitized[key] = await sanitizeSessionRpcPayload(entry, args);
|
|
124
214
|
}
|
|
125
215
|
return sanitized;
|
|
126
216
|
}
|
|
127
|
-
function sanitizeSessionRpcRawLine(line) {
|
|
217
|
+
async function sanitizeSessionRpcRawLine(line, args) {
|
|
128
218
|
const trimmed = line.trim();
|
|
129
219
|
if (!trimmed.startsWith("{")) {
|
|
130
220
|
return line;
|
|
@@ -159,7 +249,7 @@ function sanitizeSessionRpcRawLine(line) {
|
|
|
159
249
|
}
|
|
160
250
|
return JSON.stringify({
|
|
161
251
|
...parsed,
|
|
162
|
-
payload: sanitizeSessionRpcPayload(parsed.payload),
|
|
252
|
+
payload: await sanitizeSessionRpcPayload(parsed.payload, args),
|
|
163
253
|
});
|
|
164
254
|
}
|
|
165
255
|
catch {
|
|
@@ -482,6 +572,7 @@ async function readSessionLineIndex(workspaceRoot, filePath) {
|
|
|
482
572
|
}
|
|
483
573
|
async function getAgentSessionRawRows(args) {
|
|
484
574
|
const resolvedFile = resolveSessionFilePath(args.workspaceRoot, args.filePath);
|
|
575
|
+
const effectiveSessionId = args.sessionId || await readSessionIdFromSessionFile(resolvedFile).catch(() => null);
|
|
485
576
|
const index = await readSessionLineIndex(args.workspaceRoot, resolvedFile);
|
|
486
577
|
const totalLines = index.lineStartOffsets.length;
|
|
487
578
|
const sinceLine = Math.max(0, Math.floor(args.sinceLine));
|
|
@@ -595,7 +686,12 @@ async function getAgentSessionRawRows(args) {
|
|
|
595
686
|
let lineNumber = startLineIndex + 1;
|
|
596
687
|
for (const line of lines) {
|
|
597
688
|
if (line.trim()) {
|
|
598
|
-
const sanitized = sanitizeSessionRpcRawLine(line
|
|
689
|
+
const sanitized = await sanitizeSessionRpcRawLine(line, {
|
|
690
|
+
workspaceRoot: args.workspaceRoot,
|
|
691
|
+
filePath: resolvedFile,
|
|
692
|
+
lineNumber,
|
|
693
|
+
sessionId: effectiveSessionId,
|
|
694
|
+
});
|
|
599
695
|
rawRows.push({
|
|
600
696
|
id: lineNumber,
|
|
601
697
|
raw: sanitized,
|
|
@@ -613,7 +709,7 @@ async function getAgentSessionRawRows(args) {
|
|
|
613
709
|
}
|
|
614
710
|
}
|
|
615
711
|
function resolveSessionUploadsDir(workspaceRoot, sessionId) {
|
|
616
|
-
const safeSessionId = sessionId
|
|
712
|
+
const safeSessionId = sanitizeSessionPathSegment(sessionId, "session");
|
|
617
713
|
return path.join(workspaceRoot, ".doer-agent", "sessions", safeSessionId);
|
|
618
714
|
}
|
|
619
715
|
async function deleteAgentSession(workspaceRoot, filePath, sessionId) {
|
|
@@ -735,6 +831,7 @@ async function handleSessionRpcMessage(args) {
|
|
|
735
831
|
const result = await getAgentSessionRawRows({
|
|
736
832
|
workspaceRoot: args.workspaceRoot,
|
|
737
833
|
filePath: request.filePath ?? "",
|
|
834
|
+
sessionId: request.sessionId,
|
|
738
835
|
sinceLine: request.sinceLine,
|
|
739
836
|
beforeRowId: request.beforeRowId,
|
|
740
837
|
pageSize: request.pageSize,
|