@narumitw/pi-subagents 3.0.1 → 3.1.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/README.md +80 -136
- package/dist/child-communication-bridge.ts +9 -12
- package/dist/child-communication-bridge.ts.map +2 -2
- package/dist/child-readiness-probe.ts +68 -0
- package/dist/child-readiness-probe.ts.map +7 -0
- package/dist/chunks/chunk-3FCHXUKZ.ts +98 -0
- package/dist/chunks/chunk-3FCHXUKZ.ts.map +7 -0
- package/dist/chunks/{chunk-UKTWUQRM.js → chunk-YMVSJ5GE.ts} +89 -152
- package/dist/chunks/chunk-YMVSJ5GE.ts.map +7 -0
- package/dist/chunks/chunk-ZOAFWLDJ.ts +130 -0
- package/dist/chunks/chunk-ZOAFWLDJ.ts.map +7 -0
- package/dist/index.ts +1201 -120
- package/dist/index.ts.map +4 -4
- package/docs/attachments.md +37 -0
- package/docs/design-principles.md +8 -2
- package/docs/messaging.md +23 -0
- package/docs/tools.md +22 -56
- package/package.json +56 -54
- package/src/attachment-utils.ts +104 -0
- package/src/broker-credentials.ts +132 -54
- package/src/child-communication-bridge.ts +117 -125
- package/src/child-communication-tools.ts +89 -117
- package/src/child-readiness-probe.ts +72 -0
- package/src/completion-renderer.ts +46 -48
- package/src/message-broker.ts +531 -561
- package/src/model-output.ts +25 -28
- package/src/process.ts +942 -619
- package/src/resource-attachments.ts +724 -0
- package/src/runtime.ts +471 -478
- package/src/skill-attachments.ts +302 -0
- package/src/subagents.ts +20 -23
- package/src/timeout.ts +24 -0
- package/src/tools.ts +389 -325
- package/src/types.ts +45 -58
- package/src/widget.ts +85 -91
- package/dist/chunks/chunk-UKTWUQRM.js.map +0 -7
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { type Dirent, statSync } from "node:fs";
|
|
2
|
+
import { opendir as opendirAsync, realpath as realpathAsync, stat as statAsync } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import ignore from "ignore";
|
|
6
|
+
import {
|
|
7
|
+
addAttachmentIgnoreRules,
|
|
8
|
+
type IgnoreMatcher,
|
|
9
|
+
isAbortError,
|
|
10
|
+
isWithin,
|
|
11
|
+
realpath,
|
|
12
|
+
throwIfAttachmentAborted,
|
|
13
|
+
toPosixPath,
|
|
14
|
+
} from "./attachment-utils.js";
|
|
15
|
+
|
|
16
|
+
export const MAX_SKILL_SCAN_DEPTH = 32;
|
|
17
|
+
export const MAX_SKILL_SCAN_ENTRIES = 4_096;
|
|
18
|
+
export const MAX_SKILL_SCAN_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
export const MAX_SKILL_IGNORE_BYTES = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
export async function assertLoadableSkills(
|
|
22
|
+
skillPaths: string[],
|
|
23
|
+
cwd: string,
|
|
24
|
+
canonicalCwd: string,
|
|
25
|
+
projectTrusted: boolean,
|
|
26
|
+
signal?: AbortSignal,
|
|
27
|
+
): Promise<SkillScanState> {
|
|
28
|
+
const scanState: SkillScanState = {
|
|
29
|
+
entries: 0,
|
|
30
|
+
skillBytes: 0,
|
|
31
|
+
ignoreBytes: 0,
|
|
32
|
+
ancestors: new Set<string>(),
|
|
33
|
+
signal,
|
|
34
|
+
};
|
|
35
|
+
for (const skillPath of skillPaths) {
|
|
36
|
+
throwIfAttachmentAborted(signal);
|
|
37
|
+
const candidates = await collectBoundedSkillCandidates(skillPath, scanState);
|
|
38
|
+
throwIfAttachmentAborted(signal);
|
|
39
|
+
const result = loadExplicitSkills([skillPath], cwd);
|
|
40
|
+
if (result.skills.length === 0) {
|
|
41
|
+
throw new Error("Subagent skill path must contain at least one loadable Pi skill.");
|
|
42
|
+
}
|
|
43
|
+
assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
|
|
44
|
+
assertNoOmittedSkillDiagnostics(result);
|
|
45
|
+
assertNoSkillNameCollisions(result.diagnostics);
|
|
46
|
+
await assertNoSilentlyOmittedSkills(skillPath, cwd, result, canonicalCwd, projectTrusted, candidates, signal);
|
|
47
|
+
}
|
|
48
|
+
if (skillPaths.length > 1) {
|
|
49
|
+
throwIfAttachmentAborted(signal);
|
|
50
|
+
const result = loadExplicitSkills(skillPaths, cwd);
|
|
51
|
+
assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
|
|
52
|
+
assertNoSkillNameCollisions(result.diagnostics);
|
|
53
|
+
}
|
|
54
|
+
return scanState;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadExplicitSkills(skillPaths: string[], cwd: string): ReturnType<typeof loadSkills> {
|
|
58
|
+
return loadSkills({ cwd, agentDir: cwd, skillPaths, includeDefaults: false });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertNoOmittedSkillDiagnostics(result: ReturnType<typeof loadSkills>): void {
|
|
62
|
+
const loadedPaths = new Set(result.skills.map((skill) => skill.filePath));
|
|
63
|
+
const omitted = result.diagnostics.some(
|
|
64
|
+
(diagnostic) =>
|
|
65
|
+
diagnostic.type === "error" ||
|
|
66
|
+
(diagnostic.type === "warning" && (!diagnostic.path || !loadedPaths.has(diagnostic.path))),
|
|
67
|
+
);
|
|
68
|
+
if (omitted) {
|
|
69
|
+
throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertNoSkillNameCollisions(diagnostics: ReturnType<typeof loadSkills>["diagnostics"]): void {
|
|
74
|
+
if (
|
|
75
|
+
diagnostics.some((diagnostic) => diagnostic.type === "collision" && diagnostic.collision?.resourceType === "skill")
|
|
76
|
+
) {
|
|
77
|
+
throw new Error("Subagent skill attachments must not contain duplicate skill names.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertNoUntrustedProjectSkills(
|
|
82
|
+
result: ReturnType<typeof loadSkills>,
|
|
83
|
+
cwd: string,
|
|
84
|
+
canonicalCwd: string,
|
|
85
|
+
projectTrusted: boolean,
|
|
86
|
+
): void {
|
|
87
|
+
if (projectTrusted) return;
|
|
88
|
+
for (const skill of result.skills) {
|
|
89
|
+
const lexicalPath = path.resolve(skill.filePath);
|
|
90
|
+
const canonicalPath = realpath(skill.filePath, "Loaded subagent skill");
|
|
91
|
+
if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
|
|
92
|
+
throw new Error("Subagent skill cannot load a project path because the project is not trusted.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function assertNoSilentlyOmittedSkills(
|
|
98
|
+
skillPath: string,
|
|
99
|
+
cwd: string,
|
|
100
|
+
result: ReturnType<typeof loadSkills>,
|
|
101
|
+
canonicalCwd: string,
|
|
102
|
+
projectTrusted: boolean,
|
|
103
|
+
candidates: string[],
|
|
104
|
+
signal?: AbortSignal,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
if (!statSync(skillPath).isDirectory()) return;
|
|
107
|
+
const loadedPaths = new Set(result.skills.map((skill) => realpath(skill.filePath, "Loaded subagent skill")));
|
|
108
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
109
|
+
if (index > 0 && index % 64 === 0) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
110
|
+
throwIfAttachmentAborted(signal);
|
|
111
|
+
const candidateResult = loadExplicitSkills([candidate], cwd);
|
|
112
|
+
assertNoUntrustedProjectSkills(candidateResult, cwd, canonicalCwd, projectTrusted);
|
|
113
|
+
if (candidateResult.skills.length === 0) {
|
|
114
|
+
if (path.basename(candidate) === "SKILL.md") throwInvalidDeclaredSkill();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const loaded = candidateResult.skills.some((skill) =>
|
|
118
|
+
loadedPaths.has(realpath(skill.filePath, "Loaded subagent skill")),
|
|
119
|
+
);
|
|
120
|
+
if (!loaded) throwInvalidDeclaredSkill();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SkillScanState {
|
|
125
|
+
entries: number;
|
|
126
|
+
skillBytes: number;
|
|
127
|
+
ignoreBytes: number;
|
|
128
|
+
ancestors: Set<string>;
|
|
129
|
+
signal?: AbortSignal;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function collectBoundedSkillCandidates(skillPath: string, state: SkillScanState): Promise<string[]> {
|
|
133
|
+
throwIfAttachmentAborted(state.signal);
|
|
134
|
+
const stats = await statAsync(skillPath);
|
|
135
|
+
throwIfAttachmentAborted(state.signal);
|
|
136
|
+
if (!stats.isDirectory()) {
|
|
137
|
+
if (stats.isFile() && skillPath.endsWith(".md")) addSkillBytes(state, stats.size);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return collectSkillCandidates(skillPath, true, ignore(), skillPath, 0, state);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function collectSkillCandidates(
|
|
144
|
+
directory: string,
|
|
145
|
+
includeRootMarkdown: boolean,
|
|
146
|
+
ignoreMatcher: IgnoreMatcher,
|
|
147
|
+
rootDirectory: string,
|
|
148
|
+
depth: number,
|
|
149
|
+
state: SkillScanState,
|
|
150
|
+
): Promise<string[]> {
|
|
151
|
+
throwIfAttachmentAborted(state.signal);
|
|
152
|
+
if (depth > MAX_SKILL_SCAN_DEPTH) throwSkillScanLimit();
|
|
153
|
+
let canonicalDirectory: string;
|
|
154
|
+
try {
|
|
155
|
+
canonicalDirectory = await realpathAsync(directory);
|
|
156
|
+
} catch {
|
|
157
|
+
throwInvalidDeclaredSkill();
|
|
158
|
+
}
|
|
159
|
+
throwIfAttachmentAborted(state.signal);
|
|
160
|
+
if (state.ancestors.has(canonicalDirectory)) {
|
|
161
|
+
throw new Error("Subagent skill attachment must not contain a recursive directory link.");
|
|
162
|
+
}
|
|
163
|
+
state.ancestors.add(canonicalDirectory);
|
|
164
|
+
try {
|
|
165
|
+
await addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, state.signal, (_ignorePath, bytes) => {
|
|
166
|
+
state.ignoreBytes += bytes;
|
|
167
|
+
if (state.ignoreBytes > MAX_SKILL_IGNORE_BYTES) throwSkillScanLimit();
|
|
168
|
+
});
|
|
169
|
+
let directoryHandle: Awaited<ReturnType<typeof opendirAsync>>;
|
|
170
|
+
try {
|
|
171
|
+
directoryHandle = await opendirAsync(directory);
|
|
172
|
+
} catch {
|
|
173
|
+
throwInvalidDeclaredSkill();
|
|
174
|
+
}
|
|
175
|
+
const entries: Dirent[] = [];
|
|
176
|
+
try {
|
|
177
|
+
for await (const entry of directoryHandle) {
|
|
178
|
+
throwIfAttachmentAborted(state.signal);
|
|
179
|
+
state.entries++;
|
|
180
|
+
if (state.entries > MAX_SKILL_SCAN_ENTRIES) throwSkillScanLimit();
|
|
181
|
+
entries.push(entry);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (isAbortError(error) || isSkillScanLimitError(error)) throw error;
|
|
185
|
+
throwInvalidDeclaredSkill();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rootSkill = entries.find((entry) => entry.name === "SKILL.md");
|
|
189
|
+
if (rootSkill) {
|
|
190
|
+
const rootPath = path.join(directory, rootSkill.name);
|
|
191
|
+
const relativePath = toPosixPath(path.relative(rootDirectory, rootPath));
|
|
192
|
+
if (!ignoreMatcher.ignores(relativePath)) {
|
|
193
|
+
let rootStats: Awaited<ReturnType<typeof statAsync>>;
|
|
194
|
+
try {
|
|
195
|
+
rootStats = await statAsync(rootPath);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isAbortError(error)) throw error;
|
|
198
|
+
throwInvalidDeclaredSkill();
|
|
199
|
+
}
|
|
200
|
+
throwIfAttachmentAborted(state.signal);
|
|
201
|
+
if (rootStats.isFile()) {
|
|
202
|
+
addSkillBytes(state, rootStats.size);
|
|
203
|
+
return [rootPath];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const candidates: string[] = [];
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
throwIfAttachmentAborted(state.signal);
|
|
211
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
212
|
+
const candidate = path.join(directory, entry.name);
|
|
213
|
+
const relativePath = toPosixPath(path.relative(rootDirectory, candidate));
|
|
214
|
+
let candidateStats: Awaited<ReturnType<typeof statAsync>>;
|
|
215
|
+
try {
|
|
216
|
+
candidateStats = await statAsync(candidate);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (isAbortError(error)) throw error;
|
|
219
|
+
throwIfAttachmentAborted(state.signal);
|
|
220
|
+
if (
|
|
221
|
+
!ignoreMatcher.ignores(relativePath) &&
|
|
222
|
+
(entry.name === "SKILL.md" || (includeRootMarkdown && entry.name.endsWith(".md")))
|
|
223
|
+
) {
|
|
224
|
+
throwInvalidDeclaredSkill();
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
throwIfAttachmentAborted(state.signal);
|
|
229
|
+
if (ignoreMatcher.ignores(candidateStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
|
|
230
|
+
if (candidateStats.isDirectory()) {
|
|
231
|
+
candidates.push(
|
|
232
|
+
...(await collectSkillCandidates(candidate, false, ignoreMatcher, rootDirectory, depth + 1, state)),
|
|
233
|
+
);
|
|
234
|
+
} else if (
|
|
235
|
+
candidateStats.isFile() &&
|
|
236
|
+
(entry.name === "SKILL.md" || (includeRootMarkdown && entry.name.endsWith(".md")))
|
|
237
|
+
) {
|
|
238
|
+
addSkillBytes(state, candidateStats.size);
|
|
239
|
+
candidates.push(candidate);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return candidates;
|
|
243
|
+
} finally {
|
|
244
|
+
state.ancestors.delete(canonicalDirectory);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addSkillBytes(state: SkillScanState, bytes: number): void {
|
|
249
|
+
state.skillBytes += bytes;
|
|
250
|
+
if (state.skillBytes > MAX_SKILL_SCAN_BYTES) throwSkillScanLimit();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isSkillScanLimitError(error: unknown): boolean {
|
|
254
|
+
return error instanceof Error && error.name === "SkillScanLimitError";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function throwSkillScanLimit(): never {
|
|
258
|
+
const error = new Error(
|
|
259
|
+
`Subagent skill attachment exceeds traversal limits (${MAX_SKILL_SCAN_ENTRIES} entries, depth ${MAX_SKILL_SCAN_DEPTH}, ${MAX_SKILL_SCAN_BYTES} skill bytes, or ${MAX_SKILL_IGNORE_BYTES} ignore bytes).`,
|
|
260
|
+
);
|
|
261
|
+
error.name = "SkillScanLimitError";
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function throwInvalidDeclaredSkill(): never {
|
|
266
|
+
throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function assertLoadablePackageSkills(
|
|
270
|
+
packageSkillPaths: string[],
|
|
271
|
+
explicitSkillPaths: string[],
|
|
272
|
+
cwd: string,
|
|
273
|
+
scanState: SkillScanState,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const uniquePackageSkillPaths = deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths);
|
|
276
|
+
for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
|
|
277
|
+
if (index > 0 && index % 64 === 0) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
278
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
279
|
+
await collectBoundedSkillCandidates(skillPath, scanState);
|
|
280
|
+
}
|
|
281
|
+
for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
|
|
282
|
+
if (index > 0 && index % 64 === 0) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
283
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
284
|
+
const result = loadExplicitSkills([skillPath], cwd);
|
|
285
|
+
if (result.skills.length === 0) throwInvalidDeclaredSkill();
|
|
286
|
+
assertNoOmittedSkillDiagnostics(result);
|
|
287
|
+
}
|
|
288
|
+
throwIfAttachmentAborted(scanState.signal);
|
|
289
|
+
assertNoSkillNameCollisions(loadExplicitSkills([...uniquePackageSkillPaths, ...explicitSkillPaths], cwd).diagnostics);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function deduplicatePackageSkillPaths(packageSkillPaths: string[], explicitSkillPaths: string[]): string[] {
|
|
293
|
+
const seenPaths = new Set(explicitSkillPaths.map((skillPath) => realpath(skillPath, "Loaded subagent skill")));
|
|
294
|
+
const uniquePaths: string[] = [];
|
|
295
|
+
for (const skillPath of packageSkillPaths) {
|
|
296
|
+
const canonicalPath = realpath(skillPath, "Loaded subagent package skill");
|
|
297
|
+
if (seenPaths.has(canonicalPath)) continue;
|
|
298
|
+
seenPaths.add(canonicalPath);
|
|
299
|
+
uniquePaths.push(skillPath);
|
|
300
|
+
}
|
|
301
|
+
return uniquePaths;
|
|
302
|
+
}
|
package/src/subagents.ts
CHANGED
|
@@ -5,29 +5,26 @@ import { createSubagentWidgetController } from "./widget.js";
|
|
|
5
5
|
|
|
6
6
|
export type SubagentsDependencies = SubagentToolsDependencies;
|
|
7
7
|
|
|
8
|
-
export default function subagents(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const widget = createSubagentWidgetController(tools.runtime);
|
|
15
|
-
let activeSession: ExtensionContext["sessionManager"] | undefined;
|
|
16
|
-
let sessionGeneration = 0;
|
|
8
|
+
export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDependencies = {}): void {
|
|
9
|
+
registerCompletionRenderer(pi);
|
|
10
|
+
const tools = registerSubagentTools(pi, dependencies);
|
|
11
|
+
const widget = createSubagentWidgetController(tools.runtime);
|
|
12
|
+
let activeSession: ExtensionContext["sessionManager"] | undefined;
|
|
13
|
+
let sessionGeneration = 0;
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
16
|
+
activeSession = ctx.sessionManager;
|
|
17
|
+
const generation = ++sessionGeneration;
|
|
18
|
+
await tools.startSession();
|
|
19
|
+
if (generation !== sessionGeneration) return;
|
|
20
|
+
widget.start(ctx);
|
|
21
|
+
});
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
24
|
+
if (ctx.sessionManager !== activeSession) return;
|
|
25
|
+
sessionGeneration++;
|
|
26
|
+
activeSession = undefined;
|
|
27
|
+
widget.shutdown(ctx);
|
|
28
|
+
await tools.shutdown();
|
|
29
|
+
});
|
|
33
30
|
}
|
package/src/timeout.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
2
|
+
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
3
|
+
|
|
4
|
+
export function resolveTimeoutMs(timeout: number | undefined): number | undefined {
|
|
5
|
+
if (timeout === undefined) return undefined;
|
|
6
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
7
|
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
8
|
+
}
|
|
9
|
+
const timeoutMs = timeout * 1000;
|
|
10
|
+
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
11
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
12
|
+
}
|
|
13
|
+
return timeoutMs;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function prepareTimeoutArguments(args: unknown): Record<string, unknown> {
|
|
17
|
+
if (!args || typeof args !== "object") return args as Record<string, unknown>;
|
|
18
|
+
if (!Object.hasOwn(args, "timeoutMs")) return args as Record<string, unknown>;
|
|
19
|
+
const record = args as Record<string, unknown>;
|
|
20
|
+
if (typeof record.timeoutMs !== "number") return record;
|
|
21
|
+
const { timeoutMs, ...prepared } = record;
|
|
22
|
+
if (prepared.timeout === undefined) return { ...prepared, timeout: timeoutMs / 1000 };
|
|
23
|
+
return prepared;
|
|
24
|
+
}
|