larkin 0.4.27 → 0.4.29

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 (34) hide show
  1. package/dist/app/agent-cli.mjs +4 -2
  2. package/dist/app/agent-config.mjs +45 -29
  3. package/dist/app/cli.mjs +16 -6
  4. package/dist/app/local-control.mjs +16 -3
  5. package/dist/app/pi-auth-cli.mjs +227 -17
  6. package/dist/app/runtime-model-directory.mjs +3 -2
  7. package/dist/app/runtime-process.mjs +42 -5
  8. package/dist/app/setup.mjs +15 -14
  9. package/dist/dashboard/dashboard-config-controller.mjs +145 -13
  10. package/dist/dashboard/dashboard-view-model.mjs +3 -2
  11. package/dist/dashboard/web/assets/dashboard.css +1 -1
  12. package/dist/dashboard/web/assets/dashboard.js +15 -15
  13. package/dist/feishu/host-shell.cjs +68 -16
  14. package/dist/feishu/host-shell.mjs +69 -17
  15. package/dist/platform/config.cjs +67 -16
  16. package/dist/platform/config.mjs +57 -16
  17. package/dist/runtime/pi-official-auth.cjs +594 -0
  18. package/dist/runtime/pi-official-auth.mjs +53 -0
  19. package/dist/runtime/pi-provider-config.cjs +26 -3
  20. package/dist/runtime/pi-provider-config.mjs +22 -2
  21. package/dist/runtime/pi-provider-login.mjs +240 -0
  22. package/dist/runtime/runtime-adapters.mjs +24 -4
  23. package/dist/runtime/runtime-host.cjs +215 -16
  24. package/dist/runtime/runtime-host.mjs +216 -17
  25. package/dist/runtime/runtime-model-catalog.cjs +4 -3
  26. package/dist/runtime/runtime-model-catalog.mjs +4 -3
  27. package/dist/runtime/runtime-readiness.cjs +123 -3
  28. package/dist/runtime/runtime-readiness.mjs +116 -4
  29. package/dist/runtime/user-runtime.cjs +51 -0
  30. package/dist/runtime/user-runtime.mjs +41 -0
  31. package/dist/setup/bot-register.mjs +3 -3
  32. package/dist/setup/setup-agent-choice.mjs +10 -9
  33. package/dist/setup/setup-bind.mjs +2 -2
  34. package/package.json +1 -1
@@ -4,12 +4,105 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { resolveConfigDir as resolveRootConfigDir } from "../platform/root-layout.mjs";
6
6
  import { applyPiPackageDirForChild, piChildDistributionFromOverrides } from "./builtin-pi-assets.mjs";
