@itookit/dsht 0.2.4 → 0.3.2

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 (129) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +54 -25
  3. package/README.zh.md +54 -25
  4. package/dist/catalog/controller.d.ts +32 -0
  5. package/dist/catalog/controller.js +88 -0
  6. package/dist/catalog/index.d.ts +2 -0
  7. package/dist/catalog/index.js +2 -0
  8. package/dist/{cli.js → cli/index.js} +40 -24
  9. package/dist/controller/connection.d.ts +80 -0
  10. package/dist/controller/connection.js +190 -0
  11. package/dist/controller/controller.d.ts +260 -0
  12. package/dist/controller/controller.js +362 -0
  13. package/dist/controller/index.d.ts +5 -0
  14. package/dist/controller/index.js +3 -0
  15. package/dist/controller/memory-log.d.ts +35 -0
  16. package/dist/controller/memory-log.js +95 -0
  17. package/dist/cost/controller.d.ts +34 -0
  18. package/dist/cost/controller.js +107 -0
  19. package/dist/cost/index.d.ts +9 -0
  20. package/dist/cost/index.js +7 -0
  21. package/dist/cost/ledger-files.d.ts +20 -0
  22. package/dist/cost/ledger-files.js +128 -0
  23. package/dist/cost/ledger.d.ts +65 -0
  24. package/dist/cost/ledger.js +136 -0
  25. package/dist/cost/pricing.d.ts +48 -0
  26. package/dist/cost/pricing.js +141 -0
  27. package/dist/cost/records.d.ts +17 -0
  28. package/dist/cost/records.js +66 -0
  29. package/dist/cost/scanner.d.ts +22 -0
  30. package/dist/cost/scanner.js +95 -0
  31. package/dist/cost/types.d.ts +69 -0
  32. package/dist/cost/types.js +3 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +2 -0
  35. package/dist/session/connection-view.d.ts +18 -0
  36. package/dist/session/connection-view.js +1 -0
  37. package/dist/{controller.d.ts → session/controller.d.ts} +119 -137
  38. package/dist/session/controller.js +616 -0
  39. package/dist/session/export-html.d.ts +9 -0
  40. package/dist/session/export-html.js +39 -0
  41. package/dist/session/export.d.ts +9 -0
  42. package/dist/session/export.js +38 -0
  43. package/dist/{history.d.ts → session/history.d.ts} +18 -1
  44. package/dist/session/history.js +338 -0
  45. package/dist/session/index.d.ts +17 -0
  46. package/dist/session/index.js +10 -0
  47. package/dist/session/markdown.d.ts +39 -0
  48. package/dist/session/markdown.js +255 -0
  49. package/dist/session/math.d.ts +11 -0
  50. package/dist/session/math.js +82 -0
  51. package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
  52. package/dist/{navigation.js → session/navigation.js} +1 -1
  53. package/dist/{references.js → session/references.js} +1 -1
  54. package/dist/{telemetry.d.ts → session/telemetry.d.ts} +12 -1
  55. package/dist/{telemetry.js → session/telemetry.js} +18 -4
  56. package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
  57. package/dist/{transcript.js → session/transcript.js} +36 -18
  58. package/dist/session/types.d.ts +18 -0
  59. package/dist/session/types.js +2 -0
  60. package/dist/state.d.ts +41 -0
  61. package/dist/state.js +9 -0
  62. package/dist/storage/directories.d.ts +14 -0
  63. package/dist/storage/directories.js +24 -0
  64. package/dist/storage/files.d.ts +43 -0
  65. package/dist/storage/files.js +135 -0
  66. package/dist/storage/index.d.ts +3 -0
  67. package/dist/storage/index.js +3 -0
  68. package/dist/transport/auth.js +65 -0
  69. package/dist/{client.d.ts → transport/client.d.ts} +8 -1
  70. package/dist/{client.js → transport/client.js} +21 -4
  71. package/dist/transport/host.d.ts +13 -0
  72. package/dist/transport/host.js +1 -0
  73. package/dist/ui/app.d.ts +11 -0
  74. package/dist/ui/app.js +726 -0
  75. package/dist/ui/chat/header.d.ts +11 -0
  76. package/dist/ui/chat/header.js +14 -0
  77. package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
  78. package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
  79. package/dist/ui/chat/status.d.ts +79 -0
  80. package/dist/ui/chat/status.js +311 -0
  81. package/dist/ui/chat/viewport.d.ts +17 -0
  82. package/dist/ui/chat/viewport.js +14 -0
  83. package/dist/ui/commands/parse.d.ts +96 -0
  84. package/dist/ui/commands/parse.js +121 -0
  85. package/dist/ui/commands/registry.d.ts +33 -0
  86. package/dist/ui/commands/registry.js +72 -0
  87. package/dist/ui/copy-mode.d.ts +4 -0
  88. package/dist/ui/copy-mode.js +6 -0
  89. package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
  90. package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
  91. package/dist/ui/dialogs/index.d.ts +120 -0
  92. package/dist/ui/dialogs/index.js +113 -0
  93. package/dist/ui/dialogs/picker.d.ts +18 -0
  94. package/dist/ui/dialogs/picker.js +38 -0
  95. package/dist/ui/frozen.d.ts +8 -0
  96. package/dist/ui/frozen.js +7 -0
  97. package/dist/ui/input/references.d.ts +12 -0
  98. package/dist/ui/input/references.js +15 -0
  99. package/dist/ui/mount.d.ts +6 -0
  100. package/dist/ui/mount.js +11 -0
  101. package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
  102. package/dsht-m.png +0 -0
  103. package/package.json +20 -14
  104. package/dist/app.d.ts +0 -21
  105. package/dist/app.js +0 -746
  106. package/dist/auth.js +0 -108
  107. package/dist/controller.js +0 -923
  108. package/dist/cost.d.ts +0 -119
  109. package/dist/cost.js +0 -313
  110. package/dist/history.js +0 -177
  111. package/dist/status.d.ts +0 -28
  112. package/dist/status.js +0 -157
  113. package/dsht.png +0 -0
  114. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  115. /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
  116. /package/dist/{memory.js → session/memory.js} +0 -0
  117. /package/dist/{references.d.ts → session/references.d.ts} +0 -0
  118. /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
  119. /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
  120. /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
  121. /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
  122. /package/dist/{wire.js → transport/wire.js} +0 -0
  123. /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
  124. /package/dist/{input-history.js → ui/input/history.js} +0 -0
  125. /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
  126. /package/dist/{input.js → ui/input/input.js} +0 -0
  127. /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
  128. /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
  129. /package/dist/{theme.js → ui/theme/index.js} +0 -0
