@xmoon76/dsh-pi-tui 0.2.2 → 0.3.1

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/README.md CHANGED
@@ -30,7 +30,9 @@ output.
30
30
 
31
31
  ## Prerequisites
32
32
 
33
- - A DeepSeek Harness installation with profiles support (`dsh` on your `PATH`).
33
+ - A DeepSeek Harness installation with profiles support (`dsh` on your `PATH`),
34
+ version `0.1.1-rc.1` or later on the same compat line (the TUI consumes the
35
+ split credential events and the `ctx.authorization` seam of that release).
34
36
  - Node >= 22.19 (`^22.19.0 || >=24`, same range as dsh). Running from source
35
37
  needs Node with native TypeScript support (>= 23.6) or the tsx ESM hook
36
38
  (`node --import tsx/esm`, how dsh's own source launch works).
package/README.zh-CN.md CHANGED
@@ -28,7 +28,9 @@
28
28
 
29
29
  ## 前置要求
30
30
 
31
- - 一个支持 profile 的 DeepSeek Harness 安装(`dsh` 已在 `PATH` 中)。
31
+ - 一个支持 profile 的 DeepSeek Harness 安装(`dsh` 已在 `PATH` 中),版本
32
+ `0.1.1-rc.1` 或同一兼容线上的更新版本——TUI 消费该版本的拆分凭据事件
33
+ 与 `ctx.authorization` 服务。
32
34
  - Node >= 22.19(`^22.19.0 || >=24`,与 dsh 相同范围)。从源码运行需要
33
35
  原生支持 TypeScript 的 Node(>= 23.6)或 tsx ESM 钩子
34
36
  (`node --import tsx/esm`,dsh 自身源码启动的方式)。
package/cordis.patch.yml CHANGED
@@ -26,10 +26,22 @@
26
26
 
27
27
  - id: tui-app
28
28
  name: '@xmoon76/dsh-pi-tui'
29
- inject: [tuiStartup, piTuiExtensions]
29
+ inject: [tuiStartup, piTuiExtensions, authorization]
30
30
  config:
31
31
  sessionId: ''
32
32
 
33
+ # The authorization seam (`ctx.authorization`): plugin-owned credential-
34
+ # obtaining flows, one attempt per key, transport-neutral notices and
35
+ # prompts. The dsh host does NOT mount it anywhere (the base layer ships
36
+ # only `credentials` + `llm-pi-ai`), so the TUI profile inserts the row
37
+ # itself — the TUI is the interactive surface that can answer its
38
+ # prompts. `AuthorizationService` injects `credentials`, so this row
39
+ # activates only after the base's credentials row; llm-pi-ai's
40
+ # provider login flows register once both services are up (it injects
41
+ # `authorization` optionally and degrades without it).
42
+ - id: authorization
43
+ name: '@deepseek-ai/dsh-authorization'
44
+
33
45
  # The agent-preset roster. The SHIPPED root is an assembly fact of this
34
46
  # bundle: the four presets (standard/code/minimal/cordis) travel with the
35
47
  # package under config/agent-presets and carry `system` trust. The
package/dist/builtins.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TUI_STARTUP_SERVICE } from "./startup.mjs";
2
- import { N as PI_TUI_EXTENSIONS_SERVICE } from "./src-BsSlpFFF.mjs";
2
+ import { A as dshVersion, F as PI_TUI_EXTENSIONS_SERVICE } from "./src-ClAKiW0z.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  //#region src/builtins.ts
@@ -27,12 +27,13 @@ function apply(ctx) {
27
27
  if (ctx.get("tuiStartup") === void 0) return;
28
28
  const service = ctx.get(PI_TUI_EXTENSIONS_SERVICE);
29
29
  if (service === void 0) return;
30
+ const installedDsh = dshVersion();
30
31
  service.register("chrome.header.badge", {
31
32
  id: "builtin-version",
32
33
  order: 1e3,
33
- description: "The bundle version (first-party builtin)."
34
+ description: "The dsh and bundle versions (first-party builtin)."
34
35
  }, {
35
- text: `v${packageVersion()}`,
36
+ text: installedDsh === void 0 ? `tui-v${packageVersion()}` : `dsh-${installedDsh} · tui-v${packageVersion()}`,
36
37
  tone: "info"
37
38
  });
38
39
  const counters = service.register("chrome.footer.status", {
@@ -1,3 +1,3 @@
1
1
  import "./startup.mjs";
2
- import { A as apply, F as describeKey, M as name, N as PI_TUI_EXTENSIONS_SERVICE, P as API_VERSION, j as inject } from "./src-BsSlpFFF.mjs";
2
+ import { F as PI_TUI_EXTENSIONS_SERVICE, I as API_VERSION, L as describeKey, M as apply, N as inject, P as name } from "./src-ClAKiW0z.mjs";
3
3
  export { API_VERSION, PI_TUI_EXTENSIONS_SERVICE, apply, describeKey, inject, name };
package/dist/index.d.mts CHANGED
@@ -125,6 +125,16 @@ interface Config {
125
125
  sessionId?: string;
126
126
  }
127
127
  declare const Config: z<Config>;
128
+ /**
129
+ * Wire the credential surface refresh to the dsh 0.1.1-rc.1 credential
130
+ * events. The rc.1 release split the credential update event into the
131
+ * reference half and the durable-record half; both change what the footer
132
+ * model row and the welcome card show (a /login /logout, an external
133
+ * .credentials.yaml edit, or an authorization flow committing a record), so
134
+ * they share one best-effort callback. The callback must not read secrets
135
+ * or mutate providers — it only re-reads describe-level state.
136
+ */
137
+ declare function registerCredentialSurfaceRefresh(ctx: Context, refresh: () => void): void;
128
138
  /**
129
139
  * Slash commands that need no session: before the first user message
130
140
  * (deferred start) they run locally without creating one. Everything else
@@ -498,4 +508,4 @@ declare function recomposeBlank(ctx: Context, agent: {
498
508
  */
499
509
  declare function apply(ctx: Context, config: Config): void;
500
510
  //#endregion
501
- export { AgentComposition, CompactionFold, Config, HOST_COMMAND_CATALOG, InitialCatalogResolution, LOCAL_COMMANDS, PendingSubagentCall, QueueFoldResult, QueueInboxMessage, QueueNoticeSource, RecomposableSession, RecomposeOutcome, ResolveInitialCatalogOptions, SESSIONLESS_COMMANDS, SessionLockRefusedError, apply, busyAfterTurnBoundary, compactingFromLog, composeAgent, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, isPlainExitPrompt, isSubagentSettlementNotice, isUserQueueInput, matchPendingSubagentCall, name, recomposeBlank, recordedPreset, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure };
511
+ export { AgentComposition, CompactionFold, Config, HOST_COMMAND_CATALOG, InitialCatalogResolution, LOCAL_COMMANDS, PendingSubagentCall, QueueFoldResult, QueueInboxMessage, QueueNoticeSource, RecomposableSession, RecomposeOutcome, ResolveInitialCatalogOptions, SESSIONLESS_COMMANDS, SessionLockRefusedError, apply, busyAfterTurnBoundary, compactingFromLog, composeAgent, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, isPlainExitPrompt, isSubagentSettlementNotice, isUserQueueInput, matchPendingSubagentCall, name, recomposeBlank, recordedPreset, registerCredentialSurfaceRefresh, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./startup.mjs";
2
- import { C as runningProfile, D as subagentJobViewHint, E as subagentJobTranscriptId, O as subagentNoticeIsFailure, S as resumeCommand, T as shouldSteerOnEnter, _ as matchPendingSubagentCall, a as SessionLockRefusedError, b as recordedPreset, c as compactingFromLog, d as foldCompactionEvent, f as foldQueueRows, g as isUserQueueInput, h as isSubagentSettlementNotice, i as SESSIONLESS_COMMANDS, k as forkSeed, l as composeAgent, m as isPlainExitPrompt, n as HOST_COMMAND_CATALOG, o as apply, p as inject, r as LOCAL_COMMANDS, s as busyAfterTurnBoundary, t as Config, u as dangerCommand, v as name, w as shouldConsumeAdvertisedMiss, x as resolveInitialCatalog, y as recomposeBlank } from "./src-BsSlpFFF.mjs";
3
- export { Config, HOST_COMMAND_CATALOG, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, SessionLockRefusedError, apply, busyAfterTurnBoundary, compactingFromLog, composeAgent, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, isPlainExitPrompt, isSubagentSettlementNotice, isUserQueueInput, matchPendingSubagentCall, name, recomposeBlank, recordedPreset, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure };
2
+ import { C as resumeCommand, D as subagentJobTranscriptId, E as shouldSteerOnEnter, O as subagentJobViewHint, S as resolveInitialCatalog, T as shouldConsumeAdvertisedMiss, _ as matchPendingSubagentCall, a as SessionLockRefusedError, b as recordedPreset, c as compactingFromLog, d as foldCompactionEvent, f as foldQueueRows, g as isUserQueueInput, h as isSubagentSettlementNotice, i as SESSIONLESS_COMMANDS, j as forkSeed, k as subagentNoticeIsFailure, l as composeAgent, m as isPlainExitPrompt, n as HOST_COMMAND_CATALOG, o as apply, p as inject, r as LOCAL_COMMANDS, s as busyAfterTurnBoundary, t as Config, u as dangerCommand, v as name, w as runningProfile, x as registerCredentialSurfaceRefresh, y as recomposeBlank } from "./src-ClAKiW0z.mjs";
3
+ export { Config, HOST_COMMAND_CATALOG, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, SessionLockRefusedError, apply, busyAfterTurnBoundary, compactingFromLog, composeAgent, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, isPlainExitPrompt, isSubagentSettlementNotice, isUserQueueInput, matchPendingSubagentCall, name, recomposeBlank, recordedPreset, registerCredentialSurfaceRefresh, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure };
@@ -31,6 +31,8 @@ import { fileURLToPath } from "node:url";
31
31
  import { EventEmitter } from "events";
32
32
  import { Service } from "@deepseek-ai/cordis";
33
33
  import { isUserInvocable, renderSkillContent } from "@deepseek-ai/dsh-skill";
34
+ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
35
+ import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
34
36
  //#region src/present.ts
35
37
  /** Figma row titles per variant (design literals, not translatable copy). */
36
38
  const VARIANT_TITLES = {
@@ -14014,6 +14016,11 @@ var QuestionFlow = class {
14014
14016
  if (row.key === OTHER_ROW && this.editingOther) {
14015
14017
  const inputLine = this.otherInput.render(Math.max(1, width - visibleWidth(prefix)))[0] ?? "";
14016
14018
  const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
14019
+ if (question.masked === true && this.otherInput.getValue() !== "") {
14020
+ lines.push(prefix + color.textDim("•".repeat(this.otherInput.getValue().length)));
14021
+ hits.push(OTHER_ROW);
14022
+ continue;
14023
+ }
14017
14024
  lines.push(prefix + (this.otherInput.getValue() === "" ? color.textDim(row.label) : stripped));
14018
14025
  hits.push(OTHER_ROW);
14019
14026
  continue;
@@ -14326,7 +14333,7 @@ var QuestionFlow = class {
14326
14333
  const question = this.questions[qi];
14327
14334
  const draft = this.drafts[qi];
14328
14335
  if (question === void 0 || draft === void 0) continue;
14329
- const value = draft.skipped ? "(skipped)" : draft.custom !== "" && question.multiSelect !== true ? draft.custom : [...draft.selected].join(", ") + (draft.custom !== "" ? ` + ${draft.custom}` : "");
14336
+ const value = draft.skipped ? "(skipped)" : draft.custom !== "" && question.multiSelect !== true ? question.masked === true ? "•".repeat(draft.custom.length) : draft.custom : [...draft.selected].join(", ") + (draft.custom !== "" ? ` + ${draft.custom}` : "");
14330
14337
  reviewBudget = appendWrappedBudgeted(lines, `${color.textDim(`Q${qi + 1}`)} `, " ", question.question, safeWidth, reviewBudget);
14331
14338
  if (reviewBudget > 0) reviewBudget = appendWrappedBudgeted(lines, " ", " ", value === "" ? color.textDim("(no answer)") : value, safeWidth, reviewBudget);
14332
14339
  }
@@ -14387,7 +14394,8 @@ var QuestionFlow = class {
14387
14394
  if (optionless) {
14388
14395
  const inputLine = this.otherInput.render(Math.max(1, safeWidth - 2))[0] ?? "";
14389
14396
  const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
14390
- lines.push(this.otherInput.getValue() === "" ? color.textDim(" Type your answer…") : ` ${stripped}`);
14397
+ if (question.masked === true && this.otherInput.getValue() !== "") lines.push(` ${color.textDim("•".repeat(this.otherInput.getValue().length))}`);
14398
+ else lines.push(this.otherInput.getValue() === "" ? color.textDim(" Type your answer…") : ` ${stripped}`);
14391
14399
  }
14392
14400
  if (draft.skipped) lines.push(color.textDim("(skipped)"));
14393
14401
  lines.push("");
@@ -14513,8 +14521,15 @@ function parseShellWords(line) {
14513
14521
  * the user's own draft.
14514
14522
  * @module @xmoon76/dsh-pi-tui/shell-completion
14515
14523
  */
14516
- /** Hard cap on one compgen spawn (ms). */
14517
- const COMPGEN_TIMEOUT_MS = 300;
14524
+ /**
14525
+ * Hard cap on one compgen spawn (ms). GitHub Actions runners are markedly
14526
+ * slower at a `bash -lc` cold start (measured 260-310ms in CI vs ~60ms
14527
+ * locally — a login shell sources /etc/profile, which is heavy on the
14528
+ * runner image), so a single tight cap made the suite flaky there (three
14529
+ * CI failures, every one at the 300ms boundary). The cap is therefore
14530
+ * raised under GitHub Actions only; local runs keep the tight cap.
14531
+ */
14532
+ const COMPGEN_TIMEOUT_MS = process.env.GITHUB_ACTIONS === "true" ? 1500 : 300;
14518
14533
  /** Command-name cache TTL (ms): command sets change rarely. */
14519
14534
  const COMMAND_CACHE_TTL_MS = 3e4;
14520
14535
  /** Suggestion cap per request (the fork's own lists are capped too). */
@@ -21575,7 +21590,8 @@ var TuiApp = class TuiApp {
21575
21590
  ...question.detail !== void 0 ? { detail: question.detail } : {},
21576
21591
  ...question.options !== void 0 ? { options: question.options } : {},
21577
21592
  ...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
21578
- ...question.intent !== void 0 ? { intent: question.intent } : {}
21593
+ ...question.intent !== void 0 ? { intent: question.intent } : {},
21594
+ ...question.masked !== void 0 ? { masked: question.masked } : {}
21579
21595
  })), (answers) => this.settleQuestions(state, answers), () => this.settleQuestions(state, void 0)),
21580
21596
  suspendedOverlays: /* @__PURE__ */ new Set(),
21581
21597
  resolve,
@@ -25175,6 +25191,7 @@ function providerOptionsFor(entries, readSection) {
25175
25191
  route: "deepseek-official",
25176
25192
  configured: true,
25177
25193
  declared: false,
25194
+ namesCredential: true,
25178
25195
  group: "configured",
25179
25196
  settingsNs: "",
25180
25197
  settingsPath: []
@@ -25186,13 +25203,15 @@ function providerOptionsFor(entries, readSection) {
25186
25203
  const profile = entry.settingsPath.length === 0 ? section : getPath(section, entry.settingsPath);
25187
25204
  const configured = profile !== void 0;
25188
25205
  const namedRef = configured && typeof profile === "object" && profile !== null && typeof profile.apiKeyEnv === "string" ? profile.apiKeyEnv : void 0;
25189
- const ref = namedRef !== void 0 && namedRef !== "" ? namedRef : deriveKeyRef(entry.provider);
25206
+ const namesCredential = namedRef !== void 0 && namedRef !== "";
25207
+ const ref = namesCredential ? namedRef : deriveKeyRef(entry.provider);
25190
25208
  push({
25191
25209
  label: entry.displayName,
25192
25210
  ref,
25193
25211
  route: entry.provider,
25194
25212
  configured,
25195
25213
  declared: entry.declared === true,
25214
+ namesCredential,
25196
25215
  group: configured ? "configured" : entry.declared === true ? "custom" : "available",
25197
25216
  settingsNs: entry.settingsNs,
25198
25217
  settingsPath: [...entry.settingsPath]
@@ -25237,6 +25256,184 @@ function resolveCredentialArg(arg, options) {
25237
25256
  if (route !== void 0) return route.ref;
25238
25257
  if (/^[a-zA-Z][a-zA-Z0-9_]*$/.test(trimmed)) return trimmed.toUpperCase();
25239
25258
  }
25259
+ /** Map the seam's entries to /login targets, deriving the route from the
25260
+ * key's scope (llm-pi-ai flows address the provider route). */
25261
+ function authorizationTargets(entries) {
25262
+ return entries.map((entry) => ({
25263
+ kind: "authorization",
25264
+ route: credentialKeyScope(entry.key) === "llm-pi-ai" ? credentialKeyId(entry.key) : void 0,
25265
+ key: entry.key,
25266
+ label: entry.label,
25267
+ methods: entry.methods,
25268
+ inFlight: entry.inFlight
25269
+ }));
25270
+ }
25271
+ /** The flow target for a provider route, when one is registered. */
25272
+ function flowForRoute(targets, route) {
25273
+ return targets.find((target) => target.route === route);
25274
+ }
25275
+ /**
25276
+ * Merge the reference targets and the authorization flows into the /login
25277
+ * picker set, applying the rc.1 dedup rule per route:
25278
+ *
25279
+ * - explicit `apiKeyEnv` profile → the reference target wins (the flow is
25280
+ * NOT offered as a second default entry for the same route — requests
25281
+ * resolve the reference first and a "successful" flow login would leave
25282
+ * the reference unset, failing the next request);
25283
+ * - keyless route with a flow → the authorization target wins (no derived
25284
+ * `<ROUTE>_API_KEY` entry is offered as a default);
25285
+ * - keyless route without a flow → the derived reference fallback.
25286
+ *
25287
+ * Flows whose key maps no option route (foreign scopes, or catalog routes
25288
+ * the llm directory does not expose) are appended as standalone targets.
25289
+ */
25290
+ function mergeLoginTargets(options, targets) {
25291
+ const flowByRoute = /* @__PURE__ */ new Map();
25292
+ const standalone = [];
25293
+ for (const target of targets) if (target.route !== void 0) flowByRoute.set(target.route, target);
25294
+ else standalone.push(target);
25295
+ const merged = [];
25296
+ const covered = /* @__PURE__ */ new Set();
25297
+ for (const option of options) {
25298
+ const flow = flowByRoute.get(option.route);
25299
+ merged.push(option.namesCredential || flow === void 0 ? {
25300
+ kind: "reference",
25301
+ route: option.route,
25302
+ label: option.label,
25303
+ ref: option.ref,
25304
+ configured: option.configured,
25305
+ declared: option.declared,
25306
+ namesCredential: option.namesCredential
25307
+ } : flow);
25308
+ covered.add(option.route);
25309
+ }
25310
+ for (const target of targets) if (target.route === void 0 || !covered.has(target.route)) merged.push(target);
25311
+ return merged;
25312
+ }
25313
+ /**
25314
+ * The one user-facing notice body: the message first, then the page to open
25315
+ * and the code to enter, each on its own line. Never truncated — the
25316
+ * output panel wraps, so the URL stays copyable. Notices never carry
25317
+ * secrets.
25318
+ */
25319
+ function formatAuthorizationNotice(notice) {
25320
+ const lines = [notice.message];
25321
+ if (notice.url !== void 0 && notice.url !== "") lines.push("", "Open this page to continue:", notice.url);
25322
+ if (notice.code !== void 0 && notice.code !== "") lines.push("", `Code: ${notice.code}`);
25323
+ return lines.join("\n");
25324
+ }
25325
+ /**
25326
+ * Build the interaction half of one authorization attempt. One instance per
25327
+ * attempt: the notice panel is reused across progress notices, and
25328
+ * `close()` hides it when the attempt settles (the caller's finally).
25329
+ *
25330
+ * Prompt mapping:
25331
+ * - `text` and `secret` go through the question flow's free-text row;
25332
+ * `secret` is rendered masked (the real value never leaves the input's
25333
+ * memory and is never logged, put in history, or shown anywhere else);
25334
+ * - `select` goes through the picker and returns the chosen option's `id`,
25335
+ * never its label;
25336
+ * - the user closing the question is a decline → `AuthorizationDeclinedError`;
25337
+ * - the prompt's OWN signal withdrawing it is NOT a decline: the rejection
25338
+ * is passed through as-is so the flow can tell a refusal from a race.
25339
+ */
25340
+ function createAuthorizationInteraction(app) {
25341
+ let noticeHandle;
25342
+ let noticeBody = "";
25343
+ const showNotice = (notice) => {
25344
+ noticeBody = formatAuthorizationNotice(notice);
25345
+ if (noticeHandle === void 0) noticeHandle = app.openOutputViewer({
25346
+ title: "Sign in",
25347
+ initial: noticeBody,
25348
+ refresh: () => noticeBody,
25349
+ onClose: () => {
25350
+ noticeHandle = void 0;
25351
+ }
25352
+ });
25353
+ };
25354
+ const close = () => {
25355
+ noticeHandle?.();
25356
+ noticeHandle = void 0;
25357
+ };
25358
+ return {
25359
+ interaction: {
25360
+ notify: (notice) => {
25361
+ showNotice(notice);
25362
+ },
25363
+ prompt: async (prompt) => {
25364
+ if (prompt.kind === "select") {
25365
+ const signal = prompt.signal;
25366
+ return await new Promise((resolve, reject) => {
25367
+ if (signal?.aborted === true) {
25368
+ reject(cancellationError("authorization prompt withdrawn"));
25369
+ return;
25370
+ }
25371
+ let settled = false;
25372
+ let handle;
25373
+ const cleanup = () => {
25374
+ if (signal !== void 0) signal.removeEventListener("abort", onAbort);
25375
+ };
25376
+ const onAbort = () => {
25377
+ if (settled) return;
25378
+ settled = true;
25379
+ cleanup();
25380
+ handle?.close?.();
25381
+ reject(cancellationError("authorization prompt withdrawn"));
25382
+ };
25383
+ if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
25384
+ handle = app.openPicker(prompt.options.map((option) => ({
25385
+ value: option.id,
25386
+ label: option.label,
25387
+ ...option.description !== void 0 && option.description !== "" ? { description: option.description } : {}
25388
+ })), (value) => {
25389
+ if (settled) return;
25390
+ settled = true;
25391
+ cleanup();
25392
+ resolve(value);
25393
+ }, () => {
25394
+ if (settled) return;
25395
+ settled = true;
25396
+ cleanup();
25397
+ reject(new AuthorizationDeclinedError());
25398
+ }, {
25399
+ header: prompt.message,
25400
+ enableSearch: true
25401
+ });
25402
+ if (settled) handle?.close?.();
25403
+ });
25404
+ }
25405
+ try {
25406
+ const text = (await app.askQuestions([{
25407
+ id: "answer",
25408
+ question: prompt.message,
25409
+ ...prompt.placeholder !== void 0 && prompt.placeholder !== "" ? { placeholder: prompt.placeholder } : {},
25410
+ ...prompt.kind === "secret" ? { masked: true } : {}
25411
+ }], prompt.signal))[0]?.custom ?? "";
25412
+ if (text === "") throw new AuthorizationDeclinedError();
25413
+ return text;
25414
+ } catch (error) {
25415
+ if (prompt.signal?.aborted === true) throw error;
25416
+ throw new AuthorizationDeclinedError();
25417
+ }
25418
+ }
25419
+ },
25420
+ close
25421
+ };
25422
+ }
25423
+ /** The user-facing text for one authorization attempt failure, mapping the
25424
+ * seam's stable error taxonomy to copy (plan §15). `unknownErrorText` is
25425
+ * the safe fallback for provider/network errors (already passed through
25426
+ * safeErrorMessage by the caller). */
25427
+ function authorizationFailureText(error, unknownErrorText) {
25428
+ switch (error?.code) {
25429
+ case "NO_FLOW": return "login method is no longer available";
25430
+ case "UNKNOWN_METHOD": return "selected login method is no longer available";
25431
+ case "ALREADY_IN_FLIGHT": return "sign-in already in progress";
25432
+ case "NOT_COMMITTED": return "sign-in finished without storing a credential";
25433
+ case "DECLINED": return "login cancelled";
25434
+ default: return `sign-in failed: ${unknownErrorText}`;
25435
+ }
25436
+ }
25240
25437
  //#endregion
25241
25438
  //#region src/skill-catalog.ts
25242
25439
  /**
@@ -25579,6 +25776,9 @@ function presetDisplayText(preset) {
25579
25776
  }
25580
25777
  /** The sentinel picker value for the "add a brand-new provider" action row. */
25581
25778
  const ADD_PROVIDER_VALUE = "\0add-provider";
25779
+ /** The sentinel prefix for an authorization-target picker row (a credential
25780
+ * key can never start with NUL, so route values cannot collide). */
25781
+ const AUTH_VALUE_PREFIX = "\0auth:";
25582
25782
  /**
25583
25783
  * M11: the /status extension-health rows (plan §16 — /status extension
25584
25784
  * health). Reads the extension registries' live snapshots: contribution
@@ -25670,6 +25870,7 @@ function readProviderOptions(ctx) {
25670
25870
  route: "deepseek-official",
25671
25871
  configured: true,
25672
25872
  declared: false,
25873
+ namesCredential: true,
25673
25874
  group: "configured",
25674
25875
  settingsNs: "",
25675
25876
  settingsPath: []
@@ -25678,26 +25879,36 @@ function readProviderOptions(ctx) {
25678
25879
  route: option.label,
25679
25880
  configured: true,
25680
25881
  declared: false,
25882
+ namesCredential: true,
25681
25883
  group: "configured",
25682
25884
  settingsNs: "llm-pi-ai",
25683
25885
  settingsPath: ["providers", option.label]
25684
25886
  });
25685
25887
  }
25686
- /** Build the picker rows from the merged options: grouped by configured /
25687
- * available / custom via the SelectList group extension (the fork renders a
25688
- * non-interactive header row per group automatically), with the Add New
25689
- * Platform action row pinned last. */
25690
- function providerPickerRows(options) {
25888
+ /** Build the merged /login picker rows: reference targets keep their
25889
+ * configured/available/custom groups (the fork renders a non-interactive
25890
+ * header row per group), authorization targets get their own group, and the
25891
+ * Add New Platform action row is pinned last. Authorization row values
25892
+ * carry the AUTH_VALUE_PREFIX so they can never collide with a route. */
25893
+ function mergedPickerRows(merged) {
25691
25894
  const rows = [];
25692
25895
  const groupLabels = {
25693
25896
  configured: "configured",
25694
25897
  available: "available · catalog",
25695
- custom: "custom"
25898
+ custom: "custom",
25899
+ authorization: "sign in with provider"
25696
25900
  };
25697
- for (const option of options) rows.push({
25698
- value: option.route,
25699
- label: `${option.label} (${option.ref})`,
25700
- group: groupLabels[option.group]
25901
+ for (const target of merged) if (target.kind === "reference") {
25902
+ const group = target.configured ? "configured" : target.declared ? "custom" : "available";
25903
+ rows.push({
25904
+ value: target.route,
25905
+ label: `${target.label} (${target.ref})`,
25906
+ group: groupLabels[group]
25907
+ });
25908
+ } else rows.push({
25909
+ value: AUTH_VALUE_PREFIX + target.key,
25910
+ label: target.inFlight ? `${target.label} — sign-in in progress` : `${target.label} — sign in`,
25911
+ group: groupLabels.authorization
25701
25912
  });
25702
25913
  rows.push({
25703
25914
  value: ADD_PROVIDER_VALUE,
@@ -25705,6 +25916,161 @@ function providerPickerRows(options) {
25705
25916
  });
25706
25917
  return rows;
25707
25918
  }
25919
+ /** Recover an authorization target from a picked row value (the marker
25920
+ * prefix guarantees no collision with reference route values). */
25921
+ function targetFromPickerValue(merged, value) {
25922
+ if (!value.startsWith(AUTH_VALUE_PREFIX)) return void 0;
25923
+ const key = value.slice(6);
25924
+ for (const target of merged) if (target.kind === "authorization" && target.key === key) return target;
25925
+ }
25926
+ /** One-line summary of every merged target, for the unknown-target error. */
25927
+ function mergedTargetsSummary(merged) {
25928
+ return merged.map((target) => target.kind === "reference" ? `${target.label} (${target.ref})` : `${target.label} (provider sign-in)`).join(", ");
25929
+ }
25930
+ /**
25931
+ * Run one authorization attempt on the seam and report it. Method picking
25932
+ * (single method → direct; multiple → a picker), notice rendering and
25933
+ * prompts all live behind the interaction built here; the seam's stable
25934
+ * error taxonomy maps to user-facing copy (§15). On success, a catalog
25935
+ * route that is not configured yet gets a minimal keyless profile so the
25936
+ * runtime keeps reading the credential record (§12.1 — never an apiKeyEnv,
25937
+ * which would switch the request path back to a reference that is not set).
25938
+ */
25939
+ async function runAuthorizationLogin(ctx, app, runner, target, options) {
25940
+ const authorization = ctx.get("authorization");
25941
+ if (authorization === void 0) return {
25942
+ kind: "error",
25943
+ text: "authorization service unavailable"
25944
+ };
25945
+ if (target.inFlight) return {
25946
+ kind: "error",
25947
+ text: `sign-in already in progress for ${target.label}`
25948
+ };
25949
+ let method = target.methods[0]?.id;
25950
+ if (method === void 0) return {
25951
+ kind: "error",
25952
+ text: `no sign-in method available for ${target.label}`
25953
+ };
25954
+ if (target.methods.length > 1) {
25955
+ const picked = await new Promise((resolve) => {
25956
+ app.openPicker(target.methods.map((candidate) => ({
25957
+ value: candidate.id,
25958
+ label: candidate.label
25959
+ })), (value) => resolve(value), () => resolve(void 0), {
25960
+ header: `Sign in method · ${target.label}`,
25961
+ enableSearch: false
25962
+ });
25963
+ });
25964
+ if (picked === void 0) return {
25965
+ kind: "error",
25966
+ text: "login cancelled"
25967
+ };
25968
+ method = picked;
25969
+ }
25970
+ const { interaction, close } = createAuthorizationInteraction(app);
25971
+ let outcome;
25972
+ try {
25973
+ outcome = await authorization.begin({
25974
+ key: target.key,
25975
+ method,
25976
+ interaction,
25977
+ signal: runner.signal
25978
+ });
25979
+ } catch (error) {
25980
+ if (runner.signal.aborted) return {
25981
+ kind: "error",
25982
+ text: "login cancelled"
25983
+ };
25984
+ if (error?.code === "NOT_COMMITTED") runner.diag.error("authorization", {
25985
+ key: target.key,
25986
+ error: safeErrorMessage(error)
25987
+ });
25988
+ return {
25989
+ kind: "error",
25990
+ text: authorizationFailureText(error, safeErrorMessage(error))
25991
+ };
25992
+ } finally {
25993
+ close();
25994
+ }
25995
+ if (outcome.status === "cancelled") return {
25996
+ kind: "error",
25997
+ text: "login cancelled"
25998
+ };
25999
+ const profileNote = await provisionKeylessProfile(ctx, runner, target, options);
26000
+ return {
26001
+ kind: "success",
26002
+ text: `signed in to ${target.label}${profileNote}`
26003
+ };
26004
+ }
26005
+ /**
26006
+ * After a successful authorization, write a MINIMAL keyless profile for a
26007
+ * catalog route that is not configured yet (§12.1). The record alone does
26008
+ * not make the route selectable — llm-pi-ai registers a route only when
26009
+ * its settings section names it — and the profile must NOT carry
26010
+ * apiKeyEnv, or the request path would switch back to a reference that was
26011
+ * never set. Hand-declared/custom routes are left to the add wizard
26012
+ * (§12.2). Any failure degrades silently (the sign-in itself succeeded).
26013
+ * @returns a user-facing note, or '' when nothing was provisioned.
26014
+ */
26015
+ async function provisionKeylessProfile(ctx, runner, target, options) {
26016
+ if (target.route === void 0) return "";
26017
+ const option = options.find((candidate) => candidate.route === target.route);
26018
+ if (option === void 0 || option.configured || option.declared || option.settingsNs === "") return "";
26019
+ const settings = ctx.get("settings");
26020
+ if (settings === void 0) return "";
26021
+ try {
26022
+ await settings.mutate(option.settingsNs, [{
26023
+ op: "set",
26024
+ path: [...option.settingsPath],
26025
+ value: {}
26026
+ }]);
26027
+ return " — provider profile recorded";
26028
+ } catch (error) {
26029
+ runner.diag.warn("authorization", {
26030
+ key: target.key,
26031
+ note: "profile write failed",
26032
+ error: safeErrorMessage(error)
26033
+ });
26034
+ return "";
26035
+ }
26036
+ }
26037
+ /** Value prefixes for the /logout picker rows (no collision with a ref). */
26038
+ const LOGOUT_REF_VALUE = "\0ref:";
26039
+ const LOGOUT_RECORD_VALUE = "\0record:";
26040
+ /** Build the /logout picker rows: every stored credential record plus every
26041
+ * configured reference (presence only — a secret's value never leaves the
26042
+ * credentials service). Records are deduplicated by key and labelled with
26043
+ * the authorization flow's user-facing name when one owns the key (a
26044
+ * record row must say what signing out actually clears). */
26045
+ async function logoutPickerRows(credentials, options, targets) {
26046
+ const rows = [];
26047
+ const seenRefs = /* @__PURE__ */ new Set();
26048
+ for (const option of options) {
26049
+ if (seenRefs.has(option.ref)) continue;
26050
+ seenRefs.add(option.ref);
26051
+ try {
26052
+ if ((await credentials.describe(option.ref)).configured) rows.push({
26053
+ value: LOGOUT_REF_VALUE + option.ref,
26054
+ label: `${option.label} (${option.ref})`,
26055
+ group: "API keys"
26056
+ });
26057
+ } catch {}
26058
+ }
26059
+ const records = await credentials.listRecords();
26060
+ const seenKeys = /* @__PURE__ */ new Set();
26061
+ for (const record of records) {
26062
+ if (seenKeys.has(record.key)) continue;
26063
+ seenKeys.add(record.key);
26064
+ const owner = targets.find((target) => target.key === record.key);
26065
+ const label = owner !== void 0 ? `${owner.label} — stored credential${record.kind === void 0 ? "" : ` (${record.kind})`}` : `${record.key}${record.kind === void 0 ? "" : ` (${record.kind})`}`;
26066
+ rows.push({
26067
+ value: LOGOUT_RECORD_VALUE + record.key,
26068
+ label,
26069
+ group: "stored credentials"
26070
+ });
26071
+ }
26072
+ return rows;
26073
+ }
25708
26074
  /** Run the add-provider wizard: collect route/api/baseURL/displayName/key
25709
26075
  * through question flows, probe the endpoint for its models (falling back to
25710
26076
  * hand entry), review, then persist the profile + credential.
@@ -27126,7 +27492,7 @@ function registerTuiCommands(runner, initial) {
27126
27492
  });
27127
27493
  commands.register({
27128
27494
  name: "login",
27129
- description: "Set an API key — deepseek official or an llm-pi-ai provider route",
27495
+ description: "Sign in — deepseek official or an llm-pi-ai provider route (API key or provider-native flow)",
27130
27496
  input: { hint: "[<route|env-var>]" },
27131
27497
  handler: async (invocation) => {
27132
27498
  const credentials = ctx.get("credentials");
@@ -27134,39 +27500,51 @@ function registerTuiCommands(runner, initial) {
27134
27500
  kind: "error",
27135
27501
  text: "credentials service unavailable"
27136
27502
  };
27503
+ const authorization = ctx.get("authorization");
27137
27504
  const options = readProviderOptions(ctx);
27505
+ const targets = authorizationTargets(authorization?.list() ?? []);
27506
+ const merged = mergeLoginTargets(options, targets);
27138
27507
  const arg = invocation.rawInput.trim();
27139
27508
  let route;
27140
27509
  let ref;
27510
+ let target;
27141
27511
  if (arg !== "") {
27142
27512
  const known = resolveCredentialArg(arg, options);
27143
27513
  if (known !== void 0) {
27144
27514
  const option = options.find((candidate) => candidate.ref === known);
27145
27515
  if (option !== void 0) {
27146
- route = option.route;
27147
- ref = option.ref;
27516
+ const flow = option.route !== "deepseek-official" ? flowForRoute(targets, option.route) : void 0;
27517
+ if (flow !== void 0 && option.namesCredential === false) target = flow;
27518
+ else {
27519
+ route = option.route;
27520
+ ref = option.ref;
27521
+ }
27148
27522
  } else ref = known;
27149
27523
  } else if (ROUTE_PATTERN.test(arg)) {
27150
- const outcome = await askAddProvider(ctx, app, runner.signal, arg);
27151
- if (outcome.kind === "cancelled") return {
27152
- kind: "error",
27153
- text: "add provider cancelled"
27154
- };
27155
- if (outcome.kind === "error") return {
27156
- kind: "error",
27157
- text: outcome.text
27158
- };
27159
- return {
27160
- kind: "success",
27161
- text: outcome.text
27162
- };
27524
+ const flow = flowForRoute(targets, arg);
27525
+ if (flow !== void 0) target = flow;
27526
+ else {
27527
+ const outcome = await askAddProvider(ctx, app, runner.signal, arg);
27528
+ if (outcome.kind === "cancelled") return {
27529
+ kind: "error",
27530
+ text: "add provider cancelled"
27531
+ };
27532
+ if (outcome.kind === "error") return {
27533
+ kind: "error",
27534
+ text: outcome.text
27535
+ };
27536
+ return {
27537
+ kind: "success",
27538
+ text: outcome.text
27539
+ };
27540
+ }
27163
27541
  } else return {
27164
27542
  kind: "error",
27165
- text: `unknown credential target "${arg}" — ${options.map((option) => `${option.label} (${option.ref})`).join(", ")}`
27543
+ text: `unknown credential target "${arg}" — ${mergedTargetsSummary(merged)}`
27166
27544
  };
27167
- } else if (options.length > 1) {
27545
+ } else if (merged.length > 1) {
27168
27546
  const picked = await new Promise((resolve) => {
27169
- app.openPicker(providerPickerRows(options), (value) => resolve(value), () => resolve(void 0), {
27547
+ app.openPicker(mergedPickerRows(merged), (value) => resolve(value), () => resolve(void 0), {
27170
27548
  enableSearch: true,
27171
27549
  header: "login · providers",
27172
27550
  noMatchText: " no matching providers",
@@ -27194,8 +27572,22 @@ function registerTuiCommands(runner, initial) {
27194
27572
  text: outcome.text
27195
27573
  };
27196
27574
  }
27197
- route = picked;
27198
- } else route = options[0]?.route;
27575
+ const authTarget = targetFromPickerValue(merged, picked);
27576
+ if (authTarget !== void 0) target = authTarget;
27577
+ else route = picked;
27578
+ } else {
27579
+ const only = merged[0];
27580
+ if (only === void 0) return {
27581
+ kind: "error",
27582
+ text: "no credential targets available"
27583
+ };
27584
+ if (only.kind === "authorization") target = only;
27585
+ else {
27586
+ route = only.route;
27587
+ ref = only.ref;
27588
+ }
27589
+ }
27590
+ if (target !== void 0) return runAuthorizationLogin(ctx, app, runner, target, options);
27199
27591
  if (route === void 0 && ref === void 0) return {
27200
27592
  kind: "error",
27201
27593
  text: "no credential targets available"
@@ -27206,7 +27598,8 @@ function registerTuiCommands(runner, initial) {
27206
27598
  try {
27207
27599
  const key = (await app.askQuestions([{
27208
27600
  id: "key",
27209
- question: `Paste the API key for ${label}:`
27601
+ question: `Paste the API key for ${label}:`,
27602
+ masked: true
27210
27603
  }]))[0]?.custom ?? "";
27211
27604
  if (key === "") return {
27212
27605
  kind: "error",
@@ -27227,7 +27620,7 @@ function registerTuiCommands(runner, initial) {
27227
27620
  });
27228
27621
  commands.register({
27229
27622
  name: "logout",
27230
- description: "Clear a stored API key — deepseek official or an llm-pi-ai provider route",
27623
+ description: "Clear a stored credential — deepseek official or an llm-pi-ai provider route (API key or stored record)",
27231
27624
  input: { hint: "[<route|env-var>]" },
27232
27625
  handler: async (invocation) => {
27233
27626
  const credentials = ctx.get("credentials");
@@ -27235,24 +27628,72 @@ function registerTuiCommands(runner, initial) {
27235
27628
  kind: "error",
27236
27629
  text: "credentials service unavailable"
27237
27630
  };
27631
+ const authorization = ctx.get("authorization");
27238
27632
  const options = readProviderOptions(ctx);
27633
+ const targets = authorizationTargets(authorization?.list() ?? []);
27239
27634
  const arg = invocation.rawInput.trim();
27240
27635
  if (arg !== "") {
27241
27636
  const resolved = resolveCredentialArg(arg, options);
27242
- if (resolved === void 0) return {
27637
+ if (resolved !== void 0) {
27638
+ const option = options.find((candidate) => candidate.ref === resolved);
27639
+ const flow = option !== void 0 && option.route !== "deepseek-official" && !option.namesCredential ? flowForRoute(targets, option.route) : void 0;
27640
+ if (flow !== void 0) {
27641
+ await credentials.deleteRecord(flow.key);
27642
+ return {
27643
+ kind: "success",
27644
+ text: `${flow.label} signed out locally — stored credential cleared`
27645
+ };
27646
+ }
27647
+ await credentials.unset(resolved);
27648
+ return {
27649
+ kind: "success",
27650
+ text: `${resolved} cleared`
27651
+ };
27652
+ }
27653
+ const flow = flowForRoute(targets, arg.toLowerCase());
27654
+ if (flow !== void 0) {
27655
+ await credentials.deleteRecord(flow.key);
27656
+ return {
27657
+ kind: "success",
27658
+ text: `${flow.label} signed out locally — stored credential cleared`
27659
+ };
27660
+ }
27661
+ return {
27243
27662
  kind: "error",
27244
- text: `unknown credential target "${arg}" — ${options.map((option) => `${option.label} (${option.ref})`).join(", ")}`
27663
+ text: `unknown credential target "${arg}" — ${mergedTargetsSummary(mergeLoginTargets(options, targets))}`
27245
27664
  };
27246
- await credentials.unset(resolved);
27665
+ }
27666
+ const rows = await logoutPickerRows(credentials, options, targets);
27667
+ if (rows.length === 0) return {
27668
+ kind: "error",
27669
+ text: "nothing to sign out"
27670
+ };
27671
+ const picked = await new Promise((resolve) => {
27672
+ app.openPicker(rows, (value) => resolve(value), () => resolve(void 0), {
27673
+ enableSearch: true,
27674
+ header: "logout · credentials",
27675
+ noMatchText: " nothing to sign out",
27676
+ width: 76,
27677
+ maxHeight: 26,
27678
+ showHint: true
27679
+ });
27680
+ });
27681
+ if (picked === void 0) return {
27682
+ kind: "error",
27683
+ text: "logout cancelled"
27684
+ };
27685
+ if (picked.startsWith(LOGOUT_RECORD_VALUE)) {
27686
+ await credentials.deleteRecord(picked.slice(8));
27247
27687
  return {
27248
27688
  kind: "success",
27249
- text: `${resolved} cleared`
27689
+ text: `${picked.slice(8)} signed out locally — stored credential cleared`
27250
27690
  };
27251
27691
  }
27252
- await credentials.unset(options[0].ref);
27692
+ const targetRef = picked.slice(5);
27693
+ await credentials.unset(targetRef);
27253
27694
  return {
27254
27695
  kind: "success",
27255
- text: `${options[0].ref} cleared`
27696
+ text: `${targetRef} cleared`
27256
27697
  };
27257
27698
  }
27258
27699
  });
@@ -27483,6 +27924,33 @@ function appendHistoryLine(file, text, lastContent) {
27483
27924
  return true;
27484
27925
  }
27485
27926
  //#endregion
27927
+ //#region src/dsh-version.ts
27928
+ /**
27929
+ * The installed dsh (DeepSeek Harness CLI) version, resolved from the
27930
+ * launcher's real path: `process.argv[1]` is the `dsh` bin, whose realpath
27931
+ * walks up to the `@deepseek-ai/dsh/package.json` that owns it. The header
27932
+ * and the welcome card show the harness the TUI runs on, not this bundle's
27933
+ * own patch level. Undefined when the launcher path is unreadable.
27934
+ * @module @xmoon76/dsh-pi-tui/dsh-version
27935
+ */
27936
+ /** @returns the installed dsh version string, or undefined. */
27937
+ function dshVersion() {
27938
+ const bin = process.argv[1];
27939
+ if (bin === void 0) return void 0;
27940
+ try {
27941
+ let dir = dirname(realpathSync(bin));
27942
+ for (let depth = 0; depth < 8; depth += 1) {
27943
+ try {
27944
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
27945
+ if (pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string") return pkg.version;
27946
+ } catch {}
27947
+ const parent = dirname(dir);
27948
+ if (parent === dir) return void 0;
27949
+ dir = parent;
27950
+ }
27951
+ } catch {}
27952
+ }
27953
+ //#endregion
27486
27954
  //#region src/exit.ts
27487
27955
  /**
27488
27956
  * The TUI exit contract: flush the live session with a HARD timeout, record
@@ -28741,6 +29209,19 @@ const inject = [
28741
29209
  TUI_STARTUP_SERVICE
28742
29210
  ];
28743
29211
  const Config = z.object({ sessionId: z.string() });
29212
+ /**
29213
+ * Wire the credential surface refresh to the dsh 0.1.1-rc.1 credential
29214
+ * events. The rc.1 release split the credential update event into the
29215
+ * reference half and the durable-record half; both change what the footer
29216
+ * model row and the welcome card show (a /login /logout, an external
29217
+ * .credentials.yaml edit, or an authorization flow committing a record), so
29218
+ * they share one best-effort callback. The callback must not read secrets
29219
+ * or mutate providers — it only re-reads describe-level state.
29220
+ */
29221
+ function registerCredentialSurfaceRefresh(ctx, refresh) {
29222
+ ctx.on("credentials/reference-updated", refresh);
29223
+ ctx.on("credentials/record-updated", refresh);
29224
+ }
28744
29225
  /** Display window in turns; older turns collapse into a summary entry. */
28745
29226
  const WINDOW_TURNS = 15;
28746
29227
  /** Coalesced repaint interval for streaming events, in ms. */
@@ -29034,33 +29515,10 @@ function foldQueueRows(messages, mode, notified) {
29034
29515
  };
29035
29516
  }
29036
29517
  /**
29037
- * The installed dsh version (e.g. `0.1.0-rc.6`), resolved from the launcher's
29038
- * real path: `process.argv[1]` is the `dsh` bin, whose realpath walks up to
29039
- * the `@deepseek-ai/dsh/package.json` that owns it. The version the welcome
29040
- * card shows is the harness the TUI runs on, not this bundle's own patch
29041
- * level. Undefined when the launcher path is unreadable.
29042
- * @returns the installed dsh version string, or undefined.
29043
- */
29044
- function dshVersion() {
29045
- const bin = process.argv[1];
29046
- if (bin === void 0) return void 0;
29047
- try {
29048
- let dir = dirname(realpathSync(bin));
29049
- for (let depth = 0; depth < 8; depth += 1) {
29050
- try {
29051
- const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
29052
- if (pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string") return pkg.version;
29053
- } catch {}
29054
- const parent = dirname(dir);
29055
- if (parent === dir) return void 0;
29056
- dir = parent;
29057
- }
29058
- } catch {}
29059
- }
29060
- /**
29061
29518
  * The bundle's own version, read from package.json at runtime so the welcome
29062
29519
  * card never drifts from the shipped version. The DISPLAYED version prefers
29063
- * the installed dsh version (`dshVersion`), falling back to this one.
29520
+ * the installed dsh version (`dshVersion` — shared with the header badge
29521
+ * via src/dsh-version.ts), falling back to this one.
29064
29522
  * @returns the version string, or a fallback when the file is unreadable.
29065
29523
  */
29066
29524
  function packageVersion() {
@@ -31313,10 +31771,11 @@ function apply(ctx, config) {
31313
31771
  updateWelcomeCard();
31314
31772
  }
31315
31773
  });
31316
- ctx.on("credentials/updated", () => {
31774
+ const refreshCredentialSurface = () => {
31317
31775
  refreshStatus();
31318
31776
  updateWelcomeCard();
31319
- });
31777
+ };
31778
+ registerCredentialSurfaceRefresh(ctx, refreshCredentialSurface);
31320
31779
  if (liveAgent !== void 0) {
31321
31780
  app.setPlanMode(foldPlanMode(liveAgent.session.events));
31322
31781
  app.setWorking(workingFromLog(liveAgent.session.events));
@@ -31384,4 +31843,4 @@ function apply(ctx, config) {
31384
31843
  });
31385
31844
  }
31386
31845
  //#endregion
31387
- export { apply$1 as A, runningProfile as C, subagentJobViewHint as D, subagentJobTranscriptId as E, describeKey as F, name$1 as M, PI_TUI_EXTENSIONS_SERVICE as N, subagentNoticeIsFailure as O, API_VERSION as P, resumeCommand as S, shouldSteerOnEnter as T, matchPendingSubagentCall as _, SessionLockRefusedError as a, recordedPreset as b, compactingFromLog as c, foldCompactionEvent as d, foldQueueRows as f, isUserQueueInput as g, isSubagentSettlementNotice as h, SESSIONLESS_COMMANDS as i, inject$1 as j, forkSeed as k, composeAgent as l, isPlainExitPrompt as m, HOST_COMMAND_CATALOG as n, apply as o, inject as p, LOCAL_COMMANDS as r, busyAfterTurnBoundary as s, Config as t, dangerCommand as u, name as v, shouldConsumeAdvertisedMiss as w, resolveInitialCatalog as x, recomposeBlank as y };
31846
+ export { dshVersion as A, resumeCommand as C, subagentJobTranscriptId as D, shouldSteerOnEnter as E, PI_TUI_EXTENSIONS_SERVICE as F, API_VERSION as I, describeKey as L, apply$1 as M, inject$1 as N, subagentJobViewHint as O, name$1 as P, resolveInitialCatalog as S, shouldConsumeAdvertisedMiss as T, matchPendingSubagentCall as _, SessionLockRefusedError as a, recordedPreset as b, compactingFromLog as c, foldCompactionEvent as d, foldQueueRows as f, isUserQueueInput as g, isSubagentSettlementNotice as h, SESSIONLESS_COMMANDS as i, forkSeed as j, subagentNoticeIsFailure as k, composeAgent as l, isPlainExitPrompt as m, HOST_COMMAND_CATALOG as n, apply as o, inject as p, LOCAL_COMMANDS as r, busyAfterTurnBoundary as s, Config as t, dangerCommand as u, name as v, runningProfile as w, registerCredentialSurfaceRefresh as x, recomposeBlank as y };
@@ -5,6 +5,46 @@ import { Context } from "@deepseek-ai/cordis";
5
5
  declare const name = "tui-startup";
6
6
  /** Services required before the flags can be resolved. */
7
7
  declare const inject: string[];
8
+ /** One incompatible dsh harness range and the guidance it deserves. Add a
9
+ * new entry here whenever a future bundle release stops supporting an
10
+ * older (or newer) harness line; entries are checked in ORDER and the
11
+ * first whose range covers the installed dsh version wins.
12
+ *
13
+ * Range semantics: `min` is inclusive, `max` is EXCLUSIVE. An entry with
14
+ * only `max` covers everything below it (the common "too old" case); an
15
+ * entry with only `min` covers everything at or above it ("too new" —
16
+ * e.g. a harness that removed a seam we still use).
17
+ *
18
+ * `since` names the bundle release line that FIRST imposed the constraint:
19
+ * it is shown as the fallback version label (`>= <since>`) when the
20
+ * bundle's own version cannot be read, so the message stays truthful.
21
+ */
22
+ interface HarnessCompatEntry {
23
+ /** Inclusive lower bound of the incompatible range; absent = unbounded below. */
24
+ min?: string;
25
+ /** Exclusive upper bound of the incompatible range; absent = unbounded above. */
26
+ max?: string;
27
+ /** The bundle release line that first required this constraint. */
28
+ since: string;
29
+ /** Human-readable requirement, e.g. `DeepSeek Harness 0.1.1-rc.1 or later`. */
30
+ requires: string;
31
+ /** What the user must do; placeholders are NOT needed — the message
32
+ * builder fills in the requirement and the installed version. */
33
+ guidance: string;
34
+ }
35
+ /** The compatibility table. */
36
+ declare const HARNESS_COMPAT: readonly HarnessCompatEntry[];
37
+ /** The compat entry covering the installed dsh version, or undefined when
38
+ * the harness is supported. */
39
+ declare function harnessCompatEntryFor(installed: string): HarnessCompatEntry | undefined;
40
+ /** The bundle's own version label for the gate message, read from
41
+ * package.json at runtime (never hardcoded). Falls back to the release
42
+ * line that first imposed the requirement when unreadable. */
43
+ declare function bundleVersionLabel(since: string): string;
44
+ /** The actionable message printed before the loader's raw failure when the
45
+ * installed harness falls in an incompatible range. `entry` MUST be the
46
+ * entry {@link harnessCompatEntryFor} matched for `installed`. */
47
+ declare function incompatibleHarnessMessage(installed: string, entry: HarnessCompatEntry): string;
8
48
  /** Service provided by this plugin and injected by the TUI runner row. */
9
49
  declare const TUI_STARTUP_SERVICE = "tuiStartup";
10
50
  /** What the runner row reads from {@link TUI_STARTUP_SERVICE}. */
@@ -29,4 +69,4 @@ interface TuiStartupValues {
29
69
  */
30
70
  declare function apply(ctx: Context): void;
31
71
  //#endregion
32
- export { TUI_STARTUP_SERVICE, TuiStartupValues, apply, inject, name };
72
+ export { HARNESS_COMPAT, HarnessCompatEntry, TUI_STARTUP_SERVICE, TuiStartupValues, apply, bundleVersionLabel, harnessCompatEntryFor, incompatibleHarnessMessage, inject, name };
package/dist/startup.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { join } from "node:path";
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
2
3
  import { Command } from "commander";
3
4
  import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
4
5
  //#region src/startup.ts
@@ -9,10 +10,97 @@ import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
9
10
  * `cmdlineArgs`, providing a service that flag-configured rows inject.
10
11
  * @module @xmoon76/dsh-pi-tui/startup
11
12
  */
13
+ /** The installed dsh version, resolved from the launcher's real path. */
14
+ function installedDshVersion() {
15
+ const bin = process.argv[1];
16
+ if (bin === void 0) return void 0;
17
+ try {
18
+ let dir = dirname(realpathSync(bin));
19
+ for (let depth = 0; depth < 8; depth += 1) {
20
+ try {
21
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
22
+ if (pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string") return pkg.version;
23
+ } catch {}
24
+ const parent = dirname(dir);
25
+ if (parent === dir) return void 0;
26
+ dir = parent;
27
+ }
28
+ } catch {}
29
+ }
30
+ /** Prerelease-aware `a >= b` (semver ordering; mirrors src/dsh-version.ts). */
31
+ function versionAtLeast(version, minimum) {
32
+ const parse = (value) => {
33
+ const [core, prerelease = ""] = value.split("-");
34
+ return {
35
+ nums: core.split(".").map(Number),
36
+ pre: prerelease === "" ? [] : prerelease.split(".")
37
+ };
38
+ };
39
+ const a = parse(version);
40
+ const b = parse(minimum);
41
+ for (let i = 0; i < 3; i += 1) {
42
+ const pa = a.nums[i] ?? 0;
43
+ const pb = b.nums[i] ?? 0;
44
+ if (pa !== pb) return pa > pb;
45
+ }
46
+ if (a.pre.length === 0) return true;
47
+ if (b.pre.length === 0) return false;
48
+ const length = Math.max(a.pre.length, b.pre.length);
49
+ for (let i = 0; i < length; i += 1) {
50
+ const pa = a.pre[i];
51
+ const pb = b.pre[i];
52
+ if (pa === void 0) return false;
53
+ if (pb === void 0) return true;
54
+ if (pa === pb) continue;
55
+ const na = Number(pa);
56
+ const nb = Number(pb);
57
+ const naNumeric = !Number.isNaN(na);
58
+ const nbNumeric = !Number.isNaN(nb);
59
+ if (naNumeric && nbNumeric) return na > nb;
60
+ if (naNumeric !== nbNumeric) return !naNumeric;
61
+ return pa > pb;
62
+ }
63
+ return true;
64
+ }
12
65
  /** Stable Cordis plugin name. */
13
66
  const name = "tui-startup";
14
67
  /** Services required before the flags can be resolved. */
15
68
  const inject = ["cmdlineArgs"];
69
+ /** The compatibility table. */
70
+ const HARNESS_COMPAT = [{
71
+ max: "0.1.1-rc.1",
72
+ since: "0.3.0",
73
+ requires: "DeepSeek Harness 0.1.1-rc.1 or later",
74
+ guidance: "Upgrade dsh first, e.g.: npm install -g @deepseek-ai/dsh@0.1.1-rc.1"
75
+ }];
76
+ /** The compat entry covering the installed dsh version, or undefined when
77
+ * the harness is supported. */
78
+ function harnessCompatEntryFor(installed) {
79
+ return HARNESS_COMPAT.find((entry) => (entry.min === void 0 || versionAtLeast(installed, entry.min)) && (entry.max === void 0 || !versionAtLeast(installed, entry.max)));
80
+ }
81
+ /** The bundle's own version label for the gate message, read from
82
+ * package.json at runtime (never hardcoded). Falls back to the release
83
+ * line that first imposed the requirement when unreadable. */
84
+ function bundleVersionLabel(since) {
85
+ try {
86
+ const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8"));
87
+ return pkg.version === void 0 || pkg.version === "" ? `>= ${since}` : `v${pkg.version}`;
88
+ } catch {
89
+ return `>= ${since}`;
90
+ }
91
+ }
92
+ /** The actionable message printed before the loader's raw failure when the
93
+ * installed harness falls in an incompatible range. `entry` MUST be the
94
+ * entry {@link harnessCompatEntryFor} matched for `installed`. */
95
+ function incompatibleHarnessMessage(installed, entry) {
96
+ return [
97
+ `dsh-pi-tui ${bundleVersionLabel(entry.since)} requires ${entry.requires},`,
98
+ `but this installation is running dsh ${installed}.`,
99
+ "",
100
+ `${entry.guidance}`,
101
+ "Then re-run: dsh --profile pi-tui"
102
+ ].join("\n");
103
+ }
16
104
  /** Service provided by this plugin and injected by the TUI runner row. */
17
105
  const TUI_STARTUP_SERVICE = "tuiStartup";
18
106
  /** This app's command: its flags, its description, and its help text. */
@@ -33,6 +121,15 @@ Examples:
33
121
  function apply(ctx) {
34
122
  const program = tuiCommand();
35
123
  program.action(() => {
124
+ const installed = installedDshVersion();
125
+ if (installed !== void 0) {
126
+ const entry = harnessCompatEntryFor(installed);
127
+ if (entry !== void 0) {
128
+ const message = incompatibleHarnessMessage(installed, entry);
129
+ process.stderr.write(`\n${message}\n\n`);
130
+ throw new Error(message);
131
+ }
132
+ }
36
133
  const options = program.opts();
37
134
  ctx.provide(TUI_STARTUP_SERVICE, {
38
135
  ...options.session !== void 0 ? { sessionId: options.session } : {},
@@ -43,4 +140,4 @@ function apply(ctx) {
43
140
  parseCmdline(ctx, program);
44
141
  }
45
142
  //#endregion
46
- export { TUI_STARTUP_SERVICE, apply, inject, name };
143
+ export { HARNESS_COMPAT, TUI_STARTUP_SERVICE, apply, bundleVersionLabel, harnessCompatEntryFor, incompatibleHarnessMessage, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoon76/dsh-pi-tui",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "The dsh-pi-tui bundle: a TUI surface for DeepSeek Harness profiles (dsh --profile pi-tui)",
5
5
  "license": "MIT",
6
6
  "author": "XMoon <xmoonox@gmail.com>",
@@ -77,72 +77,74 @@
77
77
  "peerDependencies": {
78
78
  "@deepseek-ai/cordis": "^4.0.1",
79
79
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
80
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
81
- "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8",
82
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8",
83
- "@deepseek-ai/dsh-cmdline": "^0.1.0-rc.8",
84
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
85
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
86
- "@deepseek-ai/dsh-goal": "^0.1.0-rc.8",
87
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8",
88
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
89
- "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8",
90
- "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8",
91
- "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8",
92
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
93
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
94
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
95
- "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8",
96
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
97
- "@deepseek-ai/dsh-shell": "^0.1.0-rc.8",
98
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.8",
99
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
100
- "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8",
101
- "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8",
102
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
103
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
104
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
80
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
81
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.1",
82
+ "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.1",
83
+ "@deepseek-ai/dsh-authorization": "^0.1.1-rc.1",
84
+ "@deepseek-ai/dsh-cmdline": "^0.1.1-rc.1",
85
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
86
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.1",
87
+ "@deepseek-ai/dsh-goal": "^0.1.1-rc.1",
88
+ "@deepseek-ai/dsh-jobs": "^0.1.1-rc.1",
89
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
90
+ "@deepseek-ai/dsh-llm-retry": "^0.1.1-rc.1",
91
+ "@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.1",
92
+ "@deepseek-ai/dsh-plan-mode": "^0.1.1-rc.1",
93
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.1",
94
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
95
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
96
+ "@deepseek-ai/dsh-session-title": "^0.1.1-rc.1",
97
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
98
+ "@deepseek-ai/dsh-shell": "^0.1.1-rc.1",
99
+ "@deepseek-ai/dsh-skill": "^0.1.1-rc.1",
100
+ "@deepseek-ai/dsh-subagent": "^0.1.1-rc.1",
101
+ "@deepseek-ai/dsh-token-meter": "^0.1.1-rc.1",
102
+ "@deepseek-ai/dsh-tool-workflow": "^0.1.1-rc.1",
103
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
104
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.1",
105
+ "@deepseek-ai/dsh-user-questions": "^0.1.1-rc.1",
105
106
  "@deepseek-ai/schemastery": "^3.18.1"
106
107
  },
107
108
  "devDependencies": {
108
- "@xmoon76/pi-tui": "^0.84.3",
109
109
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
110
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
111
- "@deepseek-ai/dsh-agent-default-model": "^0.1.0-rc.8",
112
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
113
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
114
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.8",
110
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
111
+ "@deepseek-ai/dsh-agent-default-model": "^0.1.1-rc.1",
112
+ "@deepseek-ai/dsh-agent-loop": "^0.1.1-rc.1",
113
+ "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.1",
114
+ "@deepseek-ai/dsh-attachment": "^0.1.1-rc.1",
115
+ "@deepseek-ai/dsh-authorization": "^0.1.1-rc.1",
116
+ "@deepseek-ai/dsh-brand": "^0.1.1-rc.1",
117
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
118
+ "@deepseek-ai/dsh-credentials": "^0.1.1-rc.1",
119
+ "@deepseek-ai/dsh-goal": "^0.1.1-rc.1",
120
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
121
+ "@deepseek-ai/dsh-jobs": "^0.1.1-rc.1",
122
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
123
+ "@deepseek-ai/dsh-llm-retry": "^0.1.1-rc.1",
124
+ "@deepseek-ai/dsh-permission-presets": "^0.1.1-rc.1",
125
+ "@deepseek-ai/dsh-plan-mode": "^0.1.1-rc.1",
126
+ "@deepseek-ai/dsh-scope": "^0.1.1-rc.1",
127
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
128
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
129
+ "@deepseek-ai/dsh-session-title": "^0.1.1-rc.1",
130
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
131
+ "@deepseek-ai/dsh-shell": "^0.1.1-rc.1",
132
+ "@deepseek-ai/dsh-skill": "^0.1.1-rc.1",
133
+ "@deepseek-ai/dsh-subagent": "^0.1.1-rc.1",
134
+ "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
135
+ "@deepseek-ai/dsh-timeout": "^0.1.1-rc.1",
136
+ "@deepseek-ai/dsh-token-meter": "^0.1.1-rc.1",
137
+ "@deepseek-ai/dsh-tool-workflow": "^0.1.1-rc.1",
138
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
139
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.1",
140
+ "@deepseek-ai/dsh-user-questions": "^0.1.1-rc.1",
115
141
  "@deepseek-ai/schemastery": "^3.18.1",
116
- "@xterm/headless": "5.5.0",
117
142
  "@types/node": "^22.0.0",
118
- "typescript": "^5.9.0",
143
+ "@xmoon76/pi-tui": "^0.84.3",
144
+ "@xterm/headless": "5.5.0",
119
145
  "tsdown": "0.22.0",
120
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
121
- "@deepseek-ai/dsh-token-meter": "^0.1.0-rc.8",
122
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.8",
123
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
124
- "@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8",
125
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8",
126
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.8",
127
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
128
- "@deepseek-ai/dsh-goal": "^0.1.0-rc.8",
129
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
130
- "@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.8",
131
- "@deepseek-ai/dsh-llm-retry": "^0.1.0-rc.8",
132
- "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.8",
133
- "@deepseek-ai/dsh-session-title": "^0.1.0-rc.8",
134
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.8",
135
- "@deepseek-ai/dsh-shell": "^0.1.0-rc.8",
136
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.8",
137
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.8",
138
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.8",
139
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
140
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.8",
141
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.8",
142
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.8",
143
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
144
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
145
- "tsx": "^4.23.12"
146
+ "tsx": "^4.23.12",
147
+ "typescript": "^5.9.0"
146
148
  },
147
149
  "engines": {
148
150
  "node": "^22.19.0 || >=24"