dsh-ssh-tui 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.en.md +92 -8
  2. package/README.md +64 -8
  3. package/cordis.patch.yml +15 -0
  4. package/docs/terminals.md +113 -0
  5. package/lib/approval-cache.js +12 -9
  6. package/lib/approval-cache.js.map +1 -1
  7. package/lib/color-depth.js +10 -4
  8. package/lib/color-depth.js.map +1 -1
  9. package/lib/diag.js +76 -17
  10. package/lib/diag.js.map +1 -1
  11. package/lib/dialogs.js.map +1 -1
  12. package/lib/display-sock.js +304 -29
  13. package/lib/display-sock.js.map +1 -1
  14. package/lib/doctor.js +71 -37
  15. package/lib/doctor.js.map +1 -1
  16. package/lib/dsh-compat.js +206 -88
  17. package/lib/dsh-compat.js.map +1 -1
  18. package/lib/footer.js +39 -9
  19. package/lib/footer.js.map +1 -1
  20. package/lib/gateway-protocol.js +202 -0
  21. package/lib/gateway-protocol.js.map +1 -0
  22. package/lib/i18n/en.js +79 -41
  23. package/lib/i18n/en.js.map +1 -1
  24. package/lib/i18n/index.js +13 -7
  25. package/lib/i18n/index.js.map +1 -1
  26. package/lib/i18n/zh.js +79 -41
  27. package/lib/i18n/zh.js.map +1 -1
  28. package/lib/index.js +132 -31
  29. package/lib/index.js.map +1 -1
  30. package/lib/job-label.js +99 -17
  31. package/lib/job-label.js.map +1 -1
  32. package/lib/paint.js +5 -2
  33. package/lib/paint.js.map +1 -1
  34. package/lib/picker.js +5 -3
  35. package/lib/picker.js.map +1 -1
  36. package/lib/plan.js +269 -17
  37. package/lib/plan.js.map +1 -1
  38. package/lib/platform.js +373 -0
  39. package/lib/platform.js.map +1 -0
  40. package/lib/preset-authoring.js +10 -14
  41. package/lib/preset-authoring.js.map +1 -1
  42. package/lib/preset-compat.js +100 -0
  43. package/lib/preset-compat.js.map +1 -0
  44. package/lib/preset-picker.js +5 -1
  45. package/lib/preset-picker.js.map +1 -1
  46. package/lib/preset-rows.js +83 -13
  47. package/lib/preset-rows.js.map +1 -1
  48. package/lib/provider-catalog.js +7 -4
  49. package/lib/provider-catalog.js.map +1 -1
  50. package/lib/route-memory.js +11 -4
  51. package/lib/route-memory.js.map +1 -1
  52. package/lib/session-index.js +5 -0
  53. package/lib/session-index.js.map +1 -1
  54. package/lib/session-list.js +18 -1
  55. package/lib/session-list.js.map +1 -1
  56. package/lib/session-lock.js +79 -24
  57. package/lib/session-lock.js.map +1 -1
  58. package/lib/session-route.js +334 -0
  59. package/lib/session-route.js.map +1 -0
  60. package/lib/settings-routes.js +10 -0
  61. package/lib/settings-routes.js.map +1 -0
  62. package/lib/settings-subagent.js +10 -0
  63. package/lib/settings-subagent.js.map +1 -0
  64. package/lib/subagent-model.js +84 -6
  65. package/lib/subagent-model.js.map +1 -1
  66. package/lib/supergrok-token.js +4 -0
  67. package/lib/supergrok-token.js.map +1 -1
  68. package/lib/term-text.js +7 -4
  69. package/lib/term-text.js.map +1 -1
  70. package/lib/terminal-caps.js +358 -0
  71. package/lib/terminal-caps.js.map +1 -0
  72. package/lib/tool-present.js +46 -14
  73. package/lib/tool-present.js.map +1 -1
  74. package/lib/tui.js +1949 -371
  75. package/lib/tui.js.map +1 -1
  76. package/lib/types/color-depth.d.ts +3 -3
  77. package/lib/types/diag.d.ts +18 -1
  78. package/lib/types/dialogs.d.ts +4 -0
  79. package/lib/types/display-sock.d.ts +94 -2
  80. package/lib/types/doctor.d.ts +8 -1
  81. package/lib/types/dsh-compat.d.ts +141 -44
  82. package/lib/types/footer.d.ts +21 -5
  83. package/lib/types/gateway-protocol.d.ts +88 -0
  84. package/lib/types/i18n/index.d.ts +18 -12
  85. package/lib/types/index.d.ts +45 -0
  86. package/lib/types/job-label.d.ts +38 -7
  87. package/lib/types/picker.d.ts +3 -0
  88. package/lib/types/plan.d.ts +94 -4
  89. package/lib/types/platform.d.ts +215 -0
  90. package/lib/types/preset-authoring.d.ts +8 -14
  91. package/lib/types/preset-compat.d.ts +58 -0
  92. package/lib/types/preset-picker.d.ts +1 -1
  93. package/lib/types/preset-rows.d.ts +51 -9
  94. package/lib/types/route-memory.d.ts +17 -22
  95. package/lib/types/session-list.d.ts +5 -0
  96. package/lib/types/session-lock.d.ts +9 -0
  97. package/lib/types/session-route.d.ts +191 -0
  98. package/lib/types/settings-routes.d.ts +16 -0
  99. package/lib/types/settings-subagent.d.ts +24 -0
  100. package/lib/types/subagent-model.d.ts +60 -7
  101. package/lib/types/term-text.d.ts +3 -2
  102. package/lib/types/terminal-caps.d.ts +105 -0
  103. package/lib/types/tool-present.d.ts +2 -17
  104. package/lib/types/transcript-types.d.ts +44 -1
  105. package/lib/types/tui.d.ts +333 -9
  106. package/package.json +82 -55
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  export { ATTACH_RECOVERY_WINDOW_MS, attachPeerVanished } from './attach.js';
9
+ import z from '@deepseek-ai/schemastery';
9
10
  export declare const name = "ssh-tui";