@@ -0,0 +1,18 @@
1
+ /** Session-domain result types shared by the controller facade and the terminal UI. */
2
+ /** Resolved navigation-removal identity; empty marks a fresh blank, idle session eligible for immediate archival. */
3
+ export interface RemovalTarget {
4
+ kind: 'workspace' | 'session';
5
+ id: string;
6
+ name: string;
7
+ path?: string;
8
+ empty?: boolean;
9
+ }
10
+ /** Bounded search results contain navigation summaries, never complete message bodies. */
11
+ export interface HistorySearch {
12
+ items: {
13
+ seq: number;
14
+ role: string;
15
+ preview: string;
16
+ }[];
17
+ truncated: boolean;
18
+ }
@@ -0,0 +1,2 @@
1
+ /** Session-domain result types shared by the controller facade and the terminal UI. */
2
+ export {};
@@ -0,0 +1,41 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from './session/transcript.ts';
3
+ import type { HistorySearch, RemovalTarget } from './session/types.ts';
4
+ import type { ObjectValue } from './transport/wire.ts';
5
+ /** State shared by the picker and conversation view. */
6
+ export interface State {
7
+ version: number;
8
+ online: boolean;
9
+ busy: boolean;
10
+ screen: 'workspaces' | 'sessions' | 'chat' | 'path';
11
+ status: string;
12
+ error: string;
13
+ workspaces: ObjectValue[];
14
+ sessions: ObjectValue[];
15
+ showAllSessions: boolean;
16
+ workspaceId?: string;
17
+ sessionId?: string;
18
+ pending: ObjectValue[];
19
+ controlError?: string;
20
+ modelError?: string;
21
+ presetError?: string;
22
+ presets?: ObjectValue[];
23
+ defaultModel?: ObjectValue;
24
+ transcript: Transcript;
25
+ }
26
+ /** The state contract every domain controller writes through. */
27
+ export interface ControllerStore {
28
+ /** Current immutable state snapshot. */
29
+ readonly state: State;
30
+ /** Publish a state patch and notify observers. */
31
+ update(patch: Partial<State>): void;
32
+ /** Monotonic selector generation; a changed value discards in-flight session work. */
33
+ selection(): number;
34
+ /** Advance the selector generation when the selected workspace or session changes. */
35
+ bumpSelection(): void;
36
+ }
37
+ /** Build the initial state before any connection exists.
38
+ * @returns A fresh state whose transcript is empty and disconnected.
39
+ */
40
+ export declare function initialState(): State;
41
+ export type { HistorySearch, RemovalTarget };
package/dist/state.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Application state shared by the controller facade and every domain controller. */
2
+ import { Transcript } from "./session/transcript.js";
3
+ /** Build the initial state before any connection exists.
4
+ * @returns A fresh state whose transcript is empty and disconnected.
5
+ */
6
+ export function initialState() {
7
+ return { version: 0, online: false, busy: false, screen: 'workspaces', status: 'Connecting…',
8
+ error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [], transcript: new Transcript() };
9
+ }
@@ -0,0 +1,14 @@
1
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
2
+ * @param path - Directory path.
3
+ */
4
+ export declare function ensureDirectory(path: string): Promise<void>;
5
+ /** Create a directory tree and require it to stay owner-only.
6
+ * @param path - Directory path.
7
+ * @param label - Name used in the validation error, such as `Cookie directory`.
8
+ */
9
+ export declare function ensurePrivateDirectory(path: string, label: string): Promise<void>;
10
+ /** List the entry names of one directory.
11
+ * @param path - Directory path.
12
+ * @returns Entry names in filesystem order.
13
+ */
14
+ export declare function listEntries(path: string): Promise<string[]>;
@@ -0,0 +1,24 @@
1
+ /** Private directory creation and listing for the client's configuration and state. */
2
+ import { lstat, mkdir, readdir } from 'node:fs/promises';
3
+ /** Create a directory tree with owner-only permissions, leaving an existing directory as it is.
4
+ * @param path - Directory path.
5
+ */
6
+ export async function ensureDirectory(path) {
7
+ await mkdir(path, { recursive: true, mode: 0o700 });
8
+ }
9
+ /** Create a directory tree and require it to stay owner-only.
10
+ * @param path - Directory path.
11
+ * @param label - Name used in the validation error, such as `Cookie directory`.
12
+ */
13
+ export async function ensurePrivateDirectory(path, label) {
14
+ await ensureDirectory(path);
15
+ const stat = await lstat(path);
16
+ if (!stat.isDirectory() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
17
+ throw new Error(`${label} must be owned by the current user with permissions 0700`);
18
+ }
19
+ }
20
+ /** List the entry names of one directory.
21
+ * @param path - Directory path.
22
+ * @returns Entry names in filesystem order.
23
+ */
24
+ export async function listEntries(path) { return readdir(path); }
@@ -0,0 +1,43 @@
1
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
2
+ * @param path - Absolute file path.
3
+ * @returns File contents, or undefined when the file does not exist.
4
+ */
5
+ export declare function readText(path: string): Promise<string | undefined>;
6
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
7
+ * @param path - Absolute file path.
8
+ * @param label - Name used in the validation error, such as `Cookie file`.
9
+ * @returns File contents, or undefined when the file does not exist.
10
+ */
11
+ export declare function readPrivateFile(path: string, label: string): Promise<string | undefined>;
12
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
13
+ * @param path - Destination path.
14
+ * @param contents - Complete file contents.
15
+ */
16
+ export declare function writePrivateFile(path: string, contents: string): Promise<void>;
17
+ /** Create an owner-only file only when it does not exist.
18
+ * @param path - Destination path.
19
+ * @param contents - Complete file contents.
20
+ * @returns False when the file already existed, leaving it untouched.
21
+ */
22
+ export declare function createPrivateFile(path: string, contents: string): Promise<boolean>;
23
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
24
+ *
25
+ * The destination is created before the source is requested, so an existing file fails without
26
+ * contacting the host, and a source or streaming failure removes the partial file.
27
+ * @param path - Destination path; an existing file is never replaced.
28
+ * @param source - Produces the byte stream once the destination exists.
29
+ * @param signal - Cancels the write and removes the partial file.
30
+ */
31
+ export declare function writeExclusiveStream(path: string, source: () => Promise<AsyncIterable<Uint8Array>>, signal?: AbortSignal): Promise<void>;
32
+ /** Append to an owner-only file, creating it when absent.
33
+ *
34
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
35
+ * rewrite it periodically with `writePrivateFile`.
36
+ * @param path - Destination path.
37
+ * @param contents - Text to append.
38
+ */
39
+ export declare function appendPrivateFile(path: string, contents: string): Promise<void>;
40
+ /** Remove a file, treating an already absent file as success.
41
+ * @param path - File to remove.
42
+ */
43
+ export declare function removeFile(path: string): Promise<void>;
@@ -0,0 +1,135 @@
1
+ /** Every filesystem read and write the client performs; no other module opens a file. */
2
+ import { constants } from 'node:fs';
3
+ import { open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { dirname, join } from 'node:path';
6
+ /** Read a UTF-8 file; an absent file is an ordinary result, not a failure.
7
+ * @param path - Absolute file path.
8
+ * @returns File contents, or undefined when the file does not exist.
9
+ */
10
+ export async function readText(path) {
11
+ try {
12
+ return await readFile(path, 'utf8');
13
+ }
14
+ catch (error) {
15
+ if (isMissing(error))
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ /** Read a user-private file, rejecting a symlink, a foreign owner, or group/other access.
21
+ * @param path - Absolute file path.
22
+ * @param label - Name used in the validation error, such as `Cookie file`.
23
+ * @returns File contents, or undefined when the file does not exist.
24
+ */
25
+ export async function readPrivateFile(path, label) {
26
+ let handle;
27
+ try {
28
+ handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
29
+ }
30
+ catch (error) {
31
+ if (isMissing(error))
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ try {
36
+ const stat = await handle.stat();
37
+ if (!stat.isFile() || process.platform !== 'win32' && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) {
38
+ throw new Error(`${label} must be owned by the current user with permissions 0600`);
39
+ }
40
+ return await handle.readFile('utf8');
41
+ }
42
+ finally {
43
+ await handle.close();
44
+ }
45
+ }
46
+ /** Replace a file atomically with owner-only contents, leaving no partial file behind.
47
+ * @param path - Destination path.
48
+ * @param contents - Complete file contents.
49
+ */
50
+ export async function writePrivateFile(path, contents) {
51
+ const temporary = join(dirname(path), `${randomUUID()}.tmp`);
52
+ try {
53
+ await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
54
+ await rename(temporary, path);
55
+ }
56
+ finally {
57
+ await removeFile(temporary);
58
+ }
59
+ }
60
+ /** Create an owner-only file only when it does not exist.
61
+ * @param path - Destination path.
62
+ * @param contents - Complete file contents.
63
+ * @returns False when the file already existed, leaving it untouched.
64
+ */
65
+ export async function createPrivateFile(path, contents) {
66
+ try {
67
+ await writeFile(path, contents, { mode: 0o600, flag: 'wx' });
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (isCode(error, 'EEXIST'))
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
76
+ /** Stream bytes into a new owner-only file, removing it unless the write completed.
77
+ *
78
+ * The destination is created before the source is requested, so an existing file fails without
79
+ * contacting the host, and a source or streaming failure removes the partial file.
80
+ * @param path - Destination path; an existing file is never replaced.
81
+ * @param source - Produces the byte stream once the destination exists.
82
+ * @param signal - Cancels the write and removes the partial file.
83
+ */
84
+ export async function writeExclusiveStream(path, source, signal) {
85
+ const file = await open(path, 'wx', 0o600);
86
+ let complete = false;
87
+ try {
88
+ await file.writeFile(await source(), { signal });
89
+ signal?.throwIfAborted();
90
+ complete = true;
91
+ }
92
+ finally {
93
+ try {
94
+ await file.close();
95
+ }
96
+ finally {
97
+ if (!complete)
98
+ await removeFile(path);
99
+ }
100
+ }
101
+ }
102
+ /** Append to an owner-only file, creating it when absent.
103
+ *
104
+ * Appending keeps a long-lived diagnostic file cheap to extend; callers that need a bounded file
105
+ * rewrite it periodically with `writePrivateFile`.
106
+ * @param path - Destination path.
107
+ * @param contents - Text to append.
108
+ */
109
+ export async function appendPrivateFile(path, contents) {
110
+ const handle = await open(path, 'a', 0o600);
111
+ try {
112
+ await handle.writeFile(contents);
113
+ }
114
+ finally {
115
+ await handle.close();
116
+ }
117
+ }
118
+ /** Remove a file, treating an already absent file as success.
119
+ * @param path - File to remove.
120
+ */
121
+ export async function removeFile(path) {
122
+ try {
123
+ await unlink(path);
124
+ }
125
+ catch (error) {
126
+ if (!isMissing(error))
127
+ throw error;
128
+ }
129
+ }
130
+ /** Recognize one error code without assuming the error's prototype. */
131
+ function isCode(error, code) {
132
+ return error instanceof Error && 'code' in error && error.code === code;
133
+ }
134
+ /** Whether an operation failed because the path does not exist. */
135
+ function isMissing(error) { return isCode(error, 'ENOENT'); }
@@ -0,0 +1,3 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from './files.ts';
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from './directories.ts';
@@ -0,0 +1,3 @@
1
+ /** Storage domain: every filesystem operation the client performs. */
2
+ export { appendPrivateFile, createPrivateFile, readPrivateFile, readText, removeFile, writeExclusiveStream, writePrivateFile } from "./files.js";
3
+ export { ensureDirectory, ensurePrivateDirectory, listEntries } from "./directories.js";
@@ -0,0 +1,65 @@
1
+ /** Origin-scoped cookie persistence; launch tokens are never written to disk. */
2
+ import { createHash } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { ensurePrivateDirectory, readPrivateFile, writePrivateFile } from "../storage/index.js";
6
+ import { HttpError } from "./client.js";
7
+ /** Fatal authentication failure requiring a new startup token. */
8
+ export class AuthenticationRequired extends Error {
9
+ }
10
+ /** Private, atomic cookie files, separated by the complete HTTP origin. */
11
+ export class CookieStore {
12
+ directory;
13
+ constructor(directory = process.env.DSHT_AUTH_DIR
14
+ ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht', 'auth')) {
15
+ this.directory = directory;
16
+ }
17
+ /** Read an unexpired cookie, rejecting unsafe permissions or malformed storage. */
18
+ async load(origin) {
19
+ const raw = await readPrivateFile(this.path(origin), 'Cookie file');
20
+ if (raw === undefined)
21
+ return undefined;
22
+ let saved;
23
+ try {
24
+ saved = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ throw new Error('Invalid cookie file JSON. Remove the origin cookie file and log in again.');
28
+ }
29
+ if (saved === null || typeof saved !== 'object' || !('version' in saved) || saved.version !== 1
30
+ || !('origin' in saved) || saved.origin !== origin || !('cookie' in saved) || typeof saved.cookie !== 'string'
31
+ || !('expiresAt' in saved) || typeof saved.expiresAt !== 'number' || !Number.isSafeInteger(saved.expiresAt)) {
32
+ throw new Error('Invalid saved cookie. Remove the origin cookie file and log in again.');
33
+ }
34
+ return saved.expiresAt > Date.now() ? saved.cookie : undefined;
35
+ }
36
+ /** Atomically save a persistent host cookie with owner-only POSIX permissions. */
37
+ async save(origin, cookie, expiresAt) {
38
+ await ensurePrivateDirectory(this.directory, 'Cookie directory');
39
+ await writePrivateFile(this.path(origin), JSON.stringify({ version: 1, origin, cookie, expiresAt }) + '\n');
40
+ }
41
+ path(origin) {
42
+ return join(this.directory, `${createHash('sha256').update(origin).digest('hex')}.json`);
43
+ }
44
+ }
45
+ /** Reuse a validated cookie first; only authentication rejection permits token fallback. */
46
+ export async function login(client, token, store) {
47
+ const cookie = await store.load(client.base.origin);
48
+ if (cookie) {
49
+ client.restoreCookie(cookie);
50
+ try {
51
+ await client.call('session/list', { _request: {} });
52
+ return;
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof HttpError) || error.status !== 401)
56
+ throw error;
57
+ }
58
+ }
59
+ if (!token)
60
+ throw new AuthenticationRequired('Login required: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web');
61
+ await client.authenticate(token);
62
+ const session = client.persistentCookie;
63
+ if (session)
64
+ await store.save(client.base.origin, session.cookie, session.expiresAt);
65
+ }
@@ -37,13 +37,20 @@ export declare class Client {
37
37
  cookie: string;
38
38
  expiresAt: number;
39
39
  } | undefined;
40
+ /** Download the authenticated host ZIP without using a mutation or ordinary RPC deadline.
41
+ * @param sessionId - Selected session identity.
42
+ * @param signal - Cancels the streaming response.
43
+ * @returns Response whose body the caller must consume or cancel.
44
+ */
45
+ sessionLog(sessionId: string, signal: AbortSignal): Promise<Response>;
40
46
  /** Invoke an exact endpoint once. Mutations are never automatically retried.
41
47
  * @param endpoint - Namespace/method endpoint.
42
48
  * @param args - Host parameter names and JSON values.
43
49
  * @param signal - Optional caller cancellation, combined with client close and timeout.
50
+ * @param timeoutMs - Per-call timeout; null waits for caller cancellation or client close.
44
51
  * @returns The decoded result value; HTTP, remote, and cancellation errors reject.
45
52
  */
46
- call(endpoint: string, args?: ObjectValue, signal?: AbortSignal): Promise<Json | undefined>;
53
+ call(endpoint: string, args?: ObjectValue, signal?: AbortSignal, timeoutMs?: number | null): Promise<Json | undefined>;
47
54
  /** Connect the physical mux. A disconnected instance may reconnect with its cookie. */
48
55
  connect(): Promise<void>;
49
56
  /** Subscribe on the existing mux; each subscription has a fresh stream identity. */
@@ -69,18 +69,35 @@ export class Client {
69
69
  get persistentCookie() {
70
70
  return this.expiresAt === undefined ? undefined : { cookie: this.cookie, expiresAt: this.expiresAt };
71
71
  }
72
+ /** Download the authenticated host ZIP without using a mutation or ordinary RPC deadline.
73
+ * @param sessionId - Selected session identity.
74
+ * @param signal - Cancels the streaming response.
75
+ * @returns Response whose body the caller must consume or cancel.
76
+ */
77
+ async sessionLog(sessionId, signal) {
78
+ const url = new URL('/api/session.export', this.base);
79
+ url.searchParams.set('sessionId', sessionId);
80
+ const response = await fetch(url, { redirect: 'error', headers: { cookie: this.cookie },
81
+ signal: AbortSignal.any([this.lifetime.signal, signal]) });
82
+ if (!response.ok) {
83
+ await response.body?.cancel();
84
+ throw new HttpError(response.status, 'Session log export');
85
+ }
86
+ return response;
87
+ }
72
88
  /** Invoke an exact endpoint once. Mutations are never automatically retried.
73
89
  * @param endpoint - Namespace/method endpoint.
74
90
  * @param args - Host parameter names and JSON values.
75
91
  * @param signal - Optional caller cancellation, combined with client close and timeout.
92
+ * @param timeoutMs - Per-call timeout; null waits for caller cancellation or client close.
76
93
  * @returns The decoded result value; HTTP, remote, and cancellation errors reject.
77
94
  */
78
- async call(endpoint, args = {}, signal) {
95
+ async call(endpoint, args = {}, signal, timeoutMs = this.timeoutMs) {
79
96
  if (!/^[\w$-]+\/[\w$-]+$/.test(endpoint))
80
97
  throw new Error('Invalid RPC endpoint');
81
98
  const rpcId = randomUUID();
82
99
  const response = await fetch(new URL(`/api/${endpoint}`, this.base), {
83
- method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([this.signal(), signal]) : this.signal(),
100
+ method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([this.signal(timeoutMs), signal]) : this.signal(timeoutMs),
84
101
  headers: { 'content-type': 'application/json', cookie: this.cookie },
85
102
  body: JSON.stringify({ type: 'client-request', rpcId, method: endpoint, payload: { args } }),
86
103
  });
@@ -215,8 +232,8 @@ export class Client {
215
232
  await closed;
216
233
  }
217
234
  }
218
- signal() {
219
- return AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(this.timeoutMs)]);
235
+ signal(timeoutMs = this.timeoutMs) {
236
+ return timeoutMs === null ? this.lifetime.signal : AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(timeoutMs)]);
220
237
  }
221
238
  fail(error) {
222
239
  const listeners = [...this.listeners.values()];
@@ -0,0 +1,13 @@
1
+ /** Host access contract every domain controller uses for the active connection generation. */
2
+ import type { Client } from './client.ts';
3
+ /** One authenticated host connection, as seen by a domain that never owns it. */
4
+ export interface HostAccess {
5
+ /** Connected client, or undefined while offline or between generations. */
6
+ client(): Client | undefined;
7
+ /** Connected client, or a `Not connected` failure. */
8
+ require(): Client;
9
+ /** Whether the current generation has applied its host baseline. */
10
+ online(): boolean;
11
+ /** Client lifetime signal, aborted when the process closes the connection. */
12
+ signal(): AbortSignal;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { type Theme } from './theme/index.ts';
2
+ import { Controller } from '../controller/controller.ts';
3
+ /** The caller owns starting and stopping the controller around the Ink render lifetime.
4
+ * @param props - Controller, panel lifetime and semantic theme.
5
+ * @returns The rendered terminal interface.
6
+ */
7
+ export declare function App({ controller, panelLifetimeMs, theme }: {
8
+ controller: Controller;
9
+ panelLifetimeMs?: number;
10
+ theme?: Theme;
11
+ }): import("react").JSX.Element;