dsh-ssh-tui 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +63 -38
- package/README.md +44 -20
- package/cordis.patch.yml +6 -0
- package/docs/screenshots/compare.png +0 -0
- package/docs/screenshots/headless.png +0 -0
- package/docs/screenshots/slow-link.gif +0 -0
- package/docs/screenshots/workspace.png +0 -0
- package/lib/approval-reviewer.js +62 -0
- package/lib/approval-reviewer.js.map +1 -0
- package/lib/auto-approval.js +124 -0
- package/lib/auto-approval.js.map +1 -0
- package/lib/display-sock.js +583 -0
- package/lib/display-sock.js.map +1 -0
- package/lib/dsh-compat.js +45 -0
- package/lib/dsh-compat.js.map +1 -0
- package/lib/i18n/en.js +108 -14
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +2 -1
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +108 -14
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +110 -6
- package/lib/index.js.map +1 -1
- package/lib/picker.js +14 -1
- package/lib/picker.js.map +1 -1
- package/lib/provider-catalog.js +169 -0
- package/lib/provider-catalog.js.map +1 -0
- package/lib/route-memory.js +1 -1
- package/lib/route-memory.js.map +1 -1
- package/lib/session-list.js +116 -11
- package/lib/session-list.js.map +1 -1
- package/lib/session-lock.js +182 -6
- package/lib/session-lock.js.map +1 -1
- package/lib/startup.js +2 -0
- package/lib/startup.js.map +1 -1
- package/lib/subagent-model.js +1 -1
- package/lib/subagent-model.js.map +1 -1
- package/lib/tui.js +1759 -361
- package/lib/tui.js.map +1 -1
- package/lib/types/approval-reviewer.d.ts +20 -0
- package/lib/types/auto-approval.d.ts +42 -0
- package/lib/types/display-sock.d.ts +74 -0
- package/lib/types/dsh-compat.d.ts +30 -0
- package/lib/types/i18n/index.d.ts +2 -0
- package/lib/types/picker.d.ts +4 -0
- package/lib/types/provider-catalog.d.ts +35 -0
- package/lib/types/session-list.d.ts +8 -1
- package/lib/types/session-lock.d.ts +51 -0
- package/lib/types/tui.d.ts +208 -24
- package/package.json +46 -30
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ReviewInput {
|
|
2
|
+
userText: string;
|
|
3
|
+
segments: string[];
|
|
4
|
+
toolName: string;
|
|
5
|
+
command: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ReviewVerdict {
|
|
8
|
+
risk: 'low' | 'medium' | 'high';
|
|
9
|
+
authorization: 'yes' | 'no' | 'unknown';
|
|
10
|
+
approved: boolean;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const REVIEW_SYSTEM_PROMPT: string;
|
|
14
|
+
/** Assemble the compact, fence-marked user message for the reviewer. */
|
|
15
|
+
export declare function buildReviewUserMessage(input: ReviewInput): string;
|
|
16
|
+
/**
|
|
17
|
+
* Parse the reviewer's one-line JSON verdict. Anything unreadable, missing
|
|
18
|
+
* fields, or with invalid enum values returns undefined (fail-safe).
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseReviewOutput(text: string): ReviewVerdict | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex-style auto-approval classifier for the /approval auto mode.
|
|
3
|
+
*
|
|
4
|
+
* This is a UX heuristic, not a security boundary. The contract mirrors
|
|
5
|
+
* Codex's unattended posture: allow-shaped commands (reads, builds, tests,
|
|
6
|
+
* git reads) approve automatically, danger-shaped commands are REJECTED with
|
|
7
|
+
* the model informed (it adapts instead of paging the human), and only
|
|
8
|
+
* genuinely unrecognized shapes fall through to a prompt — which detaches to
|
|
9
|
+
* a rejection when nobody is watching. Real damage containment still comes
|
|
10
|
+
* from the sandbox preset and git.
|
|
11
|
+
*/
|
|
12
|
+
export type AutoApprovalMode = 'off' | 'auto';
|
|
13
|
+
export type ApprovalDecision = 'allow' | 'deny' | 'ask';
|
|
14
|
+
/**
|
|
15
|
+
* Whole-command danger patterns, checked before anything else. A match keeps
|
|
16
|
+
* the interactive prompt regardless of what else the command contains.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DANGER_PATTERNS: RegExp[];
|
|
19
|
+
/**
|
|
20
|
+
* Segment-level allow patterns: low-risk, high-frequency reads, builds, and
|
|
21
|
+
* tests. A command auto-approves only when EVERY segment matches one of these.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ALLOW_SEGMENT_PATTERNS: RegExp[];
|
|
24
|
+
/**
|
|
25
|
+
* Classify one shell command line. Danger anywhere auto-rejects; otherwise
|
|
26
|
+
* the command auto-approves only when every segment is a recognized low-risk
|
|
27
|
+
* pattern — unknown shapes ask (and detach to a rejection when unattended).
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifyCommand(command: string): ApprovalDecision;
|
|
30
|
+
/**
|
|
31
|
+
* Classify one approval request. `command` is the decoded shell command when
|
|
32
|
+
* the pending call is a shell tool. 'deny' auto-rejects (the model reads the
|
|
33
|
+
* rejection and adapts); 'ask' falls through to the interactive prompt.
|
|
34
|
+
*/
|
|
35
|
+
export declare function classifyApproval(toolName: string, command: string | undefined): ApprovalDecision;
|
|
36
|
+
/**
|
|
37
|
+
* Decode the shell command of a streamed tool call from its raw JSON args.
|
|
38
|
+
* Returns undefined for non-shell tools or unparseable args.
|
|
39
|
+
*/
|
|
40
|
+
export declare function commandFromArgs(toolName: string, args: string): string | undefined;
|
|
41
|
+
/** Parse the /approval argument into a mode. */
|
|
42
|
+
export declare function parseAutoApprovalMode(raw: string): AutoApprovalMode | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const FRAME_STDIN = 1;
|
|
2
|
+
export declare const FRAME_STDOUT = 2;
|
|
3
|
+
export declare const FRAME_RESIZE = 3;
|
|
4
|
+
export declare const FRAME_HELLO = 4;
|
|
5
|
+
export declare const FRAME_GOODBYE = 5;
|
|
6
|
+
export declare const FRAME_RTT = 6;
|
|
7
|
+
/**
|
|
8
|
+
* Drop launcher SIGTERM/SIGINT/SIGHUP so closing SSH cannot dispose the tree
|
|
9
|
+
* before hangup handling. Leaving the session with setsid() is best-effort:
|
|
10
|
+
* a TTY session leader gets EPERM and stays in the SSH process group.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detachFromSshSession(): void;
|
|
13
|
+
export declare const TUI_HOST_ENV = "DSH_TUI_HOST";
|
|
14
|
+
export declare function isTuiHostProcess(env?: NodeJS.ProcessEnv): boolean;
|
|
15
|
+
export declare function sessionSockPath(sessionId: string, dshHome?: string): string;
|
|
16
|
+
export declare function encodeFrame(type: number, payload?: Buffer): Buffer;
|
|
17
|
+
export declare function encodeResize(columns: number, rows: number): Buffer;
|
|
18
|
+
export declare function decodeResize(payload: Buffer): {
|
|
19
|
+
columns: number;
|
|
20
|
+
rows: number;
|
|
21
|
+
} | undefined;
|
|
22
|
+
export declare function encodeRtt(rttMs: number | undefined): Buffer;
|
|
23
|
+
export declare function decodeRtt(payload: Buffer): number | undefined;
|
|
24
|
+
/** Incremental decoder for one socket. */
|
|
25
|
+
export declare class FrameReader {
|
|
26
|
+
private buffer;
|
|
27
|
+
push(chunk: Buffer): Array<{
|
|
28
|
+
type: number;
|
|
29
|
+
payload: Buffer;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface DisplayHostHandlers {
|
|
33
|
+
onStdin(bytes: Buffer): void;
|
|
34
|
+
onResize(columns: number, rows: number): void;
|
|
35
|
+
onRtt?(rttMs: number | undefined): void;
|
|
36
|
+
onDetach(): void;
|
|
37
|
+
onAttach(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Host-side listener. At most one Display is attached; a new hello kicks the
|
|
41
|
+
* previous relay so two SSH windows cannot both drive the session.
|
|
42
|
+
*/
|
|
43
|
+
export declare class DisplayHost {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
private readonly handlers;
|
|
46
|
+
private server;
|
|
47
|
+
private socket;
|
|
48
|
+
private reader;
|
|
49
|
+
attached: boolean;
|
|
50
|
+
constructor(path: string, handlers: DisplayHostHandlers);
|
|
51
|
+
listen(): Promise<void>;
|
|
52
|
+
private accept;
|
|
53
|
+
sendStdout(bytes: Buffer | string): boolean;
|
|
54
|
+
sendGoodbye(): void;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function waitForDisplaySock(path: string, timeoutMs?: number, pid?: number, errFile?: string): Promise<void>;
|
|
58
|
+
export declare function hostArgvForSession(sessionId: string, argv?: string[], execArgv?: string[]): string[];
|
|
59
|
+
/** Spawn a detached Host copy of this `dsh` invocation and return its sock path. */
|
|
60
|
+
export declare function spawnDetachedHost(sessionId: string): {
|
|
61
|
+
pid: number;
|
|
62
|
+
sock: string;
|
|
63
|
+
errFile?: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function probeDisplaySock(path: string, timeoutMs?: number): Promise<boolean>;
|
|
66
|
+
export interface RelayResult {
|
|
67
|
+
/** Host sent goodbye — user exited from the attached session. */
|
|
68
|
+
reason: 'goodbye' | 'host-closed' | 'signal';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Turn this process into a Display relay until the Host hangs up or the
|
|
72
|
+
* local TTY dies. Restores the terminal before resolving.
|
|
73
|
+
*/
|
|
74
|
+
export declare function runDisplayRelay(path: string): Promise<RelayResult>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-stack shims for dsh 0.1.1-rc.2 and 0.1.2-rc.1.
|
|
3
|
+
*
|
|
4
|
+
* 0.1.2 turned the settings free functions into `SettingsProvider` methods,
|
|
5
|
+
* replaced `Session.events` with on-demand readers, and started branding
|
|
6
|
+
* namespaces at the type level only. Every shim here keeps the same runtime
|
|
7
|
+
* value on both hosts and picks the API shape that is actually present.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
11
|
+
import type { SettingsNamespace, SettingsSectionHooks } from '@deepseek-ai/dsh-settings';
|
|
12
|
+
import type z from '@deepseek-ai/schemastery';
|
|
13
|
+
/**
|
|
14
|
+
* 0.1.1-rc.2 wraps namespaces via `settingsNamespace()`; 0.1.2 brands them at
|
|
15
|
+
* the type level and takes the plain string at runtime. A cast covers both.
|
|
16
|
+
*/
|
|
17
|
+
export declare function settingsNamespace(value: string): SettingsNamespace;
|
|
18
|
+
/**
|
|
19
|
+
* Register a settings section: 0.1.2-rc.1 moved the free function onto the
|
|
20
|
+
* `settings` service as `installSection`, callable only once that service is
|
|
21
|
+
* injected (plugins apply before it, so `ctx.inject` must defer — same
|
|
22
|
+
* pattern the harness's own packages use); 0.1.1-rc.2 keeps the free
|
|
23
|
+
* function, which defers internally and is safe at apply time.
|
|
24
|
+
*/
|
|
25
|
+
export declare function installSettingsSection<T>(ctx: Context, ns: SettingsNamespace, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Read the full durable event log: 0.1.2-rc.1 replaced the `Session.events`
|
|
28
|
+
* property with on-demand readers; 0.1.1-rc.2 still exposes the property.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sessionEvents(session: object): readonly SessionEvent[];
|
|
@@ -14,10 +14,12 @@ export declare const UI_LOCALE_SCHEMA: z<Schemastery.ObjectS<{
|
|
|
14
14
|
language: z<string, string>;
|
|
15
15
|
skipUpdate: z<string, string>;
|
|
16
16
|
view: z<string, string>;
|
|
17
|
+
disconnect: z<string, string>;
|
|
17
18
|
}>, Schemastery.ObjectT<{
|
|
18
19
|
language: z<string, string>;
|
|
19
20
|
skipUpdate: z<string, string>;
|
|
20
21
|
view: z<string, string>;
|
|
22
|
+
disconnect: z<string, string>;
|
|
21
23
|
}>>;
|
|
22
24
|
export declare function localeFromTag(tag: string): Locale | undefined;
|
|
23
25
|
/** Pick zh/en from env, optionally after a saved settings value. */
|
package/lib/types/picker.d.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface CatalogPreset {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
modelIds: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Filter presets by a case-insensitive substring match on id or name. */
|
|
8
|
+
export declare function filterCatalogPresets(presets: readonly CatalogPreset[], query: string): CatalogPreset[];
|
|
9
|
+
/** One selectable provider row in the /setup wizard's first step. */
|
|
10
|
+
export interface ProviderListEntry {
|
|
11
|
+
/** Unique key: `template:<type>` for the pinned rows, `catalog:<id>` otherwise. */
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
catalog?: CatalogPreset;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Merge the pinned templates with the web-catalog presets into one list:
|
|
19
|
+
* catalog ids in `dedupeIds` drop out (the template already covers them),
|
|
20
|
+
* and `query` filters by case-insensitive substring on label/key/detail.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeProviderEntries(templates: readonly ProviderListEntry[], presets: readonly CatalogPreset[], dedupeIds: readonly string[], query: string): ProviderListEntry[];
|
|
23
|
+
/**
|
|
24
|
+
* Read the catalog. Anchors are module paths that plausibly sit inside the dsh
|
|
25
|
+
* install: the running CLI entry (`process.argv[1]`) and the plugin's module
|
|
26
|
+
* base. Returns undefined when no reachable install ships a usable catalog or
|
|
27
|
+
* the child does not answer within 10 seconds.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readProviderCatalog(anchors: Array<string | undefined>): Promise<CatalogPreset[] | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Read the catalog once per process and memoize the result: the child needs a
|
|
32
|
+
* moment to load pi-ai, so callers warm it at startup and reuse the settled
|
|
33
|
+
* value.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadProviderCatalog(anchors: Array<string | undefined>): Promise<CatalogPreset[] | undefined>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* both surfaces offer the same sessions.
|
|
5
5
|
*/
|
|
6
6
|
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
|
|
7
|
+
import { listAttachableHosts } from './session-lock.js';
|
|
7
8
|
/** Last path segment for the footer chip (`\root\genshin\srv` → `srv`). */
|
|
8
9
|
export declare function sessionCwdLabel(cwd: string): string;
|
|
9
10
|
export declare function formatFooterCwd(cwd: string): string;
|
|
@@ -28,7 +29,13 @@ export interface ResumableSession {
|
|
|
28
29
|
cwd: string;
|
|
29
30
|
/** Whether the full event log could not be inspected (corrupt/unsupported). */
|
|
30
31
|
unreadable?: boolean;
|
|
32
|
+
/** Live Host that a new SSH can attach to. */
|
|
33
|
+
attach?: {
|
|
34
|
+
pid: number;
|
|
35
|
+
sock: string;
|
|
36
|
+
state?: string;
|
|
37
|
+
};
|
|
31
38
|
}
|
|
32
39
|
/** `MM-DD HH:mm` local-time label for session lists. */
|
|
33
40
|
export declare function formatSessionTime(timestamp: number): string;
|
|
34
|
-
export declare function listResumableSessions(persistence: SessionPersistence, currentId: string): Promise<ResumableSession[]>;
|
|
41
|
+
export declare function listResumableSessions(persistence: SessionPersistence, currentId: string, listHosts?: typeof listAttachableHosts): Promise<ResumableSession[]>;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
export type SessionLockState = 'attached' | 'paused' | 'running-detached';
|
|
2
|
+
export type DisconnectPolicy = 'pause' | 'continue';
|
|
3
|
+
export type SessionLockAgentStatus = 'idle' | 'running' | 'cancelling';
|
|
1
4
|
export interface SessionLockInfo {
|
|
2
5
|
pid: number;
|
|
3
6
|
sessionId: string;
|
|
4
7
|
startedAt: string;
|
|
8
|
+
/** `/proc/sys/kernel/random/boot_id` when the lock was taken (POSIX only). */
|
|
9
|
+
bootId?: string;
|
|
10
|
+
/** `/proc/<pid>/stat` starttime (field 22) of `pid` when the lock was taken. */
|
|
11
|
+
pidStart?: string;
|
|
5
12
|
tty?: string;
|
|
13
|
+
sock?: string;
|
|
14
|
+
state?: SessionLockState;
|
|
15
|
+
disconnectPolicy?: DisconnectPolicy;
|
|
16
|
+
agentStatus?: SessionLockAgentStatus;
|
|
6
17
|
}
|
|
7
18
|
export declare class SessionLockHeldError extends Error {
|
|
8
19
|
readonly lock: SessionLockInfo;
|
|
@@ -13,14 +24,54 @@ export declare function sessionLockPath(sessionId: string, dshHome?: string): st
|
|
|
13
24
|
export declare function parseSessionLock(raw: string): SessionLockInfo | undefined;
|
|
14
25
|
/** True when `pid` still exists on this machine (best-effort). */
|
|
15
26
|
export declare function processIsAlive(pid: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* True when `pid` is genuinely the Host process that wrote `lock`.
|
|
29
|
+
*
|
|
30
|
+
* `processIsAlive` alone is not enough: the pid may have been recorded inside
|
|
31
|
+
* a different pid namespace (sandbox/container) and then be recycled by an
|
|
32
|
+
* unrelated host process — e.g. a lock written as pid 5 in a sandbox matches
|
|
33
|
+
* the forever-alive pid 5 kernel thread on the host, which used to produce a
|
|
34
|
+
* permanent false "zombie" that blocked `--resume`. We therefore verify the
|
|
35
|
+
* process identity:
|
|
36
|
+
* - new locks carry `bootId` + `pidStart` (boot_id + /proc/<pid>/stat
|
|
37
|
+
* starttime): a matching pair can only be the same process on the same
|
|
38
|
+
* boot, so a recycled or cross-namespace pid fails the check;
|
|
39
|
+
* - older locks fall back to `/proc/<pid>/cmdline`: the detached Host is
|
|
40
|
+
* always launched with `--resume=<sessionId>` in argv, so any other
|
|
41
|
+
* process (kernel threads have an empty cmdline) is proven stale.
|
|
42
|
+
* On platforms without procfs the legacy kill(pid, 0) behavior is kept.
|
|
43
|
+
*/
|
|
44
|
+
export declare function lockOwnerIsAlive(lock: SessionLockInfo): boolean;
|
|
16
45
|
export declare function formatLockHeldMessage(lock: SessionLockInfo): string;
|
|
17
46
|
export declare function sessionLockDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
47
|
+
export declare function readSessionLock(sessionId: string, dshHome?: string): Promise<{
|
|
48
|
+
path: string;
|
|
49
|
+
info: SessionLockInfo;
|
|
50
|
+
} | undefined>;
|
|
51
|
+
export declare function writeSessionLock(path: string, info: SessionLockInfo): Promise<void>;
|
|
18
52
|
export declare function acquireSessionLock(sessionId: string, options?: {
|
|
19
53
|
pid?: number;
|
|
20
54
|
tty?: string | null;
|
|
21
55
|
dshHome?: string;
|
|
56
|
+
sock?: string;
|
|
57
|
+
state?: SessionLockState;
|
|
58
|
+
disconnectPolicy?: DisconnectPolicy;
|
|
59
|
+
agentStatus?: SessionLockAgentStatus;
|
|
22
60
|
}): Promise<{
|
|
23
61
|
path: string;
|
|
24
62
|
info: SessionLockInfo;
|
|
25
63
|
}>;
|
|
26
64
|
export declare function releaseSessionLock(path: string, pid?: number): Promise<void>;
|
|
65
|
+
export type LiveHostKind = 'attachable' | 'zombie';
|
|
66
|
+
export declare function inspectLiveHost(sessionId: string, dshHome?: string): Promise<{
|
|
67
|
+
kind: LiveHostKind;
|
|
68
|
+
lock: SessionLockInfo;
|
|
69
|
+
path: string;
|
|
70
|
+
sock: string;
|
|
71
|
+
} | undefined>;
|
|
72
|
+
/** Every lock file under `$DSH_HOME/tui-locks` whose Host pid is still alive. */
|
|
73
|
+
export declare function listAttachableHosts(dshHome?: string): Promise<Array<{
|
|
74
|
+
sessionId: string;
|
|
75
|
+
lock: SessionLockInfo;
|
|
76
|
+
sock: string;
|
|
77
|
+
}>>;
|