agiagent-dev 2026.1.39 → 2026.1.40
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/agents/agiagent-tools.js +4 -0
- package/dist/agents/system-prompt.js +44 -9
- package/dist/agents/tools/gmail-tool.js +20 -1
- package/dist/agents/tools/resume-tool.d.ts +4 -0
- package/dist/agents/tools/resume-tool.js +91 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/gateway/composio-webhook.d.ts +13 -0
- package/dist/gateway/composio-webhook.js +169 -0
- package/dist/gateway/email-trigger-handler.d.ts +28 -0
- package/dist/gateway/email-trigger-handler.js +192 -0
- package/dist/gateway/hosted-db.d.ts +33 -0
- package/dist/gateway/hosted-db.js +97 -0
- package/dist/gateway/hosted-telegram.d.ts +15 -0
- package/dist/gateway/hosted-telegram.js +41 -0
- package/dist/gateway/server-http.js +5 -0
- package/dist/gateway/server-methods/composio.js +75 -1
- package/dist/infra/composio.d.ts +10 -0
- package/dist/infra/composio.js +59 -0
- package/dist/node-host/resume-docx.d.ts +21 -0
- package/dist/node-host/resume-docx.js +266 -0
- package/dist/node-host/runner.js +271 -54
- package/extensions/lobster/src/lobster-tool.test.ts +3 -3
- package/extensions/memory-lancedb/index.ts +13 -2
- package/package.json +3 -2
- package/skills/resume-docx/SKILL.md +214 -0
- package/skills/resume-docx/scripts/resume_docx_apply.py +1135 -0
- package/skills/resume-docx/scripts/resume_docx_extract.py +302 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
function truncateText(s, max = 180) {
|
|
8
|
+
const t = String(s ?? "");
|
|
9
|
+
if (t.length <= max) {
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
return `${t.slice(0, max - 3)}...`;
|
|
13
|
+
}
|
|
14
|
+
async function fileExists(p) {
|
|
15
|
+
try {
|
|
16
|
+
const st = await fs.stat(p);
|
|
17
|
+
return st.isFile();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function normalizeOsPlatform(platform) {
|
|
24
|
+
const p = platform.trim().toLowerCase();
|
|
25
|
+
return p.startsWith("win") ? "win32" : p;
|
|
26
|
+
}
|
|
27
|
+
async function findBundledScript(scriptBasename) {
|
|
28
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
// dist/node-host/* -> walk up to find package root containing skills/resume-docx/scripts
|
|
30
|
+
let cur = here;
|
|
31
|
+
for (let i = 0; i < 8; i++) {
|
|
32
|
+
const candidate = path.join(cur, "skills", "resume-docx", "scripts", scriptBasename);
|
|
33
|
+
if (await fileExists(candidate)) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
const next = path.dirname(cur);
|
|
37
|
+
if (next === cur) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
cur = next;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Bundled resume-docx script not found: ${scriptBasename}. Please update/reinstall agiagent-dev.`);
|
|
43
|
+
}
|
|
44
|
+
async function runCapture(argv, opts) {
|
|
45
|
+
const cwd = opts?.cwd;
|
|
46
|
+
const timeoutMs = typeof opts?.timeoutMs === "number" ? opts.timeoutMs : undefined;
|
|
47
|
+
return await new Promise((resolve) => {
|
|
48
|
+
let stdout = "";
|
|
49
|
+
let stderr = "";
|
|
50
|
+
let timedOut = false;
|
|
51
|
+
let settled = false;
|
|
52
|
+
let timer;
|
|
53
|
+
let child = null;
|
|
54
|
+
try {
|
|
55
|
+
child = spawn(argv[0], argv.slice(1), {
|
|
56
|
+
cwd,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
resolve({
|
|
63
|
+
exitCode: null,
|
|
64
|
+
stdout: "",
|
|
65
|
+
stderr: "",
|
|
66
|
+
error: err instanceof Error ? err.message : String(err),
|
|
67
|
+
timedOut: false,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const finalize = (exitCode, error) => {
|
|
72
|
+
if (settled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
settled = true;
|
|
76
|
+
if (timer) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
resolve({
|
|
80
|
+
exitCode,
|
|
81
|
+
stdout,
|
|
82
|
+
stderr,
|
|
83
|
+
error,
|
|
84
|
+
timedOut,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
child.stdout?.on("data", (chunk) => {
|
|
88
|
+
stdout += String(chunk ?? "");
|
|
89
|
+
});
|
|
90
|
+
child.stderr?.on("data", (chunk) => {
|
|
91
|
+
stderr += String(chunk ?? "");
|
|
92
|
+
});
|
|
93
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
try {
|
|
97
|
+
child?.kill("SIGKILL");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
}
|
|
104
|
+
child.on("error", (err) => finalize(null, err.message));
|
|
105
|
+
child.on("exit", (code) => finalize(code === null ? null : code, null));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function resolvePythonCommand() {
|
|
109
|
+
const candidates = [
|
|
110
|
+
{ argv: ["python3"], label: "python3" },
|
|
111
|
+
{ argv: ["python"], label: "python" },
|
|
112
|
+
];
|
|
113
|
+
if (normalizeOsPlatform(process.platform) === "win32") {
|
|
114
|
+
candidates.push({ argv: ["py", "-3"], label: "py -3" });
|
|
115
|
+
}
|
|
116
|
+
for (const cand of candidates) {
|
|
117
|
+
const res = await runCapture([...cand.argv, "-c", "import sys; print(sys.version_info[0])"], {
|
|
118
|
+
timeoutMs: 10_000,
|
|
119
|
+
});
|
|
120
|
+
if (res.exitCode === 0 && res.stdout.trim() === "3") {
|
|
121
|
+
return cand;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error("Python 3 not found on this device. Install Python 3 and ensure it is on PATH (python3/python or py -3 on Windows).");
|
|
125
|
+
}
|
|
126
|
+
async function ensurePythonDocx(python) {
|
|
127
|
+
const res = await runCapture([...python.argv, "-c", "import docx; print('python-docx OK')"], {
|
|
128
|
+
timeoutMs: 15_000,
|
|
129
|
+
});
|
|
130
|
+
if (res.exitCode === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const platform = normalizeOsPlatform(process.platform);
|
|
134
|
+
const recommended = platform === "win32"
|
|
135
|
+
? "py -3 -m pip install python-docx"
|
|
136
|
+
: `${python.label.includes("python3") ? "pip3" : "pip"} install python-docx`;
|
|
137
|
+
throw new Error(`Missing dependency: python-docx. Install it on your device, then retry.\nRecommended: ${recommended}`);
|
|
138
|
+
}
|
|
139
|
+
export async function resumeDocxCheck() {
|
|
140
|
+
const python = await resolvePythonCommand();
|
|
141
|
+
await ensurePythonDocx(python);
|
|
142
|
+
return { ok: true, python: python.label };
|
|
143
|
+
}
|
|
144
|
+
export async function resumeDocxExtract(params) {
|
|
145
|
+
const docxPath = String(params.docxPath ?? "").trim();
|
|
146
|
+
if (!docxPath) {
|
|
147
|
+
throw new Error("INVALID_REQUEST: docxPath required");
|
|
148
|
+
}
|
|
149
|
+
if (!docxPath.toLowerCase().endsWith(".docx")) {
|
|
150
|
+
throw new Error(`INVALID_REQUEST: docxPath must be a .docx file: ${docxPath}`);
|
|
151
|
+
}
|
|
152
|
+
const st = await fs.stat(docxPath).catch(() => null);
|
|
153
|
+
if (!st || !st.isFile()) {
|
|
154
|
+
throw new Error(`INVALID_REQUEST: docxPath is not a file: ${docxPath}`);
|
|
155
|
+
}
|
|
156
|
+
const python = await resolvePythonCommand();
|
|
157
|
+
await ensurePythonDocx(python);
|
|
158
|
+
const extractScript = await findBundledScript("resume_docx_extract.py");
|
|
159
|
+
const outPath = path.join(os.tmpdir(), `agiagent-resume-schema-${crypto.randomUUID()}.json`);
|
|
160
|
+
const res = await runCapture([...python.argv, extractScript, docxPath, "--out", outPath], {
|
|
161
|
+
timeoutMs: 120_000,
|
|
162
|
+
});
|
|
163
|
+
if (res.exitCode !== 0) {
|
|
164
|
+
throw new Error(`resume-docx extract failed (exit=${res.exitCode ?? "?"}): ${truncateText((res.stderr || res.stdout || res.error || "").trim(), 500)}`);
|
|
165
|
+
}
|
|
166
|
+
const raw = await fs.readFile(outPath, "utf8");
|
|
167
|
+
await fs.unlink(outPath).catch(() => { });
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
const mode = params.mode === "full" ? "full" : "digest";
|
|
170
|
+
if (mode === "full") {
|
|
171
|
+
return { ok: true, mode, schema: parsed };
|
|
172
|
+
}
|
|
173
|
+
const headings = Array.isArray(parsed?.headings) ? parsed.headings : [];
|
|
174
|
+
const skillsTable = parsed?.skills_table ?? null;
|
|
175
|
+
const sections = parsed?.sections ?? {};
|
|
176
|
+
const summary = sections?.summary ?? null;
|
|
177
|
+
const experience = sections?.experience ?? null;
|
|
178
|
+
const paragraphs = Array.isArray(parsed?.paragraphs) ? parsed.paragraphs : [];
|
|
179
|
+
const paragraphsSample = paragraphs
|
|
180
|
+
.filter((p) => p && typeof p === "object" && typeof p.text === "string" && p.text.trim())
|
|
181
|
+
.slice(0, 40)
|
|
182
|
+
.map((p) => ({
|
|
183
|
+
paragraph_index: p.paragraph_index,
|
|
184
|
+
text: truncateText(p.text, 220),
|
|
185
|
+
style: typeof p.style === "string" ? p.style : "",
|
|
186
|
+
}));
|
|
187
|
+
const digest = {
|
|
188
|
+
schema_version: parsed?.schema_version ?? 2,
|
|
189
|
+
docx_path: parsed?.docx_path ?? docxPath,
|
|
190
|
+
fingerprint: parsed?.fingerprint ?? null,
|
|
191
|
+
stats: parsed?.stats ?? null,
|
|
192
|
+
skills_table: skillsTable,
|
|
193
|
+
headings,
|
|
194
|
+
sections: {
|
|
195
|
+
summary: summary
|
|
196
|
+
? {
|
|
197
|
+
heading: summary.heading,
|
|
198
|
+
heading_index: summary.heading_index,
|
|
199
|
+
end_index: summary.end_index,
|
|
200
|
+
bullets: Array.isArray(summary.bullets)
|
|
201
|
+
? summary.bullets.slice(0, 30).map((b) => ({
|
|
202
|
+
paragraph_index: b.paragraph_index,
|
|
203
|
+
text: truncateText(b.text ?? "", 220),
|
|
204
|
+
isNumbered: Boolean(b.isNumbered),
|
|
205
|
+
}))
|
|
206
|
+
: [],
|
|
207
|
+
}
|
|
208
|
+
: null,
|
|
209
|
+
experience: experience
|
|
210
|
+
? {
|
|
211
|
+
heading: experience.heading,
|
|
212
|
+
heading_index: experience.heading_index,
|
|
213
|
+
end_index: experience.end_index,
|
|
214
|
+
blocks: Array.isArray(experience.blocks)
|
|
215
|
+
? experience.blocks.slice(0, 30).map((b) => ({
|
|
216
|
+
id: b.id,
|
|
217
|
+
header: truncateText(b.header ?? "", 220),
|
|
218
|
+
header_index: b.header_index ?? null,
|
|
219
|
+
first_bullet_index: b.first_bullet_index,
|
|
220
|
+
last_bullet_index: b.last_bullet_index,
|
|
221
|
+
bullet_count: b.bullet_count ?? null,
|
|
222
|
+
}))
|
|
223
|
+
: [],
|
|
224
|
+
}
|
|
225
|
+
: null,
|
|
226
|
+
},
|
|
227
|
+
paragraphs_sample: paragraphsSample,
|
|
228
|
+
};
|
|
229
|
+
return { ok: true, mode, schema: digest };
|
|
230
|
+
}
|
|
231
|
+
export async function resumeDocxApply(params) {
|
|
232
|
+
const inPath = String(params.inPath ?? "").trim();
|
|
233
|
+
const outPath = String(params.outPath ?? "").trim();
|
|
234
|
+
if (!inPath || !outPath) {
|
|
235
|
+
throw new Error("INVALID_REQUEST: inPath and outPath required");
|
|
236
|
+
}
|
|
237
|
+
if (!inPath.toLowerCase().endsWith(".docx")) {
|
|
238
|
+
throw new Error(`INVALID_REQUEST: inPath must be a .docx file: ${inPath}`);
|
|
239
|
+
}
|
|
240
|
+
if (!outPath.toLowerCase().endsWith(".docx")) {
|
|
241
|
+
throw new Error(`INVALID_REQUEST: outPath must be a .docx file: ${outPath}`);
|
|
242
|
+
}
|
|
243
|
+
const st = await fs.stat(inPath).catch(() => null);
|
|
244
|
+
if (!st || !st.isFile()) {
|
|
245
|
+
throw new Error(`INVALID_REQUEST: inPath is not a file: ${inPath}`);
|
|
246
|
+
}
|
|
247
|
+
const python = await resolvePythonCommand();
|
|
248
|
+
await ensurePythonDocx(python);
|
|
249
|
+
const applyScript = await findBundledScript("resume_docx_apply.py");
|
|
250
|
+
const patchPath = path.join(os.tmpdir(), `agiagent-resume-patch-${crypto.randomUUID()}.json`);
|
|
251
|
+
await fs.writeFile(patchPath, JSON.stringify(params.patch ?? {}, null, 2), "utf8");
|
|
252
|
+
try {
|
|
253
|
+
const res = await runCapture([...python.argv, applyScript, "--in", inPath, "--patch", patchPath, "--out", outPath], { timeoutMs: 180_000 });
|
|
254
|
+
if (res.exitCode !== 0) {
|
|
255
|
+
throw new Error(`resume-docx apply failed (exit=${res.exitCode ?? "?"}): ${truncateText((res.stderr || res.stdout || res.error || "").trim(), 700)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await fs.unlink(patchPath).catch(() => { });
|
|
260
|
+
}
|
|
261
|
+
const outStat = await fs.stat(outPath).catch(() => null);
|
|
262
|
+
if (!outStat || !outStat.isFile()) {
|
|
263
|
+
throw new Error(`resume-docx apply did not produce output: ${outPath}`);
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, outPath };
|
|
266
|
+
}
|
package/dist/node-host/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import fsPromises from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
|
@@ -21,6 +22,7 @@ import { detectMime } from "../media/mime.js";
|
|
|
21
22
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
|
22
23
|
import { VERSION } from "../version.js";
|
|
23
24
|
import { ensureNodeHostConfig, saveNodeHostConfig } from "./config.js";
|
|
25
|
+
import { resumeDocxApply, resumeDocxCheck, resumeDocxExtract } from "./resume-docx.js";
|
|
24
26
|
function resolveExecSecurity(value) {
|
|
25
27
|
return value === "deny" || value === "allowlist" || value === "full" ? value : "allowlist";
|
|
26
28
|
}
|
|
@@ -289,69 +291,150 @@ async function runCommand(argv, cwd, env, timeoutMs) {
|
|
|
289
291
|
let truncated = false;
|
|
290
292
|
let timedOut = false;
|
|
291
293
|
let settled = false;
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (outputLen >= OUTPUT_CAP) {
|
|
300
|
-
truncated = true;
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const remaining = OUTPUT_CAP - outputLen;
|
|
304
|
-
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
305
|
-
const str = slice.toString("utf8");
|
|
306
|
-
outputLen += slice.length;
|
|
307
|
-
if (target === "stdout") {
|
|
308
|
-
stdout += str;
|
|
294
|
+
const WINDOWS_INLINE_CMD_SOFT_LIMIT = 7000;
|
|
295
|
+
const POSIX_INLINE_CMD_SOFT_LIMIT = 50_000;
|
|
296
|
+
const platform = process.platform;
|
|
297
|
+
let cleanupTempFile = null;
|
|
298
|
+
const materializeShellIfNeeded = async () => {
|
|
299
|
+
if (!Array.isArray(argv) || argv.length < 2) {
|
|
300
|
+
return argv;
|
|
309
301
|
}
|
|
310
|
-
|
|
311
|
-
|
|
302
|
+
const a0 = String(argv[0] ?? "").toLowerCase();
|
|
303
|
+
if (platform === "win32") {
|
|
304
|
+
const idx = argv.findIndex((a) => String(a).toLowerCase() === "-command");
|
|
305
|
+
const cmd = idx >= 0 ? String(argv[idx + 1] ?? "") : "";
|
|
306
|
+
if (a0.includes("powershell") && idx >= 0 && cmd.length > WINDOWS_INLINE_CMD_SOFT_LIMIT) {
|
|
307
|
+
const tmp = path.join(os.tmpdir(), `agiagent-cmd-${crypto.randomUUID()}.ps1`);
|
|
308
|
+
cleanupTempFile = tmp;
|
|
309
|
+
await fsPromises.writeFile(tmp, cmd, "utf8");
|
|
310
|
+
// Keep PowerShell non-interactive and bypass policy for this one run.
|
|
311
|
+
return [
|
|
312
|
+
argv[0],
|
|
313
|
+
"-NoProfile",
|
|
314
|
+
"-NonInteractive",
|
|
315
|
+
"-ExecutionPolicy",
|
|
316
|
+
"Bypass",
|
|
317
|
+
"-File",
|
|
318
|
+
tmp,
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
return argv;
|
|
312
322
|
}
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
// POSIX shell: `/bin/sh -lc "<script>"` can also hit argv limits on some systems.
|
|
324
|
+
const cmd = argv.length >= 3 && argv[1] === "-lc" ? String(argv[2] ?? "") : "";
|
|
325
|
+
if (a0 === "/bin/sh" && argv[1] === "-lc" && cmd.length > POSIX_INLINE_CMD_SOFT_LIMIT) {
|
|
326
|
+
const tmp = path.join(os.tmpdir(), `agiagent-cmd-${crypto.randomUUID()}.sh`);
|
|
327
|
+
cleanupTempFile = tmp;
|
|
328
|
+
// Use a plain sh script; keep it simple and avoid bash-isms.
|
|
329
|
+
await fsPromises.writeFile(tmp, `${cmd}\n`, "utf8");
|
|
330
|
+
return ["/bin/sh", tmp];
|
|
315
331
|
}
|
|
332
|
+
return argv;
|
|
316
333
|
};
|
|
317
|
-
child
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
let child;
|
|
335
|
+
try {
|
|
336
|
+
// Note: materializeShellIfNeeded() is async but runCommand() is sync inside Promise ctor.
|
|
337
|
+
// We kick off an async IIFE to keep the outer signature stable.
|
|
338
|
+
void (async () => {
|
|
339
|
+
const effectiveArgv = await materializeShellIfNeeded();
|
|
340
|
+
child = spawn(effectiveArgv[0], effectiveArgv.slice(1), {
|
|
341
|
+
cwd,
|
|
342
|
+
env,
|
|
343
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
344
|
+
windowsHide: true,
|
|
345
|
+
});
|
|
346
|
+
const onChunk = (chunk, target) => {
|
|
347
|
+
if (outputLen >= OUTPUT_CAP) {
|
|
348
|
+
truncated = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const remaining = OUTPUT_CAP - outputLen;
|
|
352
|
+
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
353
|
+
const str = slice.toString("utf8");
|
|
354
|
+
outputLen += slice.length;
|
|
355
|
+
if (target === "stdout") {
|
|
356
|
+
stdout += str;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
stderr += str;
|
|
360
|
+
}
|
|
361
|
+
if (chunk.length > remaining) {
|
|
362
|
+
truncated = true;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
child.stdout?.on("data", (chunk) => onChunk(chunk, "stdout"));
|
|
366
|
+
child.stderr?.on("data", (chunk) => onChunk(chunk, "stderr"));
|
|
367
|
+
let timer;
|
|
368
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
369
|
+
timer = setTimeout(() => {
|
|
370
|
+
timedOut = true;
|
|
371
|
+
try {
|
|
372
|
+
child.kill("SIGKILL");
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// ignore
|
|
376
|
+
}
|
|
377
|
+
}, timeoutMs);
|
|
325
378
|
}
|
|
326
|
-
|
|
327
|
-
|
|
379
|
+
const finalize = (exitCode, error) => {
|
|
380
|
+
if (settled) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
settled = true;
|
|
384
|
+
if (timer) {
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
}
|
|
387
|
+
if (cleanupTempFile) {
|
|
388
|
+
void fsPromises.unlink(cleanupTempFile).catch(() => { });
|
|
389
|
+
}
|
|
390
|
+
resolve({
|
|
391
|
+
exitCode,
|
|
392
|
+
timedOut,
|
|
393
|
+
success: exitCode === 0 && !timedOut && !error,
|
|
394
|
+
stdout,
|
|
395
|
+
stderr,
|
|
396
|
+
error: error ?? null,
|
|
397
|
+
truncated,
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
child.on("error", (err) => {
|
|
401
|
+
finalize(undefined, err.message);
|
|
402
|
+
});
|
|
403
|
+
child.on("exit", (code) => {
|
|
404
|
+
finalize(code === null ? undefined : code, null);
|
|
405
|
+
});
|
|
406
|
+
})().catch((err) => {
|
|
407
|
+
// Covers failures in materializeShellIfNeeded().
|
|
408
|
+
if (cleanupTempFile) {
|
|
409
|
+
void fsPromises.unlink(cleanupTempFile).catch(() => { });
|
|
328
410
|
}
|
|
329
|
-
|
|
411
|
+
resolve({
|
|
412
|
+
exitCode: undefined,
|
|
413
|
+
timedOut: false,
|
|
414
|
+
success: false,
|
|
415
|
+
stdout: "",
|
|
416
|
+
stderr: "",
|
|
417
|
+
error: String(err),
|
|
418
|
+
truncated: false,
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
330
422
|
}
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
settled = true;
|
|
336
|
-
if (timer) {
|
|
337
|
-
clearTimeout(timer);
|
|
423
|
+
catch (err) {
|
|
424
|
+
if (cleanupTempFile) {
|
|
425
|
+
void fsPromises.unlink(cleanupTempFile).catch(() => { });
|
|
338
426
|
}
|
|
339
427
|
resolve({
|
|
340
|
-
exitCode,
|
|
341
|
-
timedOut,
|
|
342
|
-
success:
|
|
343
|
-
stdout,
|
|
344
|
-
stderr,
|
|
345
|
-
error:
|
|
346
|
-
truncated,
|
|
428
|
+
exitCode: undefined,
|
|
429
|
+
timedOut: false,
|
|
430
|
+
success: false,
|
|
431
|
+
stdout: "",
|
|
432
|
+
stderr: "",
|
|
433
|
+
error: err instanceof Error ? err.message : String(err),
|
|
434
|
+
truncated: false,
|
|
347
435
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
finalize(undefined, err.message);
|
|
351
|
-
});
|
|
352
|
-
child.on("exit", (code) => {
|
|
353
|
-
finalize(code === null ? undefined : code, null);
|
|
354
|
-
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
355
438
|
});
|
|
356
439
|
}
|
|
357
440
|
function resolveEnvPath(env) {
|
|
@@ -473,6 +556,9 @@ export async function runNodeHost(opts) {
|
|
|
473
556
|
"system.which",
|
|
474
557
|
"system.execApprovals.get",
|
|
475
558
|
"system.execApprovals.set",
|
|
559
|
+
"resume.docx.check",
|
|
560
|
+
"resume.docx.extract",
|
|
561
|
+
"resume.docx.apply",
|
|
476
562
|
"gmail.profile",
|
|
477
563
|
"gmail.labels.list",
|
|
478
564
|
"gmail.messages.search",
|
|
@@ -481,6 +567,7 @@ export async function runNodeHost(opts) {
|
|
|
481
567
|
"gmail.messages.send",
|
|
482
568
|
"gmail.messages.reply",
|
|
483
569
|
"gmail.messages.modify",
|
|
570
|
+
"gmail.drafts.create",
|
|
484
571
|
"gmail.attachments.get",
|
|
485
572
|
...(browserProxyEnabled ? ["browser.proxy", "browser.stagehand"] : []),
|
|
486
573
|
],
|
|
@@ -493,7 +580,13 @@ export async function runNodeHost(opts) {
|
|
|
493
580
|
if (evt.event === "node.invoke.request") {
|
|
494
581
|
const payload = coerceNodeInvokePayload(evt.payload);
|
|
495
582
|
if (payload) {
|
|
496
|
-
void handleInvoke(payload, client, skillBins)
|
|
583
|
+
void handleInvoke(payload, client, skillBins).catch(async (err) => {
|
|
584
|
+
// Ensure we never crash the node host on tool errors.
|
|
585
|
+
await sendInvokeResult(client, payload, {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: { code: "INTERNAL_ERROR", message: String(err) },
|
|
588
|
+
});
|
|
589
|
+
});
|
|
497
590
|
}
|
|
498
591
|
return;
|
|
499
592
|
}
|
|
@@ -538,7 +631,64 @@ export async function runNodeHost(opts) {
|
|
|
538
631
|
await new Promise(() => { });
|
|
539
632
|
}
|
|
540
633
|
async function handleInvoke(frame, client, skillBins) {
|
|
634
|
+
try {
|
|
635
|
+
await handleInvokeInner(frame, client, skillBins);
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
// Last-resort safety net: never allow tool errors to crash the node host.
|
|
639
|
+
await sendInvokeResult(client, frame, {
|
|
640
|
+
ok: false,
|
|
641
|
+
error: { code: "INTERNAL_ERROR", message: String(err) },
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function handleInvokeInner(frame, client, skillBins) {
|
|
541
646
|
const command = String(frame.command ?? "");
|
|
647
|
+
if (command === "resume.docx.check") {
|
|
648
|
+
try {
|
|
649
|
+
// Params are currently unused but keep decoding for forward-compatibility.
|
|
650
|
+
if (frame.paramsJSON) {
|
|
651
|
+
decodeParams(frame.paramsJSON);
|
|
652
|
+
}
|
|
653
|
+
const result = await resumeDocxCheck();
|
|
654
|
+
await sendInvokeResult(client, frame, { ok: true, payloadJSON: JSON.stringify(result) });
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
await sendInvokeResult(client, frame, {
|
|
658
|
+
ok: false,
|
|
659
|
+
error: { code: "INVALID_REQUEST", message: String(err) },
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (command === "resume.docx.extract") {
|
|
665
|
+
try {
|
|
666
|
+
const params = decodeParams(frame.paramsJSON);
|
|
667
|
+
const result = await resumeDocxExtract(params);
|
|
668
|
+
await sendInvokeResult(client, frame, { ok: true, payloadJSON: JSON.stringify(result) });
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
await sendInvokeResult(client, frame, {
|
|
672
|
+
ok: false,
|
|
673
|
+
error: { code: "INVALID_REQUEST", message: String(err) },
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (command === "resume.docx.apply") {
|
|
679
|
+
try {
|
|
680
|
+
const params = decodeParams(frame.paramsJSON);
|
|
681
|
+
const result = await resumeDocxApply(params);
|
|
682
|
+
await sendInvokeResult(client, frame, { ok: true, payloadJSON: JSON.stringify(result) });
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
await sendInvokeResult(client, frame, {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: { code: "INVALID_REQUEST", message: String(err) },
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
542
692
|
if (command.startsWith("gmail.")) {
|
|
543
693
|
try {
|
|
544
694
|
const result = await handleGmailInvoke(command, frame, client);
|
|
@@ -1466,6 +1616,73 @@ async function handleGmailInvoke(command, frame, client) {
|
|
|
1466
1616
|
body: JSON.stringify({ raw, threadId }),
|
|
1467
1617
|
});
|
|
1468
1618
|
}
|
|
1619
|
+
// Create a Gmail draft. Supports both new drafts and reply-in-thread drafts.
|
|
1620
|
+
// When messageId is provided the draft is threaded as a reply (fetches headers
|
|
1621
|
+
// automatically). Otherwise a standalone draft is created to the given `to`.
|
|
1622
|
+
if (command === "gmail.drafts.create") {
|
|
1623
|
+
const params = decodeParams(frame.paramsJSON);
|
|
1624
|
+
const body = String(params.body ?? "");
|
|
1625
|
+
const attachments = await loadAttachments(params.attachments);
|
|
1626
|
+
// If messageId is provided, thread as a reply (like messages.reply but draft)
|
|
1627
|
+
if (params.messageId) {
|
|
1628
|
+
const messageId = String(params.messageId).trim();
|
|
1629
|
+
const metaUsp = new URLSearchParams();
|
|
1630
|
+
metaUsp.set("format", "metadata");
|
|
1631
|
+
for (const h of ["Message-ID", "References", "Subject", "From", "Reply-To"]) {
|
|
1632
|
+
metaUsp.append("metadataHeaders", h);
|
|
1633
|
+
}
|
|
1634
|
+
const message = (await gmailFetch(token, `/messages/${encodeURIComponent(messageId)}?${metaUsp.toString()}`, { method: "GET" }));
|
|
1635
|
+
const fetchedThreadId = typeof message?.threadId === "string" ? message.threadId.trim() : "";
|
|
1636
|
+
const headers = Array.isArray(message?.payload?.headers) ? message.payload.headers : [];
|
|
1637
|
+
const from = extractEmailAddress(parseHeaderValue(headers, "Reply-To") || parseHeaderValue(headers, "From"));
|
|
1638
|
+
const origSubject = parseHeaderValue(headers, "Subject");
|
|
1639
|
+
const subject = ensureSubjectReply(origSubject);
|
|
1640
|
+
const inReplyTo = parseHeaderValue(headers, "Message-ID");
|
|
1641
|
+
const references = parseHeaderValue(headers, "References");
|
|
1642
|
+
const nextRefs = [references, inReplyTo]
|
|
1643
|
+
.map((v) => String(v ?? "").trim())
|
|
1644
|
+
.filter(Boolean)
|
|
1645
|
+
.join(" ")
|
|
1646
|
+
.trim();
|
|
1647
|
+
const mime = buildMimeMessage({
|
|
1648
|
+
headers: {
|
|
1649
|
+
To: from,
|
|
1650
|
+
Subject: subject,
|
|
1651
|
+
...(inReplyTo ? { "In-Reply-To": inReplyTo } : {}),
|
|
1652
|
+
...(nextRefs ? { References: nextRefs } : {}),
|
|
1653
|
+
},
|
|
1654
|
+
textBody: body,
|
|
1655
|
+
attachments,
|
|
1656
|
+
});
|
|
1657
|
+
const raw = base64UrlEncode(Buffer.from(mime, "utf8"));
|
|
1658
|
+
return await gmailFetch(token, "/drafts", {
|
|
1659
|
+
method: "POST",
|
|
1660
|
+
headers: { "content-type": "application/json" },
|
|
1661
|
+
body: JSON.stringify({
|
|
1662
|
+
message: { raw, ...(fetchedThreadId ? { threadId: fetchedThreadId } : {}) },
|
|
1663
|
+
}),
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
// New draft (not a reply) — to a specified recipient
|
|
1667
|
+
const toList = Array.isArray(params.to) ? params.to : [String(params.to ?? "")];
|
|
1668
|
+
const to = toList.map((v) => String(v).trim()).filter(Boolean);
|
|
1669
|
+
if (to.length === 0) {
|
|
1670
|
+
throw new Error("INVALID_REQUEST: to required for new draft");
|
|
1671
|
+
}
|
|
1672
|
+
const subject = String(params.subject ?? "").trim();
|
|
1673
|
+
const threadId = params.threadId ? String(params.threadId).trim() : "";
|
|
1674
|
+
const mime = buildMimeMessage({
|
|
1675
|
+
headers: { To: to.join(", "), Subject: subject },
|
|
1676
|
+
textBody: body,
|
|
1677
|
+
attachments,
|
|
1678
|
+
});
|
|
1679
|
+
const raw = base64UrlEncode(Buffer.from(mime, "utf8"));
|
|
1680
|
+
return await gmailFetch(token, "/drafts", {
|
|
1681
|
+
method: "POST",
|
|
1682
|
+
headers: { "content-type": "application/json" },
|
|
1683
|
+
body: JSON.stringify({ message: { raw, ...(threadId ? { threadId } : {}) } }),
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1469
1686
|
throw new Error(`INVALID_REQUEST: unknown gmail command: ${command}`);
|
|
1470
1687
|
}
|
|
1471
1688
|
function decodeParams(raw) {
|
|
@@ -84,7 +84,7 @@ describe("lobster plugin tool", () => {
|
|
|
84
84
|
const res = await tool.execute("call1", {
|
|
85
85
|
action: "run",
|
|
86
86
|
pipeline: "noop",
|
|
87
|
-
timeoutMs:
|
|
87
|
+
timeoutMs: 4000,
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
@@ -110,7 +110,7 @@ describe("lobster plugin tool", () => {
|
|
|
110
110
|
const res = await tool.execute("call-noisy", {
|
|
111
111
|
action: "run",
|
|
112
112
|
pipeline: "noop",
|
|
113
|
-
timeoutMs:
|
|
113
|
+
timeoutMs: 4000,
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
@@ -200,7 +200,7 @@ describe("lobster plugin tool", () => {
|
|
|
200
200
|
const res = await tool.execute("call-plugin-config", {
|
|
201
201
|
action: "run",
|
|
202
202
|
pipeline: "noop",
|
|
203
|
-
timeoutMs:
|
|
203
|
+
timeoutMs: 4000,
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|