larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverCodexModelCatalog = discoverCodexModelCatalog;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
6
|
+
const REASONING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
7
|
+
function cleanLabel(value, fallback) {
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
return fallback;
|
|
10
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, "").replace(/\s+/g, " ").trim().slice(0, 120) || fallback;
|
|
11
|
+
}
|
|
12
|
+
async function runCodexAppServer(call) {
|
|
13
|
+
return await new Promise((resolve, reject) => {
|
|
14
|
+
const child = (0, node_child_process_1.spawn)(call.command || "codex", call.args, {
|
|
15
|
+
...(call.cwd ? { cwd: call.cwd } : {}), env: call.env, stdio: ["pipe", "pipe", "pipe"],
|
|
16
|
+
});
|
|
17
|
+
let settled = false;
|
|
18
|
+
let stdout = "";
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
const finish = (error, value) => {
|
|
21
|
+
if (settled)
|
|
22
|
+
return;
|
|
23
|
+
settled = true;
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
26
|
+
child.kill("SIGTERM");
|
|
27
|
+
if (error)
|
|
28
|
+
reject(error);
|
|
29
|
+
else
|
|
30
|
+
resolve(value);
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
|
|
33
|
+
timer.unref?.();
|
|
34
|
+
child.on("error", () => finish(new Error("Codex model catalog command failed")));
|
|
35
|
+
child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
|
|
36
|
+
child.stderr.resume();
|
|
37
|
+
child.stdout.on("data", (chunk) => {
|
|
38
|
+
bytes += chunk.length;
|
|
39
|
+
if (bytes > call.maxBuffer)
|
|
40
|
+
return finish(new Error("Codex model catalog response exceeded limit"));
|
|
41
|
+
stdout += chunk.toString("utf8");
|
|
42
|
+
for (;;) {
|
|
43
|
+
const newline = stdout.indexOf("\n");
|
|
44
|
+
if (newline < 0)
|
|
45
|
+
break;
|
|
46
|
+
const line = stdout.slice(0, newline);
|
|
47
|
+
stdout = stdout.slice(newline + 1);
|
|
48
|
+
try {
|
|
49
|
+
const message = JSON.parse(line);
|
|
50
|
+
if (message.id === call.request.id) {
|
|
51
|
+
if (message.error)
|
|
52
|
+
return finish(new Error("Codex model catalog request failed"));
|
|
53
|
+
return finish(undefined, message.result);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { /* app-server protocol is newline-delimited JSON; ignore non-response noise */ }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.stdin.write(`${JSON.stringify({ id: "larkin-initialize", method: "initialize", params: { clientInfo: { name: "larkin", title: "Larkin", version: "0" } } })}\n`);
|
|
60
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized", params: {} })}\n`);
|
|
61
|
+
child.stdin.write(`${JSON.stringify(call.request)}\n`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function parseCatalog(value) {
|
|
65
|
+
const rows = value?.data;
|
|
66
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > 512)
|
|
67
|
+
throw new Error("Codex model catalog has an invalid model list");
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
let effectiveModel = "";
|
|
70
|
+
const models = rows.flatMap((row) => {
|
|
71
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
72
|
+
return [];
|
|
73
|
+
const item = row;
|
|
74
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
75
|
+
if (item.hidden === true || !SAFE_MODEL_ID.test(id) || seen.has(id))
|
|
76
|
+
return [];
|
|
77
|
+
seen.add(id);
|
|
78
|
+
if (item.isDefault === true)
|
|
79
|
+
effectiveModel = typeof item.model === "string" && SAFE_MODEL_ID.test(item.model) ? item.model : id;
|
|
80
|
+
const levels = Array.isArray(item.supportedReasoningEfforts)
|
|
81
|
+
? [...new Set(item.supportedReasoningEfforts.flatMap((level) => {
|
|
82
|
+
const effort = level?.reasoningEffort
|
|
83
|
+
?? level?.effort;
|
|
84
|
+
return typeof effort === "string" && REASONING_LEVELS.has(effort) ? [effort] : [];
|
|
85
|
+
}))]
|
|
86
|
+
: [];
|
|
87
|
+
const defaultReasoningEffort = typeof item.defaultReasoningEffort === "string" && levels.includes(item.defaultReasoningEffort)
|
|
88
|
+
? item.defaultReasoningEffort : undefined;
|
|
89
|
+
return [{ id, label: cleanLabel(item.displayName, id), supportedReasoningEfforts: levels,
|
|
90
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), verified: "codex-cli-visible" }];
|
|
91
|
+
});
|
|
92
|
+
if (!models.length || !effectiveModel)
|
|
93
|
+
throw new Error("Codex model catalog has no visible local default");
|
|
94
|
+
return { models, effectiveModel };
|
|
95
|
+
}
|
|
96
|
+
async function discoverCodexModelCatalog(options = {}) {
|
|
97
|
+
const execute = options.runCodexAppServer ?? runCodexAppServer;
|
|
98
|
+
const request = { id: "larkin-model-list", method: "model/list", params: { limit: 100, includeHidden: false } };
|
|
99
|
+
try {
|
|
100
|
+
return parseCatalog(await execute({ args: ["app-server", "--stdio"], ...(options.command ? { command: options.command } : {}), ...(options.cwd ? { cwd: options.cwd } : {}),
|
|
101
|
+
env: { ...process.env, ...options.env, NO_COLOR: "1" }, maxBuffer: 1024 * 1024, request, timeout: 15_000 }));
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof Error && /Codex model catalog/.test(error.message))
|
|
105
|
+
throw error;
|
|
106
|
+
throw new Error("Codex model catalog unavailable");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
3
|
+
const REASONING_LEVELS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
4
|
+
function cleanLabel(value, fallback) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return fallback;
|
|
7
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, "").replace(/\s+/g, " ").trim().slice(0, 120) || fallback;
|
|
8
|
+
}
|
|
9
|
+
async function runCodexAppServer(call) {
|
|
10
|
+
return await new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(call.command || "codex", call.args, {
|
|
12
|
+
...(call.cwd ? { cwd: call.cwd } : {}), env: call.env, stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
let settled = false;
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
const finish = (error, value) => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
23
|
+
child.kill("SIGTERM");
|
|
24
|
+
if (error)
|
|
25
|
+
reject(error);
|
|
26
|
+
else
|
|
27
|
+
resolve(value);
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(() => finish(new Error("Codex model catalog timed out")), call.timeout);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
child.on("error", () => finish(new Error("Codex model catalog command failed")));
|
|
32
|
+
child.on("exit", () => finish(new Error("Codex model catalog app-server exited before response")));
|
|
33
|
+
child.stderr.resume();
|
|
34
|
+
child.stdout.on("data", (chunk) => {
|
|
35
|
+
bytes += chunk.length;
|
|
36
|
+
if (bytes > call.maxBuffer)
|
|
37
|
+
return finish(new Error("Codex model catalog response exceeded limit"));
|
|
38
|
+
stdout += chunk.toString("utf8");
|
|
39
|
+
for (;;) {
|
|
40
|
+
const newline = stdout.indexOf("\n");
|
|
41
|
+
if (newline < 0)
|
|
42
|
+
break;
|
|
43
|
+
const line = stdout.slice(0, newline);
|
|
44
|
+
stdout = stdout.slice(newline + 1);
|
|
45
|
+
try {
|
|
46
|
+
const message = JSON.parse(line);
|
|
47
|
+
if (message.id === call.request.id) {
|
|
48
|
+
if (message.error)
|
|
49
|
+
return finish(new Error("Codex model catalog request failed"));
|
|
50
|
+
return finish(undefined, message.result);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* app-server protocol is newline-delimited JSON; ignore non-response noise */ }
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
child.stdin.write(`${JSON.stringify({ id: "larkin-initialize", method: "initialize", params: { clientInfo: { name: "larkin", title: "Larkin", version: "0" } } })}\n`);
|
|
57
|
+
child.stdin.write(`${JSON.stringify({ method: "initialized", params: {} })}\n`);
|
|
58
|
+
child.stdin.write(`${JSON.stringify(call.request)}\n`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function parseCatalog(value) {
|
|
62
|
+
const rows = value?.data;
|
|
63
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > 512)
|
|
64
|
+
throw new Error("Codex model catalog has an invalid model list");
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
let effectiveModel = "";
|
|
67
|
+
const models = rows.flatMap((row) => {
|
|
68
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
69
|
+
return [];
|
|
70
|
+
const item = row;
|
|
71
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
72
|
+
if (item.hidden === true || !SAFE_MODEL_ID.test(id) || seen.has(id))
|
|
73
|
+
return [];
|
|
74
|
+
seen.add(id);
|
|
75
|
+
if (item.isDefault === true)
|
|
76
|
+
effectiveModel = typeof item.model === "string" && SAFE_MODEL_ID.test(item.model) ? item.model : id;
|
|
77
|
+
const levels = Array.isArray(item.supportedReasoningEfforts)
|
|
78
|
+
? [...new Set(item.supportedReasoningEfforts.flatMap((level) => {
|
|
79
|
+
const effort = level?.reasoningEffort
|
|
80
|
+
?? level?.effort;
|
|
81
|
+
return typeof effort === "string" && REASONING_LEVELS.has(effort) ? [effort] : [];
|
|
82
|
+
}))]
|
|
83
|
+
: [];
|
|
84
|
+
const defaultReasoningEffort = typeof item.defaultReasoningEffort === "string" && levels.includes(item.defaultReasoningEffort)
|
|
85
|
+
? item.defaultReasoningEffort : undefined;
|
|
86
|
+
return [{ id, label: cleanLabel(item.displayName, id), supportedReasoningEfforts: levels,
|
|
87
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), verified: "codex-cli-visible" }];
|
|
88
|
+
});
|
|
89
|
+
if (!models.length || !effectiveModel)
|
|
90
|
+
throw new Error("Codex model catalog has no visible local default");
|
|
91
|
+
return { models, effectiveModel };
|
|
92
|
+
}
|
|
93
|
+
export async function discoverCodexModelCatalog(options = {}) {
|
|
94
|
+
const execute = options.runCodexAppServer ?? runCodexAppServer;
|
|
95
|
+
const request = { id: "larkin-model-list", method: "model/list", params: { limit: 100, includeHidden: false } };
|
|
96
|
+
try {
|
|
97
|
+
return parseCatalog(await execute({ args: ["app-server", "--stdio"], ...(options.command ? { command: options.command } : {}), ...(options.cwd ? { cwd: options.cwd } : {}),
|
|
98
|
+
env: { ...process.env, ...options.env, NO_COLOR: "1" }, maxBuffer: 1024 * 1024, request, timeout: 15_000 }));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error && /Codex model catalog/.test(error.message))
|
|
102
|
+
throw error;
|
|
103
|
+
throw new Error("Codex model catalog unavailable");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* pi bash 工具超时护栏扩展。
|
|
4
|
+
*
|
|
5
|
+
* 由 larkin 通过 `pi --extension/-e <bundle>` 注入,覆盖内置 `bash` 工具:
|
|
6
|
+
* - 无论模型是否传 `timeout`,都强制收窄到 <= MAX_BASH_SECONDS(默认 60s),
|
|
7
|
+
* 避免单个 bash 调用无限期占住整个 agent 回合(issue #55)。
|
|
8
|
+
* - 超时时 pi 会杀掉整个进程树(不残留卡死子进程,issue #56 的进程堆积),
|
|
9
|
+
* 并在返回的错误里明确提示:长任务必须改用后台 subagent
|
|
10
|
+
* (Agent(run_in_background: true),来自 pi-subagents 扩展)。
|
|
11
|
+
*
|
|
12
|
+
* 这是 pi 扩展,运行在 pi 进程内部;larkin 主进程不加载本模块(只通过
|
|
13
|
+
* build.mjs 单独 bundle 成单文件后注入)。pi-* 保持 external。
|
|
14
|
+
*/
|
|
15
|
+
/** 前台 bash 的硬性上限(秒)。issue #55 建议默认 60s。 */
|
|
16
|
+
const MAX_BASH_SECONDS = 60;
|
|
17
|
+
/** 解析生效超时:默认 60,允许用 LARKIN_PI_BASH_TIMEOUT_SECONDS 调低(用于快速 eval),硬上限 60。 */
|
|
18
|
+
function effectiveBashTimeout() {
|
|
19
|
+
const raw = Number.parseInt(process.env.LARKIN_PI_BASH_TIMEOUT_SECONDS || "", 10);
|
|
20
|
+
if (Number.isInteger(raw) && raw > 0)
|
|
21
|
+
return Math.min(raw, MAX_BASH_SECONDS);
|
|
22
|
+
return MAX_BASH_SECONDS;
|
|
23
|
+
}
|
|
24
|
+
export default function (pi) {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
// Built-in bash tool definition (keeps renderCall/renderResult/system-prompt metadata).
|
|
27
|
+
const builtin = createBashToolDefinition(cwd);
|
|
28
|
+
const MAX = effectiveBashTimeout();
|
|
29
|
+
const SUBAGENT_GUIDANCE = `This command exceeded the ${MAX}s foreground hard limit. ` +
|
|
30
|
+
"Long-running or deploy-style work MUST run in a background subagent instead: " +
|
|
31
|
+
"call Agent({ prompt, description, run_in_background: true }), report the returned agent id, and end the turn; " +
|
|
32
|
+
"a completion notification arrives automatically. " +
|
|
33
|
+
"Do NOT retry this command in the foreground, and do not use nohup / '&' / disown shell background jobs " +
|
|
34
|
+
"(they bypass subagent isolation and die with this run).";
|
|
35
|
+
pi.registerTool({
|
|
36
|
+
...builtin,
|
|
37
|
+
name: "bash",
|
|
38
|
+
label: "bash",
|
|
39
|
+
description: builtin.description,
|
|
40
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
41
|
+
const requested = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout : MAX;
|
|
42
|
+
// 模型显式设置 timeout > 上限 → 它已知这是长任务。立即报错引导改用后台 subagent,
|
|
43
|
+
// 不真的跑(省掉白等 + 避免产生部分副作用)。
|
|
44
|
+
if (requested > MAX) {
|
|
45
|
+
throw new Error(`timeout:${requested} exceeds the ${MAX}s foreground hard limit. ${SUBAGENT_GUIDANCE}`);
|
|
46
|
+
}
|
|
47
|
+
const timeout = Math.min(requested, MAX);
|
|
48
|
+
try {
|
|
49
|
+
return await builtin.execute(toolCallId, { ...params, timeout }, signal, onUpdate, ctx);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
if (/timed out after/i.test(message)) {
|
|
54
|
+
throw new Error(`${message}\n\n${SUBAGENT_GUIDANCE}`);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
|
|
5
|
+
import { parsePiVersion, piVersionSupportsSubagents, probeExternalPiVersion } from "./pi-subagent-injection.mjs";
|
|
6
|
+
/**
|
|
7
|
+
* pi-bash-timeout 扩展注入(bash 60s 超时护栏,issue #55/#56)。
|
|
8
|
+
*
|
|
9
|
+
* 分发:构建期把 src/runtime/pi-bash-timeout-extension.ts bundle 成单文件
|
|
10
|
+
* `dist/runtime/pi-bash-timeout.bundle.js`(pi-* 包 external),运行时通过
|
|
11
|
+
* `pi --extension/-e` 显式注入 —— builtin 与 external(用户 pi CLI)走同一
|
|
12
|
+
* 路径,不碰用户 ~/.pi 配置。与 pi-subagents 共享同一个 pi 版本门槛。
|
|
13
|
+
*/
|
|
14
|
+
/** 把 embedded bundle 落盘到 <configDir>/providers/pi/extensions/(0700/0600)。无 embedded 资产返回 null。 */
|
|
15
|
+
export function materializeEmbeddedPiBashTimeoutBundle(configDir) {
|
|
16
|
+
const embedded = globalThis.__LARKIN_EMBEDDED_PI_BASH_TIMEOUT_BUNDLE__;
|
|
17
|
+
if (!embedded || !configDir)
|
|
18
|
+
return null;
|
|
19
|
+
const dir = path.join(path.resolve(configDir), "providers", "pi", "extensions");
|
|
20
|
+
const target = path.join(dir, "pi-bash-timeout.bundle.js");
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
23
|
+
fs.chmodSync(dir, 0o700);
|
|
24
|
+
if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
|
|
25
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
26
|
+
fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
|
|
27
|
+
fs.renameSync(temporary, target);
|
|
28
|
+
fs.chmodSync(target, 0o600);
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 构建产物单文件路径;standalone 二进制回退到 embedded 资产落盘。 */
|
|
37
|
+
export function bundledPiBashTimeoutExtensionPath(configDir) {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL("./pi-bash-timeout.bundle.js", import.meta.url);
|
|
40
|
+
const resolved = fileURLToPath(url);
|
|
41
|
+
if (fs.existsSync(resolved))
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* fall through to embedded */
|
|
46
|
+
}
|
|
47
|
+
return materializeEmbeddedPiBashTimeoutBundle(configDir);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 注入决策:builtin 恒注入(内嵌 pi 版本固定);external 需探测版本(与
|
|
51
|
+
* pi-subagents 同门槛 >= 0.80.0,保证 createBashToolDefinition 可用)。
|
|
52
|
+
* 返回 `-e` 参数值,或 null(产物缺失或版本不达标)。
|
|
53
|
+
*/
|
|
54
|
+
export function resolvePiBashTimeoutExtensionArg(input, probeVersion = () => probeExternalPiVersion(input.piCommand, input.env), resolveBundle = () => bundledPiBashTimeoutExtensionPath(input.env.LARKIN_CONFIG_DIR)) {
|
|
55
|
+
const bundle = resolveBundle();
|
|
56
|
+
if (!bundle)
|
|
57
|
+
return null;
|
|
58
|
+
const version = input.distribution === "builtin" ? parsePiVersion(BUNDLED_PI_VERSION) : probeVersion();
|
|
59
|
+
return piVersionSupportsSubagents(version) ? bundle : null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/runtime/pi-bash-timeout-extension.ts
|
|
3
|
+
import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
var MAX_BASH_SECONDS = 60;
|
|
5
|
+
function effectiveBashTimeout() {
|
|
6
|
+
const raw = Number.parseInt(process.env.LARKIN_PI_BASH_TIMEOUT_SECONDS || "", 10);
|
|
7
|
+
if (Number.isInteger(raw) && raw > 0)
|
|
8
|
+
return Math.min(raw, MAX_BASH_SECONDS);
|
|
9
|
+
return MAX_BASH_SECONDS;
|
|
10
|
+
}
|
|
11
|
+
function pi_bash_timeout_extension_default(pi) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const builtin = createBashToolDefinition(cwd);
|
|
14
|
+
const MAX = effectiveBashTimeout();
|
|
15
|
+
const SUBAGENT_GUIDANCE = `This command exceeded the ${MAX}s foreground hard limit. ` + "Long-running or deploy-style work MUST run in a background subagent instead: " + "call Agent({ prompt, description, run_in_background: true }), report the returned agent id, and end the turn; " + "a completion notification arrives automatically. " + "Do NOT retry this command in the foreground, and do not use nohup / '&' / disown shell background jobs " + "(they bypass subagent isolation and die with this run).";
|
|
16
|
+
pi.registerTool({
|
|
17
|
+
...builtin,
|
|
18
|
+
name: "bash",
|
|
19
|
+
label: "bash",
|
|
20
|
+
description: builtin.description,
|
|
21
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
22
|
+
const requested = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout : MAX;
|
|
23
|
+
if (requested > MAX) {
|
|
24
|
+
throw new Error(`timeout:${requested} exceeds the ${MAX}s foreground hard limit. ${SUBAGENT_GUIDANCE}`);
|
|
25
|
+
}
|
|
26
|
+
const timeout = Math.min(requested, MAX);
|
|
27
|
+
try {
|
|
28
|
+
return await builtin.execute(toolCallId, { ...params, timeout }, signal, onUpdate, ctx);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
if (/timed out after/i.test(message)) {
|
|
32
|
+
throw new Error(`${message}
|
|
33
|
+
|
|
34
|
+
${SUBAGENT_GUIDANCE}`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
pi_bash_timeout_extension_default as default
|
|
43
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PI_THINKING_LEVELS = void 0;
|
|
37
|
+
exports.piModelId = piModelId;
|
|
38
|
+
exports.supportedPiThinkingLevels = supportedPiThinkingLevels;
|
|
39
|
+
exports.isPiThinkingLevel = isPiThinkingLevel;
|
|
40
|
+
exports.findExactPiModel = findExactPiModel;
|
|
41
|
+
exports.discoverPiModelCatalog = discoverPiModelCatalog;
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const pi_rpc_client_js_1 = require("./pi-rpc-client.cjs");
|
|
45
|
+
const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
|
|
46
|
+
exports.PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
47
|
+
function piModelId(model) { return `${model.provider}/${model.id}`; }
|
|
48
|
+
function supportedPiThinkingLevels(model) {
|
|
49
|
+
if (!model.reasoning)
|
|
50
|
+
return ["off"];
|
|
51
|
+
return exports.PI_THINKING_LEVELS.filter((level) => {
|
|
52
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
53
|
+
if (mapped === null)
|
|
54
|
+
return false;
|
|
55
|
+
return level !== "xhigh" && level !== "max" ? true : mapped !== undefined;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function isPiThinkingLevel(value) {
|
|
59
|
+
return typeof value === "string" && exports.PI_THINKING_LEVELS.includes(value);
|
|
60
|
+
}
|
|
61
|
+
function findExactPiModel(reference, models) {
|
|
62
|
+
if (!reference || reference === "default" || !reference.includes("/"))
|
|
63
|
+
return undefined;
|
|
64
|
+
return models.find((model) => piModelId(model) === reference);
|
|
65
|
+
}
|
|
66
|
+
/** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
|
|
67
|
+
async function discoverPiModelCatalog(options) {
|
|
68
|
+
const spawn = options.spawn ?? ((command, args, spawnOptions) => (0, node_child_process_1.spawn)(command, [...args], spawnOptions));
|
|
69
|
+
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
70
|
+
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
env: { ...process.env, ...options.env, ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}), NO_COLOR: "1" },
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
const client = new pi_rpc_client_js_1.PiRpcClient(child, { requestTimeoutMs: options.timeoutMs ?? 10_000 });
|
|
76
|
+
try {
|
|
77
|
+
const [availableResponse, state] = await Promise.all([
|
|
78
|
+
client.request("get_available_models"),
|
|
79
|
+
client.request("get_state"),
|
|
80
|
+
]);
|
|
81
|
+
const available = [...(availableResponse?.models ?? [])].sort((left, right) => piModelId(left).localeCompare(piModelId(right)));
|
|
82
|
+
if (!available.length)
|
|
83
|
+
throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
|
|
84
|
+
const effectiveModel = state.model ? piModelId(state.model) : null;
|
|
85
|
+
if (!effectiveModel || !available.some((model) => piModelId(model) === effectiveModel)) {
|
|
86
|
+
throw new Error(`Pi official default resolution returned an unavailable model (${effectiveModel || "none"}); refusing implicit fallback`);
|
|
87
|
+
}
|
|
88
|
+
const effectiveThinkingLevel = isPiThinkingLevel(state.thinkingLevel) ? state.thinkingLevel : "off";
|
|
89
|
+
return {
|
|
90
|
+
models: available.map((model) => {
|
|
91
|
+
const supportedReasoningEfforts = supportedPiThinkingLevels(model);
|
|
92
|
+
return {
|
|
93
|
+
id: piModelId(model),
|
|
94
|
+
label: `${model.name || model.id} · ${model.provider}`,
|
|
95
|
+
...(Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0 ? { contextWindow: Number(model.contextWindow) } : {}),
|
|
96
|
+
supportedReasoningEfforts,
|
|
97
|
+
...(piModelId(model) === effectiveModel && supportedReasoningEfforts.includes(effectiveThinkingLevel)
|
|
98
|
+
? { defaultReasoningEffort: effectiveThinkingLevel } : {}),
|
|
99
|
+
verified: "launchable",
|
|
100
|
+
};
|
|
101
|
+
}),
|
|
102
|
+
effectiveModel,
|
|
103
|
+
effectiveThinkingLevel,
|
|
104
|
+
defaultSource: "settings",
|
|
105
|
+
diagnostics: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof runtime_readiness_js_1.RuntimePrerequisiteError)
|
|
110
|
+
throw error;
|
|
111
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError((0, runtime_readiness_js_1.classifyRuntimePrerequisite)("pi", error, command));
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await client.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { PiRpcClient } from "./pi-rpc-client.mjs";
|
|
4
|
+
import { classifyRuntimePrerequisite, RuntimePrerequisiteError } from "./runtime-readiness.mjs";
|
|
5
|
+
export const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
6
|
+
export function piModelId(model) { return `${model.provider}/${model.id}`; }
|
|
7
|
+
export function supportedPiThinkingLevels(model) {
|
|
8
|
+
if (!model.reasoning)
|
|
9
|
+
return ["off"];
|
|
10
|
+
return PI_THINKING_LEVELS.filter((level) => {
|
|
11
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
12
|
+
if (mapped === null)
|
|
13
|
+
return false;
|
|
14
|
+
return level !== "xhigh" && level !== "max" ? true : mapped !== undefined;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function isPiThinkingLevel(value) {
|
|
18
|
+
return typeof value === "string" && PI_THINKING_LEVELS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
export function findExactPiModel(reference, models) {
|
|
21
|
+
if (!reference || reference === "default" || !reference.includes("/"))
|
|
22
|
+
return undefined;
|
|
23
|
+
return models.find((model) => piModelId(model) === reference);
|
|
24
|
+
}
|
|
25
|
+
/** Discover only through Pi's structured RPC protocol; no table parsing or static fallback. */
|
|
26
|
+
export async function discoverPiModelCatalog(options) {
|
|
27
|
+
const spawn = options.spawn ?? ((command, args, spawnOptions) => nodeSpawn(command, [...args], spawnOptions));
|
|
28
|
+
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
29
|
+
const child = spawn(command, ["--mode", "rpc", "--no-session"], {
|
|
30
|
+
cwd: options.cwd,
|
|
31
|
+
env: { ...process.env, ...options.env, ...(options.agentDir ? { PI_CODING_AGENT_DIR: path.resolve(options.agentDir) } : {}), NO_COLOR: "1" },
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
const client = new PiRpcClient(child, { requestTimeoutMs: options.timeoutMs ?? 10_000 });
|
|
35
|
+
try {
|
|
36
|
+
const [availableResponse, state] = await Promise.all([
|
|
37
|
+
client.request("get_available_models"),
|
|
38
|
+
client.request("get_state"),
|
|
39
|
+
]);
|
|
40
|
+
const available = [...(availableResponse?.models ?? [])].sort((left, right) => piModelId(left).localeCompare(piModelId(right)));
|
|
41
|
+
if (!available.length)
|
|
42
|
+
throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
|
|
43
|
+
const effectiveModel = state.model ? piModelId(state.model) : null;
|
|
44
|
+
if (!effectiveModel || !available.some((model) => piModelId(model) === effectiveModel)) {
|
|
45
|
+
throw new Error(`Pi official default resolution returned an unavailable model (${effectiveModel || "none"}); refusing implicit fallback`);
|
|
46
|
+
}
|
|
47
|
+
const effectiveThinkingLevel = isPiThinkingLevel(state.thinkingLevel) ? state.thinkingLevel : "off";
|
|
48
|
+
return {
|
|
49
|
+
models: available.map((model) => {
|
|
50
|
+
const supportedReasoningEfforts = supportedPiThinkingLevels(model);
|
|
51
|
+
return {
|
|
52
|
+
id: piModelId(model),
|
|
53
|
+
label: `${model.name || model.id} · ${model.provider}`,
|
|
54
|
+
...(Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0 ? { contextWindow: Number(model.contextWindow) } : {}),
|
|
55
|
+
supportedReasoningEfforts,
|
|
56
|
+
...(piModelId(model) === effectiveModel && supportedReasoningEfforts.includes(effectiveThinkingLevel)
|
|
57
|
+
? { defaultReasoningEffort: effectiveThinkingLevel } : {}),
|
|
58
|
+
verified: "launchable",
|
|
59
|
+
};
|
|
60
|
+
}),
|
|
61
|
+
effectiveModel,
|
|
62
|
+
effectiveThinkingLevel,
|
|
63
|
+
defaultSource: "settings",
|
|
64
|
+
diagnostics: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof RuntimePrerequisiteError)
|
|
69
|
+
throw error;
|
|
70
|
+
throw new RuntimePrerequisiteError(classifyRuntimePrerequisite("pi", error, command));
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await client.close();
|
|
74
|
+
}
|
|
75
|
+
}
|