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/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { bootAcrylHarnessProfile, createAcrylSessionBridge } from "acryl-harness-runtime";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
|
+
import { AuthorizationService, computeCredentialProjection } from "acryl-control";
|
|
5
6
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
7
|
import { diffLines } from "diff";
|
|
7
8
|
import { Container, Editor, HStack, Key, KeybindingsManager, ProcessTerminal, ScrollView, TUI_KEYBINDINGS, Text, TuiAltScreen, VStack, fuzzyFilter, matchesKey, setKeybindings, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
@@ -1121,7 +1122,7 @@ const ACRYL_MARK_ROWS = [
|
|
|
1121
1122
|
" ▀ "
|
|
1122
1123
|
];
|
|
1123
1124
|
/** ISO timestamp this build was produced at. */
|
|
1124
|
-
const BUILD_TIME = "2026-09-
|
|
1125
|
+
const BUILD_TIME = "2026-09-08T18:16:12.207Z";
|
|
1125
1126
|
//#endregion
|
|
1126
1127
|
//#region src/tui/statsFormat.ts
|
|
1127
1128
|
/**
|
|
@@ -2959,14 +2960,42 @@ function renderMiniTextField(state, cursorVisible, mask) {
|
|
|
2959
2960
|
return `${display.slice(0, state.cursor)}\x1b[7m${display[state.cursor] ?? " "}\x1b[0m${display.slice(state.cursor + 1)}`;
|
|
2960
2961
|
}
|
|
2961
2962
|
//#endregion
|
|
2962
|
-
//#region src/tui/
|
|
2963
|
+
//#region src/tui/listWindow.ts
|
|
2964
|
+
/**
|
|
2965
|
+
* Scrollable-list windowing shared by every overlay that paints a
|
|
2966
|
+
* selection-following list (`/login`'s provider list, `/model`'s picker and
|
|
2967
|
+
* provider list). Previously duplicated verbatim in `LoginOverlay.ts` and
|
|
2968
|
+
* `ModelProfileOverlay.ts` (specs/001-acryl-refactor-improvements-and-tech-debt, R7).
|
|
2969
|
+
* @module @tomowang/dsh-tui/tui/listWindow
|
|
2970
|
+
*/
|
|
2963
2971
|
/**
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
*
|
|
2967
|
-
*
|
|
2968
|
-
*
|
|
2972
|
+
* Rows available for a scrollable list body: terminal height minus the lines
|
|
2973
|
+
* every such screen spends on chrome (header/hint/notice/search — `chrome`
|
|
2974
|
+
* lines, caller-counted since it varies by screen). Long catalogs (~35
|
|
2975
|
+
* providers, matching that many models) previously rendered every row
|
|
2976
|
+
* unconditionally, pushing the key-legend hint line — the only place a
|
|
2977
|
+
* shortcut is documented — past the bottom of the terminal, invisible
|
|
2978
|
+
* without scrolling back. Every list-shaped view windows around the current
|
|
2979
|
+
* selection instead, so the hint line is always the last line printed and
|
|
2980
|
+
* always fits on screen.
|
|
2969
2981
|
*/
|
|
2982
|
+
function listWindow(terminalRows, chrome) {
|
|
2983
|
+
return Math.max(3, terminalRows - chrome);
|
|
2984
|
+
}
|
|
2985
|
+
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
2986
|
+
function visibleRange(count, selected, maxVisible) {
|
|
2987
|
+
if (count <= maxVisible) return {
|
|
2988
|
+
start: 0,
|
|
2989
|
+
end: count
|
|
2990
|
+
};
|
|
2991
|
+
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
2992
|
+
return {
|
|
2993
|
+
start,
|
|
2994
|
+
end: start + maxVisible
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
//#endregion
|
|
2998
|
+
//#region src/tui/modelProfile/types.ts
|
|
2970
2999
|
/**
|
|
2971
3000
|
* The `apiKeyEnv` reference a provider route falls back to when its settings
|
|
2972
3001
|
* profile names none, e.g. `my-proxy` -> `MY_PROXY_API_KEY`. Shared between
|
|
@@ -3039,32 +3068,6 @@ var ModelProfileOverlay = class {
|
|
|
3039
3068
|
}
|
|
3040
3069
|
invalidate() {}
|
|
3041
3070
|
/**
|
|
3042
|
-
* Rows available for a scrollable list body: terminal height minus the
|
|
3043
|
-
* lines every such screen spends on chrome (header/hint/notice/search —
|
|
3044
|
-
* `chrome` lines, caller-counted since it varies by screen). Long catalogs
|
|
3045
|
-
* (~35 providers, matching that many models) previously rendered every row
|
|
3046
|
-
* unconditionally, pushing the key-legend hint line — the only place a
|
|
3047
|
-
* shortcut is documented — past the bottom of the terminal, invisible
|
|
3048
|
-
* without scrolling back. Every list-shaped view here windows around the
|
|
3049
|
-
* current selection instead, so the hint line is always the last line
|
|
3050
|
-
* printed and always fits on screen.
|
|
3051
|
-
*/
|
|
3052
|
-
listWindow(chrome) {
|
|
3053
|
-
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3054
|
-
}
|
|
3055
|
-
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3056
|
-
visibleRange(count, selected, maxVisible) {
|
|
3057
|
-
if (count <= maxVisible) return {
|
|
3058
|
-
start: 0,
|
|
3059
|
-
end: count
|
|
3060
|
-
};
|
|
3061
|
-
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3062
|
-
return {
|
|
3063
|
-
start,
|
|
3064
|
-
end: start + maxVisible
|
|
3065
|
-
};
|
|
3066
|
-
}
|
|
3067
|
-
/**
|
|
3068
3071
|
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3069
3072
|
* `/model` runs as a full-screen overlay that paints over the whole
|
|
3070
3073
|
* terminal, so the notice dock underneath — where "Saved X."/"Active model
|
|
@@ -3164,8 +3167,8 @@ var ModelProfileOverlay = class {
|
|
|
3164
3167
|
const all = this.pickerItems(mp);
|
|
3165
3168
|
const filtered = this.filteredPickerItems(mp);
|
|
3166
3169
|
const chrome = lines.length + 2;
|
|
3167
|
-
const maxVisible = this.
|
|
3168
|
-
const { start, end } =
|
|
3170
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3171
|
+
const { start, end } = visibleRange(filtered.length, this.modelPickerCursor, maxVisible);
|
|
3169
3172
|
filtered.slice(start, end).forEach((item, offset) => {
|
|
3170
3173
|
const isSelected = start + offset === this.modelPickerCursor;
|
|
3171
3174
|
const via = item.authMethod === "oauth" ? " oauth" : item.authMethod === "api-key" ? " api" : "";
|
|
@@ -3224,8 +3227,8 @@ var ModelProfileOverlay = class {
|
|
|
3224
3227
|
if (busy && providers === void 0) lines.push(muted$10("Loading…"));
|
|
3225
3228
|
const filtered = this.filteredProviders(mp);
|
|
3226
3229
|
const chrome = lines.length + 2;
|
|
3227
|
-
const maxVisible = this.
|
|
3228
|
-
const { start, end } =
|
|
3230
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3231
|
+
const { start, end } = visibleRange(filtered.length, this.providerCursor, maxVisible);
|
|
3229
3232
|
filtered.slice(start, end).forEach((row, offset) => {
|
|
3230
3233
|
const index = start + offset;
|
|
3231
3234
|
const marker = row.configured ? "● " : "○ ";
|
|
@@ -3490,13 +3493,10 @@ var LoginOverlay = class {
|
|
|
3490
3493
|
tui;
|
|
3491
3494
|
store;
|
|
3492
3495
|
actions;
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
autoSkipChecked = false;
|
|
3498
|
-
listCursor = 0;
|
|
3499
|
-
searchQuery = emptyMiniTextField();
|
|
3496
|
+
view = {
|
|
3497
|
+
kind: "authType",
|
|
3498
|
+
cursor: 0
|
|
3499
|
+
};
|
|
3500
3500
|
promptField = emptyMiniTextField();
|
|
3501
3501
|
promptCursor = 0;
|
|
3502
3502
|
constructor(tui, store, actions) {
|
|
@@ -3505,22 +3505,6 @@ var LoginOverlay = class {
|
|
|
3505
3505
|
this.actions = actions;
|
|
3506
3506
|
}
|
|
3507
3507
|
invalidate() {}
|
|
3508
|
-
/** 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. */
|
|
3509
|
-
listWindow(chrome) {
|
|
3510
|
-
return Math.max(3, this.tui.terminal.rows - chrome);
|
|
3511
|
-
}
|
|
3512
|
-
/** The `[start, end)` slice of `count` items to show so `selected` stays visible within `maxVisible` rows, biased to keep it centered. */
|
|
3513
|
-
visibleRange(count, selected, maxVisible) {
|
|
3514
|
-
if (count <= maxVisible) return {
|
|
3515
|
-
start: 0,
|
|
3516
|
-
end: count
|
|
3517
|
-
};
|
|
3518
|
-
const start = Math.max(0, Math.min(selected - Math.floor(maxVisible / 2), count - maxVisible));
|
|
3519
|
-
return {
|
|
3520
|
-
start,
|
|
3521
|
-
end: start + maxVisible
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
3508
|
/**
|
|
3525
3509
|
* The global one-line notice (`store.setNotice`), rendered inline here.
|
|
3526
3510
|
* `/login` runs as a full-screen overlay that paints over the whole
|
|
@@ -3534,56 +3518,80 @@ var LoginOverlay = class {
|
|
|
3534
3518
|
const notice = this.store.getSnapshot().notice;
|
|
3535
3519
|
return notice === void 0 ? [] : [muted$9(notice)];
|
|
3536
3520
|
}
|
|
3537
|
-
/**
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3521
|
+
/**
|
|
3522
|
+
* Pure: given the loaded flows, which single method (if any) they all
|
|
3523
|
+
* share — the chooser has nothing to offer when only one method type is
|
|
3524
|
+
* registered at all. Recomputed fresh every call (no cached/latched
|
|
3525
|
+
* result), so a later-refreshed flow set is always re-evaluated instead of
|
|
3526
|
+
* replaying a stale decision (specs/001-…, R6).
|
|
3527
|
+
*/
|
|
3528
|
+
autoSkipAuthType(login) {
|
|
3529
|
+
if (login.flows === void 0) return void 0;
|
|
3541
3530
|
const hasOAuth = login.flows.some((flow) => flow.methods.some((method) => method.id === "oauth"));
|
|
3542
|
-
|
|
3543
|
-
this.authType = hasOAuth ? "oauth" : "api-key";
|
|
3544
|
-
this.step = "list";
|
|
3545
|
-
this.chooserSkipped = true;
|
|
3531
|
+
return hasOAuth === login.flows.some((flow) => flow.methods.some((method) => method.id === "api-key")) ? void 0 : hasOAuth ? "oauth" : "api-key";
|
|
3546
3532
|
}
|
|
3547
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* Pure: the view to actually render/operate against. `this.view` stays the
|
|
3535
|
+
* untouched `authType` default until the user explicitly chooses (Enter on
|
|
3536
|
+
* the chooser) — until then, this derives the auto-skipped `list` view on
|
|
3537
|
+
* every call instead of mutating `this.view` from inside `render()`. The
|
|
3538
|
+
* first `handleInput` mutation against a derived (not-yet-explicit) `list`
|
|
3539
|
+
* view reassigns `this.view` directly (see `handleProviderListInput`), so
|
|
3540
|
+
* cursor/search state persists across renders once the user interacts.
|
|
3541
|
+
*/
|
|
3542
|
+
effectiveView(login) {
|
|
3543
|
+
if (this.view.kind === "authType") {
|
|
3544
|
+
const auto = this.autoSkipAuthType(login);
|
|
3545
|
+
if (auto !== void 0) return {
|
|
3546
|
+
kind: "list",
|
|
3547
|
+
authType: auto,
|
|
3548
|
+
chooserSkipped: true,
|
|
3549
|
+
cursor: 0,
|
|
3550
|
+
searchQuery: emptyMiniTextField()
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
return this.view;
|
|
3554
|
+
}
|
|
3555
|
+
filteredFlows(login, view) {
|
|
3548
3556
|
const all = login.flows ?? [];
|
|
3549
|
-
const byType =
|
|
3550
|
-
const query =
|
|
3557
|
+
const byType = view.authType === void 0 ? all : all.filter((flow) => flow.methods.some((method) => method.id === view.authType));
|
|
3558
|
+
const query = view.searchQuery.value.trim();
|
|
3551
3559
|
return query === "" ? byType : fuzzyFilter([...byType], query, (flow) => flow.label);
|
|
3552
3560
|
}
|
|
3553
|
-
renderAuthTypeChooser(login) {
|
|
3561
|
+
renderAuthTypeChooser(login, view) {
|
|
3554
3562
|
const lines = [bold$8(secondary$8("Select authentication method"))];
|
|
3555
3563
|
lines.push(...this.noticeLines());
|
|
3556
3564
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3557
3565
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3558
3566
|
AUTH_TYPES.forEach((type, index) => {
|
|
3559
|
-
const text = `${index ===
|
|
3560
|
-
lines.push(index ===
|
|
3567
|
+
const text = `${index === view.cursor ? "› " : " "}${AUTH_TYPE_LABELS[type]}`;
|
|
3568
|
+
lines.push(index === view.cursor ? invert$4(text) : text);
|
|
3561
3569
|
});
|
|
3562
3570
|
lines.push(muted$9("↑↓ select · enter continue · esc close"));
|
|
3563
3571
|
return lines;
|
|
3564
3572
|
}
|
|
3565
|
-
renderProviderList(login) {
|
|
3573
|
+
renderProviderList(login, view) {
|
|
3566
3574
|
const lines = [bold$8(secondary$8("Select provider to configure:"))];
|
|
3567
3575
|
lines.push(...this.noticeLines());
|
|
3568
3576
|
if (login.error !== void 0) lines.push(errorColor$4(login.error));
|
|
3569
|
-
lines.push(`> ${renderMiniTextField(
|
|
3577
|
+
lines.push(`> ${renderMiniTextField(view.searchQuery, true)}`);
|
|
3570
3578
|
if (login.busy && login.flows === void 0) lines.push(muted$9("Loading…"));
|
|
3571
|
-
const flows = this.filteredFlows(login);
|
|
3579
|
+
const flows = this.filteredFlows(login, view);
|
|
3572
3580
|
const chrome = lines.length + 2;
|
|
3573
|
-
const maxVisible = this.
|
|
3574
|
-
const { start, end } =
|
|
3581
|
+
const maxVisible = listWindow(this.tui.terminal.rows, chrome);
|
|
3582
|
+
const { start, end } = visibleRange(flows.length, view.cursor, maxVisible);
|
|
3575
3583
|
flows.slice(start, end).forEach((flow, offset) => {
|
|
3576
3584
|
const index = start + offset;
|
|
3577
3585
|
const marker = flow.inFlight ? "· " : flow.configured ? "✓ " : "○ ";
|
|
3578
3586
|
const signingIn = login.signingIn === flow.key ? " — signing in…" : "";
|
|
3579
3587
|
const via = flow.authMethod === "oauth" ? " [oauth]" : flow.authMethod === "api-key" ? " [api]" : "";
|
|
3580
3588
|
const readyNote = login.signingIn !== flow.key && flow.configured ? ` — ready to use${via}` : "";
|
|
3581
|
-
const text = `${index ===
|
|
3582
|
-
lines.push(index ===
|
|
3589
|
+
const text = `${index === view.cursor ? "› " : " "}${marker}${flow.label}${signingIn}${readyNote}`;
|
|
3590
|
+
lines.push(index === view.cursor ? invert$4(text) : flow.configured ? successColor(text) : text);
|
|
3583
3591
|
});
|
|
3584
|
-
if (flows.length > maxVisible) lines.push(muted$9(`(${
|
|
3592
|
+
if (flows.length > maxVisible) lines.push(muted$9(`(${view.cursor + 1}/${flows.length})`));
|
|
3585
3593
|
if (login.flows !== void 0 && flows.length === 0) lines.push(muted$9("No matching providers."));
|
|
3586
|
-
const back =
|
|
3594
|
+
const back = view.chooserSkipped ? "esc close" : "esc back";
|
|
3587
3595
|
lines.push(muted$9(`type to search · ↑↓ select · enter sign in (or edit key/models if already configured) · ctrl+p add custom provider · ${back}`));
|
|
3588
3596
|
return lines;
|
|
3589
3597
|
}
|
|
@@ -3614,8 +3622,8 @@ var LoginOverlay = class {
|
|
|
3614
3622
|
if (overlay.kind !== "login") return [];
|
|
3615
3623
|
const { login } = overlay;
|
|
3616
3624
|
if (login.prompt !== void 0) return this.renderPrompt(login.prompt);
|
|
3617
|
-
this.
|
|
3618
|
-
return
|
|
3625
|
+
const view = this.effectiveView(login);
|
|
3626
|
+
return view.kind === "authType" ? this.renderAuthTypeChooser(login, view) : this.renderProviderList(login, view);
|
|
3619
3627
|
}
|
|
3620
3628
|
handleInput(data) {
|
|
3621
3629
|
const overlay = this.store.getSnapshot().overlay;
|
|
@@ -3625,8 +3633,9 @@ var LoginOverlay = class {
|
|
|
3625
3633
|
this.handlePromptInput(data, login.prompt);
|
|
3626
3634
|
return;
|
|
3627
3635
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3636
|
+
const view = this.effectiveView(login);
|
|
3637
|
+
if (view.kind === "authType") this.handleAuthTypeChooserInput(data, view);
|
|
3638
|
+
else this.handleProviderListInput(data, login, view);
|
|
3630
3639
|
}
|
|
3631
3640
|
handlePromptInput(data, prompt) {
|
|
3632
3641
|
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
@@ -3652,65 +3661,79 @@ var LoginOverlay = class {
|
|
|
3652
3661
|
const next = miniTextFieldInput(this.promptField, data);
|
|
3653
3662
|
if (next !== void 0) this.promptField = next;
|
|
3654
3663
|
}
|
|
3655
|
-
handleAuthTypeChooserInput(data) {
|
|
3664
|
+
handleAuthTypeChooserInput(data, view) {
|
|
3656
3665
|
if (matchesKey(data, Key.escape)) {
|
|
3657
3666
|
this.actions.closeLogin();
|
|
3658
3667
|
return;
|
|
3659
3668
|
}
|
|
3660
3669
|
if (matchesKey(data, Key.up)) {
|
|
3661
|
-
this.
|
|
3670
|
+
this.view = {
|
|
3671
|
+
kind: "authType",
|
|
3672
|
+
cursor: Math.max(0, view.cursor - 1)
|
|
3673
|
+
};
|
|
3662
3674
|
return;
|
|
3663
3675
|
}
|
|
3664
3676
|
if (matchesKey(data, Key.down)) {
|
|
3665
|
-
this.
|
|
3677
|
+
this.view = {
|
|
3678
|
+
kind: "authType",
|
|
3679
|
+
cursor: Math.min(AUTH_TYPES.length - 1, view.cursor + 1)
|
|
3680
|
+
};
|
|
3666
3681
|
return;
|
|
3667
3682
|
}
|
|
3668
|
-
if (matchesKey(data, Key.enter)) {
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3683
|
+
if (matchesKey(data, Key.enter)) this.view = {
|
|
3684
|
+
kind: "list",
|
|
3685
|
+
authType: AUTH_TYPES[view.cursor],
|
|
3686
|
+
chooserSkipped: false,
|
|
3687
|
+
cursor: 0,
|
|
3688
|
+
searchQuery: emptyMiniTextField()
|
|
3689
|
+
};
|
|
3674
3690
|
}
|
|
3675
|
-
handleProviderListInput(data, login) {
|
|
3691
|
+
handleProviderListInput(data, login, view) {
|
|
3676
3692
|
if (matchesKey(data, Key.escape)) {
|
|
3677
|
-
if (
|
|
3678
|
-
else {
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
}
|
|
3693
|
+
if (view.chooserSkipped) this.actions.closeLogin();
|
|
3694
|
+
else this.view = {
|
|
3695
|
+
kind: "authType",
|
|
3696
|
+
cursor: 0
|
|
3697
|
+
};
|
|
3682
3698
|
return;
|
|
3683
3699
|
}
|
|
3684
3700
|
if (matchesKey(data, Key.ctrl("p"))) {
|
|
3685
3701
|
this.actions.addCustomProvider();
|
|
3686
3702
|
return;
|
|
3687
3703
|
}
|
|
3688
|
-
const flows = this.filteredFlows(login);
|
|
3704
|
+
const flows = this.filteredFlows(login, view);
|
|
3689
3705
|
if (matchesKey(data, Key.up)) {
|
|
3690
|
-
this.
|
|
3706
|
+
this.view = {
|
|
3707
|
+
...view,
|
|
3708
|
+
cursor: Math.max(0, view.cursor - 1)
|
|
3709
|
+
};
|
|
3691
3710
|
return;
|
|
3692
3711
|
}
|
|
3693
3712
|
if (matchesKey(data, Key.down)) {
|
|
3694
|
-
this.
|
|
3713
|
+
this.view = {
|
|
3714
|
+
...view,
|
|
3715
|
+
cursor: Math.min(Math.max(0, flows.length - 1), view.cursor + 1)
|
|
3716
|
+
};
|
|
3695
3717
|
return;
|
|
3696
3718
|
}
|
|
3697
3719
|
if (matchesKey(data, Key.ctrl("e"))) {
|
|
3698
|
-
const flow = flows[
|
|
3720
|
+
const flow = flows[view.cursor];
|
|
3699
3721
|
if (flow !== void 0 && flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3700
3722
|
return;
|
|
3701
3723
|
}
|
|
3702
3724
|
if (matchesKey(data, Key.enter)) {
|
|
3703
|
-
const flow = flows[
|
|
3725
|
+
const flow = flows[view.cursor];
|
|
3704
3726
|
if (flow === void 0) return;
|
|
3705
3727
|
if (flow.configured && flow.key.startsWith("llm-pi-ai/")) this.actions.openProviderEditor(flow.key.slice(10));
|
|
3706
|
-
else this.actions.beginAuthorization(flow.key,
|
|
3728
|
+
else this.actions.beginAuthorization(flow.key, view.authType);
|
|
3707
3729
|
return;
|
|
3708
3730
|
}
|
|
3709
|
-
const next = miniTextFieldInput(
|
|
3710
|
-
if (next !== void 0) {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3731
|
+
const next = miniTextFieldInput(view.searchQuery, data);
|
|
3732
|
+
if (next !== void 0) this.view = {
|
|
3733
|
+
...view,
|
|
3734
|
+
searchQuery: next,
|
|
3735
|
+
cursor: 0
|
|
3736
|
+
};
|
|
3714
3737
|
}
|
|
3715
3738
|
};
|
|
3716
3739
|
//#endregion
|
|
@@ -5037,7 +5060,7 @@ var TuiApp = class {
|
|
|
5037
5060
|
gap: 1,
|
|
5038
5061
|
align: "start"
|
|
5039
5062
|
});
|
|
5040
|
-
const buildStamp = `build
|
|
5063
|
+
const buildStamp = `build a980b38 · ${(/* @__PURE__ */ new Date(BUILD_TIME)).toLocaleTimeString()}`;
|
|
5041
5064
|
const sessionInfo = new DynamicText(() => {
|
|
5042
5065
|
const { provider, model, cwd } = options;
|
|
5043
5066
|
return [
|
|
@@ -5276,6 +5299,8 @@ async function walkDirectory(cwd) {
|
|
|
5276
5299
|
}
|
|
5277
5300
|
return results;
|
|
5278
5301
|
}
|
|
5302
|
+
/** Canonical ACRYL version string used by the CLI and the release smoke checks. */
|
|
5303
|
+
const ACRYL_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
5279
5304
|
//#endregion
|
|
5280
5305
|
//#region src/tui-app/session.ts
|
|
5281
5306
|
/**
|
|
@@ -5288,7 +5313,9 @@ async function walkDirectory(cwd) {
|
|
|
5288
5313
|
* TUI shows the same capabilities the web surface does. `/clear` flushes the
|
|
5289
5314
|
* current session and re-attaches a fresh one (durable history stays on disk).
|
|
5290
5315
|
*/
|
|
5291
|
-
|
|
5316
|
+
/** The credential-record scope `dsh-llm-pi-ai` writes a `/login` sign-in under (`credentialKey('llm-pi-ai', providerId)`). */
|
|
5317
|
+
const LOGIN_RECORD_SCOPE = "llm-pi-ai";
|
|
5318
|
+
const TUI_VERSION = ACRYL_VERSION;
|
|
5292
5319
|
const PROMPT_HISTORY_LIMIT = 200;
|
|
5293
5320
|
function failUnknown(status) {
|
|
5294
5321
|
return status === "running" ? "running" : "idle";
|
|
@@ -5330,14 +5357,16 @@ function getAtPath(value, path) {
|
|
|
5330
5357
|
return current;
|
|
5331
5358
|
}
|
|
5332
5359
|
/**
|
|
5333
|
-
* `dsh-llm-pi-ai`'s own credential record
|
|
5334
|
-
* i.e. `"llm-pi-ai/<providerId>"`).
|
|
5335
|
-
*
|
|
5336
|
-
*
|
|
5337
|
-
*
|
|
5360
|
+
* `dsh-llm-pi-ai`'s own credential record key under `LOGIN_RECORD_SCOPE`
|
|
5361
|
+
* (`credentialKey('llm-pi-ai', providerId)`, i.e. `"llm-pi-ai/<providerId>"`).
|
|
5362
|
+
* A `/login` sign-in (OAuth or API key) writes here through pi-ai's own
|
|
5363
|
+
* `CredentialStore`, entirely separate from the `apiKeyEnv` reference a
|
|
5364
|
+
* manually configured provider's settings profile points at.
|
|
5365
|
+
* `acryl-control`'s `CredentialProjection` computes this internally for the
|
|
5366
|
+
* read side; only `clearApiKey`'s explicit delete still needs it directly.
|
|
5338
5367
|
*/
|
|
5339
|
-
function
|
|
5340
|
-
return
|
|
5368
|
+
function loginRecordKey(providerId) {
|
|
5369
|
+
return `${LOGIN_RECORD_SCOPE}/${providerId}`;
|
|
5341
5370
|
}
|
|
5342
5371
|
/**
|
|
5343
5372
|
* Wait out `ctx.settings`' asynchronous watcher queue after a write.
|
|
@@ -5420,46 +5449,45 @@ async function attachSession(host, resumeId) {
|
|
|
5420
5449
|
const agent = host.ctx.agents?.get?.(SessionId(id));
|
|
5421
5450
|
const session = agent?.session;
|
|
5422
5451
|
const history = [];
|
|
5423
|
-
|
|
5452
|
+
function credentialProjectionServices() {
|
|
5424
5453
|
const settingsSvc = host.ctx.get("settings");
|
|
5425
5454
|
const credentialsSvc = host.ctx.get("credentials");
|
|
5426
5455
|
const llmSvc = host.ctx.get("llm");
|
|
5427
5456
|
if (settingsSvc === void 0 || credentialsSvc === void 0 || llmSvc === void 0) return void 0;
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
}
|
|
5459
|
-
return rows;
|
|
5457
|
+
return {
|
|
5458
|
+
settings: settingsSvc,
|
|
5459
|
+
credentials: credentialsSvc,
|
|
5460
|
+
llm: llmSvc
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
async function credentialRows() {
|
|
5464
|
+
const services = credentialProjectionServices();
|
|
5465
|
+
if (services === void 0) return void 0;
|
|
5466
|
+
return computeCredentialProjection(services, {
|
|
5467
|
+
deriveApiKeyRef,
|
|
5468
|
+
loginRecordScope: LOGIN_RECORD_SCOPE
|
|
5469
|
+
});
|
|
5470
|
+
}
|
|
5471
|
+
/** Map the shared `CredentialProjectionRow` onto `/model`'s own `ProviderRow` presentation shape. */
|
|
5472
|
+
function toProviderRow(row) {
|
|
5473
|
+
return {
|
|
5474
|
+
route: row.route,
|
|
5475
|
+
displayName: row.displayName,
|
|
5476
|
+
settingsNs: row.settingsNs,
|
|
5477
|
+
settingsPath: row.settingsPath,
|
|
5478
|
+
configured: row.hasSettingsProfile,
|
|
5479
|
+
live: row.isLive,
|
|
5480
|
+
api: row.api,
|
|
5481
|
+
baseURL: row.baseURL,
|
|
5482
|
+
apiKeyRef: row.apiKeyRef,
|
|
5483
|
+
apiKeyConfigured: row.hasCredential,
|
|
5484
|
+
authMethod: row.authMethod,
|
|
5485
|
+
models: row.models,
|
|
5486
|
+
revision: row.revision
|
|
5487
|
+
};
|
|
5460
5488
|
}
|
|
5461
5489
|
async function loadProviders() {
|
|
5462
|
-
const rows = await
|
|
5490
|
+
const rows = await credentialRows();
|
|
5463
5491
|
if (rows === void 0) {
|
|
5464
5492
|
store.updateModelProfile({
|
|
5465
5493
|
providers: [],
|
|
@@ -5468,8 +5496,9 @@ async function attachSession(host, resumeId) {
|
|
|
5468
5496
|
});
|
|
5469
5497
|
return;
|
|
5470
5498
|
}
|
|
5499
|
+
const providers = rows.map(toProviderRow);
|
|
5471
5500
|
store.updateModelProfile({
|
|
5472
|
-
providers
|
|
5501
|
+
providers,
|
|
5473
5502
|
busy: false,
|
|
5474
5503
|
error: void 0,
|
|
5475
5504
|
selected: 0
|
|
@@ -5513,31 +5542,19 @@ async function attachSession(host, resumeId) {
|
|
|
5513
5542
|
} });
|
|
5514
5543
|
})();
|
|
5515
5544
|
}
|
|
5516
|
-
async
|
|
5545
|
+
const routeActivationPort = { async ensureRouteActivated(providerId) {
|
|
5517
5546
|
const settingsSvc = host.ctx.get("settings");
|
|
5518
5547
|
const llmSvc = host.ctx.get("llm");
|
|
5519
|
-
if (settingsSvc === void 0 || llmSvc === void 0) return
|
|
5548
|
+
if (settingsSvc === void 0 || llmSvc === void 0) return;
|
|
5520
5549
|
const entry = llmSvc.listConfigurableProviders().find((candidate) => candidate.provider === providerId);
|
|
5521
|
-
if (entry === void 0) return
|
|
5550
|
+
if (entry === void 0) return;
|
|
5522
5551
|
const descriptor = settingsSvc.describe({ redactSecrets: true }).find((candidate) => candidate.ns === entry.settingsNs);
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
if (existing !== void 0) {
|
|
5526
|
-
if (method === "oauth" && existing.displayName !== oauthName) {
|
|
5527
|
-
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, { displayName: oauthName }), descriptor?.revision);
|
|
5528
|
-
await flushSettingsWatchers();
|
|
5529
|
-
return true;
|
|
5530
|
-
}
|
|
5531
|
-
return false;
|
|
5532
|
-
}
|
|
5533
|
-
const section = method === "oauth" ? { displayName: oauthName } : {};
|
|
5534
|
-
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, section), descriptor?.revision);
|
|
5552
|
+
if ((descriptor === void 0 ? void 0 : getAtPath(descriptor.user, entry.settingsPath)) !== void 0) return;
|
|
5553
|
+
await settingsSvc.update(entry.settingsNs, nestAtPath(entry.settingsPath, {}), descriptor?.revision);
|
|
5535
5554
|
await flushSettingsWatchers();
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
async function loadAuthorizationFlows() {
|
|
5555
|
+
} };
|
|
5556
|
+
async function loadLoginFlows() {
|
|
5539
5557
|
const authSvc = host.ctx.get("authorization");
|
|
5540
|
-
const credentialsSvc = host.ctx.get("credentials");
|
|
5541
5558
|
if (authSvc === void 0) {
|
|
5542
5559
|
store.updateLogin({
|
|
5543
5560
|
flows: [],
|
|
@@ -5548,27 +5565,21 @@ async function attachSession(host, resumeId) {
|
|
|
5548
5565
|
}
|
|
5549
5566
|
try {
|
|
5550
5567
|
const entries = authSvc.list();
|
|
5551
|
-
const
|
|
5552
|
-
|
|
5568
|
+
const rows = await credentialRows();
|
|
5569
|
+
const byKey = new Map((rows ?? []).map((row) => [loginRecordKey(row.route), row]));
|
|
5570
|
+
const list = entries.map((entry) => {
|
|
5571
|
+
const row = byKey.get(entry.key);
|
|
5553
5572
|
return {
|
|
5554
5573
|
...entry,
|
|
5555
|
-
configured:
|
|
5556
|
-
authMethod:
|
|
5574
|
+
configured: row?.hasCredential ?? false,
|
|
5575
|
+
authMethod: row?.authMethod
|
|
5557
5576
|
};
|
|
5558
|
-
})
|
|
5577
|
+
});
|
|
5559
5578
|
store.updateLogin({
|
|
5560
5579
|
flows: list,
|
|
5561
5580
|
busy: false,
|
|
5562
5581
|
error: void 0
|
|
5563
5582
|
});
|
|
5564
|
-
const toRepair = list.filter((flow) => flow.configured && flow.authMethod === "oauth" && flow.key.startsWith("llm-pi-ai/"));
|
|
5565
|
-
if (toRepair.length > 0) (async () => {
|
|
5566
|
-
let repaired = false;
|
|
5567
|
-
for (const flow of toRepair) try {
|
|
5568
|
-
if (await ensureProviderActivated(flow.key.slice(10), "oauth")) repaired = true;
|
|
5569
|
-
} catch {}
|
|
5570
|
-
if (repaired) refreshCredentialState();
|
|
5571
|
-
})();
|
|
5572
5583
|
} catch (error) {
|
|
5573
5584
|
store.updateLogin({
|
|
5574
5585
|
busy: false,
|
|
@@ -5576,9 +5587,9 @@ async function attachSession(host, resumeId) {
|
|
|
5576
5587
|
});
|
|
5577
5588
|
}
|
|
5578
5589
|
}
|
|
5579
|
-
function
|
|
5590
|
+
function syncCredentialViews() {
|
|
5580
5591
|
loadProviders();
|
|
5581
|
-
|
|
5592
|
+
loadLoginFlows();
|
|
5582
5593
|
}
|
|
5583
5594
|
/** 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). */
|
|
5584
5595
|
function openCustomProviderDraft() {
|
|
@@ -5763,7 +5774,7 @@ async function attachSession(host, resumeId) {
|
|
|
5763
5774
|
},
|
|
5764
5775
|
login() {
|
|
5765
5776
|
store.openLogin();
|
|
5766
|
-
|
|
5777
|
+
loadLoginFlows();
|
|
5767
5778
|
},
|
|
5768
5779
|
closeLogin() {
|
|
5769
5780
|
store.closeOverlay();
|
|
@@ -5824,16 +5835,18 @@ async function attachSession(host, resumeId) {
|
|
|
5824
5835
|
});
|
|
5825
5836
|
}
|
|
5826
5837
|
};
|
|
5838
|
+
const authorizationService = new AuthorizationService({
|
|
5839
|
+
authorization: authSvc,
|
|
5840
|
+
routeActivation: routeActivationPort,
|
|
5841
|
+
onCredentialChanged: () => syncCredentialViews()
|
|
5842
|
+
});
|
|
5827
5843
|
try {
|
|
5828
|
-
if ((await
|
|
5844
|
+
if ((await authorizationService.begin({
|
|
5829
5845
|
key,
|
|
5830
5846
|
method,
|
|
5831
5847
|
interaction
|
|
5832
|
-
})).status === "authorized") {
|
|
5833
|
-
|
|
5834
|
-
if (key.startsWith("llm-pi-ai/")) await ensureProviderActivated(key.slice(10), method);
|
|
5835
|
-
refreshCredentialState();
|
|
5836
|
-
} else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5848
|
+
})).status === "authorized") store.setNotice(`Signed in to ${flow.label} — credentials saved, ready to use from /model.`);
|
|
5849
|
+
else store.updateLogin({ error: "Sign-in cancelled." });
|
|
5837
5850
|
} catch (error) {
|
|
5838
5851
|
store.updateLogin({ error: `Sign-in failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
5839
5852
|
} finally {
|
|
@@ -5878,7 +5891,7 @@ async function attachSession(host, resumeId) {
|
|
|
5878
5891
|
},
|
|
5879
5892
|
backToProviderList() {
|
|
5880
5893
|
store.updateModelProfile({ view: "list" });
|
|
5881
|
-
|
|
5894
|
+
syncCredentialViews();
|
|
5882
5895
|
},
|
|
5883
5896
|
createProvider() {
|
|
5884
5897
|
openCustomProviderDraft();
|
|
@@ -5898,14 +5911,15 @@ async function attachSession(host, resumeId) {
|
|
|
5898
5911
|
openProviderEditor(route) {
|
|
5899
5912
|
(async () => {
|
|
5900
5913
|
store.openModelProfile();
|
|
5901
|
-
const rows = await
|
|
5914
|
+
const rows = await credentialRows();
|
|
5915
|
+
const providers = rows?.map(toProviderRow);
|
|
5902
5916
|
store.updateModelProfile({
|
|
5903
|
-
providers:
|
|
5917
|
+
providers: providers ?? [],
|
|
5904
5918
|
busy: false,
|
|
5905
5919
|
error: rows === void 0 ? "Model provider settings are not available in this profile." : void 0,
|
|
5906
5920
|
selected: 0
|
|
5907
5921
|
});
|
|
5908
|
-
const row =
|
|
5922
|
+
const row = providers?.find((entry) => entry.route === route);
|
|
5909
5923
|
if (row === void 0) {
|
|
5910
5924
|
store.setNotice(`Provider "${route}" not found.`);
|
|
5911
5925
|
return;
|
|
@@ -5939,7 +5953,7 @@ async function attachSession(host, resumeId) {
|
|
|
5939
5953
|
await flushSettingsWatchers();
|
|
5940
5954
|
store.setNotice(`Saved ${draft.displayName || draft.route} — credentials stored, ready to use from /model.`);
|
|
5941
5955
|
store.updateModelProfile({ view: "list" });
|
|
5942
|
-
|
|
5956
|
+
syncCredentialViews();
|
|
5943
5957
|
} catch (error) {
|
|
5944
5958
|
store.setNotice(`save failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5945
5959
|
}
|
|
@@ -5958,7 +5972,7 @@ async function attachSession(host, resumeId) {
|
|
|
5958
5972
|
await settingsSvc.update(row.settingsNs, nestAtPath(row.settingsPath, {}), row.revision);
|
|
5959
5973
|
await flushSettingsWatchers();
|
|
5960
5974
|
store.setNotice(`Removed ${row.displayName}.`);
|
|
5961
|
-
|
|
5975
|
+
syncCredentialViews();
|
|
5962
5976
|
} catch (error) {
|
|
5963
5977
|
store.setNotice(`delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
5964
5978
|
}
|
|
@@ -5973,10 +5987,10 @@ async function attachSession(host, resumeId) {
|
|
|
5973
5987
|
}
|
|
5974
5988
|
try {
|
|
5975
5989
|
await credentialsSvc.unset(draft.apiKeyRef);
|
|
5976
|
-
await credentialsSvc.deleteRecord?.(
|
|
5990
|
+
await credentialsSvc.deleteRecord?.(loginRecordKey(draft.route));
|
|
5977
5991
|
store.setNotice(`Removed the API key for ${draft.displayName || draft.route}.`);
|
|
5978
5992
|
store.updateModelProfile({ view: "list" });
|
|
5979
|
-
|
|
5993
|
+
syncCredentialViews();
|
|
5980
5994
|
} catch (error) {
|
|
5981
5995
|
store.setNotice(`Could not remove the key: ${error instanceof Error ? error.message : String(error)}`);
|
|
5982
5996
|
}
|