10
11
  /** Core services required before the terminal channel can drive an agent. */
11
12
  export declare const inject: string[];
@@ -27,7 +28,51 @@ export interface Config {
27
28
  model?: string;
28
29
  /** Minimum milliseconds between paints; see DSH_TUI_PAINT_MS. */
29
30
  paintIntervalMs?: number;
31
+ /**
32
+ * The fields below are the TUI's live settings, i.e. the `ssh-tui` section.
33
+ * They ride on this entry's schema so 0.1.7 can project a form for it — and so
34
+ * a pre-0.1.7 `$DSH_HOME/settings.yaml` `ssh-tui:` section is imported into
35
+ * this entry rather than left behind.
36
+ */
37
+ /** UI language (`/language`); zh unless the environment says otherwise. */
38
+ language?: string;
39
+ /** Newest plugin version whose update notice was dismissed. */
40
+ skipUpdate?: string;
41
+ /** Workspace pane layout (`/view`). */
42
+ view?: string;
43
+ /** What a dropped display does (`/disconnect`). */
44
+ disconnect?: string;
45
+ /** Auto-approval mode (`/autoapproval`). */
46
+ autoApproval?: string;
47
+ /** Milliseconds a leftover finished Host waits before exiting; 0 = never. */
48
+ idleExit?: number;
30
49
  }
50
+ /** Every field above, as schemastery resolves them (all optional). */
51
+ interface ConfigFields {
52
+ sessionId?: string;
53
+ showReasoning?: boolean;
54
+ maxToolOutputLines?: number;
55
+ color?: boolean;
56
+ welcome?: string;
57
+ resume?: boolean;
58
+ resumePicker?: boolean;
59
+ provider?: string;
60
+ model?: string;
61
+ paintIntervalMs?: number;
62
+ language?: string;
63
+ skipUpdate?: string;
64
+ view?: string;
65
+ disconnect?: string;
66
+ autoApproval?: string;
67
+ idleExit?: number;
68
+ }
69
+ /**
70
+ * The entry's schema. Everything a launch supplies (`config:` in
71
+ * `cordis.patch.yml`, including its `!!js` expressions) stays ordinary,
72
+ * non-live configuration; the TUI's own settings are the live fields, which is
73
+ * what makes them visible to, and writable through, the 0.1.7 settings service.
74
+ */
75
+ export declare const Config: z<ConfigFields>;
31
76
  /**
32
77
  * Mount the SSH TUI. The `main` agent is created here after the loader
33
78
  * settles, reading the saved default provider/model from
@@ -1,20 +1,51 @@
1
1
  /**
2
- * Friendly display names for background jobs.
2
+ * Friendly display names for background jobs and subagent chips.
3
3
  *
4
- * `job_*` cards otherwise read as the raw tool vocabulary (`job_output`,
5
- * `job_id: bash-1`), which is both untranslated and hard to talk about out
6
- * loud. Each job id instead gets a two-word alias — "蔚蓝水獭", "azure otter" —
7
- * derived from the id itself rather than from a random draw, so the same job
8
- * keeps one name across its call card, its result card, and every later
4
+ * Background `job_*` cards otherwise read as the raw tool vocabulary
5
+ * (`job_output`, `job_id: bash-1`). Each job id gets a two-token alias
6
+ * (hour + weather: 昏风 / twilight wind) derived from the id itself rather
7
+ * than from a random draw, so the same
8
+ * job keeps one name across its call card, its result card, and every later
9
9
  * `job_output` / `job_kill` mention. The model-facing id stays authoritative;
10
10
  * the alias is presentation only.
11
11
  *
12
+ * Subagent chips use a different scheme: a role distilled from the parent
13
+ * spawn description, joined to one of the four directional beasts
14
+ * (青龙 / 白虎 / 朱雀 / 玄武) hashed from the child session id.
15
+ *
12
16
  * @module dsh-ssh-tui/job-label
13
17
  */
