@rudderhq/agent-runtime-utils 0.2.5-canary.8 → 0.2.5
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/server-utils.cli.d.ts +95 -0
- package/dist/server-utils.cli.d.ts.map +1 -0
- package/dist/server-utils.cli.js +788 -0
- package/dist/server-utils.cli.js.map +1 -0
- package/dist/server-utils.d.ts +4 -219
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.instructions.d.ts +41 -0
- package/dist/server-utils.instructions.d.ts.map +1 -0
- package/dist/server-utils.instructions.js +252 -0
- package/dist/server-utils.instructions.js.map +1 -0
- package/dist/server-utils.js +4 -1549
- package/dist/server-utils.js.map +1 -1
- package/dist/server-utils.process.d.ts +122 -0
- package/dist/server-utils.process.d.ts.map +1 -0
- package/dist/server-utils.process.js +249 -0
- package/dist/server-utils.process.js.map +1 -0
- package/dist/server-utils.prompts.d.ts +53 -0
- package/dist/server-utils.prompts.d.ts.map +1 -0
- package/dist/server-utils.prompts.js +271 -0
- package/dist/server-utils.prompts.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +14 -4
package/dist/server-utils.js
CHANGED
|
@@ -1,1550 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
export const runningProcesses = new Map();
|
|
7
|
-
function isChildProcessAlive(child) {
|
|
8
|
-
const pid = child.pid;
|
|
9
|
-
if (typeof pid !== "number" || pid <= 0)
|
|
10
|
-
return false;
|
|
11
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
12
|
-
return false;
|
|
13
|
-
try {
|
|
14
|
-
process.kill(pid, 0);
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
const code = error instanceof Error && "code" in error ? error.code : null;
|
|
19
|
-
return code === "EPERM";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
23
|
-
export const MAX_EXCERPT_BYTES = 32 * 1024;
|
|
24
|
-
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
|
|
25
|
-
const RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES = [
|
|
26
|
-
"../../server/resources/bundled-skills",
|
|
27
|
-
"../../skills",
|
|
28
|
-
"../../../../../server/resources/bundled-skills",
|
|
29
|
-
];
|
|
30
|
-
const DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES = [
|
|
31
|
-
".aws",
|
|
32
|
-
".azure",
|
|
33
|
-
".config/gh",
|
|
34
|
-
".config/gcloud",
|
|
35
|
-
".config/op",
|
|
36
|
-
".config/vercel",
|
|
37
|
-
".config/configstore",
|
|
38
|
-
".docker",
|
|
39
|
-
".fly",
|
|
40
|
-
".git-credentials",
|
|
41
|
-
".gnupg",
|
|
42
|
-
".kube",
|
|
43
|
-
".netrc",
|
|
44
|
-
".npmrc",
|
|
45
|
-
".ssh",
|
|
46
|
-
".vercel",
|
|
47
|
-
"Library/Application Support/gh",
|
|
48
|
-
"Library/Application Support/com.heroku.cli",
|
|
49
|
-
];
|
|
50
|
-
const DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS = [
|
|
51
|
-
{
|
|
52
|
-
command: "gh",
|
|
53
|
-
authCheckArgs: ["auth", "status"],
|
|
54
|
-
credentialEntries: [".config/gh", "Library/Application Support/gh"],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
command: "vercel",
|
|
58
|
-
authCheckArgs: ["whoami"],
|
|
59
|
-
credentialEntries: [".config/vercel", ".vercel", ".config/configstore"],
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
function normalizePathSlashes(value) {
|
|
63
|
-
return value.replaceAll("\\", "/");
|
|
64
|
-
}
|
|
65
|
-
function isMaintainerOnlySkillTarget(candidate) {
|
|
66
|
-
const normalized = normalizePathSlashes(candidate);
|
|
67
|
-
return (normalized.includes("/server/resources/bundled-skills/")
|
|
68
|
-
|| normalized.includes("/.agents/skills/"));
|
|
69
|
-
}
|
|
70
|
-
function skillLocationLabel(value) {
|
|
71
|
-
if (typeof value !== "string")
|
|
72
|
-
return null;
|
|
73
|
-
const trimmed = value.trim();
|
|
74
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
75
|
-
}
|
|
76
|
-
function buildManagedSkillOrigin() {
|
|
77
|
-
return {
|
|
78
|
-
origin: "organization_managed",
|
|
79
|
-
readOnly: false,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function compactSkillText(value) {
|
|
83
|
-
if (typeof value !== "string")
|
|
84
|
-
return null;
|
|
85
|
-
const compacted = value
|
|
86
|
-
.replace(/\r\n/g, "\n")
|
|
87
|
-
.split("\n")
|
|
88
|
-
.map((line) => line.trim())
|
|
89
|
-
.filter(Boolean)
|
|
90
|
-
.join(" ")
|
|
91
|
-
.replace(/\s+/g, " ")
|
|
92
|
-
.trim();
|
|
93
|
-
return compacted.length > 0 ? compacted : null;
|
|
94
|
-
}
|
|
95
|
-
function parseSkillFrontmatterMetadata(markdown) {
|
|
96
|
-
const match = markdown.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
97
|
-
if (!match) {
|
|
98
|
-
return { name: null, description: null };
|
|
99
|
-
}
|
|
100
|
-
const yaml = match[1];
|
|
101
|
-
const nameMatch = yaml.match(/^name:\s*["']?(.*?)["']?\s*$/m);
|
|
102
|
-
const descriptionMatch = yaml.match(/^description:\s*(?:>\s*\n((?:\s{2,}[^\n]*\n?)+)|[|]\s*\n((?:\s{2,}[^\n]*\n?)+)|["']?(.*?)["']?\s*$)/m);
|
|
103
|
-
return {
|
|
104
|
-
name: compactSkillText(nameMatch?.[1] ?? null),
|
|
105
|
-
description: compactSkillText(descriptionMatch?.[1] ?? descriptionMatch?.[2] ?? descriptionMatch?.[3] ?? null),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
async function readSkillMetadataFromDirectory(skillDir) {
|
|
109
|
-
const skillFile = path.join(skillDir, "SKILL.md");
|
|
110
|
-
try {
|
|
111
|
-
const markdown = await fs.readFile(skillFile, "utf8");
|
|
112
|
-
return parseSkillFrontmatterMetadata(markdown);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return { name: null, description: null };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
export async function readSkillMetadataFromPath(candidatePath) {
|
|
119
|
-
if (typeof candidatePath !== "string" || candidatePath.trim().length === 0) {
|
|
120
|
-
return { name: null, description: null };
|
|
121
|
-
}
|
|
122
|
-
const resolvedPath = path.resolve(candidatePath);
|
|
123
|
-
const skillDir = path.basename(resolvedPath).toLowerCase() === "skill.md"
|
|
124
|
-
? path.dirname(resolvedPath)
|
|
125
|
-
: resolvedPath;
|
|
126
|
-
return readSkillMetadataFromDirectory(skillDir);
|
|
127
|
-
}
|
|
128
|
-
function resolveInstalledEntryTarget(skillsHome, entryName, dirent, linkedPath) {
|
|
129
|
-
const fullPath = path.join(skillsHome, entryName);
|
|
130
|
-
if (dirent.isSymbolicLink()) {
|
|
131
|
-
return {
|
|
132
|
-
targetPath: linkedPath ? path.resolve(path.dirname(fullPath), linkedPath) : null,
|
|
133
|
-
kind: "symlink",
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if (dirent.isDirectory()) {
|
|
137
|
-
return { targetPath: fullPath, kind: "directory" };
|
|
138
|
-
}
|
|
139
|
-
return { targetPath: fullPath, kind: "file" };
|
|
140
|
-
}
|
|
141
|
-
export function parseObject(value) {
|
|
142
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
143
|
-
return {};
|
|
144
|
-
}
|
|
145
|
-
return value;
|
|
146
|
-
}
|
|
147
|
-
export function asString(value, fallback) {
|
|
148
|
-
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
149
|
-
}
|
|
150
|
-
export function asNumber(value, fallback) {
|
|
151
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
152
|
-
}
|
|
153
|
-
export function asBoolean(value, fallback) {
|
|
154
|
-
return typeof value === "boolean" ? value : fallback;
|
|
155
|
-
}
|
|
156
|
-
export function asStringArray(value) {
|
|
157
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
158
|
-
}
|
|
159
|
-
export function parseJson(value) {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(value);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
export function appendWithCap(prev, chunk, cap = MAX_CAPTURE_BYTES) {
|
|
168
|
-
const combined = prev + chunk;
|
|
169
|
-
return combined.length > cap ? combined.slice(combined.length - cap) : combined;
|
|
170
|
-
}
|
|
171
|
-
export function resolvePathValue(obj, dottedPath) {
|
|
172
|
-
const parts = dottedPath.split(".");
|
|
173
|
-
let cursor = obj;
|
|
174
|
-
for (const part of parts) {
|
|
175
|
-
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) {
|
|
176
|
-
return "";
|
|
177
|
-
}
|
|
178
|
-
cursor = cursor[part];
|
|
179
|
-
}
|
|
180
|
-
if (cursor === null || cursor === undefined)
|
|
181
|
-
return "";
|
|
182
|
-
if (typeof cursor === "string")
|
|
183
|
-
return cursor;
|
|
184
|
-
if (typeof cursor === "number" || typeof cursor === "boolean")
|
|
185
|
-
return String(cursor);
|
|
186
|
-
try {
|
|
187
|
-
return JSON.stringify(cursor);
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return "";
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
export function renderTemplate(template, data) {
|
|
194
|
-
return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path));
|
|
195
|
-
}
|
|
196
|
-
const ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT = 16_000;
|
|
197
|
-
function truncateIssueDocumentBody(body) {
|
|
198
|
-
if (body.length <= ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT)
|
|
199
|
-
return body;
|
|
200
|
-
return `${body.slice(0, ISSUE_DOCUMENT_PROMPT_BODY_CHAR_LIMIT).trimEnd()}\n\n[Document truncated in prompt. Fetch the full document with the Rudder CLI.]`;
|
|
201
|
-
}
|
|
202
|
-
function formatDocumentHeading(key, title) {
|
|
203
|
-
const cleanTitle = typeof title === "string" ? title.trim() : "";
|
|
204
|
-
return cleanTitle ? `### ${key} — ${cleanTitle}` : `### ${key}`;
|
|
205
|
-
}
|
|
206
|
-
function readIssueDocumentPromptIssueId(input) {
|
|
207
|
-
const planIssueId = typeof input.planDocument?.issueId === "string" ? input.planDocument.issueId.trim() : "";
|
|
208
|
-
if (planIssueId)
|
|
209
|
-
return planIssueId;
|
|
210
|
-
for (const summary of input.documentSummaries ?? []) {
|
|
211
|
-
const issueId = typeof summary.issueId === "string" ? summary.issueId.trim() : "";
|
|
212
|
-
if (issueId)
|
|
213
|
-
return issueId;
|
|
214
|
-
}
|
|
215
|
-
return "<issue-id>";
|
|
216
|
-
}
|
|
217
|
-
export function buildIssueDocumentsPrompt(input) {
|
|
218
|
-
if (!input)
|
|
219
|
-
return "";
|
|
220
|
-
const sections = [];
|
|
221
|
-
const planKey = input.planDocument?.key?.trim() || input.legacyPlanDocument?.key?.trim() || "plan";
|
|
222
|
-
const planBody = input.planDocument?.body?.trim() || input.legacyPlanDocument?.body?.trim() || "";
|
|
223
|
-
if (planBody) {
|
|
224
|
-
sections.push([
|
|
225
|
-
formatDocumentHeading(planKey, input.planDocument?.title),
|
|
226
|
-
input.legacyPlanDocument ? "Source: legacy `<plan>` block in the issue description." : `Source: issue document \`${planKey}\`.`,
|
|
227
|
-
"",
|
|
228
|
-
truncateIssueDocumentBody(planBody),
|
|
229
|
-
].join("\n"));
|
|
230
|
-
}
|
|
231
|
-
const otherDocuments = (input.documentSummaries ?? []).filter((doc) => {
|
|
232
|
-
const key = typeof doc.key === "string" ? doc.key.trim() : "";
|
|
233
|
-
return key && key !== planKey;
|
|
234
|
-
});
|
|
235
|
-
if (otherDocuments.length > 0) {
|
|
236
|
-
const issueId = readIssueDocumentPromptIssueId(input);
|
|
237
|
-
sections.push([
|
|
238
|
-
"### Additional Issue Documents",
|
|
239
|
-
...otherDocuments.map((doc) => {
|
|
240
|
-
const key = doc.key?.trim() || "document";
|
|
241
|
-
const title = doc.title?.trim();
|
|
242
|
-
const revision = typeof doc.latestRevisionNumber === "number" ? `, revision ${doc.latestRevisionNumber}` : "";
|
|
243
|
-
const titlePart = title ? ` — ${title}` : "";
|
|
244
|
-
return `- \`${key}\`${titlePart}${revision}. Fetch with \`rudder issue documents get ${issueId} ${key} --json\`.`;
|
|
245
|
-
}),
|
|
246
|
-
].join("\n"));
|
|
247
|
-
}
|
|
248
|
-
if (sections.length === 0)
|
|
249
|
-
return "";
|
|
250
|
-
return ["## Issue Documents", ...sections].join("\n\n");
|
|
251
|
-
}
|
|
252
|
-
// Default prompt templates for different wake sources
|
|
253
|
-
export const DEFAULT_AGENT_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). Continue your Rudder work.
|
|
254
|
-
|
|
255
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
256
|
-
|
|
257
|
-
{{context.issueDocumentsPrompt}}`;
|
|
258
|
-
export const ISSUE_ASSIGN_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). You have been assigned to work on an issue.
|
|
259
|
-
|
|
260
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
261
|
-
|
|
262
|
-
## Task Context
|
|
263
|
-
|
|
264
|
-
**Issue:** {{issue.title}}
|
|
265
|
-
**ID:** {{issue.id}}
|
|
266
|
-
**Status:** {{issue.status}}
|
|
267
|
-
**Priority:** {{issue.priority}}
|
|
268
|
-
|
|
269
|
-
**Description:**
|
|
270
|
-
{{issue.description}}
|
|
271
|
-
|
|
272
|
-
{{context.issueDocumentsPrompt}}
|
|
273
|
-
|
|
274
|
-
Your task is to review this issue and begin working on it. Use the available tools to explore the codebase, understand the requirements, and implement a solution.`;
|
|
275
|
-
export const COMMENT_MENTION_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). You were mentioned in a comment and your attention is needed.
|
|
276
|
-
|
|
277
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
278
|
-
|
|
279
|
-
## Context
|
|
280
|
-
|
|
281
|
-
**Issue:** {{issue.title}}
|
|
282
|
-
**ID:** {{issue.id}}
|
|
283
|
-
|
|
284
|
-
**Issue Description:**
|
|
285
|
-
{{issue.description}}
|
|
286
|
-
|
|
287
|
-
{{context.issueDocumentsPrompt}}
|
|
288
|
-
|
|
289
|
-
**Comment:**
|
|
290
|
-
From: {{comment.authorLabel}} ({{comment.authorKind}})
|
|
291
|
-
|
|
292
|
-
{{comment.body}}
|
|
293
|
-
|
|
294
|
-
Please review the comment above and respond or take action as appropriate.
|
|
295
|
-
An @mention is an explicit request for attention or collaboration, not an automatic transfer of issue ownership. Only checkout or self-assign when the comment explicitly asks you to take ownership and the normal issue workflow allows it.`;
|
|
296
|
-
export const ISSUE_COMMENTED_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). There is a new comment on an issue you own.
|
|
297
|
-
|
|
298
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
299
|
-
|
|
300
|
-
## Context
|
|
301
|
-
|
|
302
|
-
**Issue:** {{issue.title}}
|
|
303
|
-
**ID:** {{issue.id}}
|
|
304
|
-
**Status:** {{issue.status}}
|
|
305
|
-
|
|
306
|
-
**Issue Description:**
|
|
307
|
-
{{issue.description}}
|
|
308
|
-
|
|
309
|
-
{{context.issueDocumentsPrompt}}
|
|
310
|
-
|
|
311
|
-
**Latest Comment:**
|
|
312
|
-
From: {{comment.authorLabel}} ({{comment.authorKind}})
|
|
313
|
-
|
|
314
|
-
{{comment.body}}
|
|
315
|
-
|
|
316
|
-
Review the new comment and continue the issue from the current state. Respond or take action as needed.`;
|
|
317
|
-
export const ISSUE_CHANGES_REQUESTED_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). A reviewer requested changes on an issue you own.
|
|
318
|
-
|
|
319
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
320
|
-
|
|
321
|
-
## Context
|
|
322
|
-
|
|
323
|
-
**Issue:** {{issue.title}}
|
|
324
|
-
**ID:** {{issue.id}}
|
|
325
|
-
**Status:** {{issue.status}}
|
|
326
|
-
|
|
327
|
-
**Issue Description:**
|
|
328
|
-
{{issue.description}}
|
|
329
|
-
|
|
330
|
-
{{context.issueDocumentsPrompt}}
|
|
331
|
-
|
|
332
|
-
**Reviewer Comment:**
|
|
333
|
-
From: {{comment.authorLabel}} ({{comment.authorKind}})
|
|
334
|
-
|
|
335
|
-
{{comment.body}}
|
|
336
|
-
|
|
337
|
-
Review the requested changes and continue the issue from the current state. Address the reviewer feedback before handing it back for review.`;
|
|
338
|
-
export const ISSUE_RECOVERY_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). This is a recovery run, not a fresh task.
|
|
339
|
-
|
|
340
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
341
|
-
|
|
342
|
-
## Recovery Context
|
|
343
|
-
|
|
344
|
-
- Original Run ID: {{context.recovery.originalRunId}}
|
|
345
|
-
- Failure Kind: {{context.recovery.failureKind}}
|
|
346
|
-
- Failure Summary: {{context.recovery.failureSummary}}
|
|
347
|
-
- Recovery Trigger: {{context.recovery.recoveryTrigger}}
|
|
348
|
-
- Recovery Mode: {{context.recovery.recoveryMode}}
|
|
349
|
-
|
|
350
|
-
## Current Issue Context
|
|
351
|
-
|
|
352
|
-
- Issue: {{issue.title}}
|
|
353
|
-
- ID: {{issue.id}}
|
|
354
|
-
- Status: {{issue.status}}
|
|
355
|
-
- Priority: {{issue.priority}}
|
|
356
|
-
|
|
357
|
-
- Description:
|
|
358
|
-
{{issue.description}}
|
|
359
|
-
|
|
360
|
-
{{context.issueDocumentsPrompt}}
|
|
361
|
-
|
|
362
|
-
Before doing anything else, inspect what the previous run already completed and any side effects it may have caused. Continue the remaining work from the current state. Avoid blindly re-running the whole task.`;
|
|
363
|
-
export const RECOVERY_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). This is a recovery run, not a fresh task.
|
|
364
|
-
|
|
365
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
366
|
-
|
|
367
|
-
## Recovery Context
|
|
368
|
-
|
|
369
|
-
- Original Run ID: {{context.recovery.originalRunId}}
|
|
370
|
-
- Failure Kind: {{context.recovery.failureKind}}
|
|
371
|
-
- Failure Summary: {{context.recovery.failureSummary}}
|
|
372
|
-
- Recovery Trigger: {{context.recovery.recoveryTrigger}}
|
|
373
|
-
- Recovery Mode: {{context.recovery.recoveryMode}}
|
|
374
|
-
|
|
375
|
-
Before doing anything else, inspect what the previous run already completed and any side effects it may have caused. Continue the remaining work from the current state. Avoid blindly re-running the whole task.`;
|
|
376
|
-
export const ISSUE_PASSIVE_FOLLOWUP_PROMPT_TEMPLATE = `You are agent {{agent.id}} ({{agent.name}}). This is a passive issue follow-up, not a fresh assignment and not a failure recovery.
|
|
377
|
-
|
|
378
|
-
{{context.rudderWorkspace.orgResourcesPrompt}}
|
|
379
|
-
|
|
380
|
-
## Why You Were Woken
|
|
381
|
-
|
|
382
|
-
The previous run ended without sufficient issue close-out.
|
|
383
|
-
|
|
384
|
-
- Origin Run ID: {{context.passiveFollowup.originRunId}}
|
|
385
|
-
- Previous Run ID: {{context.passiveFollowup.previousRunId}}
|
|
386
|
-
- Attempt: {{context.passiveFollowup.attempt}} / {{context.passiveFollowup.maxAttempts}}
|
|
387
|
-
Reason: {{context.passiveFollowup.reason}}
|
|
388
|
-
|
|
389
|
-
## Current Issue Context
|
|
390
|
-
|
|
391
|
-
- Issue: {{issue.title}}
|
|
392
|
-
- ID: {{issue.id}}
|
|
393
|
-
- Status: {{issue.status}}
|
|
394
|
-
- Priority: {{issue.priority}}
|
|
395
|
-
|
|
396
|
-
- Description:
|
|
397
|
-
{{issue.description}}
|
|
398
|
-
|
|
399
|
-
{{context.issueDocumentsPrompt}}
|
|
400
|
-
|
|
401
|
-
Before changing the issue, inspect the current issue state and any side effects from the previous run. Then do exactly one close-out action: add a progress comment, mark the issue done, block it with a reason, or hand it off explicitly with explanation.`;
|
|
402
|
-
/**
|
|
403
|
-
* Selects the base heartbeat prompt template used by runtimes before final prompt assembly.
|
|
404
|
-
*
|
|
405
|
-
* Prompt shape by wake trigger:
|
|
406
|
-
* - assignment:
|
|
407
|
-
* "You are agent ... You have been assigned ..."
|
|
408
|
-
* Includes issue title/id/status/priority/description so the agent can start immediately.
|
|
409
|
-
* - comment.mention:
|
|
410
|
-
* "You were mentioned in a comment ..."
|
|
411
|
-
* Includes issue summary plus mention comment author/body so the agent can respond without extra fetches.
|
|
412
|
-
* Mentions request attention; ownership transfer still requires an explicit handoff.
|
|
413
|
-
* - issue_changes_requested:
|
|
414
|
-
* "A reviewer requested changes on an issue you own ..."
|
|
415
|
-
* Includes issue summary plus reviewer attribution/comment body so the assignee can act on feedback immediately.
|
|
416
|
-
* - issue_commented:
|
|
417
|
-
* "There is a new comment on an issue you own ..."
|
|
418
|
-
* Includes issue summary plus the newest comment author/body so the assignee can continue immediately.
|
|
419
|
-
* - recovery:
|
|
420
|
-
* "This is a recovery run, not a fresh task ..."
|
|
421
|
-
* Includes original run id, failure metadata, and a continue-preferred instruction to
|
|
422
|
-
* inspect prior progress/side effects before resuming.
|
|
423
|
-
* - passive issue follow-up:
|
|
424
|
-
* "This is a passive issue follow-up, not a fresh assignment ..."
|
|
425
|
-
* Includes close-out lineage and tells the agent to comment, finish, block, or hand off.
|
|
426
|
-
* - fallback:
|
|
427
|
-
* Generic "Continue your Rudder work."
|
|
428
|
-
*
|
|
429
|
-
* Concrete rendered example (comment mention):
|
|
430
|
-
* "You are agent agent-456 (Backend Worker). You were mentioned in a comment and your attention is needed.
|
|
431
|
-
* Issue: Stabilize queue worker
|
|
432
|
-
* Comment: @agent please check timeout handling in retry path."
|
|
433
|
-
*
|
|
434
|
-
* Reasoning:
|
|
435
|
-
* - Keep backward compatibility: custom configured templates always win.
|
|
436
|
-
* - Keep first-turn latency low: include the minimum task context directly in prompt text.
|
|
437
|
-
* - Keep behavior deterministic across runtimes: template selection is centralized here.
|
|
438
|
-
*
|
|
439
|
-
* See also:
|
|
440
|
-
* - doc/plans/2026-04-07-agent-prompt-context-injection.md
|
|
441
|
-
* - doc/DEVELOPING.md
|
|
442
|
-
*/
|
|
443
|
-
export function selectPromptTemplate(configuredTemplate, context) {
|
|
444
|
-
// If user configured a custom template, use it
|
|
445
|
-
if (configuredTemplate?.trim()) {
|
|
446
|
-
return configuredTemplate;
|
|
447
|
-
}
|
|
448
|
-
// Select based on wake source/reason
|
|
449
|
-
const wakeSource = String(context.wakeSource ?? "");
|
|
450
|
-
const wakeReason = String(context.wakeReason ?? "");
|
|
451
|
-
const recovery = context.recovery;
|
|
452
|
-
const hasRecoveryContext = typeof recovery === "object" &&
|
|
453
|
-
recovery !== null &&
|
|
454
|
-
!Array.isArray(recovery) &&
|
|
455
|
-
typeof recovery.originalRunId === "string";
|
|
456
|
-
if (hasRecoveryContext || wakeReason === "process_lost_retry" || wakeReason === "retry_failed_run") {
|
|
457
|
-
return typeof context.issue === "object" && context.issue !== null && !Array.isArray(context.issue)
|
|
458
|
-
? ISSUE_RECOVERY_PROMPT_TEMPLATE
|
|
459
|
-
: RECOVERY_PROMPT_TEMPLATE;
|
|
460
|
-
}
|
|
461
|
-
if (wakeReason === "issue_passive_followup") {
|
|
462
|
-
return ISSUE_PASSIVE_FOLLOWUP_PROMPT_TEMPLATE;
|
|
463
|
-
}
|
|
464
|
-
if (wakeReason === "issue_changes_requested") {
|
|
465
|
-
return ISSUE_CHANGES_REQUESTED_PROMPT_TEMPLATE;
|
|
466
|
-
}
|
|
467
|
-
if (wakeSource === "assignment" || wakeReason === "issue_assigned") {
|
|
468
|
-
return ISSUE_ASSIGN_PROMPT_TEMPLATE;
|
|
469
|
-
}
|
|
470
|
-
if (wakeSource === "comment.mention" || wakeReason === "issue_comment_mentioned") {
|
|
471
|
-
return COMMENT_MENTION_PROMPT_TEMPLATE;
|
|
472
|
-
}
|
|
473
|
-
if (wakeReason === "issue_commented") {
|
|
474
|
-
return ISSUE_COMMENTED_PROMPT_TEMPLATE;
|
|
475
|
-
}
|
|
476
|
-
return DEFAULT_AGENT_PROMPT_TEMPLATE;
|
|
477
|
-
}
|
|
478
|
-
export function joinPromptSections(sections, separator = "\n\n") {
|
|
479
|
-
return sections
|
|
480
|
-
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
481
|
-
.filter(Boolean)
|
|
482
|
-
.join(separator);
|
|
483
|
-
}
|
|
484
|
-
export const RUDDER_AGENT_OPERATING_CONTRACT = [
|
|
485
|
-
"# Rudder Agent Operating Contract",
|
|
486
|
-
"",
|
|
487
|
-
"Your home directory is `$AGENT_HOME`. Everything personal to you -- life, memory, knowledge -- lives there. Other agents may have their own folders and you may update them when necessary.",
|
|
488
|
-
"",
|
|
489
|
-
"Use these paths consistently:",
|
|
490
|
-
"",
|
|
491
|
-
"- Personal instructions live under `$AGENT_HOME/instructions`.",
|
|
492
|
-
"- Personal memory lives under `$AGENT_HOME/memory`.",
|
|
493
|
-
"- Tacit memory instruction lives at `$AGENT_HOME/instructions/MEMORY.md` and is automatically loaded when present.",
|
|
494
|
-
"- Personal skills live under `$AGENT_HOME/skills`.",
|
|
495
|
-
"- Shared organization workspace root lives under `$RUDDER_ORG_WORKSPACE_ROOT`.",
|
|
496
|
-
"- Shared organization skills live under `$RUDDER_ORG_SKILLS_DIR`.",
|
|
497
|
-
"- Shared organization plans live under `$RUDDER_ORG_PLANS_DIR`.",
|
|
498
|
-
"- Shared organization artifacts live under `$RUDDER_ORG_ARTIFACTS_DIR`.",
|
|
499
|
-
"- Durable generated outputs such as screenshots, images, mockups, reports, CSVs, handoff logs, and other user-visible files should be written under `$RUDDER_ORG_ARTIFACTS_DIR` when available.",
|
|
500
|
-
"- Use `/tmp` only for transient scratch files and temporary verification artifacts; do not put durable work product there.",
|
|
501
|
-
"- Local trusted runtimes may expose the host operator home as `$RUDDER_OPERATOR_HOME`; use it only when a local skill or script intentionally needs operator-owned desktop app or CLI state. Do not replace `$HOME` with it.",
|
|
502
|
-
"- Durable shared work output should prefer these managed workspace paths instead of ad-hoc top-level `projects/` folders.",
|
|
503
|
-
"",
|
|
504
|
-
"When you create or copy a skill under `$AGENT_HOME/skills/<slug>/`, check the agent's Skills snapshot before claiming it will load in future runs. If it is installed but not enabled, say exactly that future runs will not load it until enabled, and offer to enable it with `rudder agent skills enable <agent-id> <selection-ref>` when you have permission.",
|
|
505
|
-
"",
|
|
506
|
-
"When you write issue comments or chat replies, match the language of the user's or board's most recent substantive message unless they explicitly ask for a different language.",
|
|
507
|
-
"",
|
|
508
|
-
"When an issue comment, done comment, or blocker comment cites visual evidence from a local screenshot/image path, attach the image with the Rudder CLI `--image <path>` option instead of leaving only the filesystem path in the text.",
|
|
509
|
-
"",
|
|
510
|
-
"## Memory and Planning",
|
|
511
|
-
"",
|
|
512
|
-
"You MUST use the `para-memory-files` skill for all memory operations: storing facts, writing daily notes, creating entities, running weekly synthesis, recalling past context, and managing plans. The skill defines your three-layer memory system (knowledge graph, daily notes, tacit knowledge), the PARA folder structure, atomic fact schemas, memory decay rules, and recall/planning conventions.",
|
|
513
|
-
"",
|
|
514
|
-
"Keep stable preferences and operating lessons in `$AGENT_HOME/instructions/MEMORY.md`. Use `$AGENT_HOME/memory/YYYY-MM-DD.md` for daily notes and `$AGENT_HOME/life/` for structured long-term memory; those files are not auto-loaded.",
|
|
515
|
-
"",
|
|
516
|
-
"Invoke it whenever you need to remember, retrieve, or organize anything.",
|
|
517
|
-
"",
|
|
518
|
-
"## Safety Considerations",
|
|
519
|
-
"",
|
|
520
|
-
"- Never exfiltrate secrets or private data.",
|
|
521
|
-
"- Do not perform any destructive commands unless explicitly requested by the board.",
|
|
522
|
-
].join("\n");
|
|
523
|
-
function toPromptPath(pathValue) {
|
|
524
|
-
return pathValue.split(path.sep).join("/");
|
|
525
|
-
}
|
|
526
|
-
function isInsidePath(parentPath, childPath) {
|
|
527
|
-
const relativePath = path.relative(parentPath, childPath);
|
|
528
|
-
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
529
|
-
}
|
|
530
|
-
function displayInstructionPath(filePath, instructionsFilePath) {
|
|
531
|
-
const resolvedFilePath = path.resolve(filePath);
|
|
532
|
-
const resolvedInstructionsPath = path.resolve(instructionsFilePath);
|
|
533
|
-
const instructionsDir = path.dirname(resolvedInstructionsPath);
|
|
534
|
-
if (path.basename(instructionsDir) === "instructions") {
|
|
535
|
-
const agentHome = path.dirname(instructionsDir);
|
|
536
|
-
if (isInsidePath(agentHome, resolvedFilePath)) {
|
|
537
|
-
const relativePath = path.relative(agentHome, resolvedFilePath);
|
|
538
|
-
return relativePath ? `$AGENT_HOME/${toPromptPath(relativePath)}` : "$AGENT_HOME";
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return filePath;
|
|
542
|
-
}
|
|
543
|
-
function displayInstructionDir(filePath, instructionsFilePath) {
|
|
544
|
-
const displayPath = displayInstructionPath(filePath, instructionsFilePath);
|
|
545
|
-
const lastSlash = displayPath.lastIndexOf("/");
|
|
546
|
-
return lastSlash >= 0 ? `${displayPath.slice(0, lastSlash)}/` : "";
|
|
547
|
-
}
|
|
548
|
-
export async function loadAgentInstructionsPrefix(input) {
|
|
549
|
-
const instructionsFilePath = input.instructionsFilePath.trim();
|
|
550
|
-
const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
|
551
|
-
const displayInstructionsFilePath = instructionsFilePath
|
|
552
|
-
? displayInstructionPath(instructionsFilePath, instructionsFilePath)
|
|
553
|
-
: "";
|
|
554
|
-
const displayInstructionsDir = instructionsFilePath
|
|
555
|
-
? displayInstructionDir(instructionsFilePath, instructionsFilePath)
|
|
556
|
-
: "";
|
|
557
|
-
const warningStream = input.warningStream ?? "stdout";
|
|
558
|
-
const operatingContractSection = `${RUDDER_AGENT_OPERATING_CONTRACT}\n\n` +
|
|
559
|
-
"The above Rudder agent operating contract was injected by Rudder at runtime.";
|
|
560
|
-
const empty = {
|
|
561
|
-
prefix: operatingContractSection,
|
|
562
|
-
commandNotes: ["Loaded Rudder agent operating contract from runtime code"],
|
|
563
|
-
instructionsFilePath,
|
|
564
|
-
instructionsDir,
|
|
565
|
-
soulFilePath: null,
|
|
566
|
-
toolsFilePath: null,
|
|
567
|
-
memoryFilePath: null,
|
|
568
|
-
readFailed: false,
|
|
569
|
-
metrics: {
|
|
570
|
-
instructionsChars: operatingContractSection.length,
|
|
571
|
-
operatingContractChars: operatingContractSection.length,
|
|
572
|
-
instructionEntryChars: 0,
|
|
573
|
-
soulChars: 0,
|
|
574
|
-
toolsChars: 0,
|
|
575
|
-
memoryChars: 0,
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
if (!instructionsFilePath)
|
|
579
|
-
return empty;
|
|
580
|
-
const loadedPaths = new Set();
|
|
581
|
-
const commandNotes = [...empty.commandNotes];
|
|
582
|
-
let entrySection = "";
|
|
583
|
-
try {
|
|
584
|
-
const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
|
|
585
|
-
loadedPaths.add(path.resolve(instructionsFilePath));
|
|
586
|
-
entrySection =
|
|
587
|
-
`${instructionsContents}\n\n` +
|
|
588
|
-
`The above agent instructions were loaded from ${displayInstructionsFilePath}. ` +
|
|
589
|
-
`Resolve any relative file references from ${displayInstructionsDir}.`;
|
|
590
|
-
await input.onLog("stdout", `[rudder] Loaded agent instructions file: ${displayInstructionsFilePath}\n`);
|
|
591
|
-
}
|
|
592
|
-
catch (err) {
|
|
593
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
594
|
-
await input.onLog(warningStream, `[rudder] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
|
|
595
|
-
commandNotes.push(`Configured instructionsFilePath ${displayInstructionsFilePath}, but file could not be read; continuing without injected instructions.`);
|
|
596
|
-
}
|
|
597
|
-
async function loadSiblingInstructionFile(siblingInput) {
|
|
598
|
-
const filePath = path.join(path.dirname(instructionsFilePath), siblingInput.fileName);
|
|
599
|
-
const resolvedPath = path.resolve(filePath);
|
|
600
|
-
const displayFilePath = displayInstructionPath(filePath, instructionsFilePath);
|
|
601
|
-
const displayFileDir = displayInstructionDir(filePath, instructionsFilePath);
|
|
602
|
-
if (loadedPaths.has(resolvedPath))
|
|
603
|
-
return { path: filePath, section: "" };
|
|
604
|
-
try {
|
|
605
|
-
const contents = await fs.readFile(filePath, "utf8");
|
|
606
|
-
loadedPaths.add(resolvedPath);
|
|
607
|
-
await input.onLog("stdout", `[rudder] Loaded ${siblingInput.logLabel}: ${displayFilePath}\n`);
|
|
608
|
-
return {
|
|
609
|
-
path: filePath,
|
|
610
|
-
section: `${contents}\n\n` +
|
|
611
|
-
`The above ${siblingInput.label} were loaded from ${displayFilePath}. ` +
|
|
612
|
-
`Resolve any relative file references from ${displayFileDir}.`,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
if (err.code !== "ENOENT") {
|
|
617
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
618
|
-
await input.onLog(warningStream, `[rudder] Warning: could not read ${siblingInput.logLabel} "${filePath}": ${reason}\n`);
|
|
619
|
-
}
|
|
620
|
-
return { path: null, section: "" };
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
const soul = await loadSiblingInstructionFile({
|
|
624
|
-
fileName: "SOUL.md",
|
|
625
|
-
label: "agent role and persona instructions",
|
|
626
|
-
logLabel: "agent soul instructions file",
|
|
627
|
-
});
|
|
628
|
-
if (soul.section && soul.path) {
|
|
629
|
-
commandNotes.push(`Loaded agent soul instructions from ${displayInstructionPath(soul.path, instructionsFilePath)}`);
|
|
630
|
-
}
|
|
631
|
-
const tools = await loadSiblingInstructionFile({
|
|
632
|
-
fileName: "TOOLS.md",
|
|
633
|
-
label: "agent tool notes",
|
|
634
|
-
logLabel: "agent tool notes file",
|
|
635
|
-
});
|
|
636
|
-
if (tools.section && tools.path) {
|
|
637
|
-
commandNotes.push(`Loaded agent tool notes from ${displayInstructionPath(tools.path, instructionsFilePath)}`);
|
|
638
|
-
}
|
|
639
|
-
const memory = await loadSiblingInstructionFile({
|
|
640
|
-
fileName: "MEMORY.md",
|
|
641
|
-
label: "agent memory instructions",
|
|
642
|
-
logLabel: "agent memory instructions file",
|
|
643
|
-
});
|
|
644
|
-
if (memory.section && memory.path) {
|
|
645
|
-
commandNotes.push(`Loaded agent memory instructions from ${displayInstructionPath(memory.path, instructionsFilePath)}`);
|
|
646
|
-
}
|
|
647
|
-
const memoryFilePath = memory.section ? memory.path : null;
|
|
648
|
-
const memorySection = memory.section;
|
|
649
|
-
if (entrySection)
|
|
650
|
-
commandNotes.splice(1, 0, `Loaded agent instructions from ${displayInstructionsFilePath}`);
|
|
651
|
-
const prefix = joinPromptSections([operatingContractSection, entrySection, soul.section, tools.section, memorySection]);
|
|
652
|
-
return {
|
|
653
|
-
prefix,
|
|
654
|
-
commandNotes,
|
|
655
|
-
instructionsFilePath,
|
|
656
|
-
instructionsDir,
|
|
657
|
-
soulFilePath: soul.section ? soul.path : null,
|
|
658
|
-
toolsFilePath: tools.section ? tools.path : null,
|
|
659
|
-
memoryFilePath,
|
|
660
|
-
readFailed: !entrySection,
|
|
661
|
-
metrics: {
|
|
662
|
-
instructionsChars: prefix.length,
|
|
663
|
-
operatingContractChars: operatingContractSection.length,
|
|
664
|
-
instructionEntryChars: entrySection.length,
|
|
665
|
-
soulChars: soul.section.length,
|
|
666
|
-
toolsChars: tools.section.length,
|
|
667
|
-
memoryChars: memorySection.length,
|
|
668
|
-
},
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
export function redactEnvForLogs(env) {
|
|
672
|
-
const redacted = {};
|
|
673
|
-
for (const [key, value] of Object.entries(env)) {
|
|
674
|
-
redacted[key] = SENSITIVE_ENV_KEY.test(key) ? "***REDACTED***" : value;
|
|
675
|
-
}
|
|
676
|
-
return redacted;
|
|
677
|
-
}
|
|
678
|
-
export function buildRudderEnv(agent) {
|
|
679
|
-
const resolveHostForUrl = (rawHost) => {
|
|
680
|
-
const host = rawHost.trim();
|
|
681
|
-
if (!host || host === "0.0.0.0" || host === "::")
|
|
682
|
-
return "localhost";
|
|
683
|
-
if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]"))
|
|
684
|
-
return `[${host}]`;
|
|
685
|
-
return host;
|
|
686
|
-
};
|
|
687
|
-
const vars = {
|
|
688
|
-
RUDDER_AGENT_ID: agent.id,
|
|
689
|
-
RUDDER_ORG_ID: agent.orgId,
|
|
690
|
-
};
|
|
691
|
-
const runtimeHost = resolveHostForUrl(process.env.RUDDER_LISTEN_HOST ?? process.env.HOST ?? "localhost");
|
|
692
|
-
const runtimePort = process.env.RUDDER_LISTEN_PORT ?? process.env.PORT ?? "3100";
|
|
693
|
-
const apiUrl = process.env.RUDDER_API_URL ?? `http://${runtimeHost}:${runtimePort}`;
|
|
694
|
-
vars.RUDDER_API_URL = apiUrl;
|
|
695
|
-
return vars;
|
|
696
|
-
}
|
|
697
|
-
export function defaultPathForPlatform() {
|
|
698
|
-
if (process.platform === "win32") {
|
|
699
|
-
return "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem";
|
|
700
|
-
}
|
|
701
|
-
return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
702
|
-
}
|
|
703
|
-
function windowsPathExts(env) {
|
|
704
|
-
return (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
|
|
705
|
-
}
|
|
706
|
-
async function pathExists(candidate) {
|
|
707
|
-
try {
|
|
708
|
-
await fs.access(candidate, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
catch {
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
async function fileExists(candidate) {
|
|
716
|
-
try {
|
|
717
|
-
await fs.access(candidate, fsConstants.F_OK);
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
catch {
|
|
721
|
-
return false;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
async function resolveCommandPath(command, cwd, env) {
|
|
725
|
-
const hasPathSeparator = command.includes("/") || command.includes("\\");
|
|
726
|
-
if (hasPathSeparator) {
|
|
727
|
-
const absolute = path.isAbsolute(command) ? command : path.resolve(cwd, command);
|
|
728
|
-
return (await pathExists(absolute)) ? absolute : null;
|
|
729
|
-
}
|
|
730
|
-
const pathValue = env.PATH ?? env.Path ?? "";
|
|
731
|
-
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
732
|
-
const dirs = pathValue.split(delimiter).filter(Boolean);
|
|
733
|
-
const exts = process.platform === "win32" ? windowsPathExts(env) : [""];
|
|
734
|
-
const hasExtension = process.platform === "win32" && path.extname(command).length > 0;
|
|
735
|
-
for (const dir of dirs) {
|
|
736
|
-
const candidates = process.platform === "win32"
|
|
737
|
-
? hasExtension
|
|
738
|
-
? [path.join(dir, command)]
|
|
739
|
-
: exts.map((ext) => path.join(dir, `${command}${ext}`))
|
|
740
|
-
: [path.join(dir, command)];
|
|
741
|
-
for (const candidate of candidates) {
|
|
742
|
-
if (await pathExists(candidate))
|
|
743
|
-
return candidate;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return null;
|
|
747
|
-
}
|
|
748
|
-
function quoteForCmd(arg) {
|
|
749
|
-
if (!arg.length)
|
|
750
|
-
return '""';
|
|
751
|
-
const escaped = arg.replace(/"/g, '""');
|
|
752
|
-
return /[\s"&<>|^()]/.test(escaped) ? `"${escaped}"` : escaped;
|
|
753
|
-
}
|
|
754
|
-
async function resolveSpawnTarget(command, args, cwd, env) {
|
|
755
|
-
const resolved = await resolveCommandPath(command, cwd, env);
|
|
756
|
-
const executable = resolved ?? command;
|
|
757
|
-
if (process.platform !== "win32") {
|
|
758
|
-
return { command: executable, args };
|
|
759
|
-
}
|
|
760
|
-
if (/\.(cmd|bat)$/i.test(executable)) {
|
|
761
|
-
const shell = env.ComSpec || process.env.ComSpec || "cmd.exe";
|
|
762
|
-
const commandLine = [quoteForCmd(executable), ...args.map(quoteForCmd)].join(" ");
|
|
763
|
-
return {
|
|
764
|
-
command: shell,
|
|
765
|
-
args: ["/d", "/s", "/c", commandLine],
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
return { command: executable, args };
|
|
769
|
-
}
|
|
770
|
-
export function ensurePathInEnv(env) {
|
|
771
|
-
if (typeof env.PATH === "string" && env.PATH.length > 0)
|
|
772
|
-
return env;
|
|
773
|
-
if (typeof env.Path === "string" && env.Path.length > 0)
|
|
774
|
-
return env;
|
|
775
|
-
return { ...env, PATH: defaultPathForPlatform() };
|
|
776
|
-
}
|
|
777
|
-
function prependPathEntry(env, entry) {
|
|
778
|
-
const normalized = ensurePathInEnv(env);
|
|
779
|
-
const pathKey = typeof normalized.PATH === "string" ? "PATH" : "Path";
|
|
780
|
-
const current = normalized[pathKey] ?? "";
|
|
781
|
-
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
782
|
-
const segments = current.split(delimiter).filter(Boolean);
|
|
783
|
-
if (segments.includes(entry))
|
|
784
|
-
return normalized;
|
|
785
|
-
return {
|
|
786
|
-
...normalized,
|
|
787
|
-
[pathKey]: current.length > 0 ? `${entry}${delimiter}${current}` : entry,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
async function findAncestorWithFile(startDir, relativePath, maxDepth = 12) {
|
|
791
|
-
let current = path.resolve(startDir);
|
|
792
|
-
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
793
|
-
const candidate = path.join(current, relativePath);
|
|
794
|
-
if (await fileExists(candidate))
|
|
795
|
-
return candidate;
|
|
796
|
-
const parent = path.dirname(current);
|
|
797
|
-
if (parent === current)
|
|
798
|
-
break;
|
|
799
|
-
current = parent;
|
|
800
|
-
}
|
|
801
|
-
return null;
|
|
802
|
-
}
|
|
803
|
-
function shellQuote(arg) {
|
|
804
|
-
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
805
|
-
}
|
|
806
|
-
async function resolveRudderCliShimTarget(moduleDir) {
|
|
807
|
-
const packagedCli = await findAncestorWithFile(moduleDir, "desktop-cli.js");
|
|
808
|
-
if (packagedCli) {
|
|
809
|
-
return {
|
|
810
|
-
command: process.execPath,
|
|
811
|
-
args: [packagedCli],
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
const repoRoot = await findAncestorWithFile(moduleDir, path.join("cli", "src", "index.ts"));
|
|
815
|
-
if (!repoRoot)
|
|
816
|
-
return null;
|
|
817
|
-
const rootDir = path.dirname(path.dirname(path.dirname(repoRoot)));
|
|
818
|
-
const tsxEntry = path.join(rootDir, "cli", "node_modules", "tsx", "dist", "cli.mjs");
|
|
819
|
-
const cliSource = path.join(rootDir, "cli", "src", "index.ts");
|
|
820
|
-
if (await fileExists(tsxEntry)) {
|
|
821
|
-
return {
|
|
822
|
-
command: process.execPath,
|
|
823
|
-
args: [tsxEntry, cliSource],
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
const builtCliEntry = path.join(rootDir, "cli", "dist", "index.js");
|
|
827
|
-
if (await fileExists(builtCliEntry)) {
|
|
828
|
-
return {
|
|
829
|
-
command: process.execPath,
|
|
830
|
-
args: [builtCliEntry],
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
async function materializeRudderCliShim(target) {
|
|
836
|
-
const hash = createHash("sha1")
|
|
837
|
-
.update(JSON.stringify({ command: target.command, args: target.args, platform: process.platform }))
|
|
838
|
-
.digest("hex")
|
|
839
|
-
.slice(0, 12);
|
|
840
|
-
const shimDir = path.join(os.tmpdir(), "rudder-cli-shims", hash);
|
|
841
|
-
await fs.mkdir(shimDir, { recursive: true });
|
|
842
|
-
if (process.platform === "win32") {
|
|
843
|
-
const shimPath = path.join(shimDir, "rudder.cmd");
|
|
844
|
-
const commandLine = [quoteForCmd(target.command), ...target.args.map(quoteForCmd), "%*"].join(" ");
|
|
845
|
-
await fs.writeFile(shimPath, `@echo off\r\n${commandLine}\r\n`, "utf8");
|
|
846
|
-
return shimPath;
|
|
847
|
-
}
|
|
848
|
-
const shimPath = path.join(shimDir, "rudder");
|
|
849
|
-
const commandLine = [target.command, ...target.args].map(shellQuote).join(" ");
|
|
850
|
-
await fs.writeFile(shimPath, `#!/bin/sh\nexec ${commandLine} "$@"\n`, "utf8");
|
|
851
|
-
await fs.chmod(shimPath, 0o755);
|
|
852
|
-
return shimPath;
|
|
853
|
-
}
|
|
854
|
-
export async function ensureRudderCliInPath(moduleDir, env) {
|
|
855
|
-
const normalized = ensurePathInEnv(env);
|
|
856
|
-
const target = await resolveRudderCliShimTarget(moduleDir);
|
|
857
|
-
if (!target) {
|
|
858
|
-
return normalized;
|
|
859
|
-
}
|
|
860
|
-
const shimPath = await materializeRudderCliShim(target);
|
|
861
|
-
return prependPathEntry(normalized, path.dirname(shimPath));
|
|
862
|
-
}
|
|
863
|
-
export async function ensureAbsoluteDirectory(cwd, opts = {}) {
|
|
864
|
-
if (!path.isAbsolute(cwd)) {
|
|
865
|
-
throw new Error(`Working directory must be an absolute path: "${cwd}"`);
|
|
866
|
-
}
|
|
867
|
-
const assertDirectory = async () => {
|
|
868
|
-
const stats = await fs.stat(cwd);
|
|
869
|
-
if (!stats.isDirectory()) {
|
|
870
|
-
throw new Error(`Working directory is not a directory: "${cwd}"`);
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
try {
|
|
874
|
-
await assertDirectory();
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
catch (err) {
|
|
878
|
-
const code = err.code;
|
|
879
|
-
if (!opts.createIfMissing || code !== "ENOENT") {
|
|
880
|
-
if (code === "ENOENT") {
|
|
881
|
-
throw new Error(`Working directory does not exist: "${cwd}"`);
|
|
882
|
-
}
|
|
883
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
try {
|
|
887
|
-
await fs.mkdir(cwd, { recursive: true });
|
|
888
|
-
await assertDirectory();
|
|
889
|
-
}
|
|
890
|
-
catch (err) {
|
|
891
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
892
|
-
throw new Error(`Could not create working directory "${cwd}": ${reason}`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
export async function resolveRudderSkillsDir(moduleDir, additionalCandidates = []) {
|
|
896
|
-
const candidates = [
|
|
897
|
-
...RUDDER_SKILL_ROOT_RELATIVE_CANDIDATES.map((relativePath) => path.resolve(moduleDir, relativePath)),
|
|
898
|
-
...additionalCandidates.map((candidate) => path.resolve(candidate)),
|
|
899
|
-
];
|
|
900
|
-
const seenRoots = new Set();
|
|
901
|
-
for (const root of candidates) {
|
|
902
|
-
if (seenRoots.has(root))
|
|
903
|
-
continue;
|
|
904
|
-
seenRoots.add(root);
|
|
905
|
-
const isDirectory = await fs.stat(root).then((stats) => stats.isDirectory()).catch(() => false);
|
|
906
|
-
if (isDirectory)
|
|
907
|
-
return root;
|
|
908
|
-
}
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
export async function listRudderSkillEntries(moduleDir, additionalCandidates = []) {
|
|
912
|
-
const root = await resolveRudderSkillsDir(moduleDir, additionalCandidates);
|
|
913
|
-
if (!root)
|
|
914
|
-
return [];
|
|
915
|
-
try {
|
|
916
|
-
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
917
|
-
const skillDirectories = entries
|
|
918
|
-
.filter((entry) => entry.isDirectory())
|
|
919
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
920
|
-
const skillEntries = await Promise.all(skillDirectories.map(async (entry) => {
|
|
921
|
-
const source = path.join(root, entry.name);
|
|
922
|
-
const metadata = await readSkillMetadataFromDirectory(source);
|
|
923
|
-
return {
|
|
924
|
-
key: `rudder/${entry.name}`,
|
|
925
|
-
runtimeName: entry.name,
|
|
926
|
-
source,
|
|
927
|
-
name: metadata.name ?? entry.name,
|
|
928
|
-
description: metadata.description,
|
|
929
|
-
};
|
|
930
|
-
}));
|
|
931
|
-
return skillEntries;
|
|
932
|
-
}
|
|
933
|
-
catch {
|
|
934
|
-
return [];
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
export async function readInstalledSkillTargets(skillsHome) {
|
|
938
|
-
const entries = await fs.readdir(skillsHome, { withFileTypes: true }).catch(() => []);
|
|
939
|
-
const out = new Map();
|
|
940
|
-
for (const entry of entries) {
|
|
941
|
-
const fullPath = path.join(skillsHome, entry.name);
|
|
942
|
-
const linkedPath = entry.isSymbolicLink() ? await fs.readlink(fullPath).catch(() => null) : null;
|
|
943
|
-
out.set(entry.name, resolveInstalledEntryTarget(skillsHome, entry.name, entry, linkedPath));
|
|
944
|
-
}
|
|
945
|
-
return out;
|
|
946
|
-
}
|
|
947
|
-
export function buildPersistentSkillSnapshot(options) {
|
|
948
|
-
const { agentRuntimeType, availableEntries, desiredSkills, installed, skillsHome, locationLabel, installedDetail, missingDetail, externalConflictDetail, externalDetail, } = options;
|
|
949
|
-
const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry]));
|
|
950
|
-
const desiredSet = new Set(desiredSkills);
|
|
951
|
-
const entries = [];
|
|
952
|
-
const warnings = [...(options.warnings ?? [])];
|
|
953
|
-
for (const available of availableEntries) {
|
|
954
|
-
const installedEntry = installed.get(available.runtimeName) ?? null;
|
|
955
|
-
const desired = desiredSet.has(available.key);
|
|
956
|
-
let state = "available";
|
|
957
|
-
let managed = false;
|
|
958
|
-
let detail = null;
|
|
959
|
-
if (installedEntry?.targetPath === available.source) {
|
|
960
|
-
managed = true;
|
|
961
|
-
state = desired ? "installed" : "stale";
|
|
962
|
-
detail = installedDetail ?? null;
|
|
963
|
-
}
|
|
964
|
-
else if (installedEntry) {
|
|
965
|
-
state = "external";
|
|
966
|
-
detail = desired ? externalConflictDetail : externalDetail;
|
|
967
|
-
}
|
|
968
|
-
else if (desired) {
|
|
969
|
-
state = "missing";
|
|
970
|
-
detail = missingDetail;
|
|
971
|
-
}
|
|
972
|
-
entries.push({
|
|
973
|
-
key: available.key,
|
|
974
|
-
runtimeName: available.runtimeName,
|
|
975
|
-
description: available.description ?? null,
|
|
976
|
-
desired,
|
|
977
|
-
managed,
|
|
978
|
-
state,
|
|
979
|
-
sourcePath: available.source,
|
|
980
|
-
targetPath: path.join(skillsHome, available.runtimeName),
|
|
981
|
-
detail,
|
|
982
|
-
...buildManagedSkillOrigin(),
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
for (const desiredSkill of desiredSkills) {
|
|
986
|
-
if (availableByKey.has(desiredSkill))
|
|
987
|
-
continue;
|
|
988
|
-
warnings.push(`Desired skill "${desiredSkill}" is not available from the Rudder skills directory.`);
|
|
989
|
-
entries.push({
|
|
990
|
-
key: desiredSkill,
|
|
991
|
-
runtimeName: null,
|
|
992
|
-
desired: true,
|
|
993
|
-
managed: true,
|
|
994
|
-
state: "missing",
|
|
995
|
-
sourcePath: null,
|
|
996
|
-
targetPath: null,
|
|
997
|
-
detail: "Rudder cannot find this skill in the local runtime skills directory.",
|
|
998
|
-
origin: "external_unknown",
|
|
999
|
-
originLabel: "External or unavailable",
|
|
1000
|
-
readOnly: false,
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
for (const [name, installedEntry] of installed.entries()) {
|
|
1004
|
-
if (availableEntries.some((entry) => entry.runtimeName === name))
|
|
1005
|
-
continue;
|
|
1006
|
-
entries.push({
|
|
1007
|
-
key: name,
|
|
1008
|
-
runtimeName: name,
|
|
1009
|
-
description: null,
|
|
1010
|
-
desired: false,
|
|
1011
|
-
managed: false,
|
|
1012
|
-
state: "external",
|
|
1013
|
-
origin: "user_installed",
|
|
1014
|
-
originLabel: "User-installed",
|
|
1015
|
-
locationLabel: skillLocationLabel(locationLabel),
|
|
1016
|
-
readOnly: true,
|
|
1017
|
-
sourcePath: null,
|
|
1018
|
-
targetPath: installedEntry.targetPath ?? path.join(skillsHome, name),
|
|
1019
|
-
detail: externalDetail,
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
1023
|
-
return {
|
|
1024
|
-
agentRuntimeType,
|
|
1025
|
-
supported: true,
|
|
1026
|
-
mode: "persistent",
|
|
1027
|
-
desiredSkills,
|
|
1028
|
-
entries,
|
|
1029
|
-
warnings,
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
function normalizeConfiguredPaperclipRuntimeSkills(value) {
|
|
1033
|
-
if (!Array.isArray(value))
|
|
1034
|
-
return [];
|
|
1035
|
-
const out = [];
|
|
1036
|
-
for (const rawEntry of value) {
|
|
1037
|
-
const entry = parseObject(rawEntry);
|
|
1038
|
-
const key = asString(entry.key, asString(entry.name, "")).trim();
|
|
1039
|
-
const runtimeName = asString(entry.runtimeName, asString(entry.name, "")).trim();
|
|
1040
|
-
const source = asString(entry.source, "").trim();
|
|
1041
|
-
if (!key || !runtimeName || !source)
|
|
1042
|
-
continue;
|
|
1043
|
-
out.push({
|
|
1044
|
-
key,
|
|
1045
|
-
runtimeName,
|
|
1046
|
-
source,
|
|
1047
|
-
name: compactSkillText(asString(entry.displayName, asString(entry.name, ""))) ?? runtimeName,
|
|
1048
|
-
description: compactSkillText(typeof entry.description === "string"
|
|
1049
|
-
? entry.description
|
|
1050
|
-
: typeof entry.summary === "string"
|
|
1051
|
-
? entry.summary
|
|
1052
|
-
: null),
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
return out;
|
|
1056
|
-
}
|
|
1057
|
-
export async function readRudderRuntimeSkillEntries(config, moduleDir, additionalCandidates = []) {
|
|
1058
|
-
const configuredEntries = normalizeConfiguredPaperclipRuntimeSkills(config.rudderRuntimeSkills ?? config.paperclipRuntimeSkills);
|
|
1059
|
-
if (configuredEntries.length > 0)
|
|
1060
|
-
return configuredEntries;
|
|
1061
|
-
return listRudderSkillEntries(moduleDir, additionalCandidates);
|
|
1062
|
-
}
|
|
1063
|
-
export async function readRudderSkillMarkdown(moduleDir, skillKey) {
|
|
1064
|
-
const normalized = skillKey.trim().toLowerCase().replace(/^rudder\/rudder\//, "rudder/");
|
|
1065
|
-
if (!normalized)
|
|
1066
|
-
return null;
|
|
1067
|
-
const entries = await listRudderSkillEntries(moduleDir);
|
|
1068
|
-
const match = entries.find((entry) => entry.key === normalized);
|
|
1069
|
-
if (!match)
|
|
1070
|
-
return null;
|
|
1071
|
-
try {
|
|
1072
|
-
return await fs.readFile(path.join(match.source, "SKILL.md"), "utf8");
|
|
1073
|
-
}
|
|
1074
|
-
catch {
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
export function readRudderSkillSyncPreference(config) {
|
|
1079
|
-
const raw = config.rudderSkillSync ?? config.paperclipSkillSync;
|
|
1080
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1081
|
-
return { explicit: false, desiredSkills: [] };
|
|
1082
|
-
}
|
|
1083
|
-
const syncConfig = raw;
|
|
1084
|
-
const desiredValues = syncConfig.desiredSkills;
|
|
1085
|
-
const desired = Array.isArray(desiredValues)
|
|
1086
|
-
? desiredValues
|
|
1087
|
-
.filter((value) => typeof value === "string")
|
|
1088
|
-
.map((value) => value.trim())
|
|
1089
|
-
.filter(Boolean)
|
|
1090
|
-
: [];
|
|
1091
|
-
return {
|
|
1092
|
-
explicit: Object.prototype.hasOwnProperty.call(raw, "desiredSkills"),
|
|
1093
|
-
desiredSkills: Array.from(new Set(desired)),
|
|
1094
|
-
};
|
|
1095
|
-
}
|
|
1096
|
-
function canonicalizeDesiredRudderSkillReference(reference, availableEntries) {
|
|
1097
|
-
const normalizedReference = reference.trim().toLowerCase().replace(/^rudder\/rudder\//, "rudder/");
|
|
1098
|
-
if (!normalizedReference)
|
|
1099
|
-
return "";
|
|
1100
|
-
const exactKey = availableEntries.find((entry) => entry.key.trim().toLowerCase() === normalizedReference);
|
|
1101
|
-
if (exactKey)
|
|
1102
|
-
return exactKey.key;
|
|
1103
|
-
const byRuntimeName = availableEntries.filter((entry) => typeof entry.runtimeName === "string" && entry.runtimeName.trim().toLowerCase() === normalizedReference);
|
|
1104
|
-
if (byRuntimeName.length === 1)
|
|
1105
|
-
return byRuntimeName[0].key;
|
|
1106
|
-
const slugMatches = availableEntries.filter((entry) => entry.key.trim().toLowerCase().split("/").pop() === normalizedReference);
|
|
1107
|
-
if (slugMatches.length === 1)
|
|
1108
|
-
return slugMatches[0].key;
|
|
1109
|
-
return normalizedReference;
|
|
1110
|
-
}
|
|
1111
|
-
export function resolveRudderDesiredSkillNames(config, availableEntries) {
|
|
1112
|
-
const preference = readRudderSkillSyncPreference(config);
|
|
1113
|
-
const desiredSkills = preference.desiredSkills
|
|
1114
|
-
.map((reference) => canonicalizeDesiredRudderSkillReference(reference, availableEntries))
|
|
1115
|
-
.filter(Boolean);
|
|
1116
|
-
return Array.from(new Set(desiredSkills));
|
|
1117
|
-
}
|
|
1118
|
-
export function writeRudderSkillSyncPreference(config, desiredSkills) {
|
|
1119
|
-
const next = { ...config };
|
|
1120
|
-
const raw = next.rudderSkillSync;
|
|
1121
|
-
const current = typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
1122
|
-
? { ...raw }
|
|
1123
|
-
: {};
|
|
1124
|
-
current.desiredSkills = Array.from(new Set(desiredSkills
|
|
1125
|
-
.map((value) => value.trim())
|
|
1126
|
-
.filter(Boolean)));
|
|
1127
|
-
next.rudderSkillSync = current;
|
|
1128
|
-
return next;
|
|
1129
|
-
}
|
|
1130
|
-
function nonEmptyEnvPath(value) {
|
|
1131
|
-
return typeof value === "string" && value.trim().length > 0 ? path.resolve(value.trim()) : null;
|
|
1132
|
-
}
|
|
1133
|
-
export function resolveLocalOperatorHome(sourceEnv = process.env) {
|
|
1134
|
-
return (nonEmptyEnvPath(sourceEnv.RUDDER_OPERATOR_HOME)
|
|
1135
|
-
?? nonEmptyEnvPath(process.env.RUDDER_OPERATOR_HOME)
|
|
1136
|
-
?? nonEmptyEnvPath(process.env.HOME)
|
|
1137
|
-
?? nonEmptyEnvPath(sourceEnv.HOME)
|
|
1138
|
-
?? path.resolve(os.homedir()));
|
|
1139
|
-
}
|
|
1140
|
-
export function applyLocalCliHomeEnv(targetEnv, sourceEnv = process.env) {
|
|
1141
|
-
const home = nonEmptyEnvPath(sourceEnv.HOME) ?? path.resolve(os.homedir());
|
|
1142
|
-
targetEnv.HOME = home;
|
|
1143
|
-
const userProfile = nonEmptyEnvPath(sourceEnv.USERPROFILE);
|
|
1144
|
-
if (userProfile) {
|
|
1145
|
-
targetEnv.USERPROFILE = userProfile;
|
|
1146
|
-
}
|
|
1147
|
-
else if (process.platform === "win32") {
|
|
1148
|
-
targetEnv.USERPROFILE = home;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
async function localCliPathExists(candidate) {
|
|
1152
|
-
return fs.access(candidate).then(() => true).catch(() => false);
|
|
1153
|
-
}
|
|
1154
|
-
async function directoryIsEmpty(target) {
|
|
1155
|
-
const entries = await fs.readdir(target).catch(() => null);
|
|
1156
|
-
return Array.isArray(entries) && entries.length === 0;
|
|
1157
|
-
}
|
|
1158
|
-
async function ensureSymlinkToSource(target, source) {
|
|
1159
|
-
const existing = await fs.lstat(target).catch(() => null);
|
|
1160
|
-
if (!existing) {
|
|
1161
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
1162
|
-
await fs.symlink(source, target);
|
|
1163
|
-
return "created";
|
|
1164
|
-
}
|
|
1165
|
-
if (!existing.isSymbolicLink()) {
|
|
1166
|
-
if (existing.isDirectory() && await directoryIsEmpty(target)) {
|
|
1167
|
-
await fs.rmdir(target);
|
|
1168
|
-
await fs.symlink(source, target);
|
|
1169
|
-
return "repaired";
|
|
1170
|
-
}
|
|
1171
|
-
return "skipped";
|
|
1172
|
-
}
|
|
1173
|
-
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
1174
|
-
if (!linkedPath)
|
|
1175
|
-
return "skipped";
|
|
1176
|
-
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
1177
|
-
? linkedPath
|
|
1178
|
-
: path.resolve(path.dirname(target), linkedPath);
|
|
1179
|
-
if (resolvedLinkedPath === source)
|
|
1180
|
-
return "skipped";
|
|
1181
|
-
await fs.unlink(target);
|
|
1182
|
-
await fs.symlink(source, target);
|
|
1183
|
-
return "repaired";
|
|
1184
|
-
}
|
|
1185
|
-
export async function syncLocalCliCredentialHomeEntries(input) {
|
|
1186
|
-
const sourceHome = nonEmptyEnvPath(input.sourceHome ?? undefined) ?? path.resolve(os.homedir());
|
|
1187
|
-
const targetHome = path.resolve(input.targetHome);
|
|
1188
|
-
const linked = [];
|
|
1189
|
-
const skipped = [];
|
|
1190
|
-
if (sourceHome === targetHome)
|
|
1191
|
-
return { linked, skipped };
|
|
1192
|
-
const entries = input.entries ?? DEFAULT_LOCAL_CLI_CREDENTIAL_HOME_ENTRIES;
|
|
1193
|
-
for (const relativeEntry of entries) {
|
|
1194
|
-
const source = path.join(sourceHome, relativeEntry);
|
|
1195
|
-
if (!(await localCliPathExists(source)))
|
|
1196
|
-
continue;
|
|
1197
|
-
const target = path.join(targetHome, relativeEntry);
|
|
1198
|
-
try {
|
|
1199
|
-
const result = await ensureSymlinkToSource(target, source);
|
|
1200
|
-
if (result === "skipped")
|
|
1201
|
-
skipped.push(relativeEntry);
|
|
1202
|
-
else
|
|
1203
|
-
linked.push(relativeEntry);
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
skipped.push(relativeEntry);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
if (input.onLog && linked.length > 0) {
|
|
1210
|
-
await input.onLog("stdout", `[rudder] Shared ${linked.length} local CLI credential entr${linked.length === 1 ? "y" : "ies"} into managed HOME ${targetHome}: ${linked.join(", ")}\n`);
|
|
1211
|
-
}
|
|
1212
|
-
return { linked, skipped };
|
|
1213
|
-
}
|
|
1214
|
-
async function writeOperatorHomeShim(input) {
|
|
1215
|
-
await fs.mkdir(input.shimDir, { recursive: true });
|
|
1216
|
-
if (process.platform === "win32") {
|
|
1217
|
-
const shimPath = path.join(input.shimDir, `${input.command}.cmd`);
|
|
1218
|
-
const lines = [
|
|
1219
|
-
"@echo off",
|
|
1220
|
-
`set "HOME=${input.operatorHome}"`,
|
|
1221
|
-
`set "USERPROFILE=${input.operatorHome}"`,
|
|
1222
|
-
`${quoteForCmd(input.targetCommand)} %*`,
|
|
1223
|
-
"",
|
|
1224
|
-
];
|
|
1225
|
-
await fs.writeFile(shimPath, lines.join("\r\n"), "utf8");
|
|
1226
|
-
return shimPath;
|
|
1227
|
-
}
|
|
1228
|
-
const shimPath = path.join(input.shimDir, input.command);
|
|
1229
|
-
await fs.writeFile(shimPath, [
|
|
1230
|
-
"#!/bin/sh",
|
|
1231
|
-
`export HOME=${shellQuote(input.operatorHome)}`,
|
|
1232
|
-
`export USERPROFILE=${shellQuote(input.operatorHome)}`,
|
|
1233
|
-
`exec ${shellQuote(input.targetCommand)} "$@"`,
|
|
1234
|
-
"",
|
|
1235
|
-
].join("\n"), "utf8");
|
|
1236
|
-
await fs.chmod(shimPath, 0o755);
|
|
1237
|
-
return shimPath;
|
|
1238
|
-
}
|
|
1239
|
-
function normalizeShimCommand(input) {
|
|
1240
|
-
return typeof input === "string" ? { command: input } : input;
|
|
1241
|
-
}
|
|
1242
|
-
async function runCredentialShimAuthCheck(input) {
|
|
1243
|
-
const env = {
|
|
1244
|
-
...input.env,
|
|
1245
|
-
HOME: input.home,
|
|
1246
|
-
USERPROFILE: input.home,
|
|
1247
|
-
};
|
|
1248
|
-
return await new Promise((resolve) => {
|
|
1249
|
-
const child = spawn(input.targetCommand, [...input.args], {
|
|
1250
|
-
cwd: input.cwd,
|
|
1251
|
-
env,
|
|
1252
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
1253
|
-
});
|
|
1254
|
-
const timeout = setTimeout(() => {
|
|
1255
|
-
child.kill("SIGTERM");
|
|
1256
|
-
resolve(false);
|
|
1257
|
-
}, 1000);
|
|
1258
|
-
child.on("error", () => {
|
|
1259
|
-
clearTimeout(timeout);
|
|
1260
|
-
resolve(false);
|
|
1261
|
-
});
|
|
1262
|
-
child.on("close", (code) => {
|
|
1263
|
-
clearTimeout(timeout);
|
|
1264
|
-
resolve(code === 0);
|
|
1265
|
-
});
|
|
1266
|
-
});
|
|
1267
|
-
}
|
|
1268
|
-
async function shouldPrepareOperatorHomeShim(input) {
|
|
1269
|
-
const authCheckArgs = input.command.authCheckArgs;
|
|
1270
|
-
if (!authCheckArgs || authCheckArgs.length === 0)
|
|
1271
|
-
return true;
|
|
1272
|
-
if (input.command.credentialEntries && input.command.credentialEntries.length > 0) {
|
|
1273
|
-
const hasOperatorCredentialEntry = await Promise.all(input.command.credentialEntries.map((entry) => localCliPathExists(path.join(input.operatorHome, entry))));
|
|
1274
|
-
if (!hasOperatorCredentialEntry.some(Boolean))
|
|
1275
|
-
return false;
|
|
1276
|
-
}
|
|
1277
|
-
const managedHomeWorks = await runCredentialShimAuthCheck({
|
|
1278
|
-
targetCommand: input.targetCommand,
|
|
1279
|
-
args: authCheckArgs,
|
|
1280
|
-
cwd: input.cwd,
|
|
1281
|
-
env: input.env,
|
|
1282
|
-
home: input.targetHome,
|
|
1283
|
-
});
|
|
1284
|
-
if (managedHomeWorks)
|
|
1285
|
-
return false;
|
|
1286
|
-
return await runCredentialShimAuthCheck({
|
|
1287
|
-
targetCommand: input.targetCommand,
|
|
1288
|
-
args: authCheckArgs,
|
|
1289
|
-
cwd: input.cwd,
|
|
1290
|
-
env: input.env,
|
|
1291
|
-
home: input.operatorHome,
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
export async function ensureLocalCliCredentialShimsInPath(input) {
|
|
1295
|
-
const operatorHome = nonEmptyEnvPath(input.operatorHome ?? undefined);
|
|
1296
|
-
const targetHome = nonEmptyEnvPath(input.targetHome);
|
|
1297
|
-
if (!operatorHome || !targetHome || operatorHome === targetHome) {
|
|
1298
|
-
return ensurePathInEnv(input.env);
|
|
1299
|
-
}
|
|
1300
|
-
const normalized = ensurePathInEnv(input.env);
|
|
1301
|
-
const cwd = input.cwd ?? process.cwd();
|
|
1302
|
-
const commands = input.commands ?? DEFAULT_LOCAL_CLI_OPERATOR_HOME_SHIM_COMMANDS;
|
|
1303
|
-
const shimDir = path.join(targetHome, ".rudder", "local-cli-shims");
|
|
1304
|
-
const prepared = [];
|
|
1305
|
-
for (const rawCommand of commands) {
|
|
1306
|
-
const command = normalizeShimCommand(rawCommand);
|
|
1307
|
-
const targetCommand = await resolveCommandPath(command.command, cwd, normalized);
|
|
1308
|
-
if (!targetCommand)
|
|
1309
|
-
continue;
|
|
1310
|
-
if (path.dirname(targetCommand) === shimDir)
|
|
1311
|
-
continue;
|
|
1312
|
-
if (!(await shouldPrepareOperatorHomeShim({
|
|
1313
|
-
command,
|
|
1314
|
-
targetCommand,
|
|
1315
|
-
cwd,
|
|
1316
|
-
env: normalized,
|
|
1317
|
-
targetHome,
|
|
1318
|
-
operatorHome,
|
|
1319
|
-
}))) {
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
await writeOperatorHomeShim({ shimDir, command: command.command, targetCommand, operatorHome });
|
|
1323
|
-
prepared.push(command.command);
|
|
1324
|
-
}
|
|
1325
|
-
if (prepared.length === 0)
|
|
1326
|
-
return normalized;
|
|
1327
|
-
if (input.onLog) {
|
|
1328
|
-
await input.onLog("stdout", `[rudder] Prepared local CLI credential shim${prepared.length === 1 ? "" : "s"} for: ${prepared.join(", ")}\n`);
|
|
1329
|
-
}
|
|
1330
|
-
return prependPathEntry(normalized, shimDir);
|
|
1331
|
-
}
|
|
1332
|
-
export async function ensureRudderSkillSymlink(source, target, linkSkill = (linkSource, linkTarget) => fs.symlink(linkSource, linkTarget)) {
|
|
1333
|
-
const existing = await fs.lstat(target).catch(() => null);
|
|
1334
|
-
if (!existing) {
|
|
1335
|
-
await linkSkill(source, target);
|
|
1336
|
-
return "created";
|
|
1337
|
-
}
|
|
1338
|
-
if (!existing.isSymbolicLink()) {
|
|
1339
|
-
return "skipped";
|
|
1340
|
-
}
|
|
1341
|
-
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
1342
|
-
if (!linkedPath)
|
|
1343
|
-
return "skipped";
|
|
1344
|
-
const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath);
|
|
1345
|
-
if (resolvedLinkedPath === source) {
|
|
1346
|
-
return "skipped";
|
|
1347
|
-
}
|
|
1348
|
-
const linkedPathExists = await fs.stat(resolvedLinkedPath).then(() => true).catch(() => false);
|
|
1349
|
-
if (linkedPathExists) {
|
|
1350
|
-
return "skipped";
|
|
1351
|
-
}
|
|
1352
|
-
await fs.unlink(target);
|
|
1353
|
-
await linkSkill(source, target);
|
|
1354
|
-
return "repaired";
|
|
1355
|
-
}
|
|
1356
|
-
export async function removeMaintainerOnlySkillSymlinks(skillsHome, allowedSkillNames) {
|
|
1357
|
-
const allowed = new Set(Array.from(allowedSkillNames));
|
|
1358
|
-
try {
|
|
1359
|
-
const entries = await fs.readdir(skillsHome, { withFileTypes: true });
|
|
1360
|
-
const removed = [];
|
|
1361
|
-
for (const entry of entries) {
|
|
1362
|
-
if (allowed.has(entry.name))
|
|
1363
|
-
continue;
|
|
1364
|
-
const target = path.join(skillsHome, entry.name);
|
|
1365
|
-
const existing = await fs.lstat(target).catch(() => null);
|
|
1366
|
-
if (!existing?.isSymbolicLink())
|
|
1367
|
-
continue;
|
|
1368
|
-
const linkedPath = await fs.readlink(target).catch(() => null);
|
|
1369
|
-
if (!linkedPath)
|
|
1370
|
-
continue;
|
|
1371
|
-
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
1372
|
-
? linkedPath
|
|
1373
|
-
: path.resolve(path.dirname(target), linkedPath);
|
|
1374
|
-
if (!isMaintainerOnlySkillTarget(linkedPath) &&
|
|
1375
|
-
!isMaintainerOnlySkillTarget(resolvedLinkedPath)) {
|
|
1376
|
-
continue;
|
|
1377
|
-
}
|
|
1378
|
-
await fs.unlink(target);
|
|
1379
|
-
removed.push(entry.name);
|
|
1380
|
-
}
|
|
1381
|
-
return removed;
|
|
1382
|
-
}
|
|
1383
|
-
catch {
|
|
1384
|
-
return [];
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
export async function ensureCommandResolvable(command, cwd, env) {
|
|
1388
|
-
const resolved = await resolveCommandPath(command, cwd, env);
|
|
1389
|
-
if (resolved)
|
|
1390
|
-
return;
|
|
1391
|
-
if (command.includes("/") || command.includes("\\")) {
|
|
1392
|
-
const absolute = path.isAbsolute(command) ? command : path.resolve(cwd, command);
|
|
1393
|
-
throw new Error(`Command is not executable: "${command}" (resolved: "${absolute}")`);
|
|
1394
|
-
}
|
|
1395
|
-
throw new Error(`Command not found in PATH: "${command}"`);
|
|
1396
|
-
}
|
|
1397
|
-
export async function runChildProcess(runId, command, args, opts) {
|
|
1398
|
-
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
|
|
1399
|
-
return new Promise((resolve, reject) => {
|
|
1400
|
-
const rawMerged = { ...process.env, ...opts.env };
|
|
1401
|
-
const requestedHome = typeof opts.env.HOME === "string" && opts.env.HOME.trim().length > 0
|
|
1402
|
-
? path.resolve(opts.env.HOME)
|
|
1403
|
-
: null;
|
|
1404
|
-
const inheritedHome = typeof process.env.HOME === "string" && process.env.HOME.trim().length > 0
|
|
1405
|
-
? path.resolve(process.env.HOME)
|
|
1406
|
-
: null;
|
|
1407
|
-
const hasExplicitZdotdir = typeof opts.env.ZDOTDIR === "string" && opts.env.ZDOTDIR.trim().length > 0;
|
|
1408
|
-
// Strip Claude Code nesting-guard env vars so spawned `claude` processes
|
|
1409
|
-
// don't refuse to start with "cannot be launched inside another session".
|
|
1410
|
-
// These vars leak in when the Rudder server itself is started from
|
|
1411
|
-
// within a Claude Code session (e.g. `npx rudder run` in a terminal
|
|
1412
|
-
// owned by Claude Code) or when cron inherits a contaminated shell env.
|
|
1413
|
-
const CLAUDE_CODE_NESTING_VARS = [
|
|
1414
|
-
"CLAUDECODE",
|
|
1415
|
-
"CLAUDE_CODE_ENTRYPOINT",
|
|
1416
|
-
"CLAUDE_CODE_SESSION",
|
|
1417
|
-
"CLAUDE_CODE_PARENT_SESSION",
|
|
1418
|
-
];
|
|
1419
|
-
for (const key of CLAUDE_CODE_NESTING_VARS) {
|
|
1420
|
-
delete rawMerged[key];
|
|
1421
|
-
}
|
|
1422
|
-
const GIT_IDENTITY_ENV_VARS = [
|
|
1423
|
-
"GIT_AUTHOR_NAME",
|
|
1424
|
-
"GIT_AUTHOR_EMAIL",
|
|
1425
|
-
"GIT_COMMITTER_NAME",
|
|
1426
|
-
"GIT_COMMITTER_EMAIL",
|
|
1427
|
-
];
|
|
1428
|
-
for (const key of GIT_IDENTITY_ENV_VARS) {
|
|
1429
|
-
if (rawMerged[key] === "" && !Object.prototype.hasOwnProperty.call(opts.env, key)) {
|
|
1430
|
-
delete rawMerged[key];
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
// When Rudder isolates HOME for child agents, don't let zsh keep using the
|
|
1434
|
-
// host user's startup dir via an inherited ZDOTDIR. That mismatch makes
|
|
1435
|
-
// child `zsh -lc` invocations source the host `.zshenv` with the agent HOME.
|
|
1436
|
-
if (requestedHome && requestedHome !== inheritedHome && !hasExplicitZdotdir) {
|
|
1437
|
-
delete rawMerged.ZDOTDIR;
|
|
1438
|
-
}
|
|
1439
|
-
const mergedEnv = ensurePathInEnv(rawMerged);
|
|
1440
|
-
void resolveSpawnTarget(command, args, opts.cwd, mergedEnv)
|
|
1441
|
-
.then((target) => {
|
|
1442
|
-
if (opts.abortSignal?.aborted) {
|
|
1443
|
-
resolve({
|
|
1444
|
-
exitCode: null,
|
|
1445
|
-
signal: "SIGTERM",
|
|
1446
|
-
timedOut: false,
|
|
1447
|
-
stdout: "",
|
|
1448
|
-
stderr: "",
|
|
1449
|
-
pid: null,
|
|
1450
|
-
startedAt: null,
|
|
1451
|
-
});
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
const child = spawn(target.command, target.args, {
|
|
1455
|
-
cwd: opts.cwd,
|
|
1456
|
-
env: mergedEnv,
|
|
1457
|
-
shell: false,
|
|
1458
|
-
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1459
|
-
});
|
|
1460
|
-
const startedAt = new Date().toISOString();
|
|
1461
|
-
if (opts.stdin != null && child.stdin) {
|
|
1462
|
-
child.stdin.write(opts.stdin);
|
|
1463
|
-
child.stdin.end();
|
|
1464
|
-
}
|
|
1465
|
-
if (typeof child.pid === "number" && child.pid > 0 && opts.onSpawn) {
|
|
1466
|
-
void opts.onSpawn({ pid: child.pid, startedAt }).catch((err) => {
|
|
1467
|
-
onLogError(err, runId, "failed to record child process metadata");
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
runningProcesses.set(runId, { child, graceSec: opts.graceSec });
|
|
1471
|
-
let timedOut = false;
|
|
1472
|
-
let aborted = false;
|
|
1473
|
-
let stdout = "";
|
|
1474
|
-
let stderr = "";
|
|
1475
|
-
let logChain = Promise.resolve();
|
|
1476
|
-
const timeout = opts.timeoutSec > 0
|
|
1477
|
-
? setTimeout(() => {
|
|
1478
|
-
timedOut = true;
|
|
1479
|
-
child.kill("SIGTERM");
|
|
1480
|
-
setTimeout(() => {
|
|
1481
|
-
if (isChildProcessAlive(child)) {
|
|
1482
|
-
child.kill("SIGKILL");
|
|
1483
|
-
}
|
|
1484
|
-
}, Math.max(1, opts.graceSec) * 1000);
|
|
1485
|
-
}, opts.timeoutSec * 1000)
|
|
1486
|
-
: null;
|
|
1487
|
-
let abortCleanup = null;
|
|
1488
|
-
if (opts.abortSignal) {
|
|
1489
|
-
const onAbort = () => {
|
|
1490
|
-
aborted = true;
|
|
1491
|
-
child.kill("SIGTERM");
|
|
1492
|
-
setTimeout(() => {
|
|
1493
|
-
if (isChildProcessAlive(child)) {
|
|
1494
|
-
child.kill("SIGKILL");
|
|
1495
|
-
}
|
|
1496
|
-
}, Math.max(1, opts.graceSec) * 1000);
|
|
1497
|
-
};
|
|
1498
|
-
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1499
|
-
abortCleanup = () => opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
1500
|
-
}
|
|
1501
|
-
child.stdout?.on("data", (chunk) => {
|
|
1502
|
-
const text = String(chunk);
|
|
1503
|
-
stdout = appendWithCap(stdout, text);
|
|
1504
|
-
logChain = logChain
|
|
1505
|
-
.then(() => opts.onLog("stdout", text))
|
|
1506
|
-
.catch((err) => onLogError(err, runId, "failed to append stdout log chunk"));
|
|
1507
|
-
});
|
|
1508
|
-
child.stderr?.on("data", (chunk) => {
|
|
1509
|
-
const text = String(chunk);
|
|
1510
|
-
stderr = appendWithCap(stderr, text);
|
|
1511
|
-
logChain = logChain
|
|
1512
|
-
.then(() => opts.onLog("stderr", text))
|
|
1513
|
-
.catch((err) => onLogError(err, runId, "failed to append stderr log chunk"));
|
|
1514
|
-
});
|
|
1515
|
-
child.on("error", (err) => {
|
|
1516
|
-
if (timeout)
|
|
1517
|
-
clearTimeout(timeout);
|
|
1518
|
-
if (abortCleanup)
|
|
1519
|
-
abortCleanup();
|
|
1520
|
-
runningProcesses.delete(runId);
|
|
1521
|
-
const errno = err.code;
|
|
1522
|
-
const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? "";
|
|
1523
|
-
const msg = errno === "ENOENT"
|
|
1524
|
-
? `Failed to start command "${command}" in "${opts.cwd}". Verify adapter command, working directory, and PATH (${pathValue}).`
|
|
1525
|
-
: `Failed to start command "${command}" in "${opts.cwd}": ${err.message}`;
|
|
1526
|
-
reject(new Error(msg));
|
|
1527
|
-
});
|
|
1528
|
-
child.on("close", (code, signal) => {
|
|
1529
|
-
if (timeout)
|
|
1530
|
-
clearTimeout(timeout);
|
|
1531
|
-
if (abortCleanup)
|
|
1532
|
-
abortCleanup();
|
|
1533
|
-
runningProcesses.delete(runId);
|
|
1534
|
-
void logChain.finally(() => {
|
|
1535
|
-
resolve({
|
|
1536
|
-
exitCode: code,
|
|
1537
|
-
signal: aborted ? "SIGTERM" : signal,
|
|
1538
|
-
timedOut,
|
|
1539
|
-
stdout,
|
|
1540
|
-
stderr,
|
|
1541
|
-
pid: child.pid ?? null,
|
|
1542
|
-
startedAt,
|
|
1543
|
-
});
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1546
|
-
})
|
|
1547
|
-
.catch(reject);
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1
|
+
export * from "./server-utils.process.js";
|
|
2
|
+
export * from "./server-utils.prompts.js";
|
|
3
|
+
export * from "./server-utils.instructions.js";
|
|
4
|
+
export * from "./server-utils.cli.js";
|
|
1550
5
|
//# sourceMappingURL=server-utils.js.map
|