@itookit/dsht 0.3.7 → 0.5.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.i18n.yaml +2 -2
- package/README.md +31 -11
- package/README.zh.md +31 -11
- package/dist/cli/dsht.js +207 -19
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +295 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +60 -0
- package/dist/cli/verifier.js +242 -0
- package/dist/contracts.d.ts +344 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection.d.ts +11 -29
- package/dist/controller/connection.js +26 -60
- package/dist/controller/controller.d.ts +619 -164
- package/dist/controller/controller.js +1420 -141
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +126 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +82 -72
- package/dist/session/controller.js +211 -209
- package/dist/session/history.d.ts +9 -1
- package/dist/session/history.js +1 -9
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +25 -52
- package/dist/session/info.js +39 -25
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/telemetry.d.ts +12 -13
- package/dist/session/telemetry.js +27 -58
- package/dist/session/transcript.d.ts +0 -6
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +97 -0
- package/dist/shell/controller.js +158 -0
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +3 -0
- package/dist/shell/runner.d.ts +38 -0
- package/dist/shell/runner.js +147 -0
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +166 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/state.d.ts +14 -4
- package/dist/state.js +3 -2
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +865 -431
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/history-view.js +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +47 -0
- package/dist/ui/chat/shell-view.js +145 -0
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +6 -1
- package/dist/ui/theme/index.js +2 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -99
- package/dist/ui/commands/parse.js +0 -126
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Command discovery catalog shared by Tab completion and the `/help` panel. */
|
|
2
|
+
export const COMMAND_HINTS = [
|
|
3
|
+
{ command: '/ws', usage: '[name or ID]', description: 'List/switch workspaces; --delete name removes registration' },
|
|
4
|
+
{ command: '/resume', usage: '[title or ID]', description: 'List/switch sessions; --delete ID archives with confirmation' },
|
|
5
|
+
{ command: '/model', usage: '[provider model [effort]]', description: 'Choose a model and reasoning effort for subsequent requests' },
|
|
6
|
+
{ command: '/new', description: 'Create a session in the selected workspace' },
|
|
7
|
+
{ command: '/copy', description: 'Freeze for native selection; Esc resumes (Ctrl+S shortcut)' },
|
|
8
|
+
{ command: '/latest', description: 'Return to the live conversation' },
|
|
9
|
+
{ command: '/older', description: 'Load earlier history' },
|
|
10
|
+
{ command: '/history', usage: '[text]', description: 'List your prompts, optionally filtered' },
|
|
11
|
+
{ command: '/prompt', usage: '[text]', description: 'Use a saved shortcut prompt, or save the text as one' },
|
|
12
|
+
{ command: '/search', usage: 'text', description: 'Search history page by page and open a match' },
|
|
13
|
+
{ command: '/ssearch', usage: 'text', description: 'Search sessions in the current workspace' },
|
|
14
|
+
{ command: '/wsearch', usage: 'text', description: 'Search sessions across all workspaces' },
|
|
15
|
+
{ command: '/compact', description: 'Compact older history while the session is idle' },
|
|
16
|
+
{ command: '/cancel', description: 'Cancel the active turn' },
|
|
17
|
+
{ command: '/queue', description: 'View and remove pending input' },
|
|
18
|
+
{ command: '/plan', usage: '[off|message]', description: 'Enter or leave host plan mode' },
|
|
19
|
+
{ command: '/goal', usage: '[action|objective]', description: 'View or manage the host task goal' },
|
|
20
|
+
{ command: '/permission', usage: '[preset]', description: 'View or switch the host permission preset' },
|
|
21
|
+
{ command: '/feedback', usage: 'text', description: 'Record feedback about the session' },
|
|
22
|
+
{ command: '/handoff', description: 'Delete local HANDOFF.md, then have the agent write a handoff' },
|
|
23
|
+
{ command: '/loop', usage: '[name|stop] [score] [tries]', description: 'Run a loop.yaml record; confirm defaults; stop/answer/abort', exactOnly: true },
|
|
24
|
+
{ command: '/export', usage: '[local.zip]', description: 'Save the session log ZIP to a new local file' },
|
|
25
|
+
{ command: '/export-html', usage: '[local.html]', description: 'Save loaded conversation with diagrams and math as offline HTML' },
|
|
26
|
+
{ command: '/coredump', usage: '[tag]', description: 'Write a V8 heap snapshot for memory diagnosis' },
|
|
27
|
+
{ command: '/allow', description: 'Approve the pending request once', exactOnly: true },
|
|
28
|
+
{ command: '/deny', description: 'Reject the pending request', exactOnly: true },
|
|
29
|
+
{ command: '/status', description: 'Show full session status details' },
|
|
30
|
+
{ command: '/cost', description: 'Show cost estimates and refresh usage' },
|
|
31
|
+
{ command: '/think', usage: '[seq or live]', description: 'Inspect reasoning with user prompt summaries' },
|
|
32
|
+
{ command: '/help', description: 'Show this command list' },
|
|
33
|
+
{ command: '/quit', description: 'Exit dsht', exactOnly: true },
|
|
34
|
+
];
|
|
35
|
+
/** Command names only, in catalog order. */
|
|
36
|
+
export const COMMANDS = COMMAND_HINTS.map(hint => hint.command);
|
|
37
|
+
/** Command column text per hint, aligned in the `/help` panel. */
|
|
38
|
+
export const COMMAND_LABELS = COMMAND_HINTS.map(hint => hint.usage === undefined ? hint.command : `${hint.command} ${hint.usage}`);
|
|
39
|
+
/** Widest command column, so descriptions start on one column. */
|
|
40
|
+
export const COMMAND_LABEL_WIDTH = Math.max(...COMMAND_LABELS.map(label => label.length)) + 2;
|
|
41
|
+
/** Commands that may only run when typed in full; a prefix of one is never executed on Enter. */
|
|
42
|
+
const EXACT_ONLY = new Set(COMMAND_HINTS.filter(hint => hint.exactOnly).map(hint => hint.command));
|
|
43
|
+
/** Commands whose name starts with one token, in catalog order.
|
|
44
|
+
* @param token - First word of a draft, such as `/pro`.
|
|
45
|
+
* @returns Matching command names, empty when the token names nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function commandMatches(token) {
|
|
48
|
+
return COMMANDS.filter(command => command.startsWith(token));
|
|
49
|
+
}
|
|
50
|
+
/** Resolve one typed command token to the command it names.
|
|
51
|
+
*
|
|
52
|
+
* An exact command resolves to itself. A prefix that matches exactly one command resolves to that
|
|
53
|
+
* command too, so Enter can run it without typing the whole name; an unknown or ambiguous token,
|
|
54
|
+
* and any prefix of an `exactOnly` command, resolves to undefined so the draft stays as typed.
|
|
55
|
+
* @param token - First word of a draft, without its arguments.
|
|
56
|
+
* @returns The command name, or undefined when the token does not name one command.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCommand(token) {
|
|
59
|
+
if (COMMANDS.includes(token))
|
|
60
|
+
return token;
|
|
61
|
+
const matches = commandMatches(token);
|
|
62
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
63
|
+
return only !== undefined && !EXACT_ONLY.has(only) ? only : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Routing policy per parsed command kind.
|
|
66
|
+
*
|
|
67
|
+
* The pipeline reads this table instead of enumerating kinds itself, so a new command declares its
|
|
68
|
+
* constraints next to its syntax and no other module learns about it. A kind absent here has no
|
|
69
|
+
* constraint and may run from any screen, even while an answer is pending.
|
|
70
|
+
*/
|
|
71
|
+
/** A command that would write to the conversation another turn is already writing.
|
|
72
|
+
*
|
|
73
|
+
* `compact`, `handoff` and a `/loop` run each submit work of their own to the same session, so starting
|
|
74
|
+
* one mid-turn would interleave two writers on one conversation. The list is deliberately short: the
|
|
75
|
+
* host owns the busy rules of its own commands (`/plan`, `/goal`, `/model`, …), and a client-side deny
|
|
76
|
+
* there would contradict what the host would have accepted.
|
|
77
|
+
*/
|
|
78
|
+
/** A command that writes to the conversation another turn is writing: it runs when that turn ends.
|
|
79
|
+
*
|
|
80
|
+
* These are the operator's own commands — a compaction, a handoff, a review to start — and refusing
|
|
81
|
+
* them outright would make "I want this next" impossible to express while an agent works.
|
|
82
|
+
*/
|
|
83
|
+
const QUEUES_WHILE_RUNNING = { duringTurn: 'queue', duringLoop: 'queue' };
|
|
84
|
+
/** A command that must not run while another turn or loop owns the conversation at all. */
|
|
85
|
+
const CONFLICTS_WITH_RUNNING = { duringTurn: 'deny', duringLoop: 'deny' };
|
|
86
|
+
/** Commands that answer the operator or the host and must reach the session while it is busy.
|
|
87
|
+
*
|
|
88
|
+
* `whileBusy: 'run'` is the foreground-slot equivalent: cancelling an agent turn or settling the
|
|
89
|
+
* interaction holding it is independent of whatever long operation the client is showing, and the
|
|
90
|
+
* operator must never be told to wait for an export before they can stop the agent.
|
|
91
|
+
*/
|
|
92
|
+
const ANSWERS_WHILE_RUNNING = { duringTurn: 'run', duringLoop: 'run' };
|
|
93
|
+
const ANSWERS_WHILE_BUSY = { whileBusy: 'run' };
|
|
94
|
+
export const COMMAND_POLICY = {
|
|
95
|
+
shell: { requiresSession: true },
|
|
96
|
+
models: { requiresSession: true },
|
|
97
|
+
queue: { requiresSession: true, requiresNoInteraction: true },
|
|
98
|
+
history: { requiresSession: true },
|
|
99
|
+
historySearch: { requiresSession: true },
|
|
100
|
+
prompts: { requiresSession: true },
|
|
101
|
+
// Reading reasoning while the agent works is the point of the panel.
|
|
102
|
+
think: { requiresSession: true, ...ANSWERS_WHILE_RUNNING },
|
|
103
|
+
panel: { ...ANSWERS_WHILE_RUNNING },
|
|
104
|
+
copy: { ...ANSWERS_WHILE_RUNNING },
|
|
105
|
+
// Cancelling the turn, or settling the interaction that is holding it, must never be refused.
|
|
106
|
+
cancel: { ...ANSWERS_WHILE_RUNNING, ...ANSWERS_WHILE_BUSY },
|
|
107
|
+
approval: { ...ANSWERS_WHILE_RUNNING, ...ANSWERS_WHILE_BUSY },
|
|
108
|
+
compact: { requiresSession: true, ...QUEUES_WHILE_RUNNING },
|
|
109
|
+
handoff: { requiresSession: true, requiresNoInteraction: true, ...QUEUES_WHILE_RUNNING },
|
|
110
|
+
loop: { requiresSession: true, requiresNoInteraction: true, ...QUEUES_WHILE_RUNNING },
|
|
111
|
+
// A record list is a surface for the draft being typed; by the time a turn ends, the operator has
|
|
112
|
+
// moved on, so offering it later would be noise rather than help.
|
|
113
|
+
loops: { requiresSession: true, requiresNoInteraction: true, ...CONFLICTS_WITH_RUNNING },
|
|
114
|
+
// The host decides whether its own registered commands may run while a turn is in flight.
|
|
115
|
+
hostCommand: { requiresSession: true, requiresNoInteraction: true },
|
|
116
|
+
// Stopping is a control-lane action: it stays allowed while an approval waits, and while the very
|
|
117
|
+
// line it is meant to interrupt still owns the controller, because that is exactly when it is needed.
|
|
118
|
+
loopStop: { requiresSession: true, control: true, ...ANSWERS_WHILE_RUNNING, ...ANSWERS_WHILE_BUSY },
|
|
119
|
+
// Answering a paused run and ending it must both work while the loop is what is running.
|
|
120
|
+
loopAnswer: { requiresSession: true, ...ANSWERS_WHILE_RUNNING, ...ANSWERS_WHILE_BUSY },
|
|
121
|
+
export: { requiresSession: true },
|
|
122
|
+
exportHtml: { requiresSession: true },
|
|
123
|
+
};
|
|
124
|
+
/** Longest common prefix of the candidate commands, so Tab can extend an ambiguous draft.
|
|
125
|
+
* @param values - Command candidates.
|
|
126
|
+
* @returns The shared leading prefix.
|
|
127
|
+
*/
|
|
128
|
+
export function commonPrefix(values) {
|
|
129
|
+
let prefix = values[0] ?? '';
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
let index = 0;
|
|
132
|
+
while (index < prefix.length && index < value.length && prefix[index] === value[index])
|
|
133
|
+
index++;
|
|
134
|
+
prefix = prefix.slice(0, index);
|
|
135
|
+
}
|
|
136
|
+
return prefix;
|
|
137
|
+
}
|
|
138
|
+
/** Complete the leading slash command; an ambiguous draft extends to the shared prefix.
|
|
139
|
+
* @param input - Current composer draft.
|
|
140
|
+
* @returns The completed draft, or undefined when nothing can be completed.
|
|
141
|
+
*/
|
|
142
|
+
export function completeCommand(input) {
|
|
143
|
+
if (!input.startsWith('/') || input.includes(' '))
|
|
144
|
+
return undefined;
|
|
145
|
+
const matches = COMMANDS.filter(command => command.startsWith(input));
|
|
146
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
147
|
+
if (only !== undefined)
|
|
148
|
+
return `${only} `;
|
|
149
|
+
const prefix = commonPrefix(matches);
|
|
150
|
+
return prefix.length > input.length ? prefix : undefined;
|
|
151
|
+
}
|
|
152
|
+
/** Commands matching the current draft, shown under the composer.
|
|
153
|
+
* @param input - Current composer draft.
|
|
154
|
+
* @returns Candidate command names.
|
|
155
|
+
*/
|
|
156
|
+
export function suggestedCommands(input) {
|
|
157
|
+
return COMMANDS.filter(command => input.startsWith('/') && !input.includes(' ') && command.startsWith(input));
|
|
158
|
+
}
|
|
159
|
+
/** The catalog entry whose arguments the draft is currently typing.
|
|
160
|
+
*
|
|
161
|
+
* A usage line is only useful once the name is settled and arguments have begun, so this requires
|
|
162
|
+
* whitespace after a token that names one command: `/model ` hints the model command, while `/co `
|
|
163
|
+
* names none and `/think` is still completing a name. A unique prefix counts, because the same prefix
|
|
164
|
+
* already runs that command.
|
|
165
|
+
* @param input - Current composer draft.
|
|
166
|
+
* @returns The command's hint, or undefined when no arguments are being typed.
|
|
167
|
+
*/
|
|
168
|
+
export function argumentHint(input) {
|
|
169
|
+
if (!input.startsWith('/'))
|
|
170
|
+
return undefined;
|
|
171
|
+
const space = input.search(/\s/);
|
|
172
|
+
if (space <= 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const token = input.slice(0, space);
|
|
175
|
+
const command = COMMANDS.includes(token) ? token : resolveCommand(token);
|
|
176
|
+
return command === undefined ? undefined : COMMAND_HINTS.find(hint => hint.command === command);
|
|
177
|
+
}
|
package/dist/state.d.ts
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
/** Application state shared by the controller facade and every domain controller. */
|
|
2
2
|
import { SessionInfo } from './session/info.ts';
|
|
3
|
-
import type { HistorySearch, RemovalTarget } from './session/types.ts';
|
|
3
|
+
import type { HistorySearch, PendingInteraction, RemovalTarget } from './session/types.ts';
|
|
4
|
+
import type { ShellSnapshot } from './shell/index.ts';
|
|
4
5
|
import type { ObjectValue } from './transport/wire.ts';
|
|
5
6
|
/** State shared by the picker and conversation view. */
|
|
6
7
|
export interface State {
|
|
7
8
|
version: number;
|
|
9
|
+
/** The last failure the application recorded, for a diagnostic line and a refusal's reason.
|
|
10
|
+
*
|
|
11
|
+
* Internal on purpose (13.2-D2): a command's failure fact is its `CommandResult.outcome` plus the
|
|
12
|
+
* trace, and this is only what a connection, a session stream or an action envelope left behind.
|
|
13
|
+
* Whether an operation is running is not stored here — that is `queries.foreground`.
|
|
14
|
+
*/
|
|
15
|
+
lastFailure: string;
|
|
8
16
|
online: boolean;
|
|
9
|
-
busy: boolean;
|
|
10
17
|
screen: 'workspaces' | 'sessions' | 'chat' | 'path';
|
|
11
18
|
status: string;
|
|
12
|
-
error: string;
|
|
13
19
|
workspaces: ObjectValue[];
|
|
14
20
|
sessions: ObjectValue[];
|
|
15
21
|
showAllSessions: boolean;
|
|
16
22
|
workspaceId?: string;
|
|
17
23
|
sessionId?: string;
|
|
18
|
-
pending:
|
|
24
|
+
pending: PendingInteraction[];
|
|
19
25
|
controlError?: string;
|
|
20
26
|
modelError?: string;
|
|
21
27
|
presetError?: string;
|
|
22
28
|
presets?: ObjectValue[];
|
|
23
29
|
defaultModel?: ObjectValue;
|
|
30
|
+
/** Local `!` runs, published so the UI never reads the shell service object. */
|
|
31
|
+
shell: ShellSnapshot;
|
|
24
32
|
/** Record, prompt index, composer, view and interaction state of the selected session. */
|
|
25
33
|
session: SessionInfo;
|
|
26
34
|
}
|
|
@@ -34,6 +42,8 @@ export interface ControllerStore {
|
|
|
34
42
|
selection(): number;
|
|
35
43
|
/** Advance the selector generation when the selected workspace or session changes. */
|
|
36
44
|
bumpSelection(): void;
|
|
45
|
+
/** Whether an operation owns the client's foreground slot right now. */
|
|
46
|
+
busy(): boolean;
|
|
37
47
|
}
|
|
38
48
|
/** Build the initial state before any connection exists.
|
|
39
49
|
* @returns A fresh state whose transcript is empty and disconnected.
|
package/dist/state.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SessionInfo } from "./session/info.js";
|
|
|
4
4
|
* @returns A fresh state whose transcript is empty and disconnected.
|
|
5
5
|
*/
|
|
6
6
|
export function initialState() {
|
|
7
|
-
return { version: 0,
|
|
8
|
-
|
|
7
|
+
return { version: 0, lastFailure: '', online: false, screen: 'workspaces',
|
|
8
|
+
status: 'Connecting…', workspaces: [], sessions: [], showAllSessions: false, pending: [],
|
|
9
|
+
shell: { running: false, blocks: [] }, session: new SessionInfo() };
|
|
9
10
|
}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Terminal text hygiene shared by every layer; it knows nothing about the wire protocol.
|
|
2
|
+
*
|
|
3
|
+
* This module is deliberately dependency-free. It lives at the source root so `session/`, `ui/`
|
|
4
|
+
* and `cli/` can clean remote text without importing the transport domain — the boundary rule that
|
|
5
|
+
* `ui/` must never reach into `transport/`.
|
|
6
|
+
*/
|
|
7
|
+
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
8
|
+
export declare function safeText(value: string): string;
|
|
9
|
+
/** Return a displayable error without serializing request headers or credentials. */
|
|
10
|
+
export declare function errorText(error: unknown): string;
|
|
11
|
+
/** Reduce text that must appear in a diagnostic log to a fact that is safe to paste elsewhere.
|
|
12
|
+
*
|
|
13
|
+
* A trace or a bug report leaves the machine, so text quoted into one is first stripped of the two
|
|
14
|
+
* things that most often carry content out with it: absolute paths (which name a user, a project or a
|
|
15
|
+
* client) and anything shaped like a credential. Whitespace collapses so a multi-line child's output
|
|
16
|
+
* stays one field, and the result is bounded. This is for *diagnostic* text only: a message the
|
|
17
|
+
* operator is meant to read in full never goes through it.
|
|
18
|
+
* @param text - Text captured for diagnosis.
|
|
19
|
+
* @param limit - Longest result, in characters.
|
|
20
|
+
* @returns One bounded line with paths and credentials replaced.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sanitizeTraceText(text: string, limit?: number): string;
|
|
23
|
+
/** Fit a tool operation to one terminal row without exposing the result body.
|
|
24
|
+
* @param text - Tool name, status icon and optional operation.
|
|
25
|
+
* @param width - Available terminal columns.
|
|
26
|
+
* @returns A single line with an ellipsis when shortened.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toolLine(text: string, width: number): string;
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Terminal text hygiene shared by every layer; it knows nothing about the wire protocol.
|
|
2
|
+
*
|
|
3
|
+
* This module is deliberately dependency-free. It lives at the source root so `session/`, `ui/`
|
|
4
|
+
* and `cli/` can clean remote text without importing the transport domain — the boundary rule that
|
|
5
|
+
* `ui/` must never reach into `transport/`.
|
|
6
|
+
*/
|
|
7
|
+
import sliceAnsi from 'slice-ansi';
|
|
8
|
+
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
9
|
+
export function safeText(value) {
|
|
10
|
+
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
|
|
11
|
+
}
|
|
12
|
+
/** Return a displayable error without serializing request headers or credentials. */
|
|
13
|
+
export function errorText(error) {
|
|
14
|
+
return safeText(error instanceof Error ? error.message : String(error));
|
|
15
|
+
}
|
|
16
|
+
/** Reduce text that must appear in a diagnostic log to a fact that is safe to paste elsewhere.
|
|
17
|
+
*
|
|
18
|
+
* A trace or a bug report leaves the machine, so text quoted into one is first stripped of the two
|
|
19
|
+
* things that most often carry content out with it: absolute paths (which name a user, a project or a
|
|
20
|
+
* client) and anything shaped like a credential. Whitespace collapses so a multi-line child's output
|
|
21
|
+
* stays one field, and the result is bounded. This is for *diagnostic* text only: a message the
|
|
22
|
+
* operator is meant to read in full never goes through it.
|
|
23
|
+
* @param text - Text captured for diagnosis.
|
|
24
|
+
* @param limit - Longest result, in characters.
|
|
25
|
+
* @returns One bounded line with paths and credentials replaced.
|
|
26
|
+
*/
|
|
27
|
+
export function sanitizeTraceText(text, limit = 200) {
|
|
28
|
+
const flat = safeText(text)
|
|
29
|
+
.replace(/\s+/gu, ' ')
|
|
30
|
+
.trim()
|
|
31
|
+
// A URL keeps its origin — that is the diagnosable part — while its path goes.
|
|
32
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/[^\s/'"]+)(\/[^\s'"]*)?/giu, '$1<path>')
|
|
33
|
+
// A Windows drive path.
|
|
34
|
+
.replace(/\b[A-Za-z]:\\[^\s'"]*/gu, '<path>')
|
|
35
|
+
// A Unix absolute path, only from a boundary and only with a directory part, so an identifier such
|
|
36
|
+
// as `session/agent-busy` is left alone.
|
|
37
|
+
.replace(/(^|[\s'"=(:,])(?:\/(?:[^\s'":,)]+\/)+[^\s'":,)]*)/gu, '$1<path>')
|
|
38
|
+
// Credentials last: a secret can sit inside what the earlier rules replaced.
|
|
39
|
+
.replace(/\b(?:bearer|token|apikey|api[_-]?key|secret|password|passwd|authorization|cookie)\b\s*[:=]\s*\S+/giu, '<redacted>')
|
|
40
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/gu, '<redacted>')
|
|
41
|
+
.replace(/\b[A-Fa-f0-9]{32,}\b/gu, '<redacted>');
|
|
42
|
+
return flat.length <= limit ? flat : `${flat.slice(0, limit)}…`;
|
|
43
|
+
}
|
|
44
|
+
/** Fit a tool operation to one terminal row without exposing the result body.
|
|
45
|
+
* @param text - Tool name, status icon and optional operation.
|
|
46
|
+
* @param width - Available terminal columns.
|
|
47
|
+
* @returns A single line with an ellipsis when shortened.
|
|
48
|
+
*/
|
|
49
|
+
export function toolLine(text, width) {
|
|
50
|
+
const clean = safeText(text).replace(/\s+/gu, ' ').trim();
|
|
51
|
+
if (width < 2)
|
|
52
|
+
return width === 1 ? '…' : '';
|
|
53
|
+
const clipped = sliceAnsi(clean, 0, width);
|
|
54
|
+
return clipped.length < clean.length ? sliceAnsi(clean, 0, width - 1) + '…' : clean;
|
|
55
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** One place that reads DSH event field names and turns them into semantic host events.
|
|
2
|
+
*
|
|
3
|
+
* Raw wire stops here: no `Json`/`ObjectValue` payload survives into a `HostEvent`, so `connection/`,
|
|
4
|
+
* `session/` and the UI never learn how the host names its fields. Replying `{kind:'next'}` to an
|
|
5
|
+
* unrecognized waterfall is a wire-level obligation of the host's event chain, so it is modelled as
|
|
6
|
+
* `waterfall-delegate` rather than silently dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { type Json, type ObjectValue } from './wire.ts';
|
|
9
|
+
/** One selectable answer of a host question, already flattened out of the raw request. */
|
|
10
|
+
export interface QuestionOption {
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
/** One question of an `user-questions/request` waterfall. */
|
|
15
|
+
export interface QuestionItem {
|
|
16
|
+
id: string;
|
|
17
|
+
header?: string;
|
|
18
|
+
question: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
multiSelect: boolean;
|
|
21
|
+
options: readonly QuestionOption[];
|
|
22
|
+
}
|
|
23
|
+
/** A decoded host event with protocol field names already resolved into domain meaning. */
|
|
24
|
+
export type HostEvent = {
|
|
25
|
+
kind: 'approval-request';
|
|
26
|
+
eventId: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
description: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'question-request';
|
|
31
|
+
eventId: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
questions: readonly QuestionItem[];
|
|
34
|
+
}
|
|
35
|
+
/** A waterfall this client does not answer; the caller must reply `next` to keep the host chain moving. */
|
|
36
|
+
| {
|
|
37
|
+
kind: 'waterfall-delegate';
|
|
38
|
+
eventId: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'cancel';
|
|
41
|
+
eventId: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'agent-status';
|
|
44
|
+
sessionId: string;
|
|
45
|
+
running: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'catalog-invalidated';
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'session-error';
|
|
50
|
+
sessionId: string;
|
|
51
|
+
error: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'control';
|
|
54
|
+
frame: ControlFrame;
|
|
55
|
+
};
|
|
56
|
+
/** One host-owned pending input, flattened to the text the UI shows. */
|
|
57
|
+
export interface QueuedInput {
|
|
58
|
+
id: string;
|
|
59
|
+
placement: 'queued' | 'steering' | 'context';
|
|
60
|
+
text: string;
|
|
61
|
+
}
|
|
62
|
+
/** A capability value the host owns. Individual keys are typed incrementally as they are consumed. */
|
|
63
|
+
export type ProjectionValue = Json;
|
|
64
|
+
/** One session's projection values and the sequence they describe. */
|
|
65
|
+
export interface ProjectionSnapshot {
|
|
66
|
+
asOfSeq: number;
|
|
67
|
+
values: Readonly<Record<string, ProjectionValue>>;
|
|
68
|
+
}
|
|
69
|
+
/** One decoded `session/control` frame with protocol field names already resolved. */
|
|
70
|
+
export type ControlFrame = {
|
|
71
|
+
kind: 'baseline';
|
|
72
|
+
projections: ReadonlyMap<string, ProjectionSnapshot>;
|
|
73
|
+
queues: ReadonlyMap<string, readonly QueuedInput[]>;
|
|
74
|
+
jobs: ReadonlyMap<string, number>;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'projection';
|
|
77
|
+
sessionId: string;
|
|
78
|
+
key: string;
|
|
79
|
+
seq: number;
|
|
80
|
+
value: ProjectionValue;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'queue';
|
|
83
|
+
sessionId: string;
|
|
84
|
+
items: readonly QueuedInput[];
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'jobs';
|
|
87
|
+
sessionId: string;
|
|
88
|
+
count: number;
|
|
89
|
+
};
|
|
90
|
+
/** Decode one projection baseline, as carried by a `session/control` baseline or a follow snapshot. */
|
|
91
|
+
export declare function projectionSnapshot(value: unknown): ProjectionSnapshot | undefined;
|
|
92
|
+
/** Turn one decoded `session/control` frame into a semantic control frame.
|
|
93
|
+
* @param value - One decoded `session/control` frame.
|
|
94
|
+
* @returns The normalized frame.
|
|
95
|
+
*/
|
|
96
|
+
export declare function controlFrame(value: unknown): ControlFrame;
|
|
97
|
+
/** Turn one decoded `$events` frame into a semantic host event.
|
|
98
|
+
*
|
|
99
|
+
* Returns `undefined` for anything this client does not act on, including the `ready` handshake,
|
|
100
|
+
* which belongs to the connection lifecycle rather than to a domain.
|
|
101
|
+
* @param frame - One decoded `$events` frame.
|
|
102
|
+
* @returns The normalized event, or undefined when it is not one this client consumes.
|
|
103
|
+
*/
|
|
104
|
+
export declare function hostEvent(frame: ObjectValue): HostEvent | undefined;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** One place that reads DSH event field names and turns them into semantic host events.
|
|
2
|
+
*
|
|
3
|
+
* Raw wire stops here: no `Json`/`ObjectValue` payload survives into a `HostEvent`, so `connection/`,
|
|
4
|
+
* `session/` and the UI never learn how the host names its fields. Replying `{kind:'next'}` to an
|
|
5
|
+
* unrecognized waterfall is a wire-level obligation of the host's event chain, so it is modelled as
|
|
6
|
+
* `waterfall-delegate` rather than silently dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { array, errorText, object, string } from "./wire.js";
|
|
9
|
+
/** Host notifications that invalidate the model catalog. */
|
|
10
|
+
const CATALOG_EVENTS = new Set(['llm/adapters-updated', 'settings/document-updated', 'credentials/reference-updated']);
|
|
11
|
+
/** Require a projection watermark rather than accepting a rolled-back value. */
|
|
12
|
+
function sequence(value) {
|
|
13
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < -1)
|
|
14
|
+
throw new Error('Invalid projection watermark');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
/** Flatten one session's pending-input list. */
|
|
18
|
+
function queuedInputs(value) {
|
|
19
|
+
return array(value).map(raw => {
|
|
20
|
+
const item = object(raw);
|
|
21
|
+
if (!['queued', 'steering', 'context'].includes(string(item.placement)))
|
|
22
|
+
throw new Error('Invalid queue placement');
|
|
23
|
+
return { id: string(item.id), placement: item.placement,
|
|
24
|
+
text: array(object(item.message).content).map(object)
|
|
25
|
+
.map(block => block.type === 'text' ? string(block.text) : `[${string(block.type)}]`).join(' ') };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Count the jobs the host still considers active. */
|
|
29
|
+
function activeJobs(value) {
|
|
30
|
+
return array(value).filter(item => ['running', 'stopping'].includes(string(object(item).status))).length;
|
|
31
|
+
}
|
|
32
|
+
/** Decode one projection baseline, as carried by a `session/control` baseline or a follow snapshot. */
|
|
33
|
+
export function projectionSnapshot(value) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const baseline = object(value);
|
|
37
|
+
return { asOfSeq: sequence(baseline.asOfSeq), values: object(baseline.values) };
|
|
38
|
+
}
|
|
39
|
+
/** Turn one decoded `session/control` frame into a semantic control frame.
|
|
40
|
+
* @param value - One decoded `session/control` frame.
|
|
41
|
+
* @returns The normalized frame.
|
|
42
|
+
*/
|
|
43
|
+
export function controlFrame(value) {
|
|
44
|
+
const frame = object(value);
|
|
45
|
+
if (frame.type === 'baseline') {
|
|
46
|
+
const baseline = object(frame.value);
|
|
47
|
+
const projections = Object.entries(object(baseline.projections))
|
|
48
|
+
.map(([id, snapshot]) => [id, projectionSnapshot(snapshot)]);
|
|
49
|
+
const queues = Object.entries(object(baseline.queues)).map(([id, items]) => [id, queuedInputs(items)]);
|
|
50
|
+
const jobs = Object.entries(object(baseline.jobs)).map(([id, items]) => [id, activeJobs(items)]);
|
|
51
|
+
return { kind: 'baseline', projections: new Map(projections), queues: new Map(queues), jobs: new Map(jobs) };
|
|
52
|
+
}
|
|
53
|
+
if (frame.type === 'projection' || frame.type === 'queue' || frame.type === 'jobs') {
|
|
54
|
+
const sessionId = string(frame.sessionId);
|
|
55
|
+
if (frame.type === 'projection') {
|
|
56
|
+
if (frame.value === undefined)
|
|
57
|
+
throw new Error('Missing projection value');
|
|
58
|
+
return { kind: 'projection', sessionId, key: string(frame.key), seq: sequence(frame.seq), value: frame.value };
|
|
59
|
+
}
|
|
60
|
+
if (frame.type === 'queue')
|
|
61
|
+
return { kind: 'queue', sessionId, items: queuedInputs(frame.items) };
|
|
62
|
+
// The host names the job rows `jobs`; the loopback fixtures used `items`, so accept either.
|
|
63
|
+
return { kind: 'jobs', sessionId, count: activeJobs(frame.jobs ?? frame.items) };
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Unknown session control frame');
|
|
66
|
+
}
|
|
67
|
+
/** Read a possibly-absent string field without turning protocol drift into a throw. */
|
|
68
|
+
function optionalString(value) {
|
|
69
|
+
return typeof value === 'string' ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Flatten one raw question into the semantic shape the session domain retains. */
|
|
72
|
+
function questionItem(value) {
|
|
73
|
+
const item = object(value);
|
|
74
|
+
const options = array(item.options ?? []).map(raw => {
|
|
75
|
+
const option = object(raw);
|
|
76
|
+
const description = optionalString(option.description);
|
|
77
|
+
return { label: string(option.label), ...(description === undefined ? {} : { description }) };
|
|
78
|
+
});
|
|
79
|
+
const header = optionalString(item.header);
|
|
80
|
+
const detail = optionalString(item.detail);
|
|
81
|
+
return {
|
|
82
|
+
id: string(item.id),
|
|
83
|
+
...(header === undefined ? {} : { header }),
|
|
84
|
+
question: string(item.question),
|
|
85
|
+
...(detail === undefined ? {} : { detail }),
|
|
86
|
+
multiSelect: item.multiSelect === true,
|
|
87
|
+
options,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Decode one `$events` waterfall frame. */
|
|
91
|
+
function waterfallEvent(frame) {
|
|
92
|
+
const eventId = string(frame.eventId);
|
|
93
|
+
const sessionId = optionalString(frame.agentId) ?? '';
|
|
94
|
+
if (frame.event === 'approval/request') {
|
|
95
|
+
// The approval body is host-defined and currently rendered verbatim; formatting it here keeps
|
|
96
|
+
// the terminal output byte-identical while removing the raw object from the boundary.
|
|
97
|
+
return { kind: 'approval-request', eventId, sessionId, description: JSON.stringify(frame.request ?? null, null, 2) };
|
|
98
|
+
}
|
|
99
|
+
if (frame.event === 'user-questions/request') {
|
|
100
|
+
try {
|
|
101
|
+
const request = object(frame.request);
|
|
102
|
+
return { kind: 'question-request', eventId, sessionId, questions: array(request.questions ?? []).map(questionItem) };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// A malformed question still has to be settled, or the host's event chain blocks on it.
|
|
106
|
+
return { kind: 'waterfall-delegate', eventId };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'waterfall-delegate', eventId };
|
|
110
|
+
}
|
|
111
|
+
/** Decode one `$events` emit frame this client reacts to. */
|
|
112
|
+
function emitEvent(frame) {
|
|
113
|
+
const name = frame.event;
|
|
114
|
+
if (name === 'api-session/status') {
|
|
115
|
+
const args = array(frame.args);
|
|
116
|
+
if (typeof args[1] !== 'boolean')
|
|
117
|
+
throw new Error('Invalid session running state');
|
|
118
|
+
return { kind: 'agent-status', sessionId: string(args[0]), running: args[1] };
|
|
119
|
+
}
|
|
120
|
+
if (name === 'api-session/error') {
|
|
121
|
+
const args = array(frame.args);
|
|
122
|
+
return { kind: 'session-error', sessionId: optionalString(args[0]) ?? '', error: errorText(args[1]) };
|
|
123
|
+
}
|
|
124
|
+
if (typeof name === 'string' && CATALOG_EVENTS.has(name))
|
|
125
|
+
return { kind: 'catalog-invalidated' };
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Turn one decoded `$events` frame into a semantic host event.
|
|
129
|
+
*
|
|
130
|
+
* Returns `undefined` for anything this client does not act on, including the `ready` handshake,
|
|
131
|
+
* which belongs to the connection lifecycle rather than to a domain.
|
|
132
|
+
* @param frame - One decoded `$events` frame.
|
|
133
|
+
* @returns The normalized event, or undefined when it is not one this client consumes.
|
|
134
|
+
*/
|
|
135
|
+
export function hostEvent(frame) {
|
|
136
|
+
try {
|
|
137
|
+
if (frame.type === 'waterfall')
|
|
138
|
+
return waterfallEvent(frame);
|
|
139
|
+
if (frame.type === 'cancel')
|
|
140
|
+
return { kind: 'cancel', eventId: string(frame.eventId) };
|
|
141
|
+
if (frame.type === 'emit')
|
|
142
|
+
return emitEvent(frame);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// A frame this client cannot decode is not allowed to fail the whole subscription.
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
package/dist/transport/wire.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
/** Require a string field rather than silently accepting protocol drift. */
|
|
11
|
-
export declare function string(value: unknown): string;
|
|
12
|
-
/** Require an array field from the server. */
|
|
13
|
-
export declare function array(value: unknown): Json[];
|
|
14
|
-
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
15
|
-
export declare function safeText(value: string): string;
|
|
16
|
-
/** Return a displayable error without serializing request headers or credentials. */
|
|
17
|
-
export declare function errorText(error: unknown): string;
|
|
1
|
+
/** The wire's decoding surface.
|
|
2
|
+
*
|
|
3
|
+
* The JSON shapes, their strict readers and the displayable error text are dependency-free leaves
|
|
4
|
+
* (`src/json.ts`, `src/text.ts`), so the UI can name the same types without importing the transport
|
|
5
|
+
* domain. This module re-exports them as the wire-facing API the features already use.
|
|
6
|
+
*/
|
|
7
|
+
export type { Json, ObjectValue } from '../json.ts';
|
|
8
|
+
export { array, object, string } from '../json.ts';
|
|
9
|
+
export { errorText, safeText } from '../text.ts';
|
package/dist/transport/wire.js
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
4
|
-
throw new Error('Expected a JSON object from the server');
|
|
5
|
-
}
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
/** Require a string field rather than silently accepting protocol drift. */
|
|
9
|
-
export function string(value) {
|
|
10
|
-
if (typeof value !== 'string')
|
|
11
|
-
throw new Error('Expected a string from the server');
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
/** Require an array field from the server. */
|
|
15
|
-
export function array(value) {
|
|
16
|
-
if (!Array.isArray(value))
|
|
17
|
-
throw new Error('Expected an array from the server');
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
21
|
-
export function safeText(value) {
|
|
22
|
-
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
|
|
23
|
-
}
|
|
24
|
-
/** Return a displayable error without serializing request headers or credentials. */
|
|
25
|
-
export function errorText(error) {
|
|
26
|
-
return safeText(error instanceof Error ? error.message : String(error));
|
|
27
|
-
}
|
|
1
|
+
export { array, object, string } from "../json.js";
|
|
2
|
+
export { errorText, safeText } from "../text.js";
|