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.
- package/CHANGELOG.md +11 -0
- package/README.md +5 -9
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
- package/dist/app/agent-cli.mjs +14 -2
- package/dist/app/agent-config.mjs +26 -86
- package/dist/app/binary-entry.mjs +0 -20
- package/dist/app/cli.mjs +6 -22
- package/dist/app/internal-command.cjs +0 -2
- package/dist/app/internal-command.mjs +0 -2
- package/dist/app/runtime-model-directory.mjs +2 -16
- package/dist/app/runtime-process.mjs +0 -1
- package/dist/app/setup.mjs +52 -57
- package/dist/dashboard/dashboard-config-controller.mjs +43 -109
- package/dist/dashboard/dashboard-view-model.mjs +4 -7
- package/dist/dashboard/web/assets/dashboard.css +1 -1
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +1 -2
- package/dist/feishu/host-shell.mjs +1 -2
- package/dist/platform/config.cjs +168 -133
- package/dist/platform/config.mjs +168 -133
- package/dist/runtime/pi-artifact-provenance.mjs +3 -3
- package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
- package/dist/runtime/pi-compaction-recovery.cjs +9 -11
- package/dist/runtime/pi-compaction-recovery.mjs +8 -10
- package/dist/runtime/pi-extension-api.mjs +1 -0
- package/dist/runtime/pi-model-catalog.cjs +7 -8
- package/dist/runtime/pi-model-catalog.mjs +7 -8
- package/dist/runtime/pi-subagent-injection.mjs +17 -4
- package/dist/runtime/runtime-adapters.mjs +26 -43
- package/dist/runtime/runtime-host.cjs +21 -43
- package/dist/runtime/runtime-host.mjs +22 -44
- package/dist/runtime/runtime-model-catalog.cjs +1 -1
- package/dist/runtime/runtime-model-catalog.mjs +1 -1
- package/dist/runtime/runtime-readiness.cjs +42 -99
- package/dist/runtime/runtime-readiness.mjs +40 -98
- package/dist/runtime/user-runtime.cjs +7 -24
- package/dist/runtime/user-runtime.mjs +7 -23
- package/dist/setup/bot-register.mjs +15 -123
- package/dist/setup/setup-agent-choice.mjs +48 -74
- package/dist/setup/setup-bind.mjs +51 -151
- package/dist/setup/setup-binding.mjs +6 -9
- package/package.json +3 -2
- package/dist/app/pi-auth-cli.mjs +0 -277
- package/dist/platform/process-boundary-trace.cjs +0 -108
- package/dist/runtime/builtin-pi-assets.cjs +0 -117
- package/dist/runtime/builtin-pi-assets.mjs +0 -105
- package/dist/runtime/pi-artifact-provenance.cjs +0 -147
- package/dist/runtime/pi-distribution-label.cjs +0 -6
- package/dist/runtime/pi-distribution-label.mjs +0 -3
- package/dist/runtime/pi-inline-extensions.mjs +0 -78
- package/dist/runtime/pi-official-auth.cjs +0 -594
- package/dist/runtime/pi-official-auth.mjs +0 -544
- package/dist/runtime/pi-profile-migration.cjs +0 -764
- package/dist/runtime/pi-profile-migration.mjs +0 -719
- package/dist/runtime/pi-provider-config.cjs +0 -354
- package/dist/runtime/pi-provider-config.mjs +0 -334
- 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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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" &&
|
|
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
|
-
|
|
333
|
-
env: { ...process.env, LARKIN_PI_DISTRIBUTION: "external" },
|
|
280
|
+
env: process.env,
|
|
334
281
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
...(
|
|
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,
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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",
|
|
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.
|
|
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",
|
package/dist/app/pi-auth-cli.mjs
DELETED
|
@@ -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
|
-
}
|