@tickernelz/paperclip-pro-adapter-kimi-local 2026.925.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.
Files changed (86) hide show
  1. package/LICENSE +22 -0
  2. package/dist/cli/format-event.d.ts +2 -0
  3. package/dist/cli/format-event.d.ts.map +1 -0
  4. package/dist/cli/format-event.js +95 -0
  5. package/dist/cli/format-event.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/index.d.ts +30 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +82 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/server/acp.d.ts +19 -0
  15. package/dist/server/acp.d.ts.map +1 -0
  16. package/dist/server/acp.js +316 -0
  17. package/dist/server/acp.js.map +1 -0
  18. package/dist/server/acp.test.d.ts +2 -0
  19. package/dist/server/acp.test.d.ts.map +1 -0
  20. package/dist/server/acp.test.js +57 -0
  21. package/dist/server/acp.test.js.map +1 -0
  22. package/dist/server/engine-availability.test.d.ts +2 -0
  23. package/dist/server/engine-availability.test.d.ts.map +1 -0
  24. package/dist/server/engine-availability.test.js +41 -0
  25. package/dist/server/engine-availability.test.js.map +1 -0
  26. package/dist/server/execute.acp-failure.test.d.ts +2 -0
  27. package/dist/server/execute.acp-failure.test.d.ts.map +1 -0
  28. package/dist/server/execute.acp-failure.test.js +22 -0
  29. package/dist/server/execute.acp-failure.test.js.map +1 -0
  30. package/dist/server/execute.d.ts +3 -0
  31. package/dist/server/execute.d.ts.map +1 -0
  32. package/dist/server/execute.js +630 -0
  33. package/dist/server/execute.js.map +1 -0
  34. package/dist/server/execute.test.d.ts +2 -0
  35. package/dist/server/execute.test.d.ts.map +1 -0
  36. package/dist/server/execute.test.js +354 -0
  37. package/dist/server/execute.test.js.map +1 -0
  38. package/dist/server/index.d.ts +8 -0
  39. package/dist/server/index.d.ts.map +1 -0
  40. package/dist/server/index.js +66 -0
  41. package/dist/server/index.js.map +1 -0
  42. package/dist/server/parse.d.ts +66 -0
  43. package/dist/server/parse.d.ts.map +1 -0
  44. package/dist/server/parse.js +194 -0
  45. package/dist/server/parse.js.map +1 -0
  46. package/dist/server/parse.test.d.ts +2 -0
  47. package/dist/server/parse.test.d.ts.map +1 -0
  48. package/dist/server/parse.test.js +155 -0
  49. package/dist/server/parse.test.js.map +1 -0
  50. package/dist/server/skills.d.ts +4 -0
  51. package/dist/server/skills.d.ts.map +1 -0
  52. package/dist/server/skills.js +73 -0
  53. package/dist/server/skills.js.map +1 -0
  54. package/dist/server/test.d.ts +3 -0
  55. package/dist/server/test.d.ts.map +1 -0
  56. package/dist/server/test.js +306 -0
  57. package/dist/server/test.js.map +1 -0
  58. package/dist/server/test.test.d.ts +2 -0
  59. package/dist/server/test.test.d.ts.map +1 -0
  60. package/dist/server/test.test.js +126 -0
  61. package/dist/server/test.test.js.map +1 -0
  62. package/dist/server/utils.d.ts +2 -0
  63. package/dist/server/utils.d.ts.map +1 -0
  64. package/dist/server/utils.js +7 -0
  65. package/dist/server/utils.js.map +1 -0
  66. package/dist/ui/build-config.d.ts +3 -0
  67. package/dist/ui/build-config.d.ts.map +1 -0
  68. package/dist/ui/build-config.js +79 -0
  69. package/dist/ui/build-config.js.map +1 -0
  70. package/dist/ui/build-config.test.d.ts +2 -0
  71. package/dist/ui/build-config.test.d.ts.map +1 -0
  72. package/dist/ui/build-config.test.js +73 -0
  73. package/dist/ui/build-config.test.js.map +1 -0
  74. package/dist/ui/index.d.ts +3 -0
  75. package/dist/ui/index.d.ts.map +1 -0
  76. package/dist/ui/index.js +3 -0
  77. package/dist/ui/index.js.map +1 -0
  78. package/dist/ui/parse-stdout.d.ts +9 -0
  79. package/dist/ui/parse-stdout.d.ts.map +1 -0
  80. package/dist/ui/parse-stdout.js +110 -0
  81. package/dist/ui/parse-stdout.js.map +1 -0
  82. package/dist/ui/parse-stdout.test.d.ts +2 -0
  83. package/dist/ui/parse-stdout.test.d.ts.map +1 -0
  84. package/dist/ui/parse-stdout.test.js +84 -0
  85. package/dist/ui/parse-stdout.test.js.map +1 -0
  86. package/package.json +57 -0