18
+ /** FNV-1a over the job id: stable, dependency-free, and well spread for short ids. */
19
+ export declare function hashJobId(id: string): number;
14
20
  /**
15
21
  * One stable alias for a background job, or `undefined` when the id is empty
16
22
  * or the locale carries no vocabulary.
17
23
  * @param jobId - the native job id (`bash-1`, `pwsh-2`, …).
18
- * @returns the locale-formatted alias, e.g. `蔚蓝水獭` / `azure otter`.
24
+ * @returns the locale-formatted alias, e.g. `昏风` / `twilight wind`.
19
25
  */
20
26
  export declare function jobAlias(jobId: string): string | undefined;
27
+ /** The four directional beasts, hashed from the child session id. */
28
+ export declare const SUBAGENT_BEASTS: readonly ["azure-dragon", "white-tiger", "vermilion-bird", "black-tortoise"];
29
+ export type SubagentBeastId = (typeof SUBAGENT_BEASTS)[number];
30
+ export type SubagentRoleId = 'scout' | 'scribe' | 'artisan' | 'envoy' | 'inquirer' | 'sentinel' | 'steward' | 'courier';
31
+ /** Distill a parent spawn description into one role id. */
32
+ export declare function subagentRoleId(task: string): SubagentRoleId;
33
+ /** Hash the child session onto one of the four beasts. */
34
+ export declare function subagentBeastId(sessionId: string): SubagentBeastId;
35
+ /** Localized role label (`探路` / `scout`). */
36
+ export declare function subagentRoleLabel(role: SubagentRoleId): string;
37
+ /** Localized beast label (`青龙` / `Azure Dragon`). */
38
+ export declare function subagentBeastLabel(beast: SubagentBeastId): string;
39
+ /**
40
+ * Chip title: `探路·青龙` / `Scout · Azure Dragon`.
41
+ * Falls back to the spawn-time label when neither task nor id can be named.
42
+ */
43
+ export declare function subagentCourtesyName(input: {
44
+ sessionId: string;
45
+ task?: string;
46
+ fallback: string;
47
+ }): string;
48
+ /** True when a string looks like a session / tool-call id, not a display name. */
49
+ export declare function looksLikeOpaqueId(value: string): boolean;
50
+ /** Prefer a recorded tool name; never surface `call-<uuid>` as a title. */
51
+ export declare function displayToolName(name: string | undefined): string;
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
13
  import { openResumableSessionPager, type ResumableSession } from './session-list.js';
14
+ import { type TerminalCapabilities } from './terminal-caps.js';
14
15
  /** What the launch picker decided. */