7
- import { assertBuiltinPiAgentDirectory, BUNDLED_PI_VERSION, ownedPiCatalogAgentDir, piAgentDirectory } from "./pi-provider-config.mjs";
7
+ import { assertBuiltinPiAgentDirectory, BUNDLED_PI_VERSION, builtinPiProviderRecoveryNextAction, ownedPiCatalogAgentDir, piAgentDirectory } from "./pi-provider-config.mjs";
8
8
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
9
- function safeProviderLabel(value) {
9
+ export function safeProviderId(value) {
10
10
  const provider = typeof value === "string" ? value.trim() : "";
11
11
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
12
12
  }
13
+ function safeProviderLabel(value) {
14
+ return safeProviderId(value);
15
+ }
16
+ export function isBuiltinPiDistribution(value) {
17
+ return value === "builtin";
18
+ }
19
+ /** Model ids are `provider/model`; only the provider prefix is used for scope. */
20
+ export function configuredProviderId(model) {
21
+ const value = typeof model === "string" ? model.trim() : "";
22
+ const slash = value.indexOf("/");
23
+ return slash > 0 ? safeProviderId(value.slice(0, slash)) : null;
24
+ }
25
+ function persistedRuntime(value) {
26
+ return value === "pi" || value === "codex" || value === "claude" ? value : null;
27
+ }
28
+ export function parsePersistedAuthFailure(state) {
29
+ if (!state || typeof state !== "object" || Array.isArray(state))
30
+ return null;
31
+ const record = state;
32
+ if (record.authFailure && typeof record.authFailure === "object" && !Array.isArray(record.authFailure)) {
33
+ const failure = record.authFailure;
34
+ const kind = failure.kind === "missing-provider" || failure.kind === "generic" ? failure.kind : null;
35
+ const runtime = persistedRuntime(failure.runtime);
36
+ if (!kind || !runtime)
37
+ return null;
38
+ const provider = safeProviderId(failure.provider);
39
+ if (kind === "missing-provider" && !provider)
40
+ return null;
41
+ const piDistribution = failure.piDistribution === "builtin" || failure.piDistribution === "external"
42
+ ? failure.piDistribution : undefined;
43
+ return {
44
+ kind,
45
+ runtime,
46
+ ...(piDistribution ? { piDistribution } : {}),
47
+ provider,
48
+ };
49
+ }
50
+ const legacy = safeProviderId(record.authFailureProvider);
51
+ return legacy
52
+ ? { kind: "missing-provider", runtime: "pi", piDistribution: "builtin", provider: legacy }
53
+ : null;
54
+ }
55
+ function currentAdapterId(current) {
56
+ return current.adapterId ?? (current.runtime === "builtin-pi" ? "pi" : current.runtime);
57
+ }
58
+ function currentIsBuiltinPi(current) {
59
+ return currentAdapterId(current) === "pi"
60
+ && (isBuiltinPiDistribution(current.piDistribution) || current.runtime === "builtin-pi");
61
+ }
62
+ /**
63
+ * Missing-provider and known-provider generic markers apply only to that provider.
64
+ * Generic without a provider is conservative fallback: it still binds runtime /
65
+ * builtin distribution, but cannot reject an A→B model switch because the
66
+ * upstream provider was genuinely unavailable.
67
+ */
68
+ export function authFailureAppliesTo(current, failure) {
69
+ if (failure.kind === "missing-provider") {
70
+ return currentIsBuiltinPi(current)
71
+ && failure.runtime === "pi"
72
+ && failure.piDistribution === "builtin"
73
+ && Boolean(failure.provider)
74
+ && configuredProviderId(current.model) === failure.provider;
75
+ }
76
+ if (failure.runtime !== currentAdapterId(current))
77
+ return false;
78
+ if (failure.runtime === "pi") {
79
+ const persistedBuiltin = failure.piDistribution === "builtin";
80
+ if (currentIsBuiltinPi(current) !== persistedBuiltin)
81
+ return false;
82
+ }
83
+ if (!failure.provider)
84
+ return true;
85
+ const currentProvider = configuredProviderId(current.model);
86
+ if (currentIsBuiltinPi(current))
87
+ return currentProvider === failure.provider;
88
+ return currentProvider ? currentProvider === failure.provider : true;
89
+ }
90
+ export function readinessForPersistedAuthFailure(failure) {
91
+ if (failure.kind === "missing-provider") {
92
+ return missingProviderCredentialReadiness(failure.runtime, failure.provider);
93
+ }
94
+ return providerAuthenticationFailureReadiness(failure.runtime, failure.provider);
95
+ }
96
+ const MISSING_CREDENTIAL_REJECTION = /^(?:Pi RPC (?:prompt|steer) failed: )?(No API key found for|No login found for) ([A-Za-z0-9][A-Za-z0-9._-]{0,79})$/;
97
+ /** Narrow match for an explicit Pi absent-key / absent-login diagnostic. */
98
+ export function classifyPiMissingCredentialRejection(message) {
99
+ const text = typeof message === "string"
100
+ ? message.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim()
101
+ : "";
102
+ const match = MISSING_CREDENTIAL_REJECTION.exec(text);
103
+ const provider = match ? safeProviderLabel(match[2]) : null;
104
+ return provider && match ? { provider, diagnostic: `${match[1]} ${provider}` } : null;
105
+ }
13
106
  export function providerAuthenticationFailureReadiness(runtime, provider) {
14
107
  const label = safeProviderLabel(provider);
15
108
  return {
@@ -21,6 +114,22 @@ export function providerAuthenticationFailureReadiness(runtime, provider) {
21
114
  nextAction: "Check the provider login or API-key resolver command, then retry the Agent turn.",
22
115
  };
23
116
  }
117
+ /** P0 分类;Pi nextAction 共用 UX preset 恢复文案,不写官方 provider ID。 */
118
+ export function missingProviderCredentialReadiness(runtime, provider) {
119
+ const label = safeProviderLabel(provider);
120
+ return {
121
+ runtime,
122
+ state: "unauthenticated",
123
+ reason: label
124
+ ? `Provider ${label} is missing from this Agent's official credential store.`
125
+ : "The configured provider is missing from this Agent's official credential store.",
126
+ nextAction: runtime === "pi"
127
+ ? builtinPiProviderRecoveryNextAction({ providerId: label ?? undefined })
128
+ : label
129
+ ? `Add the missing ${label} credential to this Agent's official store, then retry the Agent turn.`
130
+ : "Add the missing provider credential to this Agent's official store, then retry the Agent turn.",
131
+ };
132
+ }
24
133
  export class RuntimePrerequisiteError extends Error {
25
134
  readiness;
26
135
  constructor(readiness) {
@@ -56,7 +165,10 @@ export function classifyRuntimePrerequisite(runtime, error, executable) {
56
165
  if (/no authenticated available models|login|credential|unauthenticated|unauthorized|oauth/i.test(reason))
57
166
  return {
58
167
  runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
59
- nextAction: runtime === "pi" ? "Run the official `pi` login flow, then retry." : `Authenticate ${runtime}, then retry.`,
168
+ // 此分类没有 distribution:外部 Pi 不能指向 Dashboard / pi-auth login。
169
+ nextAction: runtime === "pi"
170
+ ? "Run the official `pi` login flow, then retry."
171
+ : `Authenticate ${runtime}, then retry.`,
60
172
  };
61
173
  if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason))
62
174
  return {
@@ -107,7 +219,7 @@ export async function probeNativeRuntimeReadiness(options) {
107
219
  catch (error) {
108
220
  traceProcessBoundary(env, "readiness:builtin-pi-failure", { configDir: env.LARKIN_CONFIG_DIR, agentId: options.agentId, targetDir: options.agentId && env.LARKIN_CONFIG_DIR ? piAgentDirectory(env.LARKIN_CONFIG_DIR, options.agentId) : undefined, error });
109
221
  return { runtime: "pi", state: "unauthenticated", reason: error instanceof Error ? error.message : String(error),
110
- nextAction: "重新运行 larkin setup,选择内置 Pi 并配置有效 API Key。" };
222
+ nextAction: builtinPiProviderRecoveryNextAction({ agentId: options.agentId }) };
111
223
  }
112
224
  }
113
225
  const command = selectedCommand(options);
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ADAPTER_RUNTIMES = exports.USER_RUNTIMES = exports.RUNTIME_OPTIONS = void 0;
4
+ exports.isUserRuntime = isUserRuntime;
5
+ exports.isAdapterRuntime = isAdapterRuntime;
6
+ exports.runtimeOptionOf = runtimeOptionOf;
7
+ exports.toUserRuntime = toUserRuntime;
8
+ exports.runtimeOptionTarget = runtimeOptionTarget;
9
+ exports.fromUserRuntime = fromUserRuntime;
10
+ exports.piCatalogDistributionForUserRuntime = piCatalogDistributionForUserRuntime;
11
+ exports.RUNTIME_OPTIONS = ["codex", "claude", "pi", "builtin-pi"];
12
+ exports.USER_RUNTIMES = exports.RUNTIME_OPTIONS;
13
+ exports.ADAPTER_RUNTIMES = ["pi", "codex", "claude"];
14
+ function isUserRuntime(value) {
15
+ return value === "pi" || value === "builtin-pi" || value === "codex" || value === "claude";
16
+ }
17
+ function isAdapterRuntime(value) {
18
+ return value === "pi" || value === "codex" || value === "claude";
19
+ }
20
+ /** Project stored adapter + distribution to the user-facing sibling id. Legacy `runtime=pi` without distribution is `pi`, never builtin. */
21
+ function runtimeOptionOf(input) {
22
+ const { runtime, piDistribution } = input;
23
+ if (runtime === "codex" || runtime === "claude" || runtime === "builtin-pi")
24
+ return runtime;
25
+ if (runtime === "pi")
26
+ return piDistribution === "builtin" ? "builtin-pi" : "pi";
27
+ return runtime;
28
+ }
29
+ function toUserRuntime(runtime, piDistribution) {
30
+ return runtimeOptionOf({ runtime, piDistribution });
31
+ }
32
+ /** Persist a user-facing sibling as adapter id + distribution. Adapter id stays `pi` for both Pi siblings. */
33
+ function runtimeOptionTarget(option) {
34
+ if (!isUserRuntime(option))
35
+ throw new Error(`未知 runtime:${option}`);
36
+ if (option === "builtin-pi")
37
+ return { runtime: "pi", piDistribution: "builtin" };
38
+ if (option === "pi")
39
+ return { runtime: "pi", piDistribution: "external" };
40
+ return { runtime: option };
41
+ }
42
+ function fromUserRuntime(userRuntime) {
43
+ return runtimeOptionTarget(userRuntime);
44
+ }
45
+ function piCatalogDistributionForUserRuntime(userRuntime) {
46
+ if (userRuntime === "builtin-pi")
47
+ return "builtin";
48
+ if (userRuntime === "pi")
49
+ return "external";
50
+ throw new Error(`非 Pi sibling runtime:${userRuntime}`);
51
+ }
@@ -0,0 +1,41 @@
1
+ export const RUNTIME_OPTIONS = ["codex", "claude", "pi", "builtin-pi"];
2
+ export const USER_RUNTIMES = RUNTIME_OPTIONS;
3
+ export const ADAPTER_RUNTIMES = ["pi", "codex", "claude"];
4
+ export function isUserRuntime(value) {
5
+ return value === "pi" || value === "builtin-pi" || value === "codex" || value === "claude";
6
+ }
7
+ export function isAdapterRuntime(value) {
8
+ return value === "pi" || value === "codex" || value === "claude";
9
+ }
10
+ /** Project stored adapter + distribution to the user-facing sibling id. Legacy `runtime=pi` without distribution is `pi`, never builtin. */
11
+ export function runtimeOptionOf(input) {
12
+ const { runtime, piDistribution } = input;
13
+ if (runtime === "codex" || runtime === "claude" || runtime === "builtin-pi")
14
+ return runtime;
15
+ if (runtime === "pi")
16
+ return piDistribution === "builtin" ? "builtin-pi" : "pi";
17
+ return runtime;
18
+ }
19
+ export function toUserRuntime(runtime, piDistribution) {
20
+ return runtimeOptionOf({ runtime, piDistribution });
21
+ }
22
+ /** Persist a user-facing sibling as adapter id + distribution. Adapter id stays `pi` for both Pi siblings. */
23
+ export function runtimeOptionTarget(option) {
24
+ if (!isUserRuntime(option))
25
+ throw new Error(`未知 runtime:${option}`);
26
+ if (option === "builtin-pi")
27
+ return { runtime: "pi", piDistribution: "builtin" };
28
+ if (option === "pi")
29
+ return { runtime: "pi", piDistribution: "external" };
30
+ return { runtime: option };
31
+ }
32
+ export function fromUserRuntime(userRuntime) {
33
+ return runtimeOptionTarget(userRuntime);
34
+ }
35
+ export function piCatalogDistributionForUserRuntime(userRuntime) {
36
+ if (userRuntime === "builtin-pi")
37
+ return "builtin";
38
+ if (userRuntime === "pi")
39
+ return "external";
40
+ throw new Error(`非 Pi sibling runtime:${userRuntime}`);
41
+ }
@@ -719,10 +719,10 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
719
719
  const setupModel = flag("--model");
720
720
  if (piDistributionFlag === "builtin") {
721
721
  if (!setupProvider && !setupBaseUrl) {
722
- throw new Error(`builtin-pi 需要 --provider <id>(${PI_PROVIDER_PRESETS.map((p) => p.id).join("|")}|custom)或 --base-url;或改用 --runtime external-pi 使用已有 pi 环境`);
722
+ throw new Error(`builtin-pi 需要 --provider <id>(${PI_PROVIDER_PRESETS.map((p) => p.id).join("|")}|custom)或 --base-url;或改用 --runtime pi 使用已有 pi 环境`);
723
723
  }
724
724
  if (!setupApiKey)
725
- throw new Error("builtin-pi 需要 --api-key;或改用 --runtime external-pi 使用已有 pi 登录");
725
+ throw new Error("builtin-pi 需要 --api-key;或改用 --runtime pi 使用已有 pi 登录");
726
726
  // 自定义网关(--base-url)走 custom 预设:pi 会把 baseUrl 作为 provider 端点写入配置,
727
727
  // 而不是用预设厂商目录(否则会打到预设默认地址,如 api.openai.com)。
728
728
  const presetId = setupBaseUrl ? "custom" : (setupProvider ?? "custom");
@@ -763,7 +763,7 @@ Lark 必须用 --tenant lark:授权二维码是官方 /page/cli(begin 仍走
763
763
  say(`[setup 2/5] ✓ 内置 Pi provider 已配置(${presetId}${setupBaseUrl ? " / custom" : ""})`);
764
764
  }
765
765
  else if (piDistributionFlag === "external" && (setupProvider || setupApiKey || setupBaseUrl)) {
766
- throw new Error("external-pi 使用已有 pi 环境与登录,不接受 --provider/--api-key/--base-url");
766
+ throw new Error("pi 使用已有 pi 环境与登录,不接受 --provider/--api-key/--base-url");
767
767
  }
768
768
  const stagedBotFile = path.join(botsDir, `.${id}.${process.pid}.tmp`);
769
769
  try {
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { stdin, stdout } from "node:process";
3
+ import { toUserRuntime } from "../runtime/user-runtime.mjs";
3
4
  import { PI_PROVIDER_PRESETS, resolveBuiltinPiProviderSetupSelection, } from "../runtime/pi-provider-config.mjs";
4
5
  function number(value, fallback, max) {
5
6
  const parsed = value.trim() ? Number(value.trim()) : fallback;
@@ -54,20 +55,20 @@ export async function collectBuiltinPiChoice(questioner, services) {
54
55
  }
55
56
  export async function collectSetupAgentChoice(questioner, prior, services) {
56
57
  if (prior) {
57
- const keep = (await questioner.ask(`已有 Agent:${prior.runtime}/${prior.model}。直接回车保留;输入 c 才修改:`)).trim().toLowerCase();
58
+ const keep = (await questioner.ask(`已有 Agent:${toUserRuntime(prior.runtime, prior.piDistribution)}/${prior.model}。直接回车保留;输入 c 才修改:`)).trim().toLowerCase();
58
59
  if (!keep)
59
60
  return null;
60
61
  if (keep !== "c")
61
62
  throw new Error("请输入 c 修改,或直接回车保留现有配置");
62
63
  }
63
- const runtimeChoice = number(await questioner.ask("选择 Agent:\n 1. Pi(推荐)\n 2. Codex\n 3. Claude Code\n> "), 1, 3);
64
- if (runtimeChoice === 2)
65
- return { runtime: "codex" };
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);
66
65
  if (runtimeChoice === 3)
66
+ return { runtime: "codex" };
67
+ if (runtimeChoice === 4)
67
68
  return { runtime: "claude" };
68
- const sourceChoice = number(await questioner.ask("选择 Pi:\n 1. 外置 Pi(使用本机官方 pi)\n 2. 内置 Pi(无需安装 Agent CLI,推荐)\n> "), 2, 2);
69
- const distribution = sourceChoice === 1 ? "external" : "builtin";
70
- return distribution === "external" ? { runtime: "pi", distribution } : collectBuiltinPiChoice(questioner, services);
69
+ if (runtimeChoice === 2)
70
+ return { runtime: "pi", distribution: "external" };
71
+ return collectBuiltinPiChoice(questioner, services);
71
72
  }
72
73
  export async function recoverUnavailableExternalPi(initial, questioner, probe, report = () => { }, services) {
73
74
  let choice = initial;
@@ -77,8 +78,8 @@ export async function recoverUnavailableExternalPi(initial, questioner, probe, r
77
78
  const readiness = await probe();
78
79
  if (readiness.state === "ready")
79
80
  return choice;
80
- report(`外置 Pi ${readiness.state}:${readiness.reason || "prerequisite unavailable"};${readiness.nextAction || "可切换到内置 Pi"}`);
81
- const action = number(await questioner.ask("外置 Pi 当前不可用:\n 1. 切换到内置 Pi(推荐)\n 2. 重新选择 Agent\n 3. 取消 setup(保留原配置)\n> "), 1, 3);
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);
82
83
  if (action === 1)
83
84
  return collectBuiltinPiChoice(questioner, services);
84
85
  if (action === 3)
@@ -209,11 +209,11 @@ function formatExternalPiSetupFailure(error, options) {
209
209
  const alternatives = catalogModelsFromError(error);
210
210
  const parts = [];
211
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("external-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.");
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
213
  }
214
214
  if (original)
215
215
  parts.push(original);
216
- parts.push("Fix the official `pi` login and default model, then rerun `larkin setup --runtime external-pi`.");
216
+ parts.push("Fix the official `pi` login and default model, then rerun `larkin setup --runtime pi`.");
217
217
  if (options.agentExisted) {
218
218
  parts.push("This Agent already exists; after a successful setup you can switch models with `larkin model <provider>/<model>`.");
219
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [