larkin 0.4.29 → 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 (57) 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 -86
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +6 -22
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/runtime-model-directory.mjs +2 -16
  11. package/dist/app/runtime-process.mjs +0 -1
  12. package/dist/app/setup.mjs +52 -57
  13. package/dist/dashboard/dashboard-config-controller.mjs +43 -109
  14. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  15. package/dist/dashboard/web/assets/dashboard.css +1 -1
  16. package/dist/dashboard/web/assets/dashboard.js +15 -15
  17. package/dist/feishu/host-shell.cjs +1 -2
  18. package/dist/feishu/host-shell.mjs +1 -2
  19. package/dist/platform/config.cjs +168 -133
  20. package/dist/platform/config.mjs +168 -133
  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 +26 -43
  30. package/dist/runtime/runtime-host.cjs +21 -43
  31. package/dist/runtime/runtime-host.mjs +22 -44
  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 +42 -99
  35. package/dist/runtime/runtime-readiness.mjs +40 -98
  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 -277
  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.cjs +0 -594
  52. package/dist/runtime/pi-official-auth.mjs +0 -544
  53. package/dist/runtime/pi-profile-migration.cjs +0 -764
  54. package/dist/runtime/pi-profile-migration.mjs +0 -719
  55. package/dist/runtime/pi-provider-config.cjs +0 -354
  56. package/dist/runtime/pi-provider-config.mjs +0 -334
  57. package/dist/runtime/pi-provider-login.mjs +0 -240
