@sema-agent/core 1.451.0 → 1.452.0
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/observer.js +3 -0
- package/dist/agents/subagent.js +51 -12
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/exec-output-tail.d.ts +18 -1
- package/dist/core/exec-output-tail.js +38 -5
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +10 -0
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/memory.js +1 -1
- package/dist/core/runner/prepare-task.js +10 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry.d.ts +15 -9
- package/dist/core/task-registry.js +290 -53
- package/dist/core/tool-result-store.js +2 -2
- package/dist/core/tools.d.ts +5 -0
- package/dist/core/tools.js +3 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
- package/dist/engine/execution-env/node-execution-env.js +130 -20
- package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +29 -6
- package/dist/orchestration/workflow.d.ts +9 -0
- package/dist/orchestration/workflow.js +80 -5
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.js +3 -2
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/index.js +184 -120
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +2 -0
- package/dist/tools/web.js +105 -19
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare function isNotebookPath(p: string): boolean;
|
|
2
|
+
export declare const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10000;
|
|
3
|
+
export declare const NOTEBOOK_OUTPUT_TEXT_CLIP = 10000;
|
|
4
|
+
export declare const NOTEBOOK_IMAGE_BASE64_BUDGET = 262144;
|
|
5
|
+
export interface NotebookOutputImage {
|
|
6
|
+
image_data: string;
|
|
7
|
+
media_type: "image/png" | "image/jpeg";
|
|
8
|
+
}
|
|
9
|
+
export interface NotebookOutput {
|
|
10
|
+
output_type: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
image?: NotebookOutputImage;
|
|
13
|
+
}
|
|
14
|
+
export interface NotebookCell {
|
|
15
|
+
cell_id: string;
|
|
16
|
+
cellType: string;
|
|
17
|
+
source: string;
|
|
18
|
+
language?: string;
|
|
19
|
+
execution_count?: number;
|
|
20
|
+
outputs?: NotebookOutput[];
|
|
21
|
+
}
|
|
22
|
+
export type ParsedNotebook = {
|
|
23
|
+
ok: true;
|
|
24
|
+
cells: NotebookCell[];
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: "json" | "shape";
|
|
28
|
+
};
|
|
29
|
+
export declare function parseNotebookCells(text: string, notebookPath: string): ParsedNotebook;
|
|
30
|
+
export interface NotebookRender {
|
|
31
|
+
blocks: Array<{
|
|
32
|
+
type: "text";
|
|
33
|
+
text: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "image";
|
|
36
|
+
data: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
}>;
|
|
39
|
+
textChars: number;
|
|
40
|
+
imageChars: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function renderNotebookCells(cells: readonly NotebookCell[], imageBase64Budget: number): NotebookRender;
|
|
43
|
+
export declare function stripNotebookImageData(cell: NotebookCell): NotebookCell;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { imageMagicMatches } from "./safety.js";
|
|
2
|
+
export function isNotebookPath(p) {
|
|
3
|
+
return p.toLowerCase().endsWith(".ipynb");
|
|
4
|
+
}
|
|
5
|
+
export const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10_000;
|
|
6
|
+
export const NOTEBOOK_OUTPUT_TEXT_CLIP = 10_000;
|
|
7
|
+
export const NOTEBOOK_IMAGE_BASE64_BUDGET = 262_144;
|
|
8
|
+
function joinedText(v) {
|
|
9
|
+
if (v === undefined || v === null)
|
|
10
|
+
return "";
|
|
11
|
+
return Array.isArray(v) ? v.join("") : String(v);
|
|
12
|
+
}
|
|
13
|
+
function clipOutputText(s) {
|
|
14
|
+
if (s.length <= NOTEBOOK_OUTPUT_TEXT_CLIP)
|
|
15
|
+
return s;
|
|
16
|
+
const droppedLines = s.slice(NOTEBOOK_OUTPUT_TEXT_CLIP).split("\n").length;
|
|
17
|
+
return `${s.slice(0, NOTEBOOK_OUTPUT_TEXT_CLIP)}\n\n… [${droppedLines} lines truncated] …`;
|
|
18
|
+
}
|
|
19
|
+
function imageFromData(data) {
|
|
20
|
+
if (data === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
for (const mime of ["image/png", "image/jpeg"]) {
|
|
23
|
+
const v = data[mime];
|
|
24
|
+
if (typeof v !== "string")
|
|
25
|
+
continue;
|
|
26
|
+
const stripped = v.replace(/\s/g, "");
|
|
27
|
+
if (!imageMagicMatches(Buffer.from(stripped, "base64"), mime))
|
|
28
|
+
return undefined;
|
|
29
|
+
return { image_data: stripped, media_type: mime };
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function mapOutput(raw) {
|
|
34
|
+
if (raw === null || typeof raw !== "object")
|
|
35
|
+
return undefined;
|
|
36
|
+
const o = raw;
|
|
37
|
+
switch (o.output_type) {
|
|
38
|
+
case "stream":
|
|
39
|
+
return { output_type: "stream", text: clipOutputText(joinedText(o.text)) };
|
|
40
|
+
case "execute_result":
|
|
41
|
+
case "display_data": {
|
|
42
|
+
const data = o.data !== null && typeof o.data === "object" ? o.data : undefined;
|
|
43
|
+
const image = imageFromData(data);
|
|
44
|
+
return { output_type: o.output_type, text: clipOutputText(joinedText(data?.["text/plain"])), ...(image ? { image } : {}) };
|
|
45
|
+
}
|
|
46
|
+
case "error": {
|
|
47
|
+
const tb = Array.isArray(o.traceback) ? o.traceback.join("\n") : "";
|
|
48
|
+
return { output_type: "error", text: clipOutputText(`${joinedText(o.ename)}: ${joinedText(o.evalue)}\n${tb}`) };
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function parseNotebookCells(text, notebookPath) {
|
|
55
|
+
let nb;
|
|
56
|
+
try {
|
|
57
|
+
nb = JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { ok: false, reason: "json" };
|
|
61
|
+
}
|
|
62
|
+
const top = nb;
|
|
63
|
+
const rawCells = top?.cells;
|
|
64
|
+
if (!Array.isArray(rawCells) || rawCells.some((c) => c === null || typeof c !== "object")) {
|
|
65
|
+
return { ok: false, reason: "shape" };
|
|
66
|
+
}
|
|
67
|
+
const language = typeof top?.metadata?.language_info?.name === "string" ? top.metadata.language_info.name : "python";
|
|
68
|
+
const cells = rawCells.map((raw, i) => {
|
|
69
|
+
const c = raw;
|
|
70
|
+
const cellType = typeof c.cell_type === "string" ? c.cell_type : "code";
|
|
71
|
+
const cell = {
|
|
72
|
+
cell_id: typeof c.id === "string" ? c.id : `cell-${i}`,
|
|
73
|
+
cellType,
|
|
74
|
+
source: joinedText(c.source),
|
|
75
|
+
};
|
|
76
|
+
if (cellType === "code") {
|
|
77
|
+
cell.language = language;
|
|
78
|
+
if (typeof c.execution_count === "number" && c.execution_count)
|
|
79
|
+
cell.execution_count = c.execution_count;
|
|
80
|
+
if (Array.isArray(c.outputs) && c.outputs.length > 0) {
|
|
81
|
+
const mapped = c.outputs.map(mapOutput).filter((o) => o !== undefined);
|
|
82
|
+
let sum = 0;
|
|
83
|
+
let over = false;
|
|
84
|
+
for (const o of mapped) {
|
|
85
|
+
sum += (o.text?.length ?? 0) + (o.image?.image_data.length ?? 0);
|
|
86
|
+
if (sum > NOTEBOOK_CELL_OUTPUT_POINTER_CHARS) {
|
|
87
|
+
over = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
cell.outputs = over
|
|
92
|
+
? [{ output_type: "stream", text: `Outputs are too large to include. Use Bash with: cat "${notebookPath}" | jq '.cells[${i}].outputs'` }]
|
|
93
|
+
: mapped;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return cell;
|
|
97
|
+
});
|
|
98
|
+
return { ok: true, cells };
|
|
99
|
+
}
|
|
100
|
+
export function renderNotebookCells(cells, imageBase64Budget) {
|
|
101
|
+
const blocks = [];
|
|
102
|
+
let imageChars = 0;
|
|
103
|
+
const pushText = (t) => {
|
|
104
|
+
const last = blocks[blocks.length - 1];
|
|
105
|
+
if (last !== undefined && last.type === "text")
|
|
106
|
+
last.text += `\n${t}`;
|
|
107
|
+
else
|
|
108
|
+
blocks.push({ type: "text", text: t });
|
|
109
|
+
};
|
|
110
|
+
for (const cell of cells) {
|
|
111
|
+
const tags = [];
|
|
112
|
+
if (cell.cellType !== "code")
|
|
113
|
+
tags.push(`<cell_type>${cell.cellType}</cell_type>`);
|
|
114
|
+
if (cell.language !== undefined && cell.language !== "python" && cell.cellType === "code")
|
|
115
|
+
tags.push(`<language>${cell.language}</language>`);
|
|
116
|
+
pushText(`<cell id="${cell.cell_id}">${tags.join("")}${cell.source}</cell id="${cell.cell_id}">`);
|
|
117
|
+
for (const o of cell.outputs ?? []) {
|
|
118
|
+
if (o.text !== undefined && o.text !== "")
|
|
119
|
+
pushText(`\n${o.text}`);
|
|
120
|
+
if (o.image !== undefined) {
|
|
121
|
+
if (imageChars + o.image.image_data.length > imageBase64Budget) {
|
|
122
|
+
pushText("[image output omitted: notebook image budget exceeded]");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
imageChars += o.image.image_data.length;
|
|
126
|
+
blocks.push({ type: "image", data: o.image.image_data, mimeType: o.image.media_type });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const textChars = blocks.reduce((n, b) => n + (b.type === "text" ? b.text.length : 0), 0);
|
|
132
|
+
return { blocks, textChars, imageChars };
|
|
133
|
+
}
|
|
134
|
+
export function stripNotebookImageData(cell) {
|
|
135
|
+
if (cell.outputs === undefined)
|
|
136
|
+
return cell;
|
|
137
|
+
return {
|
|
138
|
+
...cell,
|
|
139
|
+
outputs: cell.outputs.map((o) => o.image === undefined ? o : { ...o, image: { ...o.image, image_data: `[${o.image.image_data.length} base64 chars omitted]` } }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { defineTool } from "../../core/tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
3
|
import { resolveKey, violationText } from "./safety.js";
|
|
4
4
|
import { buildIgnore, walk, walkIsPartial } from "./search.js";
|
|
5
5
|
const DEFAULT_MAX_CHARS = 16_000;
|
|
@@ -101,7 +101,7 @@ export function makeRepoMapTool(env, rootCanonical, additionalRoots) {
|
|
|
101
101
|
if (a.path !== undefined) {
|
|
102
102
|
const r = await resolveKey(env, rootCanonical, a.path, signal, rootCanonical, additionalRoots);
|
|
103
103
|
if (!r.ok)
|
|
104
|
-
return violationText("RepoMap", r.violation);
|
|
104
|
+
return errorResult(violationText("RepoMap", r.violation));
|
|
105
105
|
start = r.key;
|
|
106
106
|
}
|
|
107
107
|
const maxChars = Math.max(1, Math.floor(a.max_chars ?? DEFAULT_MAX_CHARS));
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -76,7 +76,7 @@ export function splitAbsoluteGlobPattern(pattern) {
|
|
|
76
76
|
export function shellQuote(s) {
|
|
77
77
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
78
78
|
}
|
|
79
|
-
function globTokenToRegExp(token, anchored,
|
|
79
|
+
function globTokenToRegExp(token, anchored, glob = false) {
|
|
80
80
|
const charToRe = (ch) => ch === "*" ? "[^/]*" : ch === "?" ? "[^/]" : ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
81
81
|
let re = "";
|
|
82
82
|
for (let i = 0; i < token.length; i++) {
|
|
@@ -93,7 +93,7 @@ function globTokenToRegExp(token, anchored, braces = false) {
|
|
|
93
93
|
}
|
|
94
94
|
else if (c === "?")
|
|
95
95
|
re += "[^/]";
|
|
96
|
-
else if (
|
|
96
|
+
else if (glob && c === "{") {
|
|
97
97
|
const close = token.indexOf("}", i);
|
|
98
98
|
if (close > i + 1) {
|
|
99
99
|
const alts = token.slice(i + 1, close).split(",").map((alt) => [...alt].map(charToRe).join(""));
|
|
@@ -103,11 +103,52 @@ function globTokenToRegExp(token, anchored, braces = false) {
|
|
|
103
103
|
else
|
|
104
104
|
re += "\\{";
|
|
105
105
|
}
|
|
106
|
+
else if (glob && c === "[") {
|
|
107
|
+
let j = i + 1;
|
|
108
|
+
let neg = false;
|
|
109
|
+
if (token[j] === "!" || token[j] === "^") {
|
|
110
|
+
neg = true;
|
|
111
|
+
j++;
|
|
112
|
+
}
|
|
113
|
+
const bodyStart = j;
|
|
114
|
+
if (token[j] === "]")
|
|
115
|
+
j++;
|
|
116
|
+
const close = token.indexOf("]", j);
|
|
117
|
+
let cls;
|
|
118
|
+
if (close !== -1) {
|
|
119
|
+
const body = token.slice(bodyStart, close).replace(/[\\[\]]/g, "\\$&").replace(/\//g, "");
|
|
120
|
+
cls = `[${neg ? "^/" : ""}${body}]`;
|
|
121
|
+
try {
|
|
122
|
+
new RegExp(cls);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
cls = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (cls !== undefined) {
|
|
129
|
+
re += cls;
|
|
130
|
+
i = close;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
re += "\\[";
|
|
134
|
+
}
|
|
106
135
|
else
|
|
107
136
|
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
108
137
|
}
|
|
109
138
|
return new RegExp(`${anchored ? "^" : "(^|/)"}${re}$`);
|
|
110
139
|
}
|
|
140
|
+
function normalizeGlobToken(token) {
|
|
141
|
+
let t = token;
|
|
142
|
+
while (t.startsWith("./"))
|
|
143
|
+
t = t.slice(2);
|
|
144
|
+
return t.replace(/^\/+/, "");
|
|
145
|
+
}
|
|
146
|
+
function globIsAnchored(token) {
|
|
147
|
+
let t = token;
|
|
148
|
+
while (t.startsWith("./"))
|
|
149
|
+
t = t.slice(2);
|
|
150
|
+
return t.includes("/");
|
|
151
|
+
}
|
|
111
152
|
export function gitignoreMatcher(content) {
|
|
112
153
|
const rules = content
|
|
113
154
|
.split(/\r?\n/)
|
|
@@ -736,9 +777,9 @@ export async function jsGrep(env, root, p, signal, guards) {
|
|
|
736
777
|
return p.path || r === abs ? r : `./${r}`;
|
|
737
778
|
};
|
|
738
779
|
if (p.glob) {
|
|
739
|
-
const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(g,
|
|
780
|
+
const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(normalizeGlobToken(g), globIsAnchored(g), true));
|
|
740
781
|
if (globRes.length > 0)
|
|
741
|
-
files = files.filter((f) => globRes.some((re) => re.test(rel(f))));
|
|
782
|
+
files = files.filter((f) => globRes.some((re) => re.test(rel(f).replace(/\\/g, "/"))));
|
|
742
783
|
}
|
|
743
784
|
let typeNote = "";
|
|
744
785
|
if (p.type) {
|
|
@@ -863,8 +904,8 @@ export async function jsGrep(env, root, p, signal, guards) {
|
|
|
863
904
|
totalContent += fileCount;
|
|
864
905
|
if (fileCount > 0) {
|
|
865
906
|
totalFiles++;
|
|
866
|
-
if (mode === "files_with_matches"
|
|
867
|
-
fileMatches.push(
|
|
907
|
+
if (mode === "files_with_matches")
|
|
908
|
+
fileMatches.push(f);
|
|
868
909
|
else if (mode === "count" && counts.length < collectCap)
|
|
869
910
|
counts.push(`${relOut(f)}:${fileCount}`);
|
|
870
911
|
}
|
|
@@ -888,7 +929,14 @@ export async function jsGrep(env, root, p, signal, guards) {
|
|
|
888
929
|
const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
|
|
889
930
|
const offNote = off > 0 ? `\n[offset ${off}]` : "";
|
|
890
931
|
if (mode === "files_with_matches") {
|
|
891
|
-
const
|
|
932
|
+
const haveMtimes = fileMatches.length > 0 && fileMatches.every((f) => walked.mtimes.has(f));
|
|
933
|
+
const sortedFileMatches = haveMtimes
|
|
934
|
+
? fileMatches
|
|
935
|
+
.map((f) => ({ r: relOut(f), m: walked.mtimes.get(f) }))
|
|
936
|
+
.sort((a, b) => b.m - a.m || (a.r < b.r ? -1 : a.r > b.r ? 1 : 0))
|
|
937
|
+
.map((x) => x.r)
|
|
938
|
+
: fileMatches.map(relOut).sort();
|
|
939
|
+
const body = sortedFileMatches.slice(off, off + cap);
|
|
892
940
|
return body.length === 0
|
|
893
941
|
? NO_MATCHES + offNote + typeNote + caveat
|
|
894
942
|
: body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat;
|
|
@@ -921,6 +969,25 @@ export function detectRipgrep(env) {
|
|
|
921
969
|
}
|
|
922
970
|
return cached;
|
|
923
971
|
}
|
|
972
|
+
async function sortRgFilesByMtime(env, root, stdout, signal) {
|
|
973
|
+
const paths = stdout.split("\n").filter((l) => l.length > 0);
|
|
974
|
+
if (paths.length === 0)
|
|
975
|
+
return stdout;
|
|
976
|
+
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
977
|
+
const toAbs = (p) => {
|
|
978
|
+
const stripped = p.startsWith("./") ? p.slice(2) : p;
|
|
979
|
+
return stripped.startsWith("/") || /^[A-Za-z]:[\\/]/.test(stripped) ? stripped : `${rootPrefix}${stripped}`;
|
|
980
|
+
};
|
|
981
|
+
const infos = await Promise.all(paths.map((p) => env.fileInfo(toAbs(p), signal)));
|
|
982
|
+
const withMtime = paths.map((p, i) => {
|
|
983
|
+
const r = infos[i];
|
|
984
|
+
return { p, m: r.ok && typeof r.value.mtimeMs === "number" ? r.value.mtimeMs : undefined };
|
|
985
|
+
});
|
|
986
|
+
const sorted = withMtime.every((x) => x.m !== undefined)
|
|
987
|
+
? [...withMtime].sort((a, b) => b.m - a.m || (a.p < b.p ? -1 : a.p > b.p ? 1 : 0))
|
|
988
|
+
: [...withMtime].sort((a, b) => (a.p < b.p ? -1 : a.p > b.p ? 1 : 0));
|
|
989
|
+
return sorted.map((x) => x.p).join("\n");
|
|
990
|
+
}
|
|
924
991
|
function formatRgStdout(stdout, p, caveat = "") {
|
|
925
992
|
const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
|
|
926
993
|
const off = Math.max(0, Math.floor(p.offset ?? 0));
|
|
@@ -1003,7 +1070,8 @@ export async function rgGrepDetailed(env, root, p, signal) {
|
|
|
1003
1070
|
}
|
|
1004
1071
|
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`);
|
|
1005
1072
|
}
|
|
1006
|
-
|
|
1073
|
+
const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
|
|
1074
|
+
return { text: formatRgStdout(orderedStdout, p) };
|
|
1007
1075
|
}
|
|
1008
1076
|
export async function runGrepDetailed(env, root, p, signal) {
|
|
1009
1077
|
if (await detectRipgrep(env))
|
|
@@ -1018,16 +1086,74 @@ export async function runGlob(env, root, pattern, opts = {}, signal) {
|
|
|
1018
1086
|
}
|
|
1019
1087
|
export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
|
|
1020
1088
|
const t0 = Date.now();
|
|
1021
|
-
const
|
|
1089
|
+
const baseIgnore = await buildIgnore(env, root, signal);
|
|
1022
1090
|
const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
|
|
1023
1091
|
const start = opts.path ? (opts.path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
|
|
1092
|
+
const pat = normalizeGlobToken(pattern);
|
|
1093
|
+
const anchored = globIsAnchored(pattern);
|
|
1094
|
+
const re = globTokenToRegExp(pat, anchored, true);
|
|
1095
|
+
const startPrefix = start.replace(/[\\/]+$/, "") + (start.includes("\\") ? "\\" : "/");
|
|
1096
|
+
const startRelPrefix = startPrefix.startsWith(rootPrefix) ? startPrefix.slice(rootPrefix.length) : startPrefix;
|
|
1097
|
+
const toStartRel = (p) => {
|
|
1098
|
+
const s = p.startsWith(startPrefix)
|
|
1099
|
+
? p.slice(startPrefix.length)
|
|
1100
|
+
: startRelPrefix.length > 0 && p.startsWith(startRelPrefix)
|
|
1101
|
+
? p.slice(startRelPrefix.length)
|
|
1102
|
+
: p;
|
|
1103
|
+
return s.replace(/\\/g, "/");
|
|
1104
|
+
};
|
|
1105
|
+
const patternSegs = pat.split("/");
|
|
1106
|
+
const groupSpansSlash = /\{[^}]*\/[^}]*\}|\[[^\]]*\/[^\]]*\]/.test(pat);
|
|
1107
|
+
const prunable = anchored && !groupSpansSlash;
|
|
1108
|
+
const firstStarStar = patternSegs.findIndex((s) => s.includes("**"));
|
|
1109
|
+
const starStarIdx = firstStarStar === -1 ? Infinity : firstStarStar;
|
|
1110
|
+
const segRes = prunable ? patternSegs.map((s) => globTokenToRegExp(s, true, true)) : [];
|
|
1111
|
+
const isLiteralSeg = (s) => s.length > 0 && !/[*?[{]/.test(s);
|
|
1112
|
+
const literalAt = prunable ? patternSegs.map((s) => (isLiteralSeg(s) ? s : undefined)) : [];
|
|
1113
|
+
const literalAfterStarStar = new Set(prunable ? patternSegs.filter((s, i) => i > starStarIdx && isLiteralSeg(s)) : []);
|
|
1114
|
+
const mayContainMatch = (dirRel) => {
|
|
1115
|
+
if (!prunable)
|
|
1116
|
+
return true;
|
|
1117
|
+
const d = dirRel.split("/");
|
|
1118
|
+
if (d.length >= patternSegs.length && starStarIdx === Infinity)
|
|
1119
|
+
return false;
|
|
1120
|
+
const lim = Math.min(d.length, starStarIdx);
|
|
1121
|
+
for (let i = 0; i < lim; i++)
|
|
1122
|
+
if (!segRes[i].test(d[i]))
|
|
1123
|
+
return false;
|
|
1124
|
+
return true;
|
|
1125
|
+
};
|
|
1126
|
+
const explicitlyNamed = (sr) => {
|
|
1127
|
+
if (!prunable)
|
|
1128
|
+
return false;
|
|
1129
|
+
const segs = sr.split("/");
|
|
1130
|
+
const k = segs.length - 1;
|
|
1131
|
+
return k < starStarIdx ? literalAt[k] === segs[k] : literalAfterStarStar.has(segs[k]);
|
|
1132
|
+
};
|
|
1133
|
+
let ignoredDirs = 0;
|
|
1134
|
+
const rescued = [];
|
|
1135
|
+
const ignore = (relPath, isDir) => {
|
|
1136
|
+
const sr = toStartRel(relPath);
|
|
1137
|
+
if (isDir && !mayContainMatch(sr))
|
|
1138
|
+
return true;
|
|
1139
|
+
if (explicitlyNamed(sr)) {
|
|
1140
|
+
if (isDir && baseIgnore(relPath, true))
|
|
1141
|
+
rescued.push(sr + "/");
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
if (rescued.length > 0 && rescued.some((r) => sr.startsWith(r)))
|
|
1145
|
+
return false;
|
|
1146
|
+
const ig = baseIgnore(relPath, isDir);
|
|
1147
|
+
if (ig && isDir)
|
|
1148
|
+
ignoredDirs++;
|
|
1149
|
+
return ig;
|
|
1150
|
+
};
|
|
1024
1151
|
const walked = await walk(env, root, start, ignore, signal);
|
|
1025
1152
|
const caveat = walkCaveat(walked, false);
|
|
1026
1153
|
const files = [...walked.files, ...walked.nameOnly];
|
|
1027
|
-
const re = globTokenToRegExp(pattern, false, true);
|
|
1028
1154
|
const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
|
|
1029
1155
|
const cap = Math.max(1, Math.floor(opts.max ?? 500));
|
|
1030
|
-
const matchedAbs = files.filter((f) => re.test(
|
|
1156
|
+
const matchedAbs = files.filter((f) => re.test(toStartRel(f)) || (!anchored && re.test(basename(f))));
|
|
1031
1157
|
const haveMtimes = matchedAbs.length > 0 && matchedAbs.every((f) => walked.mtimes.has(f));
|
|
1032
1158
|
const matchedAll = haveMtimes
|
|
1033
1159
|
? matchedAbs
|
|
@@ -1044,6 +1170,9 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
|
|
|
1044
1170
|
: countIsComplete
|
|
1045
1171
|
? `\n(Showing ${matched.length} of ${totalMatches} matching files; ${totalMatches - matched.length} more are not listed. Narrow the pattern or path to see the rest.)`
|
|
1046
1172
|
: `\n(Showing the first ${matched.length} files; there are more than ${totalMatches} matches. Narrow the pattern or path to see the rest.)`;
|
|
1047
|
-
const
|
|
1173
|
+
const ignoreNote = matched.length === 0 && ignoredDirs > 0
|
|
1174
|
+
? `\n[note: ${ignoredDirs} ignored director${ignoredDirs === 1 ? "y was" : "ies were"} not searched (dependency/build/VCS trees and .gitignore) — name a directory in the pattern (e.g. "dist/**") or pass \`path\` to include it]`
|
|
1175
|
+
: "";
|
|
1176
|
+
const text = matched.length === 0 ? "No files matched." + ignoreNote + caveat : matched.join("\n") + capNote + caveat;
|
|
1048
1177
|
return { text, filenames: matched, numFiles: matched.length, truncated, durationMs: Date.now() - t0, totalMatches, countIsComplete };
|
|
1049
1178
|
}
|
|
@@ -23,7 +23,7 @@ export function createGiteaIssueTool(opts) {
|
|
|
23
23
|
const a = args;
|
|
24
24
|
const title = String(a.title ?? "").trim();
|
|
25
25
|
if (!title) {
|
|
26
|
-
return { content: "Issue not created: a non-empty title is required.", details: { error: "empty title" } };
|
|
26
|
+
return { content: "Issue not created: a non-empty title is required.", details: { error: "empty title" }, isError: true };
|
|
27
27
|
}
|
|
28
28
|
const modelLabels = opts.allowModelLabels && Array.isArray(a.labels) ? a.labels : [];
|
|
29
29
|
const labels = [...new Set([...(opts.defaultLabels ?? []), ...modelLabels])];
|
|
@@ -43,13 +43,14 @@ export function createGiteaIssueTool(opts) {
|
|
|
43
43
|
}
|
|
44
44
|
catch (e) {
|
|
45
45
|
const msg = e instanceof Error ? e.message : String(e);
|
|
46
|
-
return { content: `Issue not created: request failed (${msg}).`, details: { error: msg } };
|
|
46
|
+
return { content: `Issue not created: request failed (${msg}).`, details: { error: msg }, isError: true };
|
|
47
47
|
}
|
|
48
48
|
if (!res.ok) {
|
|
49
49
|
const detail = await res.text().catch(() => "");
|
|
50
50
|
return {
|
|
51
51
|
content: `Issue not created: Gitea returned HTTP ${res.status}. ${detail.slice(0, 300)}`,
|
|
52
52
|
details: { status: res.status },
|
|
53
|
+
isError: true,
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
const PARSE_FAILED = Symbol("json-parse-failed");
|
|
@@ -60,6 +61,7 @@ export function createGiteaIssueTool(opts) {
|
|
|
60
61
|
content: `Issue request returned HTTP ${res.status} but the response body was not a valid issue ` +
|
|
61
62
|
`(no number) — the issue may NOT have been created; verify before retrying.`,
|
|
62
63
|
details: { status: res.status, parsed: parsed === PARSE_FAILED ? "unparseable" : "no-number" },
|
|
64
|
+
isError: true,
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
const issue = parsed;
|
package/dist/tools/monitor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { defineTool } from "../core/tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
3
|
import { hasBackgroundShell } from "../core/background-shell.js";
|
|
4
4
|
import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
|
|
5
5
|
const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
|
|
@@ -11,8 +11,9 @@ const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its
|
|
|
11
11
|
"- stderr does NOT trigger notifications, but it is captured — read it any time with TaskOutput(task_id), " +
|
|
12
12
|
"which also serves the full re-readable stdout spool.\n" +
|
|
13
13
|
"- A monitor that produces too many events is stopped automatically (the stop notification says why).\n" +
|
|
14
|
-
"- Set persistent: true for a session-resident watch with no timeout;
|
|
15
|
-
"
|
|
14
|
+
"- Set persistent: true for a session-resident watch with no watch timeout of its own; it still ends when " +
|
|
15
|
+
"the execution environment's background time budget expires, when you stop it with TaskStop(task_id), or " +
|
|
16
|
+
"when the session is released.\n" +
|
|
16
17
|
"- After starting a monitor you do not need to poll — end your turn if nothing else is pending; events will " +
|
|
17
18
|
"reach you as notifications.\n" +
|
|
18
19
|
"- Use this for logs, dev servers, build watchers, or any wait-for-a-condition loop (e.g. `tail -f app.log`, " +
|
|
@@ -44,14 +45,14 @@ export function createMonitorTool(env, opts) {
|
|
|
44
45
|
description: `How long the watch may run before it is killed, in milliseconds (default ${MONITOR_DEFAULT_TIMEOUT_MS}, max ${MONITOR_MAX_TIMEOUT_MS}). Ignored when persistent is true.`,
|
|
45
46
|
})),
|
|
46
47
|
persistent: Type.Optional(Type.Boolean({
|
|
47
|
-
description: "Set to true for a session-resident monitor with no timeout (default false). It keeps watching until you stop it with TaskStop or the session ends.",
|
|
48
|
+
description: "Set to true for a session-resident monitor with no watch timeout of its own (default false). It keeps watching until the execution environment's background time budget expires, until you stop it with TaskStop, or until the session ends.",
|
|
48
49
|
})),
|
|
49
50
|
}),
|
|
50
51
|
effect: "write",
|
|
51
52
|
execute: async (args, ctx) => {
|
|
52
53
|
const { command, description, timeout_ms, persistent } = args;
|
|
53
54
|
if (!hasBackgroundShell(env)) {
|
|
54
|
-
return "Error (Monitor): this environment does not support background processes.";
|
|
55
|
+
return errorResult("Error (Monitor): this environment does not support background processes.");
|
|
55
56
|
}
|
|
56
57
|
const isPersistent = persistent === true;
|
|
57
58
|
const timeoutMs = Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeout_ms ?? MONITOR_DEFAULT_TIMEOUT_MS)));
|
|
@@ -60,7 +61,7 @@ export function createMonitorTool(env, opts) {
|
|
|
60
61
|
...(isPersistent ? {} : { timeout: Math.ceil(timeoutMs / 1000) + 5 }),
|
|
61
62
|
});
|
|
62
63
|
if (!spawn.ok)
|
|
63
|
-
return `Error (Monitor): ${spawn.error.message}
|
|
64
|
+
return errorResult(`Error (Monitor): ${spawn.error.message}`);
|
|
64
65
|
const sessionScoped = isPersistent && opts.sessionId !== undefined;
|
|
65
66
|
const owner = sessionScoped ? opts.sessionId : (ctx.taskId ?? opts.owner);
|
|
66
67
|
const scope = ctx.principal ?? opts.scope;
|
|
@@ -85,10 +86,13 @@ export function createMonitorTool(env, opts) {
|
|
|
85
86
|
}
|
|
86
87
|
catch (e) {
|
|
87
88
|
await env.killBackground(spawn.value.shellId).catch(() => undefined);
|
|
88
|
-
return `Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated
|
|
89
|
+
return errorResult(`Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`);
|
|
89
90
|
}
|
|
91
|
+
const bgTimeout = env.backgroundCapabilities.defaultBgTimeoutSec;
|
|
90
92
|
const lifetime = isPersistent
|
|
91
|
-
?
|
|
93
|
+
? typeof bgTimeout === "number"
|
|
94
|
+
? `It is persistent: no watch timeout of its own, but the execution environment kills background processes after ${bgTimeout}s; it runs until then, until you stop it, or until the session ends.`
|
|
95
|
+
: "It is persistent: no watch timeout of its own, but the execution environment's background time budget still applies; it runs until then, until you stop it, or until the session ends."
|
|
92
96
|
: `It will be killed after ${timeoutMs}ms if still running.`;
|
|
93
97
|
return onNotify !== undefined
|
|
94
98
|
? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { defineTool } from "../core/tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "../core/tools.js";
|
|
3
3
|
import { hasScheduler, isValidCronExpr, } from "../core/scheduler.js";
|
|
4
4
|
export const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
|
|
5
5
|
export const AUTONOMOUS_LOOP_SENTINEL = "<<autonomous-loop>>";
|
|
@@ -210,33 +210,33 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
210
210
|
execute: async (args) => {
|
|
211
211
|
const a = args;
|
|
212
212
|
if (a.schedule !== undefined && a.cron !== undefined) {
|
|
213
|
-
return "Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.";
|
|
213
|
+
return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
|
|
214
214
|
}
|
|
215
215
|
if (a.schedule === undefined && a.cron === undefined) {
|
|
216
|
-
return "Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.";
|
|
216
|
+
return errorResult("Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.");
|
|
217
217
|
}
|
|
218
218
|
if (a.prompt === undefined) {
|
|
219
|
-
return "Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.";
|
|
219
|
+
return errorResult("Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.");
|
|
220
220
|
}
|
|
221
221
|
const when = a.schedule ?? { kind: "cron", expr: a.cron };
|
|
222
222
|
if (when.kind === "cron") {
|
|
223
223
|
if (!isValidCronExpr(when.expr)) {
|
|
224
|
-
return `Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed
|
|
224
|
+
return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`);
|
|
225
225
|
}
|
|
226
226
|
const deepErr = cronScheduleError(when.expr);
|
|
227
227
|
if (deepErr)
|
|
228
|
-
return `Error (CronCreate): cron expression "${when.expr}" — ${deepErr}
|
|
228
|
+
return errorResult(`Error (CronCreate): cron expression "${when.expr}" — ${deepErr}`);
|
|
229
229
|
}
|
|
230
230
|
else if (a.recurring !== undefined) {
|
|
231
|
-
return `Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring
|
|
231
|
+
return errorResult(`Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring\`.`);
|
|
232
232
|
}
|
|
233
233
|
const durable = a.durable ?? a.schedule !== undefined;
|
|
234
234
|
if (!durable) {
|
|
235
235
|
if (sched.schedulerCapabilities.supportsSessionLifetime !== true) {
|
|
236
|
-
return "Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.";
|
|
236
|
+
return errorResult("Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.");
|
|
237
237
|
}
|
|
238
238
|
if (ctx.sessionId === undefined) {
|
|
239
|
-
return "Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.";
|
|
239
|
+
return errorResult("Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.");
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
const intent = {
|
|
@@ -248,7 +248,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
248
248
|
};
|
|
249
249
|
const r = await sched.schedule(intent, schedCtx);
|
|
250
250
|
if (!r.ok)
|
|
251
|
-
return `Error (CronCreate): ${r.error.message}
|
|
251
|
+
return errorResult(`Error (CronCreate): ${r.error.message}`);
|
|
252
252
|
const humanSchedule = when.kind === "cron"
|
|
253
253
|
? cronToHuman(when.expr)
|
|
254
254
|
: when.kind === "delay"
|
|
@@ -278,7 +278,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
278
278
|
if (!r.ok) {
|
|
279
279
|
if (r.error.code === "not_found")
|
|
280
280
|
return `CronDelete: no scheduled task "${id}" (already gone or not yours).`;
|
|
281
|
-
return `Error (CronDelete): ${r.error.message}
|
|
281
|
+
return errorResult(`Error (CronDelete): ${r.error.message}`);
|
|
282
282
|
}
|
|
283
283
|
return { content: `Cancelled scheduled task ${id}.`, details: { type: "cron-delete", id } };
|
|
284
284
|
},
|
|
@@ -292,7 +292,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
292
292
|
execute: async () => {
|
|
293
293
|
const r = await sched.list(schedCtx);
|
|
294
294
|
if (!r.ok)
|
|
295
|
-
return `Error (CronList): ${r.error.message}
|
|
295
|
+
return errorResult(`Error (CronList): ${r.error.message}`);
|
|
296
296
|
if (r.value.length === 0) {
|
|
297
297
|
return { content: "No scheduled tasks.", details: { type: "cron-list", jobs: [] } };
|
|
298
298
|
}
|
|
@@ -367,7 +367,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
367
367
|
if (a.stop === true) {
|
|
368
368
|
const pending = await listPendingWakeups();
|
|
369
369
|
if ("err" in pending)
|
|
370
|
-
return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}
|
|
370
|
+
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}`);
|
|
371
371
|
const cancelledWakeups = await cancelWakeups(pending.ids);
|
|
372
372
|
return {
|
|
373
373
|
content: `Loop stopped. Cancelled ${cancelledWakeups} pending wakeup(s); recurring cron tasks are unaffected.`,
|
|
@@ -375,10 +375,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
377
|
if (a.delaySeconds === undefined || a.reason === undefined || a.prompt === undefined) {
|
|
378
|
-
return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true
|
|
378
|
+
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`);
|
|
379
379
|
}
|
|
380
380
|
if (sched.schedulerCapabilities.supportsSessionWakeup === false) {
|
|
381
|
-
return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form
|
|
381
|
+
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`);
|
|
382
382
|
}
|
|
383
383
|
const clampedDelaySeconds = Math.min(3600, Math.max(60, Math.round(a.delaySeconds)));
|
|
384
384
|
const wasClamped = clampedDelaySeconds !== a.delaySeconds;
|
|
@@ -387,7 +387,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
387
387
|
await cancelWakeups(pending.ids);
|
|
388
388
|
const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, schedCtx);
|
|
389
389
|
if (!r.ok)
|
|
390
|
-
return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}
|
|
390
|
+
return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
|
|
391
391
|
const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
|
|
392
392
|
const hhmmss = new Date(scheduledFor).toTimeString().slice(0, 8);
|
|
393
393
|
const clampNote = wasClamped ? ` (clamped to ${clampedDelaySeconds}s from your requested value)` : "";
|