larkin 0.4.28 → 0.5.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 (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +5 -9
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
  4. package/dist/app/agent-cli.mjs +14 -2
  5. package/dist/app/agent-config.mjs +26 -84
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +8 -14
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/local-control.mjs +16 -3
  11. package/dist/app/runtime-model-directory.mjs +2 -16
  12. package/dist/app/runtime-process.mjs +42 -6
  13. package/dist/app/setup.mjs +52 -57
  14. package/dist/dashboard/dashboard-config-controller.mjs +83 -24
  15. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  16. package/dist/dashboard/web/assets/dashboard.js +1 -1
  17. package/dist/feishu/host-shell.cjs +68 -17
  18. package/dist/feishu/host-shell.mjs +69 -18
  19. package/dist/platform/config.cjs +201 -138
  20. package/dist/platform/config.mjs +199 -138
  21. package/dist/runtime/pi-artifact-provenance.mjs +3 -3
  22. package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
  23. package/dist/runtime/pi-compaction-recovery.cjs +9 -11
  24. package/dist/runtime/pi-compaction-recovery.mjs +8 -10
  25. package/dist/runtime/pi-extension-api.mjs +1 -0
  26. package/dist/runtime/pi-model-catalog.cjs +7 -8
  27. package/dist/runtime/pi-model-catalog.mjs +7 -8
  28. package/dist/runtime/pi-subagent-injection.mjs +17 -4
  29. package/dist/runtime/runtime-adapters.mjs +41 -38
  30. package/dist/runtime/runtime-host.cjs +197 -20
  31. package/dist/runtime/runtime-host.mjs +198 -21
  32. package/dist/runtime/runtime-model-catalog.cjs +1 -1
  33. package/dist/runtime/runtime-model-catalog.mjs +1 -1
  34. package/dist/runtime/runtime-readiness.cjs +112 -49
  35. package/dist/runtime/runtime-readiness.mjs +103 -49
  36. package/dist/runtime/user-runtime.cjs +7 -24
  37. package/dist/runtime/user-runtime.mjs +7 -23
  38. package/dist/setup/bot-register.mjs +15 -123
  39. package/dist/setup/setup-agent-choice.mjs +48 -74
  40. package/dist/setup/setup-bind.mjs +51 -151
  41. package/dist/setup/setup-binding.mjs +6 -9
  42. package/package.json +3 -2
  43. package/dist/app/pi-auth-cli.mjs +0 -67
  44. package/dist/platform/process-boundary-trace.cjs +0 -108
  45. package/dist/runtime/builtin-pi-assets.cjs +0 -117
  46. package/dist/runtime/builtin-pi-assets.mjs +0 -105
  47. package/dist/runtime/pi-artifact-provenance.cjs +0 -147
  48. package/dist/runtime/pi-distribution-label.cjs +0 -6
  49. package/dist/runtime/pi-distribution-label.mjs +0 -3
  50. package/dist/runtime/pi-inline-extensions.mjs +0 -78
  51. package/dist/runtime/pi-official-auth.mjs +0 -491
  52. package/dist/runtime/pi-profile-migration.cjs +0 -764
  53. package/dist/runtime/pi-profile-migration.mjs +0 -719
  54. package/dist/runtime/pi-provider-config.cjs +0 -331
  55. package/dist/runtime/pi-provider-config.mjs +0 -314
@@ -1,92 +1,66 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin, stdout } from "node:process";
3
- import { toUserRuntime } from "../runtime/user-runtime.mjs";
4
- import { PI_PROVIDER_PRESETS, resolveBuiltinPiProviderSetupSelection, } from "../runtime/pi-provider-config.mjs";
3
+ import { RUNTIME_OPTIONS, toUserRuntime } from "../runtime/user-runtime.mjs";
4
+ import { resolveRuntimeExecutable, runtimeInstallNextAction } from "../runtime/runtime-readiness.mjs";
5
+ function runtimeCommand(runtime, env) {
6
+ if (runtime === "pi")
7
+ return env.LARKIN_PI_COMMAND || "pi";
8
+ if (runtime === "codex")
9
+ return env.LARKIN_CODEX_COMMAND || "codex";
10
+ return env.LARKIN_CLAUDE_COMMAND || "claude";
11
+ }
12
+ export function listRuntimeInstallStatuses(env = process.env) {
13
+ return RUNTIME_OPTIONS.map((runtime) => {
14
+ if (resolveRuntimeExecutable(runtimeCommand(runtime, env), env))
15
+ return { runtime, installed: true };
16
+ return {
17
+ runtime,
18
+ installed: false,
19
+ reason: `${runtime} is not installed`,
20
+ nextAction: runtimeInstallNextAction(runtime),
21
+ };
22
+ });
23
+ }
24
+ export function missingRuntimeInstallMessage(runtime, env = process.env) {
25
+ if (runtime !== "pi" && runtime !== "codex" && runtime !== "claude")
26
+ return null;
27
+ const status = listRuntimeInstallStatuses(env).find((entry) => entry.runtime === runtime);
28
+ if (!status || status.installed)
29
+ return null;
30
+ return `${status.reason};${status.nextAction}`;
31
+ }
32
+ const RUNTIME_LABELS = {
33
+ pi: "pi(本机官方 pi CLI)",
34
+ codex: "Codex",
35
+ claude: "Claude Code",
36
+ };
5
37
  function number(value, fallback, max) {
6
38
  const parsed = value.trim() ? Number(value.trim()) : fallback;
7
39
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > max)
8
40
  throw new Error(`请选择 1-${max}`);