@@ -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.29",
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,277 +0,0 @@
1
- import fs from "node:fs";
2
- import * as larkinConfig from "../platform/config.mjs";
3
- import { createOfficialPiCredentialRuntime, officialPiAuthStatus, } from "../runtime/pi-official-auth.mjs";
4
- import { configureBuiltinPiProvider, listBuiltinPiProviderCatalog, logoutBuiltinPiProvider, } from "../runtime/pi-provider-login.mjs";
5
- import { MAX_BUILTIN_PI_API_KEY_LENGTH } from "../runtime/pi-provider-config.mjs";
6
- const APP_ID = /^cli_[A-Za-z0-9]+$/;
7
- const USAGE = "Usage: larkin pi-auth status [--agent <App ID>] [--json] | larkin pi-auth providers [--json] | larkin pi-auth login <preset|custom> --agent <App ID> [--model <model>] [--base-url <url>] [--api-key-stdin] [--json] | larkin pi-auth logout <provider> [--agent <App ID>]";
8
- const API_KEY_ARGV_MESSAGE = "existing-Agent login does not accept --api-key. Use --api-key-stdin or an interactive TTY prompt.";
9
- const API_KEY_BUDGET_MESSAGE = "API Key 不能为空或包含控制字符";
10
- const STDIN_CHUNK_BYTES = 1024;
11
- const STDIN_BOM_BYTES = 3;
12
- const STDIN_TRAILING_NEWLINE_SLACK = 32;
13
- const MAX_STDIN_API_KEY_BYTES = MAX_BUILTIN_PI_API_KEY_LENGTH + STDIN_BOM_BYTES + STDIN_TRAILING_NEWLINE_SLACK;
14
- function flag(args, name) {
15
- const index = args.indexOf(name);
16
- return index >= 0 ? args[index + 1] : undefined;
17
- }
18
- function hasApiKeyArgv(args) {
19
- return args.some((argument) => argument === "--api-key" || argument.startsWith("--api-key="));
20
- }
21
- function consumeFlags(args, allowed, unary) {
22
- let index = 0;
23
- while (index < args.length) {
24
- const argument = args[index];
25
- if (!argument.startsWith("--"))
26
- return false;
27
- if (unary.has(argument)) {
28
- index += 1;
29
- continue;
30
- }
31
- if (allowed.has(argument) && args[index + 1] && !args[index + 1].startsWith("--")) {
32
- index += 2;
33
- continue;
34
- }
35
- return false;
36
- }
37
- return true;
38
- }
39
- /** 去掉 BOM 与全部尾部换行;保留内部控制字符给后续校验拒绝。 */
40
- export function normalizeStdinApiKey(raw) {
41
- return raw.replace(/^\uFEFF/, "").replace(/[\r\n]+$/g, "");
42
- }
43
- function assertStdinApiKeyBudget(size) {
44
- if (size > MAX_STDIN_API_KEY_BYTES)
45
- throw new Error(API_KEY_BUDGET_MESSAGE);
46
- }
47
- function finishStdinApiKey(chunks) {
48
- const normalized = normalizeStdinApiKey(Buffer.concat(chunks).toString("utf8"));
49
- if (normalized.length > MAX_BUILTIN_PI_API_KEY_LENGTH)
50
- throw new Error(API_KEY_BUDGET_MESSAGE);
51
- return normalized;
52
- }
53
- function readFdChunk(fd, buffer) {
54
- return new Promise((resolve, reject) => {
55
- fs.read(fd, buffer, 0, buffer.length, null, (error, bytesRead) => {
56
- if (error)
57
- reject(error);
58
- else
59
- resolve(bytesRead);
60
- });
61
- });
62
- }
63
- /** 按块读取 fd,超过 key 上限立即停止,不等待管道 EOF。 */
64
- async function readBoundedFdSecret(fd) {
65
- const chunks = [];
66
- let size = 0;
67
- const buffer = Buffer.alloc(STDIN_CHUNK_BYTES);
68
- while (true) {
69
- const bytesRead = await readFdChunk(fd, buffer);
70
- if (bytesRead === 0)
71
- break;
72
- size += bytesRead;
73
- assertStdinApiKeyBudget(size);
74
- chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
75
- }
76
- return finishStdinApiKey(chunks);
77
- }
78
- async function readBoundedStreamSecret(stdin) {
79
- const chunks = [];
80
- let size = 0;
81
- for await (const chunk of stdin) {
82
- const bytes = Buffer.from(chunk);
83
- size += bytes.length;
84
- assertStdinApiKeyBudget(size);
85
- chunks.push(bytes);
86
- }
87
- return finishStdinApiKey(chunks);
88
- }
89
- export async function readStdinSecret(stdin) {
90
- const stream = stdin;
91
- if (typeof stream.fd === "number" && stream.fd >= 0 && !stream.isTTY) {
92
- if (typeof stream.pause === "function")
93
- stream.pause();
94
- try {
95
- return await readBoundedFdSecret(stream.fd);
96
- }
97
- catch (error) {
98
- if (error instanceof Error && error.message === API_KEY_BUDGET_MESSAGE)
99
- throw error;
100
- // 回退到异步收集,例如测试传入的假 stream。
101
- }
102
- }
103
- return readBoundedStreamSecret(stdin);
104
- }
105
- function createNoEchoQuestioner(stdin, stdout) {
106
- return {
107
- close() { },
108
- secret(prompt) {
109
- if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
110
- throw new Error("non-interactive API-key login requires --api-key-stdin");
111
- }
112
- stdout.write(prompt);
113
- const wasRaw = stdin.isRaw;
114
- stdin.setRawMode(true);
115
- stdin.resume();
116
- return new Promise((resolve, reject) => {
117
- let value = "";
118
- const finish = (error) => {
119
- stdin.off("data", onData);
120
- stdin.setRawMode(Boolean(wasRaw));
121
- stdin.pause();
122
- stdout.write("\n");
123
- if (error)
124
- reject(error);
125
- else
126
- resolve(value);
127
- };
128
- const onData = (chunk) => {
129
- const bytes = Buffer.from(chunk);
130
- for (const byte of bytes) {
131
- if (byte === 3)
132
- return finish(new Error("pi-auth login cancelled"));
133
- if (byte === 10 || byte === 13)
134
- return finish();
135
- if (byte === 8 || byte === 127) {
136
- value = value.slice(0, -1);
137
- continue;
138
- }
139
- if (byte >= 32 && byte <= 126)
140
- value += String.fromCharCode(byte);
141
- }
142
- };
143
- stdin.on("data", onData);
144
- });
145
- },
146
- };
147
- }
148
- function resolveAgentId(args, env) {
149
- const loaded = larkinConfig.loadConfig(env);
150
- const agentId = flag(args, "--agent") || loaded.config.activeAgent || undefined;
151
- if (!agentId || !APP_ID.test(agentId))
152
- throw new Error("请用 --agent <App ID> 选择 Agent");
153
- const agent = loaded.config.agents[agentId];
154
- if (!agent)
155
- throw new Error(`Agent ${agentId} 不存在`);
156
- if (agent.runtime !== "pi" || agent.piDistribution !== "builtin")
157
- throw new Error(`Agent ${agentId} 不是内置 Pi`);
158
- return agentId;
159
- }
160
- export async function main(args = process.argv.slice(2), env = process.env, io = { stdin: process.stdin, stdout: process.stdout, isTTY: Boolean(process.stdin.isTTY) }) {
161
- const action = args[0] || "status";
162
- const json = args.includes("--json");
163
- if (hasApiKeyArgv(args))
164
- throw new Error(API_KEY_ARGV_MESSAGE);
165
- if (action === "providers") {
166
- if (!consumeFlags(args.slice(1), new Set(), new Set(["--json"])))
167
- throw new Error(USAGE);
168
- const providers = listBuiltinPiProviderCatalog();
169
- if (json) {
170
- console.log(JSON.stringify({ providers }));
171
- return;
172
- }
173
- console.log("Builtin Pi providers:");
174
- for (const entry of providers) {
175
- const model = entry.defaultModel ? ` default=${entry.defaultModel}` : " (requires --base-url and --model)";
176
- console.log(` ${entry.id} ${entry.name}${model}`);
177
- }
178
- return;
179
- }
180
- if (action === "login") {
181
- const preset = args[1];
182
- const rest = args.slice(2);
183
- if (!preset || preset.startsWith("--")
184
- || !consumeFlags(rest, new Set(["--agent", "--model", "--base-url"]), new Set(["--json", "--api-key-stdin"]))) {
185
- throw new Error(USAGE);
186
- }
187
- const agentId = resolveAgentId(rest, env);
188
- const stdinKey = rest.includes("--api-key-stdin");
189
- let apiKey = "";
190
- if (stdinKey)
191
- apiKey = await readStdinSecret(io.stdin);
192
- else if (io.isTTY) {
193
- const questioner = createNoEchoQuestioner(io.stdin, io.stdout);
194
- try {
195
- apiKey = await questioner.secret("API Key:\n> ");
196
- }
197
- finally {
198
- questioner.close();
199
- }
200
- }
201
- else {
202
- throw new Error("non-interactive API-key login requires --api-key-stdin");
203
- }
204
- const result = await configureBuiltinPiProvider({
205
- agentId,
206
- preset,
207
- apiKey,
208
- model: flag(rest, "--model"),
209
- baseUrl: flag(rest, "--base-url"),
210
- env,
211
- });
212
- const payload = {
213
- agentId: result.agentId,
214
- provider: result.provider,
215
- model: result.model,
216
- credentialType: result.credentialType,
217
- applyState: result.applyState,
218
- ...(result.applyError ? { applyError: result.applyError } : {}),
219
- };
220
- if (json)
221
- console.log(JSON.stringify(payload));
222
- else {
223
- const pending = result.applyState === "applied"
224
- ? "applied"
225
- : result.applyState === "pending"
226
- ? `pending${result.applyError ? ` (${result.applyError})` : ""}`
227
- : "saved; will apply on larkin start";
228
- console.log(`Agent ${result.agentId} provider=${result.provider} model=${result.model} credential=${result.credentialType} apply=${pending}`);
229
- }
230
- return;
231
- }
232
- const requestedAgent = flag(args, "--agent");
233
- let index = action === "logout" ? 2 : 1;
234
- let valid = action === "status" || action === "logout";
235
- while (valid && index < args.length) {
236
- const argument = args[index];
237
- if (argument === "--json" && action === "status") {
238
- index += 1;
239
- continue;
240
- }
241
- if (argument === "--agent" && args[index + 1]) {
242
- index += 2;
243
- continue;
244
- }
245
- valid = false;
246
- }
247
- if (!valid)
248
- throw new Error(USAGE);
249
- const loaded = larkinConfig.loadConfig(env);
250
- const agentId = requestedAgent || loaded.config.activeAgent || undefined;
251
- if (!agentId || !APP_ID.test(agentId))
252
- throw new Error("请用 --agent <App ID> 选择 Agent");
253
- const agent = loaded.config.agents[agentId];
254
- if (!agent)
255
- throw new Error(`Agent ${agentId} 不存在`);
256
- if (agent.runtime !== "pi" || agent.piDistribution !== "builtin")
257
- throw new Error(`Agent ${agentId} 不是内置 Pi`);
258
- if (action === "status") {
259
- const runtime = await createOfficialPiCredentialRuntime(loaded.configDir, agentId);
260
- const statuses = await officialPiAuthStatus(runtime);
261
- if (json)
262
- console.log(JSON.stringify({ agentId, credentials: statuses }));
263
- else if (statuses.length === 0)
264
- console.log(`Agent ${agentId} 尚无已配置的官方 Pi credential`);
265
- else {
266
- console.log(`Agent ${agentId} 官方 Pi credential:`);
267
- for (const entry of statuses)
268
- console.log(` ${entry.providerName} (${entry.providerId}): ${entry.credentialType}/${entry.source}${entry.stored ? " [stored]" : " [ambient]"}`);
269
- }
270
- return;
271
- }
272
- const providerId = args[1];
273
- if (!providerId || providerId.startsWith("--"))
274
- throw new Error("logout 需要安全的 provider ID");
275
- await logoutBuiltinPiProvider({ agentId, providerId, env });
276
- console.log(`Agent ${agentId} 已退出 ${providerId};其他 provider 未修改`);
277
- }