15
16
  export type SessionPickerResult = {
16
17
  kind: 'resume';
@@ -144,5 +145,7 @@ export interface SessionPickerOptions {
144
145
  stdin?: NodeJS.ReadStream;
145
146
  stdout?: NodeJS.WriteStream;
146
147
  openPager?: typeof openResumableSessionPager;
148
+ /** The terminal to act on; defaults to reading the environment (tests inject). */
149
+ terminalCaps?: TerminalCapabilities;
147
150
  }
148
151
  export declare function showSessionPicker(ctx: Context, color: boolean, signal?: AbortSignal, options?: SessionPickerOptions): Promise<SessionPickerResult>;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Plan dock, todo lists, /find, prompt-injection cards, and compact errors.
3
3
  */
4
- import type { DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
4
+ import { type TextSegment } from './term-text.js';
5
+ import type { DiffDisplayLine, DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
5
6
  export declare const MAX_SUBAGENT_LOGS = 80;
6
7
  export declare const TODO_STATUS_MARK: Record<PlanTodoItem['status'], string>;
7
8
  /** True while a plan still belongs in the dock (latest incomplete work). */
@@ -78,10 +79,99 @@ export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
78
79
  export declare function todoSummary(value: unknown): string;
79
80
  /** Compact ask_user_question summary from tool arguments. */
80
81
  export declare function askSummary(value: unknown): string;
81
- /** One-line subagent card header used while collapsed. */
82
- export declare function subagentHeaderText(row: Extract<Row, {
82
+ /** First non-empty line, collapsed to a single scan line. */
83
+ export declare function firstDisplayLine(text: string): string;
84
+ /** Collapse a child-session blob to one short chip/wait-card line. */
85
+ export declare function clipSubagentActivity(text: string, maxChars?: number): string;
86
+ /** Running / ok / aborted / error → ANSI for the status dot and status word. */
87
+ export declare function subagentStateColor(status: Extract<Row, {
83
88
  kind: 'subagent';
84
- }>, now?: number): string;
89
+ }>['status']): '33' | '32' | '31' | '90';
90
+ /**
91
+ * Rebuild a subagent chip from the parent spawn tool call that survives in the
92
+ * session log. Live `subagent/start` is not replayed, so resume would otherwise
93
+ * show a generic tool card titled from the English description ("probe").
94
+ */
95
+ export declare function subagentRowFromSpawnTool(input: {
96
+ callId: string;
97
+ task: string;
98
+ provider?: string;
99
+ /** Model route for the child, when the caller knows it. */
100
+ modelProvider?: string;
101
+ local?: boolean;
102
+ status?: Extract<Row, {
103
+ kind: 'subagent';
104
+ }>['status'];
105
+ startedAt?: number;
106
+ endedAt?: number;
107
+ output?: string;
108
+ }): Extract<Row, {
109
+ kind: 'subagent';
110
+ }>;
111
+ /**
112
+ * Courtesy title: distilled role plus a directional beast.
113
+ *
114
+ * The beast comes from the child's own session id, never the parent call id a
115
+ * replayed chip was built with: the same child must keep its symbol across a
116
+ * `--resume`.
117
+ */
118
+ export declare function subagentDisplayName(row: Extract<Row, {
119
+ kind: 'subagent';
120
+ }>): string;
121
+ /**
122
+ * Short activity for the collapsed chip and wait card: prefer the parent
123
+ * task name, else a clipped last log line. Never the full child transcript.
124
+ */
125
+ export declare function subagentChipSummary(row: Extract<Row, {
126
+ kind: 'subagent';
127
+ }>): string;
128
+ /** Header + SGR spans: identity color, status dot/word, muted summary. */
129
+ export declare function buildSubagentHeader(input: {
130
+ focused: boolean;
131
+ title: string;
132
+ status: Extract<Row, {
133
+ kind: 'subagent';
134
+ }>['status'];
135
+ elapsedLabel: string;
136
+ summary: string;
137
+ spinner?: string;
138
+ inspectHint?: string;
139
+ /** Different provider from the parent: paint the title cyan, not violet. */
140
+ foreign?: boolean;
141
+ }): {
142
+ plain: string;
143
+ segments: TextSegment[];
144
+ };
145
+ /** Map a folded child-session event onto an existing display role. */
146
+ export declare function subagentLogDisplayKind(entry: SubagentLogEntry, status: Extract<Row, {
147
+ kind: 'subagent';
148
+ }>['status']): DisplayKind;
149
+ /**
150
+ * Overlay body: session line, stop reason, then the clipped child log.
151
+ *
152
+ * The child's session id is the one `/subagents kill` takes, so it belongs on
153
+ * this line — the collapsed chip stays free of opaque ids.
154
+ */
155
+ export declare function subagentInspectLines(row: Extract<Row, {
156
+ kind: 'subagent';
157
+ }>): DiffDisplayLine[];
158
+ /**
159
+ * Classify a child-session error so the chip can say *why* it died, not just
160
+ * that it ended. Quota and expired auth get a command that actually helps;
161
+ * everything else keeps a clipped diagnostic.
162
+ */
163
+ export declare function describeSubagentFailure(input: {
164
+ stopReason?: string;
165
+ message?: string;
166
+ provider?: string;
167
+ }): {
168
+ hint: string;
169
+ kind: 'quota' | 'auth' | 'effort' | 'error';
170
+ } | undefined;
85
171
  export declare function appendSubagentLog(row: Extract<Row, {
86
172
  kind: 'subagent';
87
173
  }>, entry: SubagentLogEntry): void;
174
+ /** Fold a child user/plugin blob: reminders become one inject chip, not raw XML. */
175
+ export declare function foldSubagentUserLog(row: Extract<Row, {
176
+ kind: 'subagent';
177
+ }>, text: string, sourceKind?: string, plugin?: string): void;
@@ -0,0 +1,215 @@
1
+ /** This process is running on Windows. */
2
+ export declare const IS_WINDOWS: boolean;
3
+ /** Windows delivers resizes on the stream; everyone else raises SIGWINCH. */
4
+ export declare function usesSigwinch(platform?: NodeJS.Platform): boolean;
5
+ /** The launcher's environment file: a shell fragment or a `cmd` script. */
6
+ export declare function envFileName(platform?: NodeJS.Platform): string;
7
+ /** The shell a Windows user has: PowerShell, where POSIX code would say bash. */
8
+ export declare function shellName(platform?: NodeJS.Platform): string;
9
+ /**
10
+ * Whether lock liveness is decided by asking the OS about the process (Windows:
11
+ * `Get-Process` + creation time) rather than by reading `/proc` (Linux) or the
12
+ * command line (darwin).
13
+ *
14
+ * `lockOwnerIsAlive` returns early into `windowsProcessMatchesLock` when this is
15
+ * true; the POSIX body below it has no meaning there (`/proc` does not exist).
16
+ */
17
+ export declare function usesProcessIdentity(platform?: NodeJS.Platform): boolean;
18
+ /**
19
+ * How to start the background Host so it outlives this process *and* does not
20
+ * make its own children flash console windows on Windows.
21
+ *
22
+ * POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
23
+ * hung-up terminal.
24
+ *
25
+ * Windows is the opposite, and the trade-off is forced: `detached: true` maps to
26
+ * DETACHED_PROCESS, and Windows ignores CREATE_NO_WINDOW (what `windowsHide`
27
+ * sets) when DETACHED_PROCESS is present. Every console child the Host then
28
+ * starts — each tool call, every shell, node, git — allocates its own console,
29
+ * which is a visible window flashing over the TUI. So the Host is spawned
30
+ * non-detached and inherits the launcher's console; descendants inherit it too
31
+ * instead of creating one.
32
+ *
33
+ * The price *was* on the lifecycle side, and it was measured (on the Windows CI
34
+ * leg by `scripts/tui-mock-probe.mjs --busy`, not reasoned about): libuv assigns
35
+ * a non-detached child to its global job object, which is created with
36
+ * KILL_ON_JOB_CLOSE. The launcher died, the job closed, and the Host was
37
+ * terminated with it — a closed terminal window ended the session's compute.
38
+ *
39
+ * This function still returns `detached: false` there, because it describes a
40
+ * *direct* spawn, and a direct spawn cannot have both properties: Node exposes
41
+ * `detached` (DETACHED_PROCESS, which makes Windows ignore CREATE_NO_WINDOW) and
42
+ * `windowsHide`, but not `CREATE_NEW_CONSOLE`. The way out is to not spawn the
43
+ * Host directly on Windows: {@link hostBootstrapCommand} starts it through the
44
+ * OS PowerShell, which *can* ask for a new console and hide it. This direct path
45
+ * stays as the fallback for a Windows box without PowerShell.
46
+ */
47
+ export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
48
+ detached: boolean;
49
+ windowsHide: boolean;
50
+ };
51
+ /**
52
+ * The Windows PowerShell that ships with the operating system.
53
+ *
54
+ * `powershell.exe` and not `pwsh.exe`: the latter is PowerShell 7, an optional
55
+ * install, while 5.1 is part of Windows 10/11. `%SystemRoot%` is where it lives
56
+ * (`%windir%` is the legacy spelling of the same thing); when neither is set
57
+ * there is nothing to find, and the caller falls back to a direct spawn.
58
+ */
59
+ export declare function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string | undefined;
60
+ /** A PowerShell single-quoted literal; `''` is the only escape inside one. */
61
+ export declare function psQuote(value: string): string;
62
+ /**
63
+ * Quote one argv array into a Windows command line, the way `CreateProcess`
64
+ * parses it back (the rule from "Everyone quotes command line arguments the
65
+ * wrong way"): wrap in double quotes when the argument is empty or contains
66
+ * whitespace or a quote, double the backslashes that precede a quote, and double
67
+ * trailing backslashes before the closing quote. `Start-Process -ArgumentList`
68
+ * joins its array with spaces and adds no quoting of its own, so the line has to
69
+ * be right before it is handed over — a `DSH_HOME` with a space in it is the
70
+ * normal case, not the exotic one.
71
+ */
72
+ export declare function windowsCommandLine(argv: string[]): string;
73
+ /** `-EncodedCommand` wants base64 of UTF-16LE, which is also what dodges quoting. */
74
+ export declare function encodePowerShellCommand(script: string): string;
75
+ /**
76
+ * The bootstrap script: start the Host with **its own console, hidden**, and
77
+ * print its pid so the launcher can watch it.
78
+ *
79
+ * `Start-Process -WindowStyle Hidden` is the only way to ask for this from a
80
+ * Node process. It is ShellExecuteEx/CreateProcess with `CREATE_NEW_CONSOLE` and
81
+ * `SW_HIDE`: the Host gets a console of its own, so closing the user's terminal
82
+ * no longer takes it down, and that console is invisible, so the tool calls that
83
+ * inherit it do not flash. `-PassThru` gives the object whose `Id` is printed;
84
+ * `-RedirectStandardError` keeps the Host's stderr log, which the direct spawn
85
+ * used to feed through an inherited fd.
86
+ *
87
+ * The whole script travels as an encoded command, so nothing in it is ever
88
+ * re-parsed by a shell.
89
+ */
90
+ export declare function hiddenConsoleHostScript(options: {
91
+ execPath: string;
92
+ argv: string[];
93
+ /** Where the Host's stderr goes; omitted only in tests. */
94
+ stderrFile?: string;
95
+ /** Where the Host's pid is written for the launcher to read. */
96
+ pidFile: string;
97
+ }): string;
98
+ /**
99
+ * Set on the Host's environment by the bootstrap below, through
100
+ * {@link bootstrapEnv}.
101
+ *
102
+ * The Host cannot ask whether it has a console of its own — Node exposes no such
103
+ * question — so the launcher tells it. That is what lets the one build where the
104
+ * lifecycle promise does not hold (Windows, no PowerShell, direct child) say so
105
+ * at boot instead of letting the user discover it by closing the window.
106
+ */
107
+ export declare const TUI_HOST_START_ENV = "DSH_TUI_HOST_START";
108
+ /** Marker value for a Host started with a hidden console of its own. */
109
+ export declare const TUI_HOST_START_BOOTSTRAP = "hidden-console";
110
+ /** The environment the bootstrap hands to the Host: the marker is added here. */
111
+ export declare function bootstrapEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
112
+ /**
113
+ * Whether this Host survives its terminal being closed.
114
+ *
115
+ * POSIX always does (`setsid`); Windows does exactly when the bootstrap started
116
+ * it. A Windows Host without the marker is the fallback direct child, which
117
+ * libuv's `KILL_ON_JOB_CLOSE` job takes down with the launcher — the one case
118
+ * worth a boot notice.
119
+ */
120
+ export declare function hostHasOwnConsole(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
121
+ /** How the Host is started when it must not be a direct child. */
122
+ export interface HostBootstrapCommand {
123
+ command: string;
124
+ args: string[];
125
+ }
126
+ /**
127
+ * The hidden-console bootstrap for this platform, or `undefined` when the Host
128
+ * should be spawned directly.
129
+ *
130
+ * Windows only, and only when the OS PowerShell is really there: the caller
131
+ * falls back to {@link hostSpawnOptions}, which keeps the boot working (without
132
+ * the survival property) on a machine where PowerShell is missing or blocked.
133
+ * `exists` is injectable so the decision is assertable on Linux.
134
+ */
135
+ export declare function hostBootstrapCommand(options: {
136
+ platform?: NodeJS.Platform;
137
+ env?: NodeJS.ProcessEnv;
138
+ exists?: (path: string) => boolean;
139
+ execPath: string;
140
+ argv: string[];
141
+ stderrFile?: string;
142
+ pidFile: string;
143
+ }): HostBootstrapCommand | undefined;
144
+ /**
145
+ * A path a human can read, with the platform's own shorthand.
146
+ *
147
+ * `~/.dsh/env.sh` is the POSIX form; Windows users know `%USERPROFILE%`, and a
148
+ * `C:\Users\...` prefix spelled out is noise in a one-line hint.
149
+ */
150
+ export declare function displayHomePath(home: string, file: string, options?: {
151
+ platform?: NodeJS.Platform;
152
+ env?: NodeJS.ProcessEnv;
153
+ userHome?: string;
154
+ }): string;
155
+ /**
156
+ * Who owns the files this plugin writes, and how to keep it that way.
157
+ *
158
+ * On POSIX the `mode` passed to `writeFile`/`mkdir` (`0o600`, `0o700`) is the
159
+ * whole story. **On Windows it is silently ignored**, and the files that matter
160
+ * here are not cosmetic: `env.cmd` carries API keys, the SuperGrok token file
161
+ * carries an OAuth grant, and the lock/socket directories carry session
162
+ * metadata. What they get instead is the ACL inherited from their parent — fine
163
+ * under `%USERPROFILE%\.dsh`, and *not* fine when `DSH_HOME` points somewhere
164
+ * shared (`C:\dsh`, a network share, a machine where `Users` can read the
165
+ * directory), which is exactly when nobody notices.
166
+ *
167
+ * So the intent is applied explicitly: `icacls` with inheritance removed and a
168
+ * single grant to the current user. The argv is built by a pure function so it
169
+ * can be asserted on Linux; applying it is best-effort by design — a machine
170
+ * without `icacls`, or a path held open by another process, must not fail the
171
+ * write that just succeeded. Failing closed here would mean a TUI that cannot
172
+ * save its own settings.
173
+ */
174
+ /**
175
+ * The account `icacls` should grant, or undefined when the environment has none.
176
+ *
177
+ * `%USERDOMAIN%\%USERNAME%` where both are present: on a domain-joined machine a
178
+ * bare name can resolve to the machine-local account of the same name, and a
179
+ * grant to the wrong account — with inheritance already removed — would leave
180
+ * the user's own API keys inaccessible to them.
181
+ */
182
+ export declare function aclUserName(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
183
+ /**
184
+ * The exact `icacls` arguments for one path.
185
+ *
186
+ * `/inheritance:r` drops what the parent offered (this is the part that matters:
187
+ * adding a grant without removing inheritance leaves `Users` in place), and
188
+ * `(OI)(CI)` makes a directory's grant apply to what is created inside it —
189
+ * without it every new lock file would need its own call.
190
+ */
191
+ export declare function restrictPathArgs(path: string, user: string, options?: {
192
+ directory?: boolean;
193
+ }): string[];
194
+ /** Directories and files whose contents must not be readable by other users. */
195
+ export interface RestrictDeps {
196
+ /** Runner for `icacls`; injected by tests. Returns true on success. */
197
+ run?: (command: string, args: string[]) => boolean;
198
+ platform?: NodeJS.Platform;
199
+ env?: NodeJS.ProcessEnv;
200
+ }
201
+ /**
202
+ * Apply `mode` on POSIX, a single-user ACL on Windows.
203
+ *
204
+ * Returns whether the restriction was applied. Never throws: the caller has
205
+ * already written the file, and a permission call is not worth losing it over.
206
+ */
207
+ export declare function restrictPathToUser(path: string, options?: {
208
+ mode: number;
209
+ directory?: boolean;
210
+ } & RestrictDeps): Promise<boolean>;
211
+ /** Synchronous twin, for the sites that create their file with `openSync`. */
212
+ export declare function restrictPathToUserSync(path: string, options?: {
213
+ mode: number;
214
+ directory?: boolean;
215
+ } & RestrictDeps): boolean;
@@ -20,21 +20,13 @@
20
20
  * every later launch falls back to the default.
21
21
  * @module dsh-ssh-tui/preset-authoring
22
22
  */
23
- import { type AgentPreset, type PresetMetadata } from '@deepseek-ai/dsh-agent-presets';
23
+ import { presetDirectory, type AgentPreset, type PresetMetadata } from './preset-compat.js';
24
+ export { presetDirectory };
24
25
  /** The id shape discovery accepts; a directory that fails it is skipped. */
25
26
  export declare const PRESET_ID_PATTERN: RegExp;
26
27
  /** Whether an id would be discovered at all. */
27
28
  export declare function validatePresetId(id: string): boolean;
28
- /**
29
- * The directory a preset owns.
30
- *
31
- * `AgentPreset.path` is the composition file the preset publishes, not the
32
- * directory, so anything that writes beside it (display metadata) starts here.
33
- * @param preset - a discovered preset.
34
- * @returns the absolute preset directory.
35
- */
36
- export declare function presetDirectory(preset: AgentPreset): string;
37
- export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'running' | 'empty-metadata';
29
+ export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'managed' | 'running' | 'empty-metadata';
38
30
  export interface PresetRefusal {
39
31
  error: PresetRefusalCode;
40
32
  /** The id the refusal is about, when it names one. */
@@ -78,9 +70,11 @@ export interface PresetCompositionView {
78
70
  }
79
71
  /**
80
72
  * The authoring subset of the service, feature-detected rather than assumed:
81
- * the plugin still supports 0.1.2-rc.1, whose `agentPresets` may predate
82
- * `copy`, `remove`, `read`, and `compositionInventory`. A missing member turns
83
- * into a clear message instead of a crash.
73
+ * the two supported lines expose different preset services under the same
74
+ * `agentPresets` name. 0.1.5's `dsh-agent-presets` carries `copy`, `remove`,
75
+ * `read` and `compositionInventory`; the 0.1.7 registry
76
+ * (`dsh-agent-preset-registry`) does not. A missing member turns into a clear
77
+ * message instead of a crash.
84
78
  */
85
79
  export interface PresetAuthoringApi {
86
80
  readonly authorable?: boolean;
@@ -0,0 +1,58 @@
1
+ /** The optional display-metadata file beside a preset's composition. */
2
+ export declare const METADATA_FILE = "preset.yml";
3
+ /** Display text a preset may publish about itself. */
4
+ export interface PresetMetadata {
5
+ /** Human-facing name; falls back to the preset id when absent. */
6
+ readonly name?: string;
7
+ /** One sentence on what this preset is for. */
8
+ readonly description?: string;
9
+ /** Position within its group; lower comes first. */
10
+ readonly order?: number;
11
+ }
12
+ /** Where a preset's composition came from, on the line that still records it. */
13
+ export type PresetTrust = 'system' | 'user';
14
+ /**
15
+ * One preset, as much of it as this plugin reads.
16
+ *
17
+ * `id`/`name`/`description`/`order`/`broken` are the fields both lines agree on.
18
+ * `path` (the composition file) and `trust` (the root it was discovered under)
19
+ * only exist on the 0.1.5 line; every consumer here treats them as optional,
20
+ * and the authoring paths refuse rather than guess when they are gone.
21
+ */
22
+ export interface AgentPreset {
23
+ readonly id: string;
24
+ readonly name?: string;
25
+ readonly description?: string;
26
+ readonly order?: number;
27
+ readonly broken?: string;
28
+ /** 0.1.5 line only: absolute path of the preset's agent composition file. */
29
+ readonly path?: string;
30
+ /** 0.1.5 line only: trust recorded from the root it was discovered under. */
31
+ readonly trust?: PresetTrust;
32
+ }
33
+ /**
34
+ * The directory a preset owns, when the host still exposes one.
35
+ *
36
+ * `AgentPreset.path` is the composition file the preset publishes, not the
37
+ * directory, so anything that writes beside it (display metadata) starts here.
38
+ * `undefined` on a host that lists declarative presets instead of directories.
39
+ * @param preset - a discovered preset.
40
+ * @returns the absolute preset directory, or undefined when there is none.
41
+ */
42
+ export declare function presetDirectory(preset: AgentPreset): string | undefined;
43
+ /**
44
+ * Read a preset's display metadata. Every failure degrades to no metadata: a
45
+ * preset whose display text is missing, malformed, or unreadable still mounts.
46
+ * @param directory - the preset directory to read `preset.yml` from.
47
+ * @returns the published name/description/order, or `{}`.
48
+ */
49
+ export declare function readPresetMetadata(directory: string): Promise<PresetMetadata>;
50
+ /**
51
+ * Render display metadata as the file's contents.
52
+ *
53
+ * Absent fields are omitted rather than written empty, so a preset with no
54
+ * description does not ship a key that reads as an intentional blank.
55
+ * @param metadata - the display text to store.
56
+ * @returns the YAML document, or undefined when there is nothing to store.
57
+ */
58
+ export declare function renderPresetMetadata(metadata: PresetMetadata): string | undefined;
@@ -9,7 +9,7 @@
9
9
  * service — the caller passes what `list()` returned.
10
10
  * @module dsh-ssh-tui/preset-picker
11
11
  */
12
- import type { AgentPreset } from '@deepseek-ai/dsh-agent-presets';
12
+ import type { AgentPreset } from './preset-compat.js';
13
13
  /** One option as the question dialog wants it, plus the id it stands for. */
14
14
  export interface PresetPickerOption {
15
15
  id: string;