acryl-cli-linux-x64 0.1.35 → 0.1.36
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/package.json +1 -1
- package/runtime/lib/bin.js +218 -206
- package/runtime/lib/index.js +220 -206
- package/runtime/lib/types/tui/actions.d.ts +2 -1
- package/runtime/lib/types/tui/listWindow.d.ts +24 -0
- package/runtime/lib/types/tui/login/LoginOverlay.d.ts +19 -13
- package/runtime/lib/types/tui/login/types.d.ts +2 -1
- package/runtime/lib/types/tui/modelProfile/ModelProfileOverlay.d.ts +0 -14
- package/runtime/lib/types/tui/modelProfile/types.d.ts +3 -2
- package/runtime/lib/types/yly/yly-pet.d.ts +1 -1
- package/runtime/lib/types/yly/yly-programs.d.ts +1 -1
- package/runtime/node_modules/.modules.yaml +1 -1
- package/runtime/node_modules/.package-map.json +1 -1
- package/runtime/node_modules/acryl-control/lib/index.js +66 -1
- package/runtime/node_modules/acryl-control/lib/types/authorization/service.d.ts +23 -0
- package/runtime/node_modules/acryl-control/lib/types/authorization/types.d.ts +46 -0
- package/runtime/node_modules/acryl-control/lib/types/credential/projection.d.ts +63 -0
- package/runtime/node_modules/acryl-control/lib/types/credential/types.d.ts +38 -0
- package/runtime/node_modules/acryl-control/lib/types/index.d.ts +4 -0
- package/runtime/node_modules/acryl-control/package.json +1 -1
- package/runtime/node_modules/acryl-harness-runtime/package.json +1 -1
- package/runtime/package.json +2 -2
- package/runtime/receipt.json +2 -2
package/runtime/lib/bin.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, relative, resolve } from "node:path";
|
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { bootAcrylHarnessProfile, createAcrylSessionBridge } from "acryl-harness-runtime";
|
|
9
|
+
import { AuthorizationService, computeCredentialProjection } from "acryl-control";
|
|
9
10
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
11
|
import { diffLines } from "diff";
|
|
11
12
|
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, fuzzyFilter, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
@@ -1072,7 +1073,7 @@ const ACRYL_MARK_ROWS = [
|
|
|
1072
1073
|
" ▀ "
|
|
1073
1074
|
];
|
|
1074
1075
|
/** ISO timestamp this build was produced at. */
|
|
1075
|
-
const BUILD_TIME = "2026-09-
|
|
1076
|
+
const BUILD_TIME = "2026-09-08T18:16:12.207Z";
|
|
1076
1077
|
//#endregion
|
|
1077
1078
|
//#region src/tui/statsFormat.ts
|
|
1078
1079
|
/**
|
|
@@ -2910,14 +2911,42 @@ function renderMiniTextField(state, cursorVisible, mask) {
|
|
|
2910
2911
|
return `${display.slice(0, state.cursor)}\x1b[7m${display[state.cursor] ?? " "}\x1b[0m${display.slice(state.cursor + 1)}`;
|
|
2911
2912
|
}
|
|
2912
2913
|
//#endregion
|
|
2913
|
-
//#region src/tui/
|
|
2914
|
+
//#region src/tui/listWindow.ts
|
|
2915
|
+
/**
|
|
2916
|
+
* Scrollable-list windowing shared by every overlay that paints a
|
|
2917
|
+
* selection-following list (`/login`'s provider list, `/model`'s picker and
|
|
2918
|
+
* provider list). Previously duplicated verbatim in `LoginOverlay.ts` and
|
|
2919
|
+
* `ModelProfileOverlay.ts` (specs/001-acryl-refactor-improvements-and-tech-debt, R7).
|
|
2920
|
+
* @module @tomowang/dsh-tui/tui/listWindow
|
|
2921
|
+
*/
|
|
2914
2922
|
/**
|
|
2915
|
-
*
|
|
2916
|
-
*
|
|
2917
|
-
*
|
|
2918
|
-
*
|
|
2919
|
-
*
|
|
2923
|
+
* Rows available for a scrollable list body: terminal height minus the lines
|
|
2924
|
+
* every such screen spends on chrome (header/hint/notice/search — `chrome`
|
|
2925
|
+
* lines, caller-counted since it varies by screen). Long catalogs (~35
|
|
2926
|
+
* providers, matching that many models) previously rendered every row
|
|
2927
|
+
* unconditionally, pushing the key-legend hint line — the only place a
|
|
2928
|
+
* shortcut is documented — past the bottom of the terminal, invisible
|
|
2929
|
+
* without scrolling back. Every list-shaped view windows around the current
|
|
2930
|
+
* selection instead, so the hint line is always the last line printed and
|
|
2931
|
+
* always fits on screen.
|
|
2920
2932
|
*/
|
|
2933
|
+
function listWindow(terminalRows, chrome) {
|
|
2934
|
+
return Math.max(3, terminalRows - chrome);
|
|
2935
|
+
}
|
|
2936
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
2937
|
+
function visibleRange(count, selected, maxVisible) {
|
|
2938
|
+
if (count <= maxVisible) return {
|
|
2939
|
+
start: 0,
|
|
2940
|
+
end: count
|
|
2941
|
+
};
|
|
2942
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
2943
|
+
return {
|
|
2944
|
+
start,
|
|
2945
|
+
end: start + maxVisible
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
//#endregion
|
|
2949
|
+
//#region src/tui/modelProfile/types.ts
|
|
2921
2950
|
/**
|
|
2922
2951
|
* The `apiKeyEnv` reference a provider route falls back to when its settings
|
|
2923
2952
|
* profile names none, e.g. `my-proxy` -> `MY_PROXY_API_KEY`. Shared between
|
|
@@ -2990,32 +3019,6 @@ var ModelProfileOverlay = class {
|
|
|
2990
3019
|
}
|
|
2991
3020
|
invalidate() {}
|
|
2992
3021
|
/**
|
|
2993
|
-
* Rows available for a scrollable list body: terminal height minus the
|
|
2994
|
-
* lines every such screen spends on chrome (header/hint/notice/search —
|
|
2995
|
-
* `chrome` lines, caller-counted since it varies by screen). Long catalogs
|
|
2996
|
-
* (~35 providers, matching that many models) previously rendered every row
|
|
2997
|
-
* unconditionally, pushing the key-legend hint line — the only place a
|
|
2998
|
-
* shortcut is documented — past the bottom of the terminal, invisible
|
|
2999
|
-
* without scrolling back. Every list-shaped view here windows around the
|
|
3000
|
-
* current selection instead, so the hint line is always the last line
|
|
3001
|
-
* printed and always fits on screen.
|
|
3002
|
-
*/
|
|
3003
|
-
listWindow(chrome) {
|
|
3004
|
-
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3005
|
-
}
|
|
3006
|
-
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3007
|
-
visibleRange(count, selected, maxVisible) {
|
|
3008
|
-
if (count <= maxVisible) return {
|
|
3009
|
-
start: 0,
|
|
3010
|
-
end: count
|
|
3011
|
-
};
|
|
3012
|
-
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3013
|
-
return {
|
|
3014
|
-
start,
|
|
3015
|
-
end: start + maxVisible
|
|
3016
|
-
};
|
|
3017
|
-
}
|
|
3018
|
-
/**
|
|
3019
3022
|
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3020
3023
|
* `/model` runs as a full-screen overlay that paints over the whole
|
|
3021
3024
|
* terminal, so the notice dock underneath — where "Saved X."/"Active model
|
|
@@ -3115,8 +3118,8 @@ var ModelProfileOverlay = class {
|
|
|
3115
3118
|
const all = this.pickerItems(mp);
|
|
3116
3119
|
const filtered = this.filteredPickerItems(mp);
|
|
3117
3120
|
const chrome = lines.length + 2;
|
|
3118
|
-
const maxVisible = this.
|
|
3119
|
-
const { start, end } =
|
|
3121
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3122
|
+
const { start, end } = visibleRange(filtered.length, this.modelPickerCursor, maxVisible);
|
|
3120
3123
|
filtered.slice(start, end).forEach((item, offset) => {
|
|
3121
3124
|
const isSelected = start + offset === this.modelPickerCursor;
|
|
3122
3125
|
const via = item.authMethod === "oauth" ? " oauth" : item.authMethod === "api-key" ? " api" : "";
|
|
@@ -3175,8 +3178,8 @@ var ModelProfileOverlay = class {
|
|
|
3175
3178
|
if (busy && providers === void 0) lines.push(muted$10("Loading…"));
|
|
3176
3179
|
const filtered = this.filteredProviders(mp);
|
|
3177
3180
|
const chrome = lines.length + 2;
|
|
3178
|
-
const maxVisible = this.
|
|
3179
|
-
const { start, end } =
|
|
3181
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3182
|
+
const { start, end } = visibleRange(filtered.length, this.providerCursor, maxVisible);
|
|
3180
3183
|
filtered.slice(start, end).forEach((row, offset) => {
|
|
3181
3184
|
const index = start + offset;
|
|
3182
3185
|
const marker = row.configured ? "● " : "○ ";
|
|
@@ -3441,13 +3444,10 @@ var LoginOverlay = class {
|
|
|
3441
3444
|
tui;
|
|
3442
3445
|
store;
|
|
3443
3446
|
actions;
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
autoSkipChecked = false;
|
|
3449
|
-
listCursor = 0;
|
|
3450
|
-
searchQuery = emptyMiniTextField();
|
|
3447
|
+
view = {
|
|
3448
|
+
kind: "authType",
|
|
3449
|
+
cursor: 0
|
|
3450
|
+
};
|
|
3451
3451
|
promptField = emptyMiniTextField();
|
|
3452
3452
|
promptCursor = 0;
|
|
3453
3453
|
constructor(tui, store, actions) {
|
|
@@ -3456,22 +3456,6 @@ var LoginOverlay = class {
|
|
|
3456
3456
|
this.actions = actions;
|
|
3457
3457
|
}
|
|
3458
3458
|
invalidate() {}
|
|
3459
|
-
/** See `ModelProfileOverlay.listWindow` — same overflow bug, same fix: a long provider list (~35 catalog entries) must never push the key-legend hint line past the bottom of the terminal. */
|
|
3460
|
-
listWindow(chrome) {
|
|
3461
|
-
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3462
|
-
}
|
|
3463
|
-
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3464
|
-
visibleRange(count, selected, maxVisible) {
|
|
3465
|
-
if (count <= maxVisible) return {
|
|
3466
|
-
start: 0,
|
|
3467
|
-
end: count
|
|
3468
|
-
};
|
|
3469
|
-
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3470
|
-
return {
|
|
3471
|
-
start,
|
|
3472
|
-
end: start + maxVisible
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
3459
|
/**
|
|
3476
3460
|
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3477
3461
|
* `/login` runs as a full-screen overlay that paints over the whole
|
|
@@ -3485,56 +3469,80 @@ var LoginOverlay = class {
|
|
|
3485
3469
|
const notice = this.store.getSnapshot().notice;
|
|
3486
3470
|
return notice === void 0 ? [] : [muted$9(notice)];
|
|
3487
3471
|
}
|
|
3488
|
-
/**
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3472
|
+
/**
|
|
3473
|
+
* Pure: given the loaded flows, which single method (if any) they all
|
|
3474
|
+
* share — the chooser has nothing to offer when only one method type is
|
|
3475
|
+
* registered at all. Recomputed fresh every call (no cached/latched
|
|
3476
|
+
* result), so a later-refreshed flow set is always re-evaluated instead of
|
|
3477
|
+
* replaying a stale decision (specs/001-…, R6).
|
|
3478
|
+
*/
|
|
3479
|
+
autoSkipAuthType(login) {
|
|
3480
|
+
if (login.flows === void 0) return void 0;
|
|
3492
3481
|
const hasOAuth = login.flows.some((flow) => flow.methods.some((method) => method.id === "oauth"));
|
|
3493
|
-
|
|
3494
|
-
this.authType = hasOAuth ? "oauth" : "api-key";
|
|
3495
|
-
this.step = "list";
|
|
3496
|
-
this.chooserSkipped = true;
|
|
3482
|
+
return hasOAuth === login.flows.some((flow) => flow.methods.some((method) => method.id === "api-key")) ? void 0 : hasOAuth ? "oauth" : "api-key";
|
|
3497
3483
|
}
|
|
3498
|
-
|
|
3484
|
+
/**
|
|
3485
|
+
* Pure: the view to actually render/operate against. `this.view` stays the
|
|
3486
|
+
* untouched `authType` default until the user explicitly chooses (Enter on
|
|
3487
|
+
* the chooser) — until then, this derives the auto-skipped `list` view on
|
|
3488
|
+
* every call instead of mutating `this.view` from inside `render()`. The
|
|
3489
|
+
* first `handleInput` mutation against a derived (not-yet-explicit) `list`
|
|
3490
|
+
* view reassigns `this.view` directly (see `handleProviderListInput`), so
|
|
3491
|
+
* cursor/search state persists across renders once the user interacts.
|
|
3492
|
+
*/
|
|
3493
|
+
effectiveView(login) {
|
|
3494
|
+
if (this.view.kind === "authType") {
|
|
3495
|
+
const auto = this.autoSkipAuthType(login);
|
|
3496
|
+
if (auto !== void 0) return {
|
|
3497
|
+
kind: "list",
|
|
3498
|
+
authType: auto,
|
|
3499
|
+
chooserSkipped: true,
|
|
3500
|
+
cursor: 0,
|
|
3501
|
+
searchQuery: emptyMiniTextField()
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
return this.view;
|
|
3505
|
+
}
|
|
3506
|
+
filteredFlows(login, view) {
|
|
3499
3507
|
const all = login.flows ?? [];
|
|
3500
|
-
const byType =
|
|
3501
|
-
const query =
|
|
3508
|
+
const byType = view.authType === void 0 ? all : all.filter((flow) => flow.methods.some((method) => method.id === view.authType));
|
|
3509
|
+
const query = view.searchQuery.value.trim();
|
|
3502
3510
|
return query === "" ? byType : fuzzyFilter([...byType], query, (flow) => flow.label);
|
|
3503
3511
|
}
|
|
3504
|
-
renderAuthTypeChooser(login) {
|
|
3512
|
+
renderAuthTypeChooser(login, view) {
|
|
3505
3513
|
const lines = [bold$8(secondary$8("Select authentication method"))];
|
|
3506
3514
|
lines.push(...this.noticeLines());
|
|
3507
3515
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3508
3516
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3509
3517
|
AUTH_TYPES.forEach((type, index) => {
|
|
3510
|
-
const text = `${index ===
|
|
3511
|
-
lines.push(index ===
|
|
3518
|
+
const text = `${index === view.cursor ? "› " : " "}${AUTH_TYPE_LABELS[type]}`;
|
|
3519
|
+
lines.push(index === view.cursor ? invert$4(text) : text);
|
|
3512
3520
|
});
|
|
3513
3521
|
lines.push(muted$9("↑↓ select · enter continue · esc close"));
|
|
3514
3522
|
return lines;
|
|
3515
3523
|
}
|
|
3516
|
-
renderProviderList(login) {
|
|
3524
|
+
renderProviderList(login, view) {
|
|
3517
3525
|
const lines = [bold$8(secondary$8("Select provider to configure:"))];
|
|
3518
3526
|
lines.push(...this.noticeLines());
|
|
3519
3527
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3520
|
-
lines.push(`> ${renderMiniTextField(
|
|
3528
|
+
lines.push(`> ${renderMiniTextField(view.searchQuery, true)}`);
|
|
3521
3529
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3522
|
-
const flows = this.filteredFlows(login);
|
|
3530
|
+
const flows = this.filteredFlows(login, view);
|
|
3523
3531
|
const chrome = lines.length + 2;
|
|
3524
|
-
const maxVisible = this.
|
|
3525
|
-
const { start, end } =
|
|
3532
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3533
|
+
const { start, end } = visibleRange(flows.length, view.cursor, maxVisible);
|
|
3526
3534
|
flows.slice(start, end).forEach((flow, offset) => {
|
|
3527
3535
|
const index = start + offset;
|
|
3528
3536
|
const marker = flow.inFlight ? "· " : flow.configured ? "✓ " : "○ ";
|
|
3529
3537
|
const signingIn = login.signingIn === flow.key ? " — signing in…" : "";
|
|
3530
3538
|
const via = flow.authMethod === "oauth" ? " [oauth]" : flow.authMethod === "api-key" ? " [api]" : "";
|
|
3531
3539
|
const readyNote = login.signingIn !== flow.key && flow.configured ? ` — ready to use${via}` : "";
|
|
3532
|
-
const text = `${index ===
|
|
3533
|
-
lines.push(index ===
|
|
3540
|
+
const text = `${index === view.cursor ? "› " : " "}${marker}${flow.label}${signingIn}${readyNote}`;
|
|
3541
|
+
lines.push(index === view.cursor ? invert$4(text) : flow.configured ? successColor(text) : text);
|
|
3534
3542
|
});
|
|
3535
|
-
if (flows.length > maxVisible) lines.push(muted$9(`(${
|
|
3543
|
+
if (flows.length > maxVisible) lines.push(muted$9(`(${view.cursor + 1}/${flows.length})`));
|
|
3536
3544
|
if (login.flows !== void 0 && flows.length === 0) lines.push(muted$9("No matching providers."));
|
|
3537
|
-
const back =
|
|
3545
|
+
const back = view.chooserSkipped ? "esc close" : "esc back";
|
|
3538
3546
|
lines.push(muted$9(`type to search · ↑↓ select · enter sign in (or edit key/models if already configured) · ctrl+p add custom provider · ${back}`));
|
|
3539
3547
|
return lines;
|
|
3540
3548
|
}
|
|
@@ -3565,8 +3573,8 @@ var LoginOverlay = class {
|
|
|
3565
3573
|
if (overlay.kind !== "login") return [];
|
|
3566
3574
|
const { login } = overlay;
|
|
3567
3575
|
if (login.prompt !== void 0) return this.renderPrompt(login.prompt);
|
|
3568
|
-
this.
|
|
3569
|
-
return
|
|
3576
|
+
const view = this.effectiveView(login);
|
|
3577
|
+
return view.kind === "authType" ? this.renderAuthTypeChooser(login, view) : this.renderProviderList(login, view);
|
|
3570
3578
|
}
|
|
3571
3579
|
handleInput(data) {
|
|
3572
3580
|
const overlay = this.store.getSnapshot().overlay;
|
|
@@ -3576,8 +3584,9 @@ var LoginOverlay = class {
|
|
|
3576
3584
|
this.handlePromptInput(data, login.prompt);
|
|
3577
3585
|
return;
|
|
3578
3586
|
}
|
|
3579
|
-
|
|
3580
|
-
|
|
3587
|
+
const view = this.effectiveView(login);
|
|
3588
|
+
if (view.kind === "authType") this.handleAuthTypeChooserInput(data, view);
|
|
3589
|
+
else this.handleProviderListInput(data, login, view);
|
|
3581
3590
|
}
|
|
3582
3591
|
handlePromptInput(data, prompt) {
|
|
3583
3592
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
@@ -3603,65 +3612,79 @@ var LoginOverlay = class {
|
|
|
3603
3612
|
const next = miniTextFieldInput(this.promptField, data);
|
|
3604
3613
|
if (next !== void 0) this.promptField = next;
|
|
3605
3614
|
}
|
|
3606
|
-
handleAuthTypeChooserInput(data) {
|
|
3615
|
+
handleAuthTypeChooserInput(data, view) {
|
|
3607
3616
|
if (matchesKey(data, Key.escape)) {
|
|
3608
3617
|
this.actions.closeLogin();
|
|
3609
3618
|
return;
|
|
3610
3619
|
}
|
|
3611
3620
|
if (matchesKey(data, Key.up)) {
|
|
3612
|
-
this.
|
|
3621
|
+
this.view = {
|
|
3622
|
+
kind: "authType",
|
|
3623
|
+
cursor: Math.max(0, view.cursor - 1)
|
|
3624
|
+
};
|
|
3613
3625
|
return;
|
|
3614
3626
|
}
|
|
3615
3627
|
if (matchesKey(data, Key.down)) {
|
|
3616
|
-
this.
|
|
3628
|
+
this.view = {
|
|
3629
|
+
kind: "authType",
|
|
3630
|
+
cursor: Math.min(AUTH_TYPES.length - 1, view.cursor + 1)
|
|
3631
|
+
};
|
|
3617
3632
|
return;
|
|
3618
3633
|
}
|
|
3619
|
-
if (matchesKey(data, Key.enter)) {
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3634
|
+
if (matchesKey(data, Key.enter)) this.view = {
|
|
3635
|
+
kind: "list",
|
|
3636
|
+
authType: AUTH_TYPES[view.cursor],
|
|
3637
|
+
chooserSkipped: false,
|
|
3638
|
+
cursor: 0,
|
|
3639
|
+
searchQuery: emptyMiniTextField()
|
|
3640
|
+
};
|
|
3625
3641
|
}
|
|
3626
|
-
handleProviderListInput(data, login) {
|
|
3642
|
+
handleProviderListInput(data, login, view) {
|
|
3627
3643
|
if (matchesKey(data, Key.escape)) {
|
|
3628
|
-
if (
|
|
3629
|
-
else {
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
}
|
|
3644
|
+
if (view.chooserSkipped) this.actions.closeLogin();
|
|
3645
|
+
else this.view = {
|
|
3646
|
+
kind: "authType",
|
|
3647
|
+
cursor: 0
|
|
3648
|
+
};
|
|
3633
3649
|
return;
|
|
3634
3650
|
}
|
|
3635
3651
|
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3636
3652
|
this.actions.addCustomProvider();
|
|
3637
3653
|
return;
|
|
3638
3654
|
}
|
|
3639
|
-
const flows = this.filteredFlows(login);
|
|
3655
|
+
const flows = this.filteredFlows(login, view);
|
|
3640
3656
|
if (matchesKey(data, Key.up)) {
|
|
3641
|
-
this.
|
|
3657
|
+
this.view = {
|
|
3658
|
+
...view,
|
|
3659
|
+
cursor: Math.max(0, view.cursor - 1)
|
|
3660
|
+
};
|
|
3642
3661
|
return;
|
|
3643
3662
|
}
|
|
3644
3663
|
if (matchesKey(data, Key.down)) {
|
|
3645
|
-
this.
|
|
3664
|
+
this.view = {
|
|
3665
|
+
...view,
|
|
3666
|
+
cursor: Math.min(Math.max(0, flows.length - 1), view.cursor + 1)
|
|
3667
|
+
};
|
|
3646
3668
|
return;
|
|
3647
3669
|
}
|
|
3648
3670
|
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3649
|
-
const flow = flows[
|
|
3671
|
+
const flow = flows[view.cursor];
|
|
3650
3672
|
if (flow !== void 0 && flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3651
3673
|
return;
|
|
3652
3674
|
}
|
|
3653
3675
|
if (matchesKey(data, Key.enter)) {
|
|
3654
|
-
const flow = flows[
|
|
3676
|
+
const flow = flows[view.cursor];
|
|
3655
3677
|
if (flow === void 0) return;
|
|
3656
3678
|
if (flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3657
|
-
else this.actions.beginAuthorization(flow.key,
|
|
3679
|
+
else this.actions.beginAuthorization(flow.key, view.authType);
|
|
3658
3680
|
return;
|
|
3659
3681
|
}
|
|
3660
|
-
const next = miniTextFieldInput(
|
|
3661
|
-
if (next !== void 0) {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3682
|
+
const next = miniTextFieldInput(view.searchQuery, data);
|
|
3683
|
+
if (next !== void 0) this.view = {
|
|
3684
|
+
...view,
|
|
3685
|
+
searchQuery: next,
|
|
3686
|
+
cursor: 0
|
|
3687
|
+
};
|
|
3665
3688
|
}
|
|
3666
3689
|
};
|
|
3667
3690
|
//#endregion
|
|
@@ -4988,7 +5011,7 @@ var TuiApp = class {
|
|
|
4988
5011
|
gap: 1,
|
|
4989
5012
|
align: "start"
|
|
4990
5013
|
});
|
|
4991
|
-
const buildStamp = `build
|
|
5014
|
+
const buildStamp = `build a980b38 · ${(/* @__PURE__ */ new Date(BUILD_TIME)).toLocaleTimeString()}`;
|
|
4992
5015
|
const sessionInfo = new DynamicText(() => {
|
|
4993
5016
|
const { provider, model, cwd } = options;
|
|
4994
5017
|
return [
|
|
@@ -5241,6 +5264,8 @@ const ACRYL_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
|
5241
5264
|
* TUI shows the same capabilities the web surface does. `/clear` flushes the
|
|
5242
5265
|
* current session and re-attaches a fresh one (durable history stays on disk).
|
|
5243
5266
|
*/
|
|
5267
|
+
/** The credential-record scope `dsh-llm-pi-ai` writes a `/login` sign-in under (`credentialKey('llm-pi-ai', providerId)`). */
|
|
5268
|
+
const LOGIN_RECORD_SCOPE = "llm-pi-ai";
|
|
5244
5269
|
const TUI_VERSION = ACRYL_VERSION;
|
|
5245
5270
|
const PROMPT_HISTORY_LIMIT = 200;
|
|
5246
5271
|
function failUnknown(status) {
|
|
@@ -5283,14 +5308,16 @@ function getAtPath(value, path) {
|
|
|
5283
5308
|
return current;
|
|
5284
5309
|
}
|
|
5285
5310
|
/**
|
|
5286
|
-
* `dsh-llm-pi-ai`'s own credential record
|
|
5287
|
-
* i.e. `"llm-pi-ai/<providerId>"`).
|
|
5288
|
-
*
|
|
5289
|
-
*
|
|
5290
|
-
*
|
|
5311
|
+
* `dsh-llm-pi-ai`'s own credential record key under `LOGIN_RECORD_SCOPE`
|
|
5312
|
+
* (`credentialKey('llm-pi-ai', providerId)`, i.e. `"llm-pi-ai/<providerId>"`).
|
|
5313
|
+
* A `/login` sign-in (OAuth or API key) writes here through pi-ai's own
|
|
5314
|
+
* `CredentialStore`, entirely separate from the `apiKeyEnv` reference a
|
|
5315
|
+
* manually configured provider's settings profile points at.
|
|
5316
|
+
* `acryl-control`'s `CredentialProjection` computes this internally for the
|
|
5317
|
+
* read side; only `clearApiKey`'s explicit delete still needs it directly.
|
|
5291
5318
|
*/
|
|
5292
|
-
function
|
|
5293
|
-
return
|
|
5319
|
+
function loginRecordKey(providerId) {
|
|
5320
|
+
return `${LOGIN_RECORD_SCOPE}/${providerId}`;
|
|
5294
5321
|
}
|
|
5295
5322
|
/**
|
|
5296
5323
|
* Wait out `ctx.settings`' asynchronous watcher queue after a write.
|
|
@@ -5373,46 +5400,45 @@ async function attachSession(host, resumeId) {
|
|
|
5373
5400
|
const agent = host.ctx.agents?.get?.(SessionId(id));
|
|
5374
5401
|
const session = agent?.session;
|
|
5375
5402
|
const history = [];
|
|
5376
|
-
|
|
5403
|
+
function credentialProjectionServices() {
|
|
5377
5404
|
const settingsSvc = host.ctx.get("settings");
|
|
5378
5405
|
const credentialsSvc = host.ctx.get("credentials");
|
|
5379
5406
|
const llmSvc = host.ctx.get("llm");
|
|
5380
5407
|
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0) return void 0;
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
}
|
|
5412
|
-
return rows;
|
|
5408
|
+
return {
|
|
5409
|
+
settings: settingsSvc,
|
|
5410
|
+
credentials: credentialsSvc,
|
|
5411
|
+
llm: llmSvc
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
async function credentialRows() {
|
|
5415
|
+
const services = credentialProjectionServices();
|
|
5416
|
+
if (services === void 0) return void 0;
|
|
5417
|
+
return computeCredentialProjection(services, {
|
|
5418
|
+
deriveApiKeyRef,
|
|
5419
|
+
loginRecordScope: LOGIN_RECORD_SCOPE
|
|
5420
|
+
});
|
|
5421
|
+
}
|
|
5422
|
+
/** Map the shared `CredentialProjectionRow` onto `/model`'s own `ProviderRow` presentation shape. */
|
|
5423
|
+
function toProviderRow(row) {
|
|
5424
|
+
return {
|
|
5425
|
+
route: row.route,
|
|
5426
|
+
displayName: row.displayName,
|
|
5427
|
+
settingsNs: row.settingsNs,
|
|
5428
|
+
settingsPath: row.settingsPath,
|
|
5429
|
+
configured: row.hasSettingsProfile,
|
|
5430
|
+
live: row.isLive,
|
|
5431
|
+
api: row.api,
|
|
5432
|
+
baseURL: row.baseURL,
|
|
5433
|
+
apiKeyRef: row.apiKeyRef,
|
|
5434
|
+
apiKeyConfigured: row.hasCredential,
|
|
5435
|
+
authMethod: row.authMethod,
|
|
5436
|
+
models: row.models,
|
|
5437
|
+
revision: row.revision
|
|
5438
|
+
};
|
|
5413
5439
|
}
|
|
5414
5440
|
async function loadProviders() {
|
|
5415
|
-
const rows = await
|
|
5441
|
+
const rows = await credentialRows();
|
|
5416
5442
|
if (rows === void 0) {
|
|
5417
5443
|
store.updateModelProfile({
|
|
5418
5444
|
providers: [],
|
|
@@ -5421,8 +5447,9 @@ async function attachSession(host, resumeId) {
|
|
|
5421
5447
|
});
|
|
5422
5448
|
return;
|
|
5423
5449
|
}
|
|
5450
|
+
const providers = rows.map(toProviderRow);
|
|
5424
5451
|
store.updateModelProfile({
|
|
5425
|
-
providers
|
|
5452
|
+
providers,
|
|
5426
5453
|
busy: false,
|
|
5427
5454
|
error: void 0,
|
|
5428
5455
|
selected: 0
|
|
@@ -5466,31 +5493,19 @@ async function attachSession(host, resumeId) {
|
|
|
5466
5493
|
} });
|
|
5467
5494
|
})();
|
|
5468
5495
|
}
|
|
5469
|
-
async
|
|
5496
|
+
const routeActivationPort = { async ensureRouteActivated(providerId) {
|
|
5470
5497
|
const settingsSvc = host.ctx.get("settings");
|
|
5471
5498
|
const llmSvc = host.ctx.get("llm");
|
|
5472
|
-
if (settingsSvc === void 0 || llmSvc === void 0) return
|
|
5499
|
+
if (settingsSvc === void 0 || llmSvc === void 0) return;
|
|
5473
5500
|
const entry = llmSvc.listConfigurableProviders().find((candidate) => candidate.provider === providerId);
|
|
5474
|
-
if (entry === void 0) return
|
|
5501
|
+
if (entry === void 0) return;
|
|
5475
5502
|
const descriptor = settingsSvc.describe({ redactSecrets: true }).find((candidate) => candidate.ns === entry.settingsNs);
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
if (existing !== void 0) {
|
|
5479
|
-
if (method === "oauth" && existing.displayName !== oauthName) {
|
|
5480
|
-
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, { displayName: oauthName }), descriptor?.revision);
|
|
5481
|
-
await flushSettingsWatchers();
|
|
5482
|
-
return true;
|
|
5483
|
-
}
|
|
5484
|
-
return false;
|
|
5485
|
-
}
|
|
5486
|
-
const section = method === "oauth" ? { displayName: oauthName } : {};
|
|
5487
|
-
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, section), descriptor?.revision);
|
|
5503
|
+
if ((descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath)) !== void 0) return;
|
|
5504
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, {}), descriptor?.revision);
|
|
5488
5505
|
await flushSettingsWatchers();
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
async function loadAuthorizationFlows() {
|
|
5506
|
+
} };
|
|
5507
|
+
async function loadLoginFlows() {
|
|
5492
5508
|
const authSvc = host.ctx.get("authorization");
|
|
5493
|
-
const credentialsSvc = host.ctx.get("credentials");
|
|
5494
5509
|
if (authSvc === void 0) {
|
|
5495
5510
|
store.updateLogin({
|
|
5496
5511
|
flows: [],
|
|
@@ -5501,27 +5516,21 @@ async function attachSession(host, resumeId) {
|
|
|
5501
5516
|
}
|
|
5502
5517
|
try {
|
|
5503
5518
|
const entries = authSvc.list();
|
|
5504
|
-
const
|
|
5505
|
-
|
|
5519
|
+
const rows = await credentialRows();
|
|
5520
|
+
const byKey = new Map((rows ?? []).map((row) => [loginRecordKey(row.route), row]));
|
|
5521
|
+
const list = entries.map((entry) => {
|
|
5522
|
+
const row = byKey.get(entry.key);
|
|
5506
5523
|
return {
|
|
5507
5524
|
...entry,
|
|
5508
|
-
configured:
|
|
5509
|
-
authMethod:
|
|
5525
|
+
configured: row?.hasCredential ?? false,
|
|
5526
|
+
authMethod: row?.authMethod
|
|
5510
5527
|
};
|
|
5511
|
-
})
|
|
5528
|
+
});
|
|
5512
5529
|
store.updateLogin({
|
|
5513
5530
|
flows: list,
|
|
5514
5531
|
busy: false,
|
|
5515
5532
|
error: void 0
|
|
5516
5533
|
});
|
|
5517
|
-
const toRepair = list.filter((flow) => flow.configured && flow.authMethod === "oauth" && flow.key.startsWith("llm-pi-ai/"));
|
|
5518
|
-
if (toRepair.length > 0) (async () => {
|
|
5519
|
-
let repaired = false;
|
|
5520
|
-
for (const flow of toRepair) try {
|
|
5521
|
-
if (await ensureProviderActivated(flow.key.slice(10), "oauth")) repaired = true;
|
|
5522
|
-
} catch {}
|
|
5523
|
-
if (repaired) refreshCredentialState();
|
|
5524
|
-
})();
|
|
5525
5534
|
} catch (error) {
|
|
5526
5535
|
store.updateLogin({
|
|
5527
5536
|
busy: false,
|
|
@@ -5529,9 +5538,9 @@ async function attachSession(host, resumeId) {
|
|
|
5529
5538
|
});
|
|
5530
5539
|
}
|
|
5531
5540
|
}
|
|
5532
|
-
function
|
|
5541
|
+
function syncCredentialViews() {
|
|
5533
5542
|
loadProviders();
|
|
5534
|
-
|
|
5543
|
+
loadLoginFlows();
|
|
5535
5544
|
}
|
|
5536
5545
|
/** Open `/model`'s blank custom-provider draft — assumes `/model` is (or is about to become) the open overlay. Shared by `createProvider` (already there) and `addCustomProvider` (getting there first). */
|
|
5537
5546
|
function openCustomProviderDraft() {
|
|
@@ -5716,7 +5725,7 @@ async function attachSession(host, resumeId) {
|
|
|
5716
5725
|
},
|
|
5717
5726
|
login() {
|
|
5718
5727
|
store.openLogin();
|
|
5719
|
-
|
|
5728
|
+
loadLoginFlows();
|
|
5720
5729
|
},
|
|
5721
5730
|
closeLogin() {
|
|
5722
5731
|
store.closeOverlay();
|
|
@@ -5777,16 +5786,18 @@ async function attachSession(host, resumeId) {
|
|
|
5777
5786
|
});
|
|
5778
5787
|
}
|
|
5779
5788
|
};
|
|
5789
|
+
const authorizationService = new AuthorizationService({
|
|
5790
|
+
authorization: authSvc,
|
|
5791
|
+
routeActivation: routeActivationPort,
|
|
5792
|
+
onCredentialChanged: () => syncCredentialViews()
|
|
5793
|
+
});
|
|
5780
5794
|
try {
|
|
5781
|
-
if ((await
|
|
5795
|
+
if ((await authorizationService.begin({
|
|
5782
5796
|
key,
|
|
5783
5797
|
method,
|
|
5784
5798
|
interaction
|
|
5785
|
-
})).status === "authorized") {
|
|
5786
|
-
|
|
5787
|
-
if (key.startsWith("llm-pi-ai/")) await ensureProviderActivated(key.slice(10), method);
|
|
5788
|
-
refreshCredentialState();
|
|
5789
|
-
} else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5799
|
+
})).status === "authorized") store.setNotice(`Signed in to ${flow.label} — credentials saved, ready to use from /model.`);
|
|
5800
|
+
else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5790
5801
|
} catch (error) {
|
|
5791
5802
|
store.updateLogin({ error: `Sign-in failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
5792
5803
|
} finally {
|
|
@@ -5831,7 +5842,7 @@ async function attachSession(host, resumeId) {
|
|
|
5831
5842
|
},
|
|
5832
5843
|
backToProviderList() {
|
|
5833
5844
|
store.updateModelProfile({ view: "list" });
|
|
5834
|
-
|
|
5845
|
+
syncCredentialViews();
|
|
5835
5846
|
},
|
|
5836
5847
|
createProvider() {
|
|
5837
5848
|
openCustomProviderDraft();
|
|
@@ -5851,14 +5862,15 @@ async function attachSession(host, resumeId) {
|
|
|
5851
5862
|
openProviderEditor(route) {
|
|
5852
5863
|
(async () => {
|
|
5853
5864
|
store.openModelProfile();
|
|
5854
|
-
const rows = await
|
|
5865
|
+
const rows = await credentialRows();
|
|
5866
|
+
const providers = rows?.map(toProviderRow);
|
|
5855
5867
|
store.updateModelProfile({
|
|
5856
|
-
providers:
|
|
5868
|
+
providers: providers ?? [],
|
|
5857
5869
|
busy: false,
|
|
5858
5870
|
error: rows === void 0 ? "Model provider settings are not available in this profile." : void 0,
|
|
5859
5871
|
selected: 0
|
|
5860
5872
|
});
|
|
5861
|
-
const row =
|
|
5873
|
+
const row = providers?.find((entry) => entry.route === route);
|
|
5862
5874
|
if (row === void 0) {
|
|
5863
5875
|
store.setNotice(`Provider "${route}" not found.`);
|
|
5864
5876
|
return;
|
|
@@ -5892,7 +5904,7 @@ async function attachSession(host, resumeId) {
|
|
|
5892
5904
|
await flushSettingsWatchers();
|
|
5893
5905
|
store.setNotice(`Saved ${draft.displayName || draft.route} — credentials stored, ready to use from /model.`);
|
|
5894
5906
|
store.updateModelProfile({ view: "list" });
|
|
5895
|
-
|
|
5907
|
+
syncCredentialViews();
|
|
5896
5908
|
} catch (error) {
|
|
5897
5909
|
store.setNotice(`save failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5898
5910
|
}
|
|
@@ -5911,7 +5923,7 @@ async function attachSession(host, resumeId) {
|
|
|
5911
5923
|
await settingsSvc.update(row.settingsNs, nestAtPath(row.settingsPath, {}), row.revision);
|
|
5912
5924
|
await flushSettingsWatchers();
|
|
5913
5925
|
store.setNotice(`Removed ${row.displayName}.`);
|
|
5914
|
-
|
|
5926
|
+
syncCredentialViews();
|
|
5915
5927
|
} catch (error) {
|
|
5916
5928
|
store.setNotice(`delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5917
5929
|
}
|
|
@@ -5926,10 +5938,10 @@ async function attachSession(host, resumeId) {
|
|
|
5926
5938
|
}
|
|
5927
5939
|
try {
|
|
5928
5940
|
await credentialsSvc.unset(draft.apiKeyRef);
|
|
5929
|
-
await credentialsSvc.deleteRecord?.(
|
|
5941
|
+
await credentialsSvc.deleteRecord?.(loginRecordKey(draft.route));
|
|
5930
5942
|
store.setNotice(`Removed the API key for ${draft.displayName || draft.route}.`);
|
|
5931
5943
|
store.updateModelProfile({ view: "list" });
|
|
5932
|
-
|
|
5944
|
+
syncCredentialViews();
|
|
5933
5945
|
} catch (error) {
|
|
5934
5946
|
store.setNotice(`Could not remove the key: ${error instanceof Error ? error.message : String(error)}`);
|
|
5935
5947
|
}
|
|
@@ -6223,7 +6235,7 @@ async function runAcryl(args, supplied = {}) {
|
|
|
6223
6235
|
return;
|
|
6224
6236
|
}
|
|
6225
6237
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
6226
|
-
dependencies.write("acryl-
|
|
6238
|
+
dependencies.write("acryl-cli: stdin and stdout must both be TTYs; use `acryl tui --json` for a headless probe");
|
|
6227
6239
|
dependencies.exit(1);
|
|
6228
6240
|
return;
|
|
6229
6241
|
}
|