9
41
  return parsed;
10
42
  }
11
- export async function collectBuiltinPiChoice(questioner, services) {
12
- const lines = PI_PROVIDER_PRESETS.map((preset, index) => ` ${index + 1}. ${preset.name} — ${preset.baseUrl}`).join("\n");
13
- const providerChoice = number(await questioner.ask(`选择模型服务 / 认证:\n${lines}\n 5. Custom OpenAI-compatible\n 6. 官方 Pi provider 登录(API Key / OAuth / subscription)\n 7. 查看或退出已有官方 Pi 认证\n> `), 1, 7);
14
- if (providerChoice === 7) {
15
- if (!services)
16
- throw new Error("当前 setup 上下文无法管理官方 Pi 认证");
17
- const status = await services.status();
18
- if (status.length === 0)
19
- services.report("尚无已配置的官方 Pi credential");
20
- else
21
- status.forEach((entry, index) => services.report(` ${index + 1}. ${entry.providerName} (${entry.providerId}) — ${entry.credentialType}/${entry.source}${entry.stored ? ",已存储" : ",ambient"}`));
22
- const answer = (await questioner.ask("输入要 logout 的序号;直接回车返回:")).trim();
23
- if (answer) {
24
- const selected = number(answer, 0, status.length);
25
- await services.logout(status[selected - 1].providerId);
26
- services.report(`已退出 ${status[selected - 1].providerName};未修改其他 provider`);
27
- }
28
- return collectBuiltinPiChoice(questioner, services);
29
- }
30
- if (providerChoice === 6) {
31
- if (!services)
32
- throw new Error("当前 setup 上下文无法读取官方 Pi auth registry");
33
- const providers = await services.providers();
34
- const methods = providers.flatMap((provider) => provider.methods.map((method) => ({ provider, method })));
35
- if (methods.length === 0)
36
- throw new Error("捆绑官方 Pi auth registry 没有可交互登录方式");
37
- const methodLines = methods.map(({ provider, method }, index) => ` ${index + 1}. ${provider.name} — ${method.name} [${method.type}]`).join("\n");
38
- const methodChoice = number(await questioner.ask(`选择官方 Pi 登录方式:\n${methodLines}\n> `), 1, methods.length);
39
- const selected = methods[methodChoice - 1];
40
- const defaultModel = selected.provider.models[0] || "";
41
- const model = (await questioner.ask(`模型 ID${defaultModel ? `(默认 ${defaultModel})` : "(provider/model)"}:`)).trim() || defaultModel;
42
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/.test(model))
43
- throw new Error("模型 ID 必须是安全的 provider/model");
44
- if (!model.startsWith(`${selected.provider.id}/`))
45
- throw new Error(`模型 ID 必须属于 ${selected.provider.id}`);
46
- return { runtime: "pi", distribution: "builtin", preset: "official", providerId: selected.provider.id,
47
- authType: selected.method.type, model };
48
- }
49
- const preset = providerChoice === 5 ? "custom" : PI_PROVIDER_PRESETS[providerChoice - 1].id;
50
- const selected = preset === "custom" ? null : PI_PROVIDER_PRESETS.find((candidate) => candidate.id === preset);
51
- const baseUrl = preset === "custom" ? await questioner.ask("Base URL(https;localhost 可用 http):") : selected.baseUrl;
52
- const model = (await questioner.ask(`模型 ID${selected ? `(默认 ${selected.defaultModel})` : ""}:`)).trim() || selected?.defaultModel || "";
53
- const validated = resolveBuiltinPiProviderSetupSelection({ distribution: "builtin", preset, model, ...(baseUrl ? { baseUrl } : {}) });
54
- return { runtime: "pi", distribution: "builtin", preset, model: validated.model, ...(preset === "custom" ? { baseUrl: validated.baseUrl } : {}) };
43
+ function formatRuntimeMenu(statuses) {
44
+ const lines = statuses.map((status, index) => {
45
+ const mark = status.installed ? "installed" : "not installed";
46
+ return ` ${index + 1}. ${RUNTIME_LABELS[status.runtime]} [${mark}]`;
47
+ });
48
+ return `选择 Agent:\n${lines.join("\n")}\n> `;
55
49
  }
