@andreroggeri/adapter-pi-local-serialized 0.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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/cli/format-event.d.ts +2 -0
- package/dist/cli/format-event.d.ts.map +1 -0
- package/dist/cli/format-event.js +99 -0
- package/dist/cli/format-event.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/server/concurrency.d.ts +2 -0
- package/dist/server/concurrency.d.ts.map +1 -0
- package/dist/server/concurrency.js +25 -0
- package/dist/server/concurrency.js.map +1 -0
- package/dist/server/execute.d.ts +3 -0
- package/dist/server/execute.d.ts.map +1 -0
- package/dist/server/execute.js +675 -0
- package/dist/server/execute.js.map +1 -0
- package/dist/server/execute.remote.test.d.ts +2 -0
- package/dist/server/execute.remote.test.d.ts.map +1 -0
- package/dist/server/execute.remote.test.js +466 -0
- package/dist/server/execute.remote.test.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +51 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/models.d.ts +20 -0
- package/dist/server/models.d.ts.map +1 -0
- package/dist/server/models.js +163 -0
- package/dist/server/models.js.map +1 -0
- package/dist/server/models.test.d.ts +2 -0
- package/dist/server/models.test.d.ts.map +1 -0
- package/dist/server/models.test.js +22 -0
- package/dist/server/models.test.js.map +1 -0
- package/dist/server/parse.d.ts +23 -0
- package/dist/server/parse.d.ts.map +1 -0
- package/dist/server/parse.js +195 -0
- package/dist/server/parse.js.map +1 -0
- package/dist/server/parse.test.d.ts +2 -0
- package/dist/server/parse.test.d.ts.map +1 -0
- package/dist/server/parse.test.js +249 -0
- package/dist/server/parse.test.js.map +1 -0
- package/dist/server/skills.d.ts +8 -0
- package/dist/server/skills.d.ts.map +1 -0
- package/dist/server/skills.js +69 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/test.d.ts +3 -0
- package/dist/server/test.d.ts.map +1 -0
- package/dist/server/test.js +309 -0
- package/dist/server/test.js.map +1 -0
- package/dist/ui/build-config.d.ts +3 -0
- package/dist/ui/build-config.d.ts.map +1 -0
- package/dist/ui/build-config.js +78 -0
- package/dist/ui/build-config.js.map +1 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/parse-stdout.d.ts +4 -0
- package/dist/ui/parse-stdout.d.ts.map +1 -0
- package/dist/ui/parse-stdout.js +271 -0
- package/dist/ui/parse-stdout.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { inferOpenAiCompatibleBiller } from "@paperclipai/adapter-utils";
|
|
6
|
+
import { adapterExecutionTargetIsRemote, adapterExecutionTargetRemoteCwd, overrideAdapterExecutionTargetRemoteCwd, adapterExecutionTargetSessionIdentity, adapterExecutionTargetSessionMatches, adapterExecutionTargetUsesManagedHome, adapterExecutionTargetUsesPaperclipBridge, describeAdapterExecutionTarget, ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetFile, ensureAdapterExecutionTargetRuntimeCommandInstalled, prepareAdapterExecutionTargetRuntime, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeoutSec, resolveAdapterExecutionTargetCommandForLogs, runAdapterExecutionTargetProcess, runAdapterExecutionTargetShellCommand, startAdapterExecutionTargetPaperclipBridge, } from "@paperclipai/adapter-utils/execution-target";
|
|
7
|
+
import { asString, asNumber, asStringArray, parseObject, buildPaperclipEnv, joinPromptSections, buildInvocationEnvForLogs, ensureAbsoluteDirectory, ensurePaperclipSkillSymlink, ensurePathInEnv, refreshPaperclipWorkspaceEnvForExecution, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, resolvePaperclipDesiredSkillNames, removeMaintainerOnlySkillSymlinks, renderTemplate, renderPaperclipWakePrompt, stringifyPaperclipWakePayload, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, } from "@paperclipai/adapter-utils/server-utils";
|
|
8
|
+
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
|
9
|
+
import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js";
|
|
10
|
+
import { ensurePiModelConfiguredAndAvailable } from "./models.js";
|
|
11
|
+
import { withSerializedExecution } from "./concurrency.js";
|
|
12
|
+
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
|
13
|
+
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips");
|
|
15
|
+
const PI_AGENT_SKILLS_DIR = path.join(os.homedir(), ".pi", "agent", "skills");
|
|
16
|
+
function firstNonEmptyLine(text) {
|
|
17
|
+
return (text
|
|
18
|
+
.split(/\r?\n/)
|
|
19
|
+
.map((line) => line.trim())
|
|
20
|
+
.find(Boolean) ?? "");
|
|
21
|
+
}
|
|
22
|
+
function parseModelProvider(model) {
|
|
23
|
+
if (!model)
|
|
24
|
+
return null;
|
|
25
|
+
const trimmed = model.trim();
|
|
26
|
+
if (!trimmed.includes("/"))
|
|
27
|
+
return null;
|
|
28
|
+
return trimmed.slice(0, trimmed.indexOf("/")).trim() || null;
|
|
29
|
+
}
|
|
30
|
+
function parseModelId(model) {
|
|
31
|
+
if (!model)
|
|
32
|
+
return null;
|
|
33
|
+
const trimmed = model.trim();
|
|
34
|
+
if (!trimmed.includes("/"))
|
|
35
|
+
return trimmed || null;
|
|
36
|
+
return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null;
|
|
37
|
+
}
|
|
38
|
+
async function ensurePiSkillsInjected(onLog, skillsEntries, desiredSkillNames) {
|
|
39
|
+
const desiredSet = new Set(desiredSkillNames ?? skillsEntries.map((entry) => entry.key));
|
|
40
|
+
const selectedEntries = skillsEntries.filter((entry) => desiredSet.has(entry.key));
|
|
41
|
+
if (selectedEntries.length === 0)
|
|
42
|
+
return;
|
|
43
|
+
await fs.mkdir(PI_AGENT_SKILLS_DIR, { recursive: true });
|
|
44
|
+
const removedSkills = await removeMaintainerOnlySkillSymlinks(PI_AGENT_SKILLS_DIR, selectedEntries.map((entry) => entry.runtimeName));
|
|
45
|
+
for (const skillName of removedSkills) {
|
|
46
|
+
await onLog("stderr", `[paperclip] Removed maintainer-only Pi skill "${skillName}" from ${PI_AGENT_SKILLS_DIR}\n`);
|
|
47
|
+
}
|
|
48
|
+
for (const entry of selectedEntries) {
|
|
49
|
+
const target = path.join(PI_AGENT_SKILLS_DIR, entry.runtimeName);
|
|
50
|
+
try {
|
|
51
|
+
const result = await ensurePaperclipSkillSymlink(entry.source, target);
|
|
52
|
+
if (result === "skipped")
|
|
53
|
+
continue;
|
|
54
|
+
await onLog("stderr", `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}\n`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
await onLog("stderr", `[paperclip] Failed to inject Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function buildPiSkillsDir(config) {
|
|
62
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-pi-skills-"));
|
|
63
|
+
const target = path.join(tmp, "skills");
|
|
64
|
+
await fs.mkdir(target, { recursive: true });
|
|
65
|
+
const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
|
66
|
+
const desiredNames = new Set(resolvePaperclipDesiredSkillNames(config, availableEntries));
|
|
67
|
+
for (const entry of availableEntries) {
|
|
68
|
+
if (!desiredNames.has(entry.key))
|
|
69
|
+
continue;
|
|
70
|
+
await fs.symlink(entry.source, path.join(target, entry.runtimeName));
|
|
71
|
+
}
|
|
72
|
+
return target;
|
|
73
|
+
}
|
|
74
|
+
function resolvePiBiller(env, provider) {
|
|
75
|
+
return inferOpenAiCompatibleBiller(env, null) ?? provider ?? "unknown";
|
|
76
|
+
}
|
|
77
|
+
async function ensureSessionsDir() {
|
|
78
|
+
await fs.mkdir(PAPERCLIP_SESSIONS_DIR, { recursive: true });
|
|
79
|
+
return PAPERCLIP_SESSIONS_DIR;
|
|
80
|
+
}
|
|
81
|
+
function buildSessionPath(agentId, timestamp) {
|
|
82
|
+
const safeTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
83
|
+
return path.join(PAPERCLIP_SESSIONS_DIR, `${safeTimestamp}-${agentId}.jsonl`);
|
|
84
|
+
}
|
|
85
|
+
function buildRemoteSessionPath(runtimeRootDir, agentId, timestamp) {
|
|
86
|
+
const safeTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
87
|
+
return path.posix.join(runtimeRootDir, "sessions", `${safeTimestamp}-${agentId}.jsonl`);
|
|
88
|
+
}
|
|
89
|
+
function normalizeExecutionCwd(candidate, remote) {
|
|
90
|
+
return remote ? path.posix.normalize(candidate) : path.resolve(candidate);
|
|
91
|
+
}
|
|
92
|
+
function executionCwdsMatch(saved, current, remote) {
|
|
93
|
+
return normalizeExecutionCwd(saved, remote) === normalizeExecutionCwd(current, remote);
|
|
94
|
+
}
|
|
95
|
+
function readSessionHeaderCwd(raw) {
|
|
96
|
+
const headerLine = raw
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.map((line) => line.trim())
|
|
99
|
+
.find(Boolean);
|
|
100
|
+
if (!headerLine)
|
|
101
|
+
return null;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(headerLine);
|
|
104
|
+
if (parsed.type !== "session")
|
|
105
|
+
return null;
|
|
106
|
+
const cwd = typeof parsed.cwd === "string" ? parsed.cwd.trim() : "";
|
|
107
|
+
return cwd.length > 0 ? cwd : null;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function readSavedSessionCwd(input) {
|
|
114
|
+
if (!input.sessionPath.trim())
|
|
115
|
+
return null;
|
|
116
|
+
if (!adapterExecutionTargetIsRemote(input.executionTarget)) {
|
|
117
|
+
try {
|
|
118
|
+
return readSessionHeaderCwd(await fs.readFile(input.sessionPath, "utf8"));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const sessionHeader = await runAdapterExecutionTargetShellCommand(input.runId, input.executionTarget, `if [ -f ${shellQuote(input.sessionPath)} ]; then head -n 1 ${shellQuote(input.sessionPath)}; fi`, {
|
|
126
|
+
cwd: input.cwd,
|
|
127
|
+
env: input.env,
|
|
128
|
+
timeoutSec: input.timeoutSec > 0 ? Math.min(input.timeoutSec, 15) : 15,
|
|
129
|
+
graceSec: input.graceSec,
|
|
130
|
+
});
|
|
131
|
+
if (sessionHeader.timedOut || (sessionHeader.exitCode ?? 0) !== 0)
|
|
132
|
+
return null;
|
|
133
|
+
return readSessionHeaderCwd(sessionHeader.stdout);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function execute(ctx) {
|
|
140
|
+
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
|
|
141
|
+
const executionTarget = readAdapterExecutionTarget({
|
|
142
|
+
executionTarget: ctx.executionTarget,
|
|
143
|
+
legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
|
|
144
|
+
});
|
|
145
|
+
const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
|
|
146
|
+
const promptTemplate = asString(config.promptTemplate, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
|
|
147
|
+
const command = asString(config.command, "pi");
|
|
148
|
+
const model = asString(config.model, "").trim();
|
|
149
|
+
const thinking = asString(config.thinking, "").trim();
|
|
150
|
+
// Parse model into provider and model id
|
|
151
|
+
const provider = parseModelProvider(model);
|
|
152
|
+
const modelId = parseModelId(model);
|
|
153
|
+
const workspaceContext = parseObject(context.paperclipWorkspace);
|
|
154
|
+
const workspaceCwd = asString(workspaceContext.cwd, "");
|
|
155
|
+
const workspaceSource = asString(workspaceContext.source, "");
|
|
156
|
+
const workspaceId = asString(workspaceContext.workspaceId, "");
|
|
157
|
+
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
|
158
|
+
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
|
159
|
+
const agentHome = asString(workspaceContext.agentHome, "");
|
|
160
|
+
const workspaceHints = Array.isArray(context.paperclipWorkspaces)
|
|
161
|
+
? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
|
|
162
|
+
: [];
|
|
163
|
+
const configuredCwd = asString(config.cwd, "");
|
|
164
|
+
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
|
|
165
|
+
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
|
|
166
|
+
const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
|
|
167
|
+
let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
|
|
168
|
+
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
|
169
|
+
if (!executionTargetIsRemote) {
|
|
170
|
+
await ensureSessionsDir();
|
|
171
|
+
}
|
|
172
|
+
const piSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
|
173
|
+
const desiredPiSkillNames = resolvePaperclipDesiredSkillNames(config, piSkillEntries);
|
|
174
|
+
if (!executionTargetIsRemote) {
|
|
175
|
+
await ensurePiSkillsInjected(onLog, piSkillEntries, desiredPiSkillNames);
|
|
176
|
+
}
|
|
177
|
+
// Build environment
|
|
178
|
+
const envConfig = parseObject(config.env);
|
|
179
|
+
const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0;
|
|
180
|
+
const env = { ...buildPaperclipEnv(agent) };
|
|
181
|
+
env.PAPERCLIP_RUN_ID = runId;
|
|
182
|
+
const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
|
|
183
|
+
(typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
|
|
184
|
+
null;
|
|
185
|
+
const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
|
|
186
|
+
? context.wakeReason.trim()
|
|
187
|
+
: null;
|
|
188
|
+
const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
|
|
189
|
+
(typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
|
|
190
|
+
null;
|
|
191
|
+
const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0
|
|
192
|
+
? context.approvalId.trim()
|
|
193
|
+
: null;
|
|
194
|
+
const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
|
|
195
|
+
? context.approvalStatus.trim()
|
|
196
|
+
: null;
|
|
197
|
+
const linkedIssueIds = Array.isArray(context.issueIds)
|
|
198
|
+
? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
199
|
+
: [];
|
|
200
|
+
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
|
201
|
+
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
|
202
|
+
if (wakeTaskId)
|
|
203
|
+
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
|
204
|
+
if (issueWorkMode)
|
|
205
|
+
env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
|
206
|
+
if (wakeReason)
|
|
207
|
+
env.PAPERCLIP_WAKE_REASON = wakeReason;
|
|
208
|
+
if (wakeCommentId)
|
|
209
|
+
env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
|
|
210
|
+
if (approvalId)
|
|
211
|
+
env.PAPERCLIP_APPROVAL_ID = approvalId;
|
|
212
|
+
if (approvalStatus)
|
|
213
|
+
env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
|
214
|
+
if (linkedIssueIds.length > 0)
|
|
215
|
+
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
|
216
|
+
if (wakePayloadJson)
|
|
217
|
+
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
|
218
|
+
refreshPaperclipWorkspaceEnvForExecution({
|
|
219
|
+
env,
|
|
220
|
+
envConfig,
|
|
221
|
+
workspaceCwd: effectiveWorkspaceCwd,
|
|
222
|
+
workspaceSource,
|
|
223
|
+
workspaceId,
|
|
224
|
+
workspaceRepoUrl,
|
|
225
|
+
workspaceRepoRef,
|
|
226
|
+
workspaceHints,
|
|
227
|
+
agentHome,
|
|
228
|
+
executionTargetIsRemote,
|
|
229
|
+
executionCwd: effectiveExecutionCwd,
|
|
230
|
+
});
|
|
231
|
+
if (!hasExplicitApiKey && authToken) {
|
|
232
|
+
env.PAPERCLIP_API_KEY = authToken;
|
|
233
|
+
}
|
|
234
|
+
// Prepend installed skill `bin/` dirs to PATH so an agent's bash tool can
|
|
235
|
+
// invoke skill binaries (e.g. `paperclip-get-issue`) by name. Without this,
|
|
236
|
+
// any pi_local agent whose AGENTS.md calls a skill command via bash hits
|
|
237
|
+
// exit 127 "command not found". Only include skills that ensurePiSkillsInjected
|
|
238
|
+
// actually linked — otherwise non-injected skills' binaries would be reachable
|
|
239
|
+
// to the agent.
|
|
240
|
+
const injectedSkillKeys = new Set(desiredPiSkillNames);
|
|
241
|
+
const skillBinDirs = piSkillEntries
|
|
242
|
+
.filter((entry) => injectedSkillKeys.has(entry.key) && entry.source.length > 0)
|
|
243
|
+
.map((entry) => path.join(entry.source, "bin"));
|
|
244
|
+
const mergedEnv = ensurePathInEnv({ ...process.env, ...env });
|
|
245
|
+
const pathKey = typeof mergedEnv.Path === "string" && mergedEnv.Path.length > 0 && !mergedEnv.PATH
|
|
246
|
+
? "Path"
|
|
247
|
+
: "PATH";
|
|
248
|
+
const basePath = mergedEnv[pathKey] ?? "";
|
|
249
|
+
if (skillBinDirs.length > 0) {
|
|
250
|
+
const existing = basePath.split(path.delimiter).filter(Boolean);
|
|
251
|
+
const additions = skillBinDirs.filter((dir) => !existing.includes(dir));
|
|
252
|
+
if (additions.length > 0) {
|
|
253
|
+
mergedEnv[pathKey] = [...additions, basePath].filter(Boolean).join(path.delimiter);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const runtimeEnv = Object.fromEntries(Object.entries(mergedEnv).filter((entry) => typeof entry[1] === "string"));
|
|
257
|
+
const timeoutSec = resolveAdapterExecutionTargetTimeoutSec(executionTarget, asNumber(config.timeoutSec, 0));
|
|
258
|
+
const graceSec = asNumber(config.graceSec, 20);
|
|
259
|
+
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
|
260
|
+
runId,
|
|
261
|
+
target: executionTarget,
|
|
262
|
+
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
|
263
|
+
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
|
264
|
+
cwd,
|
|
265
|
+
env: runtimeEnv,
|
|
266
|
+
timeoutSec,
|
|
267
|
+
graceSec,
|
|
268
|
+
onLog,
|
|
269
|
+
});
|
|
270
|
+
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, {
|
|
271
|
+
installCommand: SANDBOX_INSTALL_COMMAND,
|
|
272
|
+
timeoutSec,
|
|
273
|
+
});
|
|
274
|
+
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
|
275
|
+
let loggedEnv = buildInvocationEnvForLogs(env, {
|
|
276
|
+
runtimeEnv,
|
|
277
|
+
includeRuntimeKeys: ["HOME"],
|
|
278
|
+
resolvedCommand,
|
|
279
|
+
});
|
|
280
|
+
if (!executionTargetIsRemote) {
|
|
281
|
+
await ensurePiModelConfiguredAndAvailable({
|
|
282
|
+
model,
|
|
283
|
+
command,
|
|
284
|
+
cwd,
|
|
285
|
+
env: runtimeEnv,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const extraArgs = (() => {
|
|
289
|
+
const fromExtraArgs = asStringArray(config.extraArgs);
|
|
290
|
+
if (fromExtraArgs.length > 0)
|
|
291
|
+
return fromExtraArgs;
|
|
292
|
+
return asStringArray(config.args);
|
|
293
|
+
})();
|
|
294
|
+
let restoreRemoteWorkspace = null;
|
|
295
|
+
let remoteRuntimeRootDir = null;
|
|
296
|
+
let localSkillsDir = null;
|
|
297
|
+
let remoteSkillsDir = null;
|
|
298
|
+
let paperclipBridge = null;
|
|
299
|
+
if (executionTargetIsRemote) {
|
|
300
|
+
try {
|
|
301
|
+
localSkillsDir = await buildPiSkillsDir(config);
|
|
302
|
+
await onLog("stdout", `[paperclip] Syncing workspace and Pi runtime assets to ${describeAdapterExecutionTarget(executionTarget)}.\n`);
|
|
303
|
+
const preparedRemoteRuntime = await prepareAdapterExecutionTargetRuntime({
|
|
304
|
+
runId,
|
|
305
|
+
target: executionTarget,
|
|
306
|
+
adapterKey: "pi",
|
|
307
|
+
timeoutSec,
|
|
308
|
+
workspaceLocalDir: cwd,
|
|
309
|
+
installCommand: SANDBOX_INSTALL_COMMAND,
|
|
310
|
+
detectCommand: command,
|
|
311
|
+
assets: [
|
|
312
|
+
{
|
|
313
|
+
key: "skills",
|
|
314
|
+
localDir: localSkillsDir,
|
|
315
|
+
followSymlinks: true,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
restoreRemoteWorkspace = () => preparedRemoteRuntime.restoreWorkspace();
|
|
320
|
+
effectiveExecutionCwd = preparedRemoteRuntime.workspaceRemoteDir ?? effectiveExecutionCwd;
|
|
321
|
+
refreshPaperclipWorkspaceEnvForExecution({
|
|
322
|
+
env,
|
|
323
|
+
envConfig,
|
|
324
|
+
workspaceCwd: effectiveWorkspaceCwd,
|
|
325
|
+
workspaceSource,
|
|
326
|
+
workspaceId,
|
|
327
|
+
workspaceRepoUrl,
|
|
328
|
+
workspaceRepoRef,
|
|
329
|
+
workspaceHints,
|
|
330
|
+
agentHome,
|
|
331
|
+
executionTargetIsRemote,
|
|
332
|
+
executionCwd: effectiveExecutionCwd,
|
|
333
|
+
});
|
|
334
|
+
if (adapterExecutionTargetUsesManagedHome(executionTarget) && preparedRemoteRuntime.runtimeRootDir) {
|
|
335
|
+
env.HOME = preparedRemoteRuntime.runtimeRootDir;
|
|
336
|
+
}
|
|
337
|
+
remoteRuntimeRootDir = preparedRemoteRuntime.runtimeRootDir;
|
|
338
|
+
remoteSkillsDir = preparedRemoteRuntime.assetDirs.skills ?? null;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
await Promise.allSettled([
|
|
342
|
+
restoreRemoteWorkspace?.(),
|
|
343
|
+
localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
|
|
344
|
+
]);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd);
|
|
349
|
+
if (executionTargetIsRemote && adapterExecutionTargetUsesPaperclipBridge(runtimeExecutionTarget)) {
|
|
350
|
+
paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
|
|
351
|
+
runId,
|
|
352
|
+
target: runtimeExecutionTarget,
|
|
353
|
+
runtimeRootDir: remoteRuntimeRootDir,
|
|
354
|
+
adapterKey: "pi",
|
|
355
|
+
timeoutSec,
|
|
356
|
+
hostApiToken: env.PAPERCLIP_API_KEY,
|
|
357
|
+
onLog,
|
|
358
|
+
});
|
|
359
|
+
if (paperclipBridge) {
|
|
360
|
+
Object.assign(env, paperclipBridge.env);
|
|
361
|
+
loggedEnv = buildInvocationEnvForLogs(env, {
|
|
362
|
+
runtimeEnv: Object.fromEntries(Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter((entry) => typeof entry[1] === "string")),
|
|
363
|
+
includeRuntimeKeys: ["HOME"],
|
|
364
|
+
resolvedCommand,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
|
369
|
+
const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
|
|
370
|
+
const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
|
|
371
|
+
const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution);
|
|
372
|
+
const sessionTargetMatches = adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget);
|
|
373
|
+
const sessionParamsCwdMatches = runtimeSessionCwd.length === 0 ||
|
|
374
|
+
executionCwdsMatch(runtimeSessionCwd, effectiveExecutionCwd, executionTargetIsRemote);
|
|
375
|
+
const savedSessionCwd = runtimeSessionId.length > 0
|
|
376
|
+
? await readSavedSessionCwd({
|
|
377
|
+
runId,
|
|
378
|
+
sessionPath: runtimeSessionId,
|
|
379
|
+
executionTarget: runtimeExecutionTarget ?? null,
|
|
380
|
+
cwd,
|
|
381
|
+
env,
|
|
382
|
+
timeoutSec,
|
|
383
|
+
graceSec,
|
|
384
|
+
})
|
|
385
|
+
: null;
|
|
386
|
+
const sessionHeaderCwdMatches = runtimeSessionId.length === 0 ||
|
|
387
|
+
(savedSessionCwd !== null &&
|
|
388
|
+
executionCwdsMatch(savedSessionCwd, effectiveExecutionCwd, executionTargetIsRemote));
|
|
389
|
+
const canResumeSession = runtimeSessionId.length > 0 &&
|
|
390
|
+
sessionTargetMatches &&
|
|
391
|
+
sessionParamsCwdMatches &&
|
|
392
|
+
sessionHeaderCwdMatches;
|
|
393
|
+
const sessionPath = canResumeSession
|
|
394
|
+
? runtimeSessionId
|
|
395
|
+
: executionTargetIsRemote && remoteRuntimeRootDir
|
|
396
|
+
? buildRemoteSessionPath(remoteRuntimeRootDir, agent.id, new Date().toISOString())
|
|
397
|
+
: buildSessionPath(agent.id, new Date().toISOString());
|
|
398
|
+
if (runtimeSessionId && !canResumeSession) {
|
|
399
|
+
const staleSessionCwdNote = savedSessionCwd !== null && !sessionHeaderCwdMatches
|
|
400
|
+
? ` Pi stored cwd "${savedSessionCwd}" in the session header, so Paperclip will start a fresh session for "${effectiveExecutionCwd}".`
|
|
401
|
+
: "";
|
|
402
|
+
await onLog("stdout", executionTargetIsRemote
|
|
403
|
+
? `[paperclip] Pi session "${runtimeSessionId}" does not match the current remote execution state and will not be resumed in "${effectiveExecutionCwd}".${staleSessionCwdNote} Starting a fresh remote session.\n`
|
|
404
|
+
: `[paperclip] Pi session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".${staleSessionCwdNote}\n`);
|
|
405
|
+
}
|
|
406
|
+
if (!canResumeSession) {
|
|
407
|
+
if (executionTargetIsRemote) {
|
|
408
|
+
await ensureAdapterExecutionTargetFile(runId, runtimeExecutionTarget, sessionPath, {
|
|
409
|
+
cwd,
|
|
410
|
+
env,
|
|
411
|
+
timeoutSec: 15,
|
|
412
|
+
graceSec: 5,
|
|
413
|
+
onLog,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
try {
|
|
418
|
+
await fs.writeFile(sessionPath, "", { flag: "wx" });
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
if (err.code !== "EEXIST") {
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Handle instructions file and build system prompt extension
|
|
428
|
+
const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
|
|
429
|
+
const resolvedInstructionsFilePath = instructionsFilePath
|
|
430
|
+
? path.resolve(cwd, instructionsFilePath)
|
|
431
|
+
: "";
|
|
432
|
+
const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
|
|
433
|
+
let systemPromptExtension = "";
|
|
434
|
+
let instructionsReadFailed = false;
|
|
435
|
+
if (resolvedInstructionsFilePath) {
|
|
436
|
+
try {
|
|
437
|
+
const instructionsContents = await fs.readFile(resolvedInstructionsFilePath, "utf8");
|
|
438
|
+
systemPromptExtension =
|
|
439
|
+
`${instructionsContents}\n\n` +
|
|
440
|
+
`The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` +
|
|
441
|
+
`Resolve any relative file references from ${instructionsFileDir}.\n\n` +
|
|
442
|
+
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
instructionsReadFailed = true;
|
|
446
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
447
|
+
await onLog("stdout", `[paperclip] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`);
|
|
448
|
+
// Fall back to base prompt template
|
|
449
|
+
systemPromptExtension = promptTemplate;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
systemPromptExtension = promptTemplate;
|
|
454
|
+
}
|
|
455
|
+
const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
|
|
456
|
+
const templateData = {
|
|
457
|
+
agentId: agent.id,
|
|
458
|
+
companyId: agent.companyId,
|
|
459
|
+
runId,
|
|
460
|
+
company: { id: agent.companyId },
|
|
461
|
+
agent,
|
|
462
|
+
run: { id: runId, source: "on_demand" },
|
|
463
|
+
context,
|
|
464
|
+
};
|
|
465
|
+
const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, templateData);
|
|
466
|
+
const renderedBootstrapPrompt = !canResumeSession && bootstrapPromptTemplate.trim().length > 0
|
|
467
|
+
? renderTemplate(bootstrapPromptTemplate, templateData).trim()
|
|
468
|
+
: "";
|
|
469
|
+
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
|
|
470
|
+
const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
|
|
471
|
+
const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
|
472
|
+
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
|
473
|
+
const userPrompt = joinPromptSections([
|
|
474
|
+
renderedBootstrapPrompt,
|
|
475
|
+
wakePrompt,
|
|
476
|
+
sessionHandoffNote,
|
|
477
|
+
renderedHeartbeatPrompt,
|
|
478
|
+
]);
|
|
479
|
+
const promptMetrics = {
|
|
480
|
+
systemPromptChars: renderedSystemPromptExtension.length,
|
|
481
|
+
promptChars: userPrompt.length,
|
|
482
|
+
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
|
483
|
+
wakePromptChars: wakePrompt.length,
|
|
484
|
+
sessionHandoffChars: sessionHandoffNote.length,
|
|
485
|
+
heartbeatPromptChars: renderedHeartbeatPrompt.length,
|
|
486
|
+
};
|
|
487
|
+
const commandNotes = (() => {
|
|
488
|
+
if (!resolvedInstructionsFilePath)
|
|
489
|
+
return [];
|
|
490
|
+
if (instructionsReadFailed) {
|
|
491
|
+
return [
|
|
492
|
+
`Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`,
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
return [
|
|
496
|
+
`Loaded agent instructions from ${resolvedInstructionsFilePath}`,
|
|
497
|
+
`Appended instructions + path directive to system prompt (relative references from ${instructionsFileDir}).`,
|
|
498
|
+
];
|
|
499
|
+
})();
|
|
500
|
+
const buildArgs = (sessionFile) => {
|
|
501
|
+
const args = [];
|
|
502
|
+
// Use JSON mode for structured output with print mode (non-interactive)
|
|
503
|
+
args.push("--mode", "json");
|
|
504
|
+
args.push("-p"); // Non-interactive mode: process prompt and exit
|
|
505
|
+
// Use --append-system-prompt to extend Pi's default system prompt
|
|
506
|
+
args.push("--append-system-prompt", renderedSystemPromptExtension);
|
|
507
|
+
if (provider)
|
|
508
|
+
args.push("--provider", provider);
|
|
509
|
+
if (modelId)
|
|
510
|
+
args.push("--model", modelId);
|
|
511
|
+
if (thinking)
|
|
512
|
+
args.push("--thinking", thinking);
|
|
513
|
+
args.push("--tools", "read,bash,edit,write,grep,find,ls");
|
|
514
|
+
args.push("--session", sessionFile);
|
|
515
|
+
args.push("--skill", remoteSkillsDir ?? PI_AGENT_SKILLS_DIR);
|
|
516
|
+
if (extraArgs.length > 0)
|
|
517
|
+
args.push(...extraArgs);
|
|
518
|
+
// Add the user prompt as the last argument
|
|
519
|
+
args.push(userPrompt);
|
|
520
|
+
return args;
|
|
521
|
+
};
|
|
522
|
+
const runAttempt = async (sessionFile) => {
|
|
523
|
+
const args = buildArgs(sessionFile);
|
|
524
|
+
if (onMeta) {
|
|
525
|
+
await onMeta({
|
|
526
|
+
adapterType: "pi_local",
|
|
527
|
+
command: resolvedCommand,
|
|
528
|
+
cwd: effectiveExecutionCwd,
|
|
529
|
+
commandNotes,
|
|
530
|
+
commandArgs: args,
|
|
531
|
+
env: loggedEnv,
|
|
532
|
+
prompt: userPrompt,
|
|
533
|
+
promptMetrics,
|
|
534
|
+
context,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
// Buffer stdout by lines to handle partial JSON chunks
|
|
538
|
+
let stdoutBuffer = "";
|
|
539
|
+
const bufferedOnLog = async (stream, chunk) => {
|
|
540
|
+
if (stream === "stderr") {
|
|
541
|
+
// Pass stderr through immediately (not JSONL)
|
|
542
|
+
await onLog(stream, chunk);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Buffer stdout and emit only complete lines
|
|
546
|
+
stdoutBuffer += chunk;
|
|
547
|
+
const lines = stdoutBuffer.split("\n");
|
|
548
|
+
// Keep the last (potentially incomplete) line in the buffer
|
|
549
|
+
stdoutBuffer = lines.pop() || "";
|
|
550
|
+
// Emit complete lines
|
|
551
|
+
for (const line of lines) {
|
|
552
|
+
if (line) {
|
|
553
|
+
await onLog(stream, line + "\n");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
|
|
558
|
+
cwd,
|
|
559
|
+
env: executionTargetIsRemote ? env : runtimeEnv,
|
|
560
|
+
timeoutSec,
|
|
561
|
+
graceSec,
|
|
562
|
+
onSpawn,
|
|
563
|
+
onLog: bufferedOnLog,
|
|
564
|
+
});
|
|
565
|
+
// Flush any remaining buffer content
|
|
566
|
+
if (stdoutBuffer) {
|
|
567
|
+
await onLog("stdout", stdoutBuffer);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
proc,
|
|
571
|
+
rawStderr: proc.stderr,
|
|
572
|
+
parsed: parsePiJsonl(proc.stdout),
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
const toResult = (attempt, clearSessionOnMissingSession = false) => {
|
|
576
|
+
if (attempt.proc.timedOut) {
|
|
577
|
+
return {
|
|
578
|
+
exitCode: attempt.proc.exitCode,
|
|
579
|
+
signal: attempt.proc.signal,
|
|
580
|
+
timedOut: true,
|
|
581
|
+
errorMessage: `Timed out after ${timeoutSec}s`,
|
|
582
|
+
clearSession: clearSessionOnMissingSession,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const resolvedSessionId = clearSessionOnMissingSession ? null : sessionPath;
|
|
586
|
+
const resolvedSessionParams = resolvedSessionId
|
|
587
|
+
? {
|
|
588
|
+
sessionId: resolvedSessionId,
|
|
589
|
+
cwd: effectiveExecutionCwd,
|
|
590
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
591
|
+
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
|
592
|
+
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
|
593
|
+
...(executionTargetIsRemote
|
|
594
|
+
? {
|
|
595
|
+
remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget),
|
|
596
|
+
}
|
|
597
|
+
: {}),
|
|
598
|
+
}
|
|
599
|
+
: null;
|
|
600
|
+
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
|
601
|
+
const rawExitCode = attempt.proc.exitCode;
|
|
602
|
+
const parsedError = attempt.parsed.errors.find((error) => error.trim().length > 0) ?? "";
|
|
603
|
+
const effectiveExitCode = (rawExitCode ?? 0) === 0 && parsedError ? 1 : rawExitCode;
|
|
604
|
+
const fallbackErrorMessage = parsedError || stderrLine || `Pi exited with code ${rawExitCode ?? -1}`;
|
|
605
|
+
return {
|
|
606
|
+
exitCode: effectiveExitCode,
|
|
607
|
+
signal: attempt.proc.signal,
|
|
608
|
+
timedOut: false,
|
|
609
|
+
errorMessage: (effectiveExitCode ?? 0) === 0 ? null : fallbackErrorMessage,
|
|
610
|
+
usage: {
|
|
611
|
+
inputTokens: attempt.parsed.usage.inputTokens,
|
|
612
|
+
outputTokens: attempt.parsed.usage.outputTokens,
|
|
613
|
+
cachedInputTokens: attempt.parsed.usage.cachedInputTokens,
|
|
614
|
+
},
|
|
615
|
+
sessionId: resolvedSessionId,
|
|
616
|
+
sessionParams: resolvedSessionParams,
|
|
617
|
+
sessionDisplayId: resolvedSessionId,
|
|
618
|
+
provider: provider,
|
|
619
|
+
biller: resolvePiBiller(runtimeEnv, provider),
|
|
620
|
+
model: model,
|
|
621
|
+
billingType: "unknown",
|
|
622
|
+
costUsd: attempt.parsed.usage.costUsd,
|
|
623
|
+
resultJson: {
|
|
624
|
+
stdout: attempt.proc.stdout,
|
|
625
|
+
stderr: attempt.proc.stderr,
|
|
626
|
+
},
|
|
627
|
+
summary: attempt.parsed.finalMessage ?? attempt.parsed.messages.join("\n\n").trim(),
|
|
628
|
+
clearSession: Boolean(clearSessionOnMissingSession),
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
try {
|
|
632
|
+
return await withSerializedExecution(agent.id, onLog, async () => {
|
|
633
|
+
const initial = await runAttempt(sessionPath);
|
|
634
|
+
const initialFailed = !initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || initial.parsed.errors.length > 0);
|
|
635
|
+
if (canResumeSession &&
|
|
636
|
+
initialFailed &&
|
|
637
|
+
isPiUnknownSessionError(initial.proc.stdout, initial.rawStderr)) {
|
|
638
|
+
await onLog("stdout", `[paperclip] Pi session "${runtimeSessionId}" is unavailable; retrying with a fresh session.\n`);
|
|
639
|
+
const newSessionPath = executionTargetIsRemote && remoteRuntimeRootDir
|
|
640
|
+
? buildRemoteSessionPath(remoteRuntimeRootDir, agent.id, new Date().toISOString())
|
|
641
|
+
: buildSessionPath(agent.id, new Date().toISOString());
|
|
642
|
+
if (executionTargetIsRemote) {
|
|
643
|
+
await ensureAdapterExecutionTargetFile(runId, executionTarget, newSessionPath, {
|
|
644
|
+
cwd,
|
|
645
|
+
env,
|
|
646
|
+
timeoutSec: 15,
|
|
647
|
+
graceSec: 5,
|
|
648
|
+
onLog,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
try {
|
|
653
|
+
await fs.writeFile(newSessionPath, "", { flag: "wx" });
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
if (err.code !== "EEXIST") {
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const retry = await runAttempt(newSessionPath);
|
|
662
|
+
return toResult(retry, true);
|
|
663
|
+
}
|
|
664
|
+
return toResult(initial);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
await Promise.all([
|
|
669
|
+
paperclipBridge?.stop(),
|
|
670
|
+
restoreRemoteWorkspace?.(),
|
|
671
|
+
localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
|
|
672
|
+
]);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=execute.js.map
|