@@ -0,0 +1,630 @@
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 { adapterExecutionTargetIsRemote, adapterExecutionTargetRemoteCwd, overrideAdapterExecutionTargetRemoteCwd, adapterExecutionTargetSessionIdentity, adapterExecutionTargetSessionMatches, adapterExecutionTargetUsesManagedHome, adapterExecutionTargetUsesPaperclipBridge, describeAdapterExecutionTarget, ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetRuntimeCommandInstalled, prepareAdapterExecutionTargetRuntime, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeoutSec, resolveAdapterExecutionTargetCommandForLogs, runAdapterExecutionTargetProcess, startAdapterExecutionTargetPaperclipBridge, } from "@tickernelz/paperclip-pro-adapter-utils/execution-target";
6
+ import { asNumber, asString, asStringArray, buildPaperclipEnv, buildRuntimeToolsEnv, buildInvocationEnvForLogs, ensureAbsoluteDirectory, joinPromptSections, ensurePathInEnv, refreshPaperclipWorkspaceEnvForExecution, isPaperclipSkillSourceMissing, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, resolveLegacyPaperclipDesiredSkillNames, parseObject, renderTemplate, renderPaperclipWakePrompt, selectPaperclipTaskMarkdown, selectInitialCommunicationGuidance, isPaperclipRecoveryWakePayload, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
7
+ import { SANDBOX_INSTALL_COMMAND, modelSupportsEffort, resolveKimiThinkingEffort, } from "../index.js";
8
+ import { describeKimiFailure, detectKimiAuthRequired, extractKimiRuntimeEvents, isKimiSessionUnrecoverableError, isKimiTransientNetworkError, parseKimiJsonl, } from "./parse.js";
9
+ import { createKimiAcpExecutor, resolveKimiExecutionEngineForRun, } from "./acp.js";
10
+ import { firstNonEmptyLine } from "./utils.js";
11
+ const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
12
+ const executeKimiAcp = createKimiAcpExecutor();
13
+ /**
14
+ * Wrap `onLog` so each complete kimi stream-json stdout line is also mapped to
15
+ * `onEvent` runtime events (assistant snippet, tool name). This keeps the raw
16
+ * run log intact while lighting up the issue-thread activity indicator, which
17
+ * reads `currentToolName` / `lastAssistantSnippet` / `lastEventAt` derived from
18
+ * `onEvent` rather than from the raw log stream. Stdout arrives in arbitrary
19
+ * chunks, so lines are buffered and split on newlines. `flush` must be called
20
+ * once the process exits so the final line reaches `onEvent` even when kimi
21
+ * closes stdout without a trailing newline (otherwise the last assistant
22
+ * message or tool call would be missing from live status).
23
+ */
24
+ function createKimiEventForwardingLog(onLog, onEvent) {
25
+ if (!onEvent)
26
+ return { log: onLog, flush: async () => { } };
27
+ let buffer = "";
28
+ const emitLine = async (raw) => {
29
+ const line = raw.trim();
30
+ if (!line)
31
+ return;
32
+ for (const event of extractKimiRuntimeEvents(line)) {
33
+ await onEvent({ eventType: event.eventType, stream: "stdout", message: event.message, payload: event.payload });
34
+ }
35
+ };
36
+ return {
37
+ log: async (stream, chunk) => {
38
+ await onLog(stream, chunk);
39
+ if (stream !== "stdout")
40
+ return;
41
+ buffer += chunk;
42
+ let newlineIndex;
43
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
44
+ const line = buffer.slice(0, newlineIndex);
45
+ buffer = buffer.slice(newlineIndex + 1);
46
+ await emitLine(line);
47
+ }
48
+ },
49
+ flush: async () => {
50
+ const remaining = buffer;
51
+ buffer = "";
52
+ await emitLine(remaining);
53
+ },
54
+ };
55
+ }
56
+ function hasNonEmptyEnvValue(env, key) {
57
+ const raw = env[key];
58
+ return typeof raw === "string" && raw.trim().length > 0;
59
+ }
60
+ function resolveKimiBillingType(env) {
61
+ return hasNonEmptyEnvValue(env, "KIMI_MODEL_API_KEY") ? "api" : "subscription";
62
+ }
63
+ /**
64
+ * Headless-safe environment for unattended `kimi -p` runs. CI=1 disables
65
+ * theme detection, NO_COLOR=1 keeps stdout parseable, and
66
+ * KIMI_CODE_NO_AUTO_UPDATE=1 skips the update preflight. User-configured
67
+ * values always win.
68
+ */
69
+ function buildKimiHeadlessEnv(env) {
70
+ const next = { ...env };
71
+ if (!next.CI?.trim())
72
+ next.CI = "1";
73
+ if (!next.NO_COLOR?.trim())
74
+ next.NO_COLOR = "1";
75
+ if (!next.KIMI_CODE_NO_AUTO_UPDATE?.trim())
76
+ next.KIMI_CODE_NO_AUTO_UPDATE = "1";
77
+ if (!next.TERM?.trim())
78
+ next.TERM = "dumb";
79
+ return next;
80
+ }
81
+ function buildKimiRuntimeEnv(env) {
82
+ return Object.fromEntries(Object.entries(ensurePathInEnv({ ...process.env, ...buildKimiHeadlessEnv(env) })).filter((entry) => typeof entry[1] === "string"));
83
+ }
84
+ function renderPaperclipEnvNote(env) {
85
+ const paperclipKeys = Object.keys(env)
86
+ .filter((key) => key.startsWith("PAPERCLIP_"))
87
+ .sort();
88
+ if (paperclipKeys.length === 0)
89
+ return "";
90
+ return [
91
+ "Paperclip runtime note:",
92
+ `The following PAPERCLIP_* environment variables are available in this run: ${paperclipKeys.join(", ")}`,
93
+ "Do not assume these variables are missing without checking your shell environment.",
94
+ "",
95
+ "",
96
+ ].join("\n");
97
+ }
98
+ function renderApiAccessNote(env) {
99
+ if (!hasNonEmptyEnvValue(env, "PAPERCLIP_API_URL") || !hasNonEmptyEnvValue(env, "PAPERCLIP_API_KEY"))
100
+ return "";
101
+ return [
102
+ "Paperclip API access note:",
103
+ "Use shell commands with curl to make Paperclip API requests when needed.",
104
+ "Include X-Paperclip-Run-Id on mutating requests.",
105
+ "",
106
+ "",
107
+ ].join("\n");
108
+ }
109
+ async function buildKimiSkillsDir(config) {
110
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-kimi-skills-"));
111
+ const target = path.join(tmp, "skills");
112
+ await fs.mkdir(target, { recursive: true });
113
+ const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
114
+ const desiredNames = new Set(resolveLegacyPaperclipDesiredSkillNames(config, availableEntries));
115
+ for (const entry of availableEntries) {
116
+ if (!desiredNames.has(entry.key))
117
+ continue;
118
+ if (isPaperclipSkillSourceMissing(entry))
119
+ continue;
120
+ await fs.symlink(entry.source, path.join(target, entry.runtimeName));
121
+ }
122
+ return target;
123
+ }
124
+ export async function execute(ctx) {
125
+ const engineSelection = await resolveKimiExecutionEngineForRun(ctx);
126
+ if (engineSelection.unavailableReason) {
127
+ return {
128
+ exitCode: 1,
129
+ signal: null,
130
+ timedOut: false,
131
+ errorCode: "adapter_engine_unavailable",
132
+ errorMessage: engineSelection.unavailableReason,
133
+ resultJson: {
134
+ executionRecovery: { kind: "bootstrap", providerWorkStarted: false },
135
+ },
136
+ };
137
+ }
138
+ if (engineSelection.engine === "acp") {
139
+ return executeKimiAcp(ctx);
140
+ }
141
+ const { runId, agent, runtime, config, context, onLog, onMeta, onEvent, onSpawn, authToken } = ctx;
142
+ const executionTarget = readAdapterExecutionTarget({
143
+ executionTarget: ctx.executionTarget,
144
+ legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
145
+ });
146
+ const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
147
+ const promptTemplate = asString(config.promptTemplate, context.conversationMode === true
148
+ ? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
149
+ : DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
150
+ const command = asString(config.command, "kimi");
151
+ const model = asString(config.model, "").trim();
152
+ const workspaceContext = parseObject(context.paperclipWorkspace);
153
+ const workspaceCwd = asString(workspaceContext.cwd, "");
154
+ const workspaceSource = asString(workspaceContext.source, "");
155
+ const workspaceId = asString(workspaceContext.workspaceId, "");
156
+ const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
157
+ const workspaceRepoRef = asString(workspaceContext.repoRef, "");
158
+ const agentHome = asString(workspaceContext.agentHome, "");
159
+ const workspaceHints = Array.isArray(context.paperclipWorkspaces)
160
+ ? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
161
+ : [];
162
+ const configuredCwd = asString(config.cwd, "");
163
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
164
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
165
+ const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
166
+ let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
167
+ await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
168
+ const kimiSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
169
+ const desiredKimiSkillNames = resolveLegacyPaperclipDesiredSkillNames(config, kimiSkillEntries);
170
+ const envConfig = parseObject(config.env);
171
+ const hasExplicitApiKey = typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0;
172
+ const env = {
173
+ ...buildPaperclipEnv(agent),
174
+ ...buildRuntimeToolsEnv(ctx.runtimeTools),
175
+ };
176
+ env.PAPERCLIP_RUN_ID = runId;
177
+ const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
178
+ (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
179
+ null;
180
+ const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
181
+ ? context.wakeReason.trim()
182
+ : null;
183
+ const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
184
+ (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
185
+ null;
186
+ const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0
187
+ ? context.approvalId.trim()
188
+ : null;
189
+ const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
190
+ ? context.approvalStatus.trim()
191
+ : null;
192
+ const linkedIssueIds = Array.isArray(context.issueIds)
193
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
194
+ : [];
195
+ const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
196
+ if (wakeTaskId)
197
+ env.PAPERCLIP_TASK_ID = wakeTaskId;
198
+ if (issueWorkMode)
199
+ env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
200
+ if (wakeReason)
201
+ env.PAPERCLIP_WAKE_REASON = wakeReason;
202
+ if (wakeCommentId)
203
+ env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
204
+ if (approvalId)
205
+ env.PAPERCLIP_APPROVAL_ID = approvalId;
206
+ if (approvalStatus)
207
+ env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
208
+ if (linkedIssueIds.length > 0)
209
+ env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
210
+ refreshPaperclipWorkspaceEnvForExecution({
211
+ env,
212
+ envConfig,
213
+ workspaceCwd: effectiveWorkspaceCwd,
214
+ workspaceSource,
215
+ workspaceId,
216
+ workspaceRepoUrl,
217
+ workspaceRepoRef,
218
+ workspaceHints,
219
+ agentHome,
220
+ executionTargetIsRemote,
221
+ executionCwd: effectiveExecutionCwd,
222
+ });
223
+ if (!hasExplicitApiKey && authToken) {
224
+ env.PAPERCLIP_API_KEY = authToken;
225
+ }
226
+ // Forward configured thinking effort as KIMI_MODEL_THINKING_EFFORT. Kimi has
227
+ // no per-invocation effort flag; this env var is an operational override that
228
+ // applies to Kimi providers (managed OAuth models included). Only send it for
229
+ // models that advertise support_efforts, and never clobber an explicit value.
230
+ const configuredEffort = asString(config.effort, "").trim();
231
+ if (configuredEffort && modelSupportsEffort(model) && !hasNonEmptyEnvValue(env, "KIMI_MODEL_THINKING_EFFORT")) {
232
+ const kimiEffort = resolveKimiThinkingEffort(configuredEffort);
233
+ if (kimiEffort)
234
+ env.KIMI_MODEL_THINKING_EFFORT = kimiEffort;
235
+ }
236
+ const runtimeEnv = buildKimiRuntimeEnv(env);
237
+ const billingType = resolveKimiBillingType(runtimeEnv);
238
+ const timeoutSec = resolveAdapterExecutionTargetTimeoutSec(executionTarget, asNumber(config.timeoutSec, 0));
239
+ const graceSec = asNumber(config.graceSec, 20);
240
+ await ensureAdapterExecutionTargetRuntimeCommandInstalled({
241
+ runId,
242
+ target: executionTarget,
243
+ installCommand: ctx.runtimeCommandSpec?.installCommand,
244
+ detectCommand: ctx.runtimeCommandSpec?.detectCommand,
245
+ cwd,
246
+ env: runtimeEnv,
247
+ timeoutSec,
248
+ graceSec,
249
+ onLog,
250
+ });
251
+ await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, {
252
+ installCommand: SANDBOX_INSTALL_COMMAND,
253
+ timeoutSec,
254
+ });
255
+ const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
256
+ const extraArgs = (() => {
257
+ const fromExtraArgs = asStringArray(config.extraArgs);
258
+ if (fromExtraArgs.length > 0)
259
+ return fromExtraArgs;
260
+ return asStringArray(config.args);
261
+ })();
262
+ let restoreRemoteWorkspace = null;
263
+ let localSkillsDir = null;
264
+ let remoteSkillsDir = null;
265
+ let remoteRuntimeRootDir = null;
266
+ let paperclipBridge = null;
267
+ if (executionTargetIsRemote) {
268
+ try {
269
+ localSkillsDir = await buildKimiSkillsDir(config);
270
+ await onLog("stdout", `[paperclip] Syncing workspace and Kimi runtime assets to ${describeAdapterExecutionTarget(executionTarget)}.\n`);
271
+ const preparedExecutionTargetRuntime = await prepareAdapterExecutionTargetRuntime({
272
+ runId,
273
+ target: executionTarget,
274
+ adapterKey: "kimi",
275
+ timeoutSec,
276
+ workspaceLocalDir: cwd,
277
+ installCommand: SANDBOX_INSTALL_COMMAND,
278
+ detectCommand: command,
279
+ onProgress: (line) => onLog("stdout", line),
280
+ onRuntimeProgress: ctx.onRuntimeProgress,
281
+ assets: [{
282
+ key: "skills",
283
+ localDir: localSkillsDir,
284
+ followSymlinks: true,
285
+ }],
286
+ });
287
+ restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line));
288
+ effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd;
289
+ refreshPaperclipWorkspaceEnvForExecution({
290
+ env,
291
+ envConfig,
292
+ workspaceCwd: effectiveWorkspaceCwd,
293
+ workspaceSource,
294
+ workspaceId,
295
+ workspaceRepoUrl,
296
+ workspaceRepoRef,
297
+ workspaceHints,
298
+ agentHome,
299
+ executionTargetIsRemote,
300
+ executionCwd: effectiveExecutionCwd,
301
+ });
302
+ remoteRuntimeRootDir = preparedExecutionTargetRuntime.runtimeRootDir;
303
+ const managedHome = adapterExecutionTargetUsesManagedHome(executionTarget);
304
+ const managedRemoteHomeDir = managedHome && preparedExecutionTargetRuntime.runtimeRootDir
305
+ ? preparedExecutionTargetRuntime.runtimeRootDir
306
+ : null;
307
+ if (managedRemoteHomeDir) {
308
+ env.HOME = managedRemoteHomeDir;
309
+ }
310
+ // Deliver the synced skills snapshot via --skills-dir (see buildArgs)
311
+ // from its isolated per-run location instead of copying it over the
312
+ // shared $KIMI_CODE_HOME/skills home. Overwriting the shared home would
313
+ // delete Kimi skills installed by the operator or other agents that
314
+ // Paperclip does not own.
315
+ if (desiredKimiSkillNames.length > 0 && preparedExecutionTargetRuntime.assetDirs.skills) {
316
+ remoteSkillsDir = preparedExecutionTargetRuntime.assetDirs.skills;
317
+ }
318
+ }
319
+ catch (error) {
320
+ await Promise.allSettled([
321
+ restoreRemoteWorkspace?.(),
322
+ localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
323
+ ]);
324
+ throw error;
325
+ }
326
+ }
327
+ const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd);
328
+ if (executionTargetIsRemote && adapterExecutionTargetUsesPaperclipBridge(executionTarget)) {
329
+ paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
330
+ runId,
331
+ target: runtimeExecutionTarget,
332
+ runtimeRootDir: remoteRuntimeRootDir,
333
+ adapterKey: "kimi",
334
+ timeoutSec,
335
+ hostApiToken: env.PAPERCLIP_API_KEY,
336
+ onLog,
337
+ });
338
+ if (paperclipBridge) {
339
+ Object.assign(env, paperclipBridge.env);
340
+ }
341
+ }
342
+ // Local runs deliver desired skills via `--skills-dir` (see buildArgs) from a
343
+ // dedicated per-run directory, rather than symlinking into the user's
344
+ // ~/.kimi-code/skills home. This keeps skill loading reliable and isolated
345
+ // without polluting the operator's Kimi install. Remote runs sync skills into
346
+ // the remote skills home above, so this only applies to local execution.
347
+ if (!executionTargetIsRemote && desiredKimiSkillNames.length > 0) {
348
+ localSkillsDir = await buildKimiSkillsDir(config);
349
+ await onLog("stderr", `[paperclip] Prepared ${desiredKimiSkillNames.length} Kimi skill(s) for --skills-dir delivery.\n`);
350
+ }
351
+ const runtimeSessionParams = parseObject(runtime.sessionParams);
352
+ const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
353
+ const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
354
+ const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution);
355
+ const canResumeSession = runtimeSessionId.length > 0 &&
356
+ (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(effectiveExecutionCwd)) &&
357
+ adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget);
358
+ const sessionId = canResumeSession ? runtimeSessionId : null;
359
+ if (executionTargetIsRemote && runtimeSessionId && !canResumeSession) {
360
+ await onLog("stdout", `[paperclip] Kimi session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`);
361
+ }
362
+ else if (runtimeSessionId && !canResumeSession) {
363
+ await onLog("stdout", `[paperclip] Kimi session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".\n`);
364
+ }
365
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
366
+ const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
367
+ let instructionsPrefix = "";
368
+ if (instructionsFilePath) {
369
+ try {
370
+ const instructionsContents = await fs.readFile(instructionsFilePath, "utf8");
371
+ instructionsPrefix =
372
+ `${instructionsContents}\n\n` +
373
+ `The above agent instructions were loaded from ${instructionsFilePath}. ` +
374
+ `Resolve any relative file references from ${instructionsDir}. ` +
375
+ `This base directory is authoritative for sibling instruction files such as ` +
376
+ `./HEARTBEAT.md, ./SOUL.md, and ./TOOLS.md; do not resolve those from the parent agent directory.\n\n`;
377
+ }
378
+ catch (err) {
379
+ const reason = err instanceof Error ? err.message : String(err);
380
+ await onLog("stdout", `[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
381
+ }
382
+ }
383
+ const commandNotes = (() => {
384
+ const notes = ["Prompt is passed to Kimi via -p for non-interactive execution."];
385
+ notes.push("Added --output-format stream-json for structured headless output.");
386
+ notes.push("Set headless env (CI=1, NO_COLOR=1, KIMI_CODE_NO_AUTO_UPDATE=1) so unattended runs skip interactive prompts and update preflight.");
387
+ if (hasNonEmptyEnvValue(env, "KIMI_MODEL_THINKING_EFFORT")) {
388
+ notes.push(`Set KIMI_MODEL_THINKING_EFFORT=${env.KIMI_MODEL_THINKING_EFFORT} for model ${model}.`);
389
+ }
390
+ const effectiveSkillsDir = executionTargetIsRemote ? remoteSkillsDir : localSkillsDir;
391
+ if (effectiveSkillsDir) {
392
+ notes.push(`Loading ${desiredKimiSkillNames.length} desired skill(s) via --skills-dir ${effectiveSkillsDir}.`);
393
+ }
394
+ if (!executionTargetIsRemote && instructionsFilePath) {
395
+ notes.push(`Added --add-dir ${path.dirname(instructionsFilePath)} so sibling instruction files are readable.`);
396
+ }
397
+ if (!instructionsFilePath)
398
+ return notes;
399
+ if (instructionsPrefix.length > 0) {
400
+ notes.push(`Loaded agent instructions from ${instructionsFilePath}`, `Prepended instructions + path directive to prompt (relative references from ${instructionsDir}).`);
401
+ return notes;
402
+ }
403
+ notes.push(`Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`);
404
+ return notes;
405
+ })();
406
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
407
+ const templateData = {
408
+ agentId: agent.id,
409
+ companyId: agent.companyId,
410
+ runId,
411
+ company: { id: agent.companyId },
412
+ agent,
413
+ run: { id: runId, source: "on_demand" },
414
+ context,
415
+ };
416
+ const renderedBootstrapPrompt = !sessionId && bootstrapPromptTemplate.trim().length > 0
417
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
418
+ : "";
419
+ const taskContextNote = context.conversationMode === true
420
+ ? selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId), includeCommunicationGuidance: false })
421
+ : "";
422
+ const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
423
+ conversationMode: context.conversationMode === true,
424
+ resumedSession: Boolean(sessionId),
425
+ suppressIssueDescription: taskContextNote.length > 0,
426
+ });
427
+ const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
428
+ const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
429
+ ? ""
430
+ : renderTemplate(promptTemplate, templateData);
431
+ const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
432
+ const paperclipEnvNote = renderPaperclipEnvNote(env);
433
+ const apiAccessNote = renderApiAccessNote(env);
434
+ const basePrompt = joinPromptSections([
435
+ instructionsPrefix,
436
+ renderedBootstrapPrompt,
437
+ wakePrompt,
438
+ taskContextNote,
439
+ sessionHandoffNote,
440
+ paperclipEnvNote,
441
+ apiAccessNote,
442
+ renderedPrompt,
443
+ ]);
444
+ const promptMetrics = {
445
+ promptChars: basePrompt.length,
446
+ instructionsChars: instructionsPrefix.length,
447
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
448
+ wakePromptChars: wakePrompt.length,
449
+ taskContextChars: taskContextNote.length,
450
+ sessionHandoffChars: sessionHandoffNote.length,
451
+ runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
452
+ heartbeatPromptChars: renderedPrompt.length,
453
+ };
454
+ const buildArgs = (resumeSessionId, prompt) => {
455
+ const args = ["--output-format", "stream-json"];
456
+ if (resumeSessionId)
457
+ args.push("-r", resumeSessionId);
458
+ if (model)
459
+ args.push("-m", model);
460
+ // Make the agent instructions directory readable so Kimi can open sibling
461
+ // instruction files (./HEARTBEAT.md, ./SOUL.md, ./TOOLS.md) referenced by
462
+ // the prepended entry file. Local-only: the directory is a host path that
463
+ // is not synced to remote execution targets.
464
+ if (!executionTargetIsRemote && instructionsFilePath) {
465
+ args.push("--add-dir", path.dirname(instructionsFilePath));
466
+ }
467
+ // Load desired Paperclip skills from the dedicated per-run directory
468
+ // (local snapshot, or the synced remote snapshot) instead of the shared
469
+ // skills home. Only passed when skills are desired so unconfigured agents
470
+ // keep Kimi's default skill discovery.
471
+ const effectiveSkillsDir = executionTargetIsRemote ? remoteSkillsDir : localSkillsDir;
472
+ if (effectiveSkillsDir) {
473
+ args.push("--skills-dir", effectiveSkillsDir);
474
+ }
475
+ if (extraArgs.length > 0)
476
+ args.push(...extraArgs);
477
+ args.push("-p", prompt);
478
+ return args;
479
+ };
480
+ const runAttempt = async (resumeSessionId) => {
481
+ const prompt = joinPromptSections([
482
+ selectInitialCommunicationGuidance(context, { resumedSession: Boolean(resumeSessionId) }),
483
+ basePrompt,
484
+ ]);
485
+ const args = buildArgs(resumeSessionId, prompt);
486
+ const invocationEnv = buildKimiHeadlessEnv(env);
487
+ const invocationRuntimeEnv = buildKimiRuntimeEnv(env);
488
+ const loggedEnv = buildInvocationEnvForLogs(invocationEnv, {
489
+ runtimeEnv: invocationRuntimeEnv,
490
+ includeRuntimeKeys: ["HOME"],
491
+ resolvedCommand,
492
+ });
493
+ if (onMeta) {
494
+ await onMeta({
495
+ adapterType: "kimi_local",
496
+ command: resolvedCommand,
497
+ cwd: effectiveExecutionCwd,
498
+ commandNotes,
499
+ commandArgs: args.map((value, index) => (index === args.length - 1 ? `<prompt ${prompt.length} chars>` : value)),
500
+ env: loggedEnv,
501
+ prompt,
502
+ promptMetrics: { ...promptMetrics, promptChars: prompt.length },
503
+ context,
504
+ });
505
+ }
506
+ const eventForwarder = createKimiEventForwardingLog(onLog, onEvent);
507
+ const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
508
+ cwd,
509
+ env: invocationEnv,
510
+ timeoutSec,
511
+ graceSec,
512
+ onSpawn,
513
+ onRuntimeProgress: ctx.onRuntimeProgress,
514
+ onLog: eventForwarder.log,
515
+ runLogTail: paperclipBridge?.runLogTail,
516
+ settleRunDisposition: paperclipBridge?.settleRunDisposition,
517
+ });
518
+ await eventForwarder.flush();
519
+ return {
520
+ proc,
521
+ parsed: parseKimiJsonl(proc.stdout),
522
+ };
523
+ };
524
+ const toResult = (attempt, clearSessionOnMissingSession = false, isRetry = false) => {
525
+ const authMeta = detectKimiAuthRequired({
526
+ stdout: attempt.proc.stdout,
527
+ stderr: attempt.proc.stderr,
528
+ });
529
+ const networkUnavailable = isKimiTransientNetworkError(attempt.proc.stdout, attempt.proc.stderr);
530
+ if (attempt.proc.timedOut) {
531
+ return {
532
+ exitCode: attempt.proc.exitCode,
533
+ signal: attempt.proc.signal,
534
+ timedOut: true,
535
+ errorMessage: `Timed out after ${timeoutSec}s`,
536
+ errorCode: authMeta.requiresAuth
537
+ ? "kimi_auth_required"
538
+ : networkUnavailable
539
+ ? "kimi_network_unavailable"
540
+ : null,
541
+ clearSession: clearSessionOnMissingSession,
542
+ };
543
+ }
544
+ const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
545
+ const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
546
+ const structuredFailure = describeKimiFailure({
547
+ errorMessage: attempt.parsed.errorMessage,
548
+ stderr: attempt.proc.stderr,
549
+ });
550
+ const fallbackErrorMessage = parsedError ||
551
+ structuredFailure ||
552
+ stderrLine ||
553
+ (attempt.proc.signal
554
+ ? `Kimi was terminated by signal ${attempt.proc.signal}`
555
+ : `Kimi exited with code ${attempt.proc.exitCode ?? -1}`);
556
+ // A null exit code means the process never exited normally (e.g. killed by
557
+ // a signal). Timeouts are handled earlier; treat any other non-zero or
558
+ // null exit as a failure so a signaled kill is never reported as success.
559
+ const failed = attempt.proc.exitCode === null || attempt.proc.exitCode !== 0;
560
+ // On retry, don't fall back to old session ID — the old session was stale
561
+ const canFallbackToRuntimeSession = !isRetry;
562
+ const resolvedSessionId = attempt.parsed.sessionId
563
+ ?? (canFallbackToRuntimeSession ? (runtimeSessionId ?? runtime.sessionId ?? null) : null);
564
+ const resolvedSessionParams = resolvedSessionId
565
+ ? {
566
+ sessionId: resolvedSessionId,
567
+ cwd: effectiveExecutionCwd,
568
+ ...(workspaceId ? { workspaceId } : {}),
569
+ ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
570
+ ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
571
+ ...(executionTargetIsRemote
572
+ ? {
573
+ remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget),
574
+ }
575
+ : {}),
576
+ }
577
+ : null;
578
+ const resultJson = {
579
+ toolCalls: attempt.parsed.toolCalls,
580
+ toolResults: attempt.parsed.toolResults,
581
+ ...(failed ? { stderr: attempt.proc.stderr } : {}),
582
+ };
583
+ return {
584
+ exitCode: attempt.proc.exitCode,
585
+ signal: attempt.proc.signal,
586
+ timedOut: false,
587
+ errorMessage: failed ? fallbackErrorMessage : null,
588
+ // Forward the transport-level error code from the run-disposition seam
589
+ // first. A lost duplex control channel surfaces the typed
590
+ // `duplex_channel_lost` code before any provider classification.
591
+ errorCode: attempt.proc.errorCode
592
+ ? attempt.proc.errorCode
593
+ : failed && authMeta.requiresAuth
594
+ ? "kimi_auth_required"
595
+ : failed && networkUnavailable
596
+ ? "kimi_network_unavailable"
597
+ : null,
598
+ sessionId: resolvedSessionId,
599
+ sessionParams: resolvedSessionParams,
600
+ sessionDisplayId: resolvedSessionId,
601
+ provider: "moonshot",
602
+ biller: "moonshot",
603
+ model: model || null,
604
+ billingType,
605
+ resultJson,
606
+ summary: attempt.parsed.summary,
607
+ clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId),
608
+ };
609
+ };
610
+ try {
611
+ const initial = await runAttempt(sessionId);
612
+ if (sessionId &&
613
+ !initial.proc.timedOut &&
614
+ (initial.proc.exitCode ?? 0) !== 0 &&
615
+ isKimiSessionUnrecoverableError(initial.proc.stdout, initial.proc.stderr)) {
616
+ await onLog("stdout", `[paperclip] Kimi resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`);
617
+ const retry = await runAttempt(null);
618
+ return toResult(retry, true, true);
619
+ }
620
+ return toResult(initial);
621
+ }
622
+ finally {
623
+ await Promise.all([
624
+ paperclipBridge?.stop(),
625
+ restoreRemoteWorkspace?.(),
626
+ localSkillsDir ? fs.rm(path.dirname(localSkillsDir), { recursive: true, force: true }).catch(() => undefined) : Promise.resolve(),
627
+ ]);
628
+ }
629
+ }
630
+ //# sourceMappingURL=execute.js.map