56
- export async function collectSetupAgentChoice(questioner, prior, services) {
50
+ export async function collectSetupAgentChoice(questioner, prior, statuses = listRuntimeInstallStatuses()) {
57
51
  if (prior) {
58
- const keep = (await questioner.ask(`已有 Agent:${toUserRuntime(prior.runtime, prior.piDistribution)}/${prior.model}。直接回车保留;输入 c 才修改:`)).trim().toLowerCase();
52
+ const keep = (await questioner.ask(`已有 Agent:${toUserRuntime(prior.runtime)}/${prior.model}。直接回车保留;输入 c 才修改:`)).trim().toLowerCase();
59
53
  if (!keep)
60
54
  return null;
61
55
  if (keep !== "c")
62
56
  throw new Error("请输入 c 修改,或直接回车保留现有配置");
63
57
  }
64
- const runtimeChoice = number(await questioner.ask("选择 Agent:\n 1. builtin-pi(Larkin 捆绑,推荐)\n 2. pi(本机官方 pi CLI)\n 3. Codex\n 4. Claude Code\n> "), 1, 4);
65
- if (runtimeChoice === 3)
66
- return { runtime: "codex" };
67
- if (runtimeChoice === 4)
68
- return { runtime: "claude" };
69
- if (runtimeChoice === 2)
70
- return { runtime: "pi", distribution: "external" };
71
- return collectBuiltinPiChoice(questioner, services);
72
- }
73
- export async function recoverUnavailableExternalPi(initial, questioner, probe, report = () => { }, services) {
74
- let choice = initial;
75
- for (let attempt = 1; attempt <= 3; attempt += 1) {
76
- if (choice?.runtime !== "pi" || choice.distribution !== "external")
77
- return choice;
78
- const readiness = await probe();
79
- if (readiness.state === "ready")
80
- return choice;
81
- report(`外置 Pi ${readiness.state}:${readiness.reason || "prerequisite unavailable"};${readiness.nextAction || "可切换到 builtin-pi"}`);
82
- const action = number(await questioner.ask("外置 Pi 当前不可用:\n 1. 切换到 builtin-pi(推荐)\n 2. 重新选择 Agent\n 3. 取消 setup(保留原配置)\n> "), 1, 3);
83
- if (action === 1)
84
- return collectBuiltinPiChoice(questioner, services);
85
- if (action === 3)
86
- throw new Error("setup 已取消;未修改 Agent/config");
87
- choice = await collectSetupAgentChoice(questioner, undefined, services);
58
+ const runtimeChoice = number(await questioner.ask(formatRuntimeMenu(statuses)), 1, statuses.length);
59
+ const selected = statuses[runtimeChoice - 1];
60
+ if (!selected.installed) {
61
+ throw new Error(`${selected.reason};${selected.nextAction}`);
88
62
  }
89
- throw new Error("外置 Pi 恢复选择已达到 3 次;未修改 Agent/config,请修复后重试");
63
+ return { runtime: selected.runtime };
90
64
  }
91
65
  export function terminalSetupQuestioner() {
92
66
  let rl = null;
@@ -150,7 +124,7 @@ export function terminalSetupQuestioner() {
150
124
  const onData = (chunk) => {
151
125
  for (const byte of chunk) {
152
126
  if (byte === 3)
153
- return finish(new Error("setup 已取消;未保存 API Key"));
127
+ return finish(new Error("setup 已取消;未保存输入"));
154
128
  if (byte === 10 || byte === 13)
155
129
  return finish();
156
130
  if (byte === 8 || byte === 127) {
@@ -9,12 +9,8 @@ import { fileURLToPath } from "node:url";
9
9
  import { isWindows } from "../platform/secure-metadata.mjs";
10
10
  import { TargetRootLayout } from "../platform/root-layout.mjs";
11
11
  import { planSingleRootBinding } from "./setup-binding.mjs";
12
- import { configureBuiltinPiProviderModel, piAgentDirectory } from "../runtime/pi-provider-config.mjs";
13
- import { applyPiProfileMigration, clearStalePiProfileMigrationLock, preparePiProfileMigration, releasePiProfileMigrationLock, rollbackPiProfileMigration, } from "../runtime/pi-profile-migration.mjs";
14
12
  import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
15
13
  import { calculatePiCompactionSettings } from "../runtime/pi-compaction-recovery.mjs";
16
- import { terminalSetupQuestioner } from "./setup-agent-choice.mjs";
17
- import { beginBuiltinPiCredentialTransaction, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
18
14
  import * as larkinConfigImport from "../platform/config.mjs";
19
15
  import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
20
16
  import { loadValidatedBotCredential } from "./run-credential-preflight.mjs";
@@ -36,6 +32,7 @@ const options = {
36
32
  agent: flag("--agent"),
37
33
  profile: flag("--profile"),
38
34
  runtime: flag("--runtime"),
35
+ model: flag("--model"),
39
36
  selectionFile: flag("--selection-file"),
40
37
  yes: has("--yes"),
41
38
  list: has("--list"),
@@ -69,12 +66,6 @@ function larkJson(args) {
69
66
  return { ok: false, json: null, raw: stdout, err: stderr || stdout };
70
67
  }
71
68
  }
72
- function openAuthUrl(url) {
73
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32.exe" : "xdg-open";
74
- const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
75
- const result = spawnSync(command, args, { stdio: "ignore", timeout: 5_000 });
76
- return result.status === 0;
77
- }
78
69
  function listProfilesFromEnv(env) {
79
70
  const official = resolveOfficialLarkCli({ env });
80
71
  const result = spawnSync(official.command, [...official.argsPrefix, "profile", "list"], { encoding: "utf8", env });
@@ -176,9 +167,10 @@ function readSetupSelection(fileArg) {
176
167
  const bytes = fs.readFileSync(file, "utf8");
177
168
  fs.unlinkSync(file);
178
169
  const raw = JSON.parse(bytes);
179
- if (!raw || !["pi", "codex", "claude"].includes(raw.runtime))
180
- die("setup Agent 选择无效");
181
- return raw;
170
+ const runtime = raw?.runtime;
171
+ if (runtime === "pi" || runtime === "codex" || runtime === "claude")
172
+ return { runtime };
173
+ return die("setup Agent 选择无效");
182
174
  }
183
175
  function fabricateAttachment(root, serverId) {
184
176
  const directory = path.join(root, "computer", "servers", serverId);
@@ -208,12 +200,9 @@ function formatExternalPiSetupFailure(error, options) {
208
200
  const original = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ").trim();
209
201
  const alternatives = catalogModelsFromError(error);
210
202
  const parts = [];
211
- if (/external Pi (?:auth\.json|models\.json|settings\.json) is required|external Pi profile|external Pi auth\.json has an unsafe mode|external Pi models\.json has an unsafe mode|external Pi settings\.json has an unsafe mode/.test(original)) {
212
- parts.push("pi requires the official file-backed Pi profile containing auth.json (0600), models.json, and settings.json, normally after `pi` login and default-model selection. Environment-variable-only credentials are unsupported.");
213
- }
214
203
  if (original)
215
204
  parts.push(original);
216
- parts.push("Fix the official `pi` login and default model, then rerun `larkin setup --runtime pi`.");
205
+ parts.push("Run the official `pi` login flow, then retry `larkin setup --runtime pi`.");
217
206
  if (options.agentExisted) {
218
207
  parts.push("This Agent already exists; after a successful setup you can switch models with `larkin model <provider>/<model>`.");
219
208
  }
@@ -222,19 +211,6 @@ function formatExternalPiSetupFailure(error, options) {
222
211
  }
223
212
  return parts.join(" ");
224
213
  }
225
- function safeProviderFailure(error) {
226
- const message = error instanceof Error ? error.message : String(error);
227
- if (/\b(?:401|403)\b|unauth|invalid.*(?:key|token)|credential/i.test(message)) {
228
- return new Error("内置 Pi provider 拒绝认证(401/403);请重新登录或检查 credential");
229
- }
230
- if (/timeout|超时|ETIMEDOUT/i.test(message))
231
- return new Error("内置 Pi provider readiness 超时;请检查网络和 endpoint");
232
- if (/ENOTFOUND|ECONN|fetch failed|network|TLS/i.test(message))
233
- return new Error("内置 Pi provider endpoint 不可达;请检查网络、TLS 和 Base URL");
234
- if (/model|模型/i.test(message))
235
- return new Error("内置 Pi provider 不接受所选模型;请检查 provider/model");
236
- return new Error("内置 Pi provider readiness 失败;credential/config 已回滚,请检查 provider 状态后重试");
237
- }
238
214
  function printAgents(config) {
239
215
  const names = Object.keys(config.agents);
240
216
  if (!names.length) {
@@ -248,9 +224,6 @@ function printAgents(config) {
248
224
  }
249
225
  }
250
226
  export async function main() {
251
- if (has("--model")) {
252
- die("setup 不支持配置 model;请用 larkin model <id> 单独切换(bun run model -- <id>,带合法值校验)");
253
- }
254
227
  if (options.help) {
255
228
  say(`larkin setup(内部绑定阶段)
256
229
 
@@ -260,13 +233,14 @@ export async function main() {
260
233
  参数(都可省略;单 bot 单 agent 场景零参数即可):
261
234
  --agent <App ID> 必须与所选 profile 的 App ID 一致;缺省自动使用该 App ID
262
235
  --profile <name> 复用已有 lark-cli profile(可用 profile 名或 appId;缺省交互选择)
263
- --runtime <runtime> pi / codex / claude;缺省沿用已有 agent 的 runtime,全新 agent 默认 pi
236
+ --runtime <runtime> pi / codex / claude;缺省沿用已有 agent 的 runtime
237
+ --model <id> 可选;按 runtime 目录校验后写入。省略则沿用已有值或 runtime 默认
264
238
  --yes 非交互接受默认项
265
239
  --list 查看已经配置的 Agent
266
240
  --help 显示帮助
267
241
 
268
242
  普通用户请直接运行 larkin setup;该内部阶段只负责绑定已取得的凭证。
269
- 模型不在 setup 配置:沿用已有值(换 runtime 时重置为该 runtime 默认),切换用 larkin model <id>。`);
243
+ 省略 --model 时沿用已有值(换 runtime 时重置为该 runtime 默认);也可用 larkin model <id> 事后切换。`);
270
244
  return;
271
245
  }
272
246
  const loaded = loadConfig();
@@ -287,66 +261,55 @@ export async function main() {
287
261
  verifyBot(profile);
288
262
  const requestedAgent = options.agent || profile.appId;
289
263
  const prior = config.agents[profile.appId];
290
- const runtime = selection?.runtime || options.runtime || prior?.runtime || "pi";
291
- const piDistribution = runtime === "pi"
292
- ? (selection?.runtime === "pi" ? selection.distribution : prior?.piDistribution || "external")
293
- : undefined;
264
+ const runtime = selection?.runtime || options.runtime || prior?.runtime;
265
+ if (!runtime)
266
+ die("必须指定 --runtime pi|codex|claude");
294
267
  validateRuntime(runtime);
268
+ const requestedModel = options.model?.trim() || undefined;
295
269
  const defaultModel = larkinConfig.defaultModelFor(runtime);
296
- const selectedModel = selection?.runtime === "pi" && selection.distribution === "builtin" ? selection.model : undefined;
297
- let targetModelId = selectedModel || (prior?.runtime === runtime ? prior.model : defaultModel);
270
+ let targetModelId = requestedModel && requestedModel !== "default"
271
+ ? requestedModel
272
+ : (prior?.runtime === runtime ? prior.model : defaultModel);
298
273
  let resolvedExternalModel;
299
- let pendingMigration = null;
300
- let setupConfigCommitted = false;
301
- let providerTransaction = null;
302
- let providerPublished = false;
303
- const authAbort = new AbortController();
304
- const cancelOnSignal = (signal) => {
305
- authAbort.abort();
306
- if (!setupConfigCommitted && pendingMigration) {
307
- try {
308
- rollbackPiProfileMigration(pendingMigration.state);
309
- }
310
- catch { /* best effort */ }
311
- }
312
- if (!providerPublished)
313
- providerTransaction?.rollback();
314
- process.exit(signal === "SIGINT" ? 130 : 143);
315
- };
316
- const onSigint = () => cancelOnSignal("SIGINT");
317
- const onSigterm = () => cancelOnSignal("SIGTERM");
318
- process.once("SIGINT", onSigint);
319
- process.once("SIGTERM", onSigterm);
320
274
  let runtimeModels = larkinConfig.loadRuntimeModels()[runtime];
321
- let authQuestioner = null;
322
275
  try {
323
- if (runtime === "pi" && piDistribution === "external" && targetModelId === "default") {
324
- fs.mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
276
+ if (runtime === "pi" && (targetModelId === "default" || requestedModel)) {
325
277
  try {
326
- clearStalePiProfileMigrationLock(CFG_DIR, profile.appId);
327
- const plan = preparePiProfileMigration(process.env, CFG_DIR, profile.appId, "external");
328
- pendingMigration = plan;
329
- applyPiProfileMigration(plan);
330
278
  const catalog = await discoverPiModelCatalog({
331
279
  cwd: CFG_DIR,
332
- agentDir: piAgentDirectory(CFG_DIR, profile.appId),
333
- env: { ...process.env, LARKIN_PI_DISTRIBUTION: "external" },
280
+ env: process.env,
334
281
  });
335
- const effective = catalog.effectiveModel;
336
- const entry = effective ? catalog.models.find((model) => model.id === effective) : undefined;
337
- if (!effective || !entry) {
338
- throw new Error("Pi official default resolution returned an unavailable model");
282
+ if (targetModelId === "default") {
283
+ const effective = catalog.effectiveModel;
284
+ const entry = effective ? catalog.models.find((model) => model.id === effective) : undefined;
285
+ if (!effective || !entry) {
286
+ throw new Error("Pi official default resolution returned an unavailable model");
287
+ }
288
+ if (!entry.contextWindow) {
289
+ const alternatives = catalog.models
290
+ .filter((model) => Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0)
291
+ .map((model) => model.id);
292
+ throw Object.assign(new Error("Pi effective model is missing a context window"), { catalogModels: alternatives });
293
+ }
294
+ calculatePiCompactionSettings(entry.contextWindow);
295
+ targetModelId = effective;
296
+ resolvedExternalModel = effective;
297
+ runtimeModels = [{ id: effective, supportedReasoningEfforts: entry.supportedReasoningEfforts }];
339
298
  }
340
- if (!entry.contextWindow) {
341
- const alternatives = catalog.models
342
- .filter((model) => Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0)
343
- .map((model) => model.id);
344
- throw Object.assign(new Error("Pi effective model is missing a context window"), { catalogModels: alternatives });
299
+ else {
300
+ const entry = catalog.models.find((model) => model.id === targetModelId);
301
+ if (!entry)
302
+ throw new Error(`目标模型 pi/${targetModelId} 不在 runtime 模型目录中;未修改 Agent 配置`);
303
+ if (!entry.contextWindow) {
304
+ const alternatives = catalog.models
305
+ .filter((model) => Number.isFinite(model.contextWindow) && Number(model.contextWindow) > 0)
306
+ .map((model) => model.id);
307
+ throw Object.assign(new Error("Pi effective model is missing a context window"), { catalogModels: alternatives });
308
+ }
309
+ calculatePiCompactionSettings(entry.contextWindow);
310
+ resolvedExternalModel = targetModelId;
311
+ runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: entry.supportedReasoningEfforts }];
345
312
  }
346
- calculatePiCompactionSettings(entry.contextWindow);
347
- targetModelId = effective;
348
- resolvedExternalModel = effective;
349
- runtimeModels = [{ id: effective, supportedReasoningEfforts: entry.supportedReasoningEfforts }];
350
313
  }
351
314
  catch (error) {
352
315
  throw new Error(formatExternalPiSetupFailure(error, { agentExisted: Boolean(prior) }));
@@ -358,6 +321,9 @@ export async function main() {
358
321
  : [];
359
322
  runtimeModels = [{ id: targetModelId, supportedReasoningEfforts: preservedEffort }];
360
323
  }
324
+ else if (requestedModel && requestedModel !== "default") {
325
+ resolvedExternalModel = requestedModel;
326
+ }
361
327
  const targetModel = runtimeModels.find((model) => model.id === targetModelId);
362
328
  if (!targetModel)
363
329
  throw new Error(`目标模型 ${runtime}/${targetModelId} 不在 runtime 模型目录中;未修改 Agent 配置`);
@@ -366,67 +332,14 @@ export async function main() {
366
332
  profile,
367
333
  requestedAgent,
368
334
  runtime: selection?.runtime || options.runtime,
369
- ...(runtime === "pi" && piDistribution ? { piDistribution } : {}),
370
- ...(selectedModel || resolvedExternalModel ? { model: selectedModel || resolvedExternalModel } : {}),
335
+ ...(resolvedExternalModel ? { model: resolvedExternalModel } : {}),
371
336
  defaultModel,
372
337
  supportedReasoningEfforts: targetModel.supportedReasoningEfforts || [],
373
338
  now: new Date().toISOString(),
374
339
  });
375
340
  const bound = stored.agents[profile.appId];
376
341
  fs.mkdirSync(CFG_DIR, { recursive: true });
377
- const authAlreadyCompleted = selection?.runtime === "pi" && selection.distribution === "builtin"
378
- && selection.authCompleted === true;
379
- const readinessAlreadyCompleted = authAlreadyCompleted
380
- && selection.readinessCompleted === true;
381
- providerTransaction = selection?.runtime === "pi" && selection.distribution === "builtin"
382
- && !authAlreadyCompleted ? beginBuiltinPiCredentialTransaction(CFG_DIR, profile.appId)
383
- : null;
384
- if (selection?.runtime === "pi" && selection.distribution === "builtin") {
385
- const official = selection.preset === "official" ? selection : null;
386
- const configured = official ? null : configureBuiltinPiProviderModel(CFG_DIR, profile.appId, selection);
387
- const providerId = official?.providerId || configured.provider;
388
- const authType = official?.authType || "api_key";
389
- let piRuntime = null;
390
- if (!authAlreadyCompleted) {
391
- piRuntime = await createOfficialPiModelRuntime(CFG_DIR, profile.appId);
392
- authQuestioner = terminalSetupQuestioner();
393
- say(`正在通过捆绑官方 Pi 登录 ${providerId}(${authType})…`);
394
- try {
395
- await runOfficialPiLogin(piRuntime, providerId, authType, createOfficialPiAuthInteraction({
396
- questioner: authQuestioner,
397
- report: (message) => say(message),
398
- openUrl: openAuthUrl,
399
- signal: authAbort.signal,
400
- }));
401
- }
402
- catch {
403
- throw new Error(`官方 Pi ${providerId} 登录失败或已取消;credential/config 未修改`);
404
- }
405
- }
406
- if (!readinessAlreadyCompleted) {
407
- piRuntime ??= await createOfficialPiModelRuntime(CFG_DIR, profile.appId);
408
- say("正在验证内置 Pi provider(受控单轮,不发送飞书(Lark)消息)…");
409
- try {
410
- if (!(process.env.LARKIN_TEST_SKIP_BUILTIN_PI_PROVIDER_TURN === "1" && process.env.LARKIN_TEST_BOT_REGISTER_MODULE)) {
411
- await verifyOfficialPiProviderTurn(piRuntime, selection.model, authAbort.signal);
412
- }
413
- }
414
- catch (error) {
415
- throw safeProviderFailure(error);
416
- }
417
- }
418
- }
419
342
  larkinConfig.commitSetupConfig(process.env, loaded.revision, stored);
420
- setupConfigCommitted = true;
421
- providerTransaction?.commit();
422
- providerPublished = true;
423
- if (pendingMigration) {
424
- try {
425
- releasePiProfileMigrationLock(pendingMigration.state);
426
- }
427
- catch { /* post-commit lock release is best-effort; do not roll back credentials */ }
428
- pendingMigration = null;
429
- }
430
343
  fabricateAttachment(layout.root, stored.serverId);
431
344
  say(`\n✓ 已写配置: ${CFG_FILE}`);
432
345
  say(` agent=${profile.appId} runtime=${bound.runtime} model=${bound.model}`);
@@ -438,21 +351,8 @@ export async function main() {
438
351
  say(" larkin setup");
439
352
  }
440
353
  catch (error) {
441
- if (!setupConfigCommitted && pendingMigration) {
442
- try {
443
- rollbackPiProfileMigration(pendingMigration.state);
444
- }
445
- catch { /* best effort */ }
446
- pendingMigration = null;
447
- }
448
- providerTransaction?.rollback();
449
354
  throw error;
450
355
  }
451
- finally {
452
- authQuestioner?.close?.();
453
- process.off("SIGINT", onSigint);
454
- process.off("SIGTERM", onSigterm);
455
- }
456
356
  }
457
357
  if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
458
358
  main().catch((error) => die(error instanceof Error ? error.message : String(error)));
@@ -5,14 +5,13 @@ function cloneAgent(agent) {
5
5
  return {
6
6
  runtime: agent.runtime,
7
7
  model: agent.model,
8
- ...(agent.piDistribution ? { piDistribution: agent.piDistribution } : {}),
9
8
  ...(agent.effort ? { effort: agent.effort } : {}),
10
9
  ...(agent.mentionPolicy ? { mentionPolicy: agent.mentionPolicy } : {}),
11
10
  ...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies } : {}),
12
11
  ...(agent.createdAt ? { createdAt: agent.createdAt } : {}),
13
12
  };
14
13
  }
15
- export function planSingleRootBinding({ config, profile, requestedAgent, runtime, piDistribution, model, defaultModel, supportedReasoningEfforts, now, }) {
14
+ export function planSingleRootBinding({ config, profile, requestedAgent, runtime, model, defaultModel, supportedReasoningEfforts, now, }) {
16
15
  if (!/^cli_[A-Za-z0-9]+$/.test(profile.appId))
17
16
  throw new Error(`profile.appId 不是合法飞书 App ID:${profile.appId}`);
18
17
  if (typeof defaultModel !== "string" || !defaultModel)
@@ -25,22 +24,20 @@ export function planSingleRootBinding({ config, profile, requestedAgent, runtime
25
24
  }
26
25
  const agents = Object.fromEntries(Object.entries(config.agents).map(([key, agent]) => [key, cloneAgent(agent)]));
27
26
  const prior = agents[profile.appId];
28
- const nextRuntime = runtime || prior?.runtime || "pi";
29
- const nextPiDistribution = nextRuntime === "pi"
30
- ? piDistribution || (prior?.runtime === "pi" ? prior.piDistribution : undefined)
31
- : undefined;
27
+ const nextRuntime = runtime || prior?.runtime;
28
+ if (!nextRuntime)
29
+ throw new Error("必须指定 --runtime pi|codex|claude");
32
30
  if (prior) {
33
- const { effort, piDistribution: _priorPiDistribution, ...withoutEffort } = prior;
31
+ const { effort, ...withoutEffort } = prior;
34
32
  agents[profile.appId] = {
35
33
  ...withoutEffort,
36
34
  runtime: nextRuntime,
37
35
  model: model || (prior.runtime === nextRuntime ? prior.model : defaultModel),
38
- ...(nextPiDistribution ? { piDistribution: nextPiDistribution } : {}),
39
36
  ...(effort && supportedReasoningEfforts.includes(effort) ? { effort } : {}),
40
37
  };
41
38
  }
42
39
  else {
43
- agents[profile.appId] = { runtime: nextRuntime, model: model || "default", ...(nextPiDistribution ? { piDistribution: nextPiDistribution } : {}), createdAt: now };
40
+ agents[profile.appId] = { runtime: nextRuntime, model: model || "default", createdAt: now };
44
41
  }
45
42
  return {
46
43
  version: 4,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.28",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [
@@ -9,6 +9,7 @@
9
9
  "assets/",
10
10
  "scripts/npm/",
11
11
  "README.md",
12
+ "CHANGELOG.md",
12
13
  "LICENSE",
13
14
  "SECURITY.md",
14
15
  "CONTRIBUTING.md",
@@ -88,7 +89,6 @@
88
89
  "test:live:agent-cli-routing-codex": "bun run build && LARKIN_RUN_AGENT_CLI_ROUTING_CODEX_LIVE=1 bun test --max-concurrency 1 test/live/agent-cli-routing-codex-live.test.mjs"
89
90
  },
90
91
  "dependencies": {
91
- "@earendil-works/pi-coding-agent": "0.84.2",
92
92
  "@larksuite/channel": "^0.4.0",
93
93
  "@opentelemetry/api": "^1.9.1",
94
94
  "@opentelemetry/resources": "^2.10.0",
@@ -104,6 +104,7 @@
104
104
  "undici": "8.5.0"
105
105
  },
106
106
  "devDependencies": {
107
+ "@earendil-works/pi-coding-agent": "0.84.2",
107
108
  "@tailwindcss/vite": "^4.3.3",
108
109
  "@testing-library/jest-dom": "^7.0.0",
109
110
  "@testing-library/react": "^16.3.2",
@@ -1,67 +0,0 @@
1
- import * as larkinConfig from "../platform/config.mjs";
2
- import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiLogoutRuntime, logoutOfficialPiProvider, officialPiAuthStatus, } from "../runtime/pi-official-auth.mjs";
3
- const APP_ID = /^cli_[A-Za-z0-9]+$/;
4
- function flag(args, name) {
5
- const index = args.indexOf(name);
6
- return index >= 0 ? args[index + 1] : undefined;
7
- }
8
- export async function main(args = process.argv.slice(2), env = process.env) {
9
- const action = args[0] || "status";
10
- const json = args.includes("--json");
11
- const requestedAgent = flag(args, "--agent");
12
- let index = action === "logout" ? 2 : 1;
13
- let valid = action === "status" || action === "logout";
14
- while (valid && index < args.length) {
15
- const argument = args[index];
16
- if (argument === "--json" && action === "status") {
17
- index += 1;
18
- continue;
19
- }
20
- if (argument === "--agent" && args[index + 1]) {
21
- index += 2;
22
- continue;
23
- }
24
- valid = false;
25
- }
26
- if (!valid) {
27
- throw new Error("Usage: larkin pi-auth status [--agent <App ID>] [--json] | larkin pi-auth logout <provider> [--agent <App ID>]");
28
- }
29
- const loaded = larkinConfig.loadConfig(env);
30
- const agentId = requestedAgent || loaded.config.activeAgent || undefined;
31
- if (!agentId || !APP_ID.test(agentId))
32
- throw new Error("请用 --agent <App ID> 选择 Agent");
33
- const agent = loaded.config.agents[agentId];
34
- if (!agent)
35
- throw new Error(`Agent ${agentId} 不存在`);
36
- if (agent.runtime !== "pi" || agent.piDistribution !== "builtin")
37
- throw new Error(`Agent ${agentId} 不是内置 Pi`);
38
- if (action === "status") {
39
- const runtime = await createOfficialPiCredentialRuntime(loaded.configDir, agentId);
40
- const statuses = await officialPiAuthStatus(runtime);
41
- if (json)
42
- console.log(JSON.stringify({ agentId, credentials: statuses }));
43
- else if (statuses.length === 0)
44
- console.log(`Agent ${agentId} 尚无已配置的官方 Pi credential`);
45
- else {
46
- console.log(`Agent ${agentId} 官方 Pi credential:`);
47
- for (const entry of statuses)
48
- console.log(` ${entry.providerName} (${entry.providerId}): ${entry.credentialType}/${entry.source}${entry.stored ? " [stored]" : " [ambient]"}`);
49
- }
50
- return;
51
- }
52
- const providerId = args[1];
53
- if (!providerId || providerId.startsWith("--") || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(providerId)) {
54
- throw new Error("logout 需要安全的 provider ID");
55
- }
56
- const transaction = beginBuiltinPiCredentialTransaction(loaded.configDir, agentId);
57
- try {
58
- const runtime = await createOfficialPiLogoutRuntime(loaded.configDir, agentId);
59
- await logoutOfficialPiProvider(runtime, providerId);
60
- transaction.commit();
61
- }
62
- catch (error) {
63
- transaction.rollback();
64
- throw error;
65
- }
66
- console.log(`Agent ${agentId} 已退出 ${providerId};其他 provider 未修改`);
67
- }