@aexol/spectral 0.9.211 → 0.9.215
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/dist/agent/agents.d.ts.map +1 -1
- package/dist/agent/agents.js +40 -0
- package/dist/backend/generated-images.d.ts +90 -0
- package/dist/backend/generated-images.d.ts.map +1 -0
- package/dist/backend/generated-images.js +214 -0
- package/dist/backend/machine-routing.d.ts +25 -0
- package/dist/backend/machine-routing.d.ts.map +1 -0
- package/dist/backend/machine-routing.js +35 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +31 -6
- package/dist/extensions/browser/browser-service.d.ts +75 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +600 -34
- package/dist/extensions/browser/index.d.ts +10 -4
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +44 -5
- package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -1
- package/dist/extensions/browser/stream/stream-tool.js +42 -14
- package/dist/extensions/browser/tools/core.d.ts.map +1 -1
- package/dist/extensions/browser/tools/core.js +63 -2
- package/dist/extensions/desktop-control/index.d.ts +55 -0
- package/dist/extensions/desktop-control/index.d.ts.map +1 -0
- package/dist/extensions/desktop-control/index.js +633 -0
- package/dist/extensions/desktop-control/keys.d.ts +33 -0
- package/dist/extensions/desktop-control/keys.d.ts.map +1 -0
- package/dist/extensions/desktop-control/keys.js +170 -0
- package/dist/extensions/desktop-control/linux.d.ts +26 -0
- package/dist/extensions/desktop-control/linux.d.ts.map +1 -0
- package/dist/extensions/desktop-control/linux.js +195 -0
- package/dist/extensions/desktop-control/macos.d.ts +53 -0
- package/dist/extensions/desktop-control/macos.d.ts.map +1 -0
- package/dist/extensions/desktop-control/macos.js +382 -0
- package/dist/extensions/desktop-control/safety.d.ts +89 -0
- package/dist/extensions/desktop-control/safety.d.ts.map +1 -0
- package/dist/extensions/desktop-control/safety.js +157 -0
- package/dist/extensions/desktop-control/types.d.ts +79 -0
- package/dist/extensions/desktop-control/types.d.ts.map +1 -0
- package/dist/extensions/desktop-control/types.js +10 -0
- package/dist/extensions/desktop-control/windows.d.ts +31 -0
- package/dist/extensions/desktop-control/windows.d.ts.map +1 -0
- package/dist/extensions/desktop-control/windows.js +276 -0
- package/dist/extensions/desktop-screenshot/index.d.ts +103 -0
- package/dist/extensions/desktop-screenshot/index.d.ts.map +1 -1
- package/dist/extensions/desktop-screenshot/index.js +363 -17
- package/dist/extensions/image-generation/index.d.ts.map +1 -1
- package/dist/extensions/image-generation/index.js +133 -34
- package/dist/extensions/seo/index.d.ts.map +1 -1
- package/dist/extensions/seo/index.js +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts +4 -0
- package/dist/extensions/seo/tools/backend-bridge.d.ts.map +1 -0
- package/dist/extensions/seo/tools/backend-bridge.js +378 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +2 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +1 -1
- package/dist/extensions/seo/tools/report.d.ts +10 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +1 -1
- package/dist/extensions/spectral-vision-fallback.d.ts.map +1 -1
- package/dist/extensions/spectral-vision-fallback.js +11 -1
- package/dist/generated/zeus/const.d.ts.map +1 -1
- package/dist/generated/zeus/const.js +148 -4
- package/dist/generated/zeus/index.d.ts +649 -0
- package/dist/generated/zeus/index.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.d.ts.map +1 -1
- package/dist/mcp/tool-registrar.js +5 -2
- package/dist/mcp/types.d.ts +5 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/relay/client.d.ts +11 -0
- package/dist/relay/client.d.ts.map +1 -1
- package/dist/relay/client.js +24 -6
- package/dist/relay/dispatcher.d.ts +12 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +44 -42
- package/dist/sdk/ai/providers/faux.js +1 -1
- package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openai-completions.js +57 -16
- package/dist/sdk/ai/providers/openrouter-images.d.ts.map +1 -1
- package/dist/sdk/ai/providers/openrouter-images.js +45 -9
- package/dist/sdk/ai/providers/simple-options.d.ts.map +1 -1
- package/dist/sdk/ai/providers/simple-options.js +2 -0
- package/dist/sdk/ai/types.d.ts +37 -1
- package/dist/sdk/ai/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/image-source.d.ts +121 -0
- package/dist/sdk/ai/utils/image-source.d.ts.map +1 -0
- package/dist/sdk/ai/utils/image-source.js +410 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +14 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +15 -9
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/sdk/coding-agent/modes/rpc/rpc-mode.js +16 -3
- package/dist/sdk/coding-agent/utils/image-resize.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/image-resize.js +7 -3
- package/dist/server/agent-bridge.d.ts +14 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +105 -26
- package/dist/server/handlers/queue.d.ts.map +1 -1
- package/dist/server/handlers/queue.js +11 -34
- package/dist/server/image-attachments.d.ts +109 -0
- package/dist/server/image-attachments.d.ts.map +1 -0
- package/dist/server/image-attachments.js +195 -0
- package/dist/server/session-stream.d.ts +69 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +388 -32
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +10 -31
- package/dist/server/ui/terminal-confirm.d.ts +145 -0
- package/dist/server/ui/terminal-confirm.d.ts.map +1 -0
- package/dist/server/ui/terminal-confirm.js +333 -0
- package/dist/server/wire.d.ts +30 -5
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/storage.js
CHANGED
|
@@ -34,6 +34,7 @@ import { mkdirSync, readFileSync } from "node:fs";
|
|
|
34
34
|
import { dirname, join, resolve } from "node:path";
|
|
35
35
|
import { getConfigDir } from "../config.js";
|
|
36
36
|
import { stripJsoncComments } from "../studio-binding.js";
|
|
37
|
+
import { coerceImages, parseImagesJson } from "./image-attachments.js";
|
|
37
38
|
/**
|
|
38
39
|
* Schema version — informational, stamped into every new database.
|
|
39
40
|
*
|
|
@@ -234,35 +235,6 @@ function applyBindingFields(project, rowBinding) {
|
|
|
234
235
|
studioTeamId: binding.teamId,
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
|
-
/** Tables we own — used by the migration drop step. */
|
|
238
|
-
/** Best-effort parse of the `images_json` column into `ImageAttachment[]`. */
|
|
239
|
-
function parseImagesJson(raw) {
|
|
240
|
-
if (!raw || raw === "")
|
|
241
|
-
return undefined;
|
|
242
|
-
try {
|
|
243
|
-
const parsed = JSON.parse(raw);
|
|
244
|
-
if (!Array.isArray(parsed))
|
|
245
|
-
return undefined;
|
|
246
|
-
const out = [];
|
|
247
|
-
for (const i of parsed) {
|
|
248
|
-
if (i !== null &&
|
|
249
|
-
typeof i === "object" &&
|
|
250
|
-
typeof i.data === "string" &&
|
|
251
|
-
typeof i.mimeType === "string") {
|
|
252
|
-
out.push({
|
|
253
|
-
data: i.data,
|
|
254
|
-
mimeType: i.mimeType,
|
|
255
|
-
width: typeof i.width === "number" ? i.width : undefined,
|
|
256
|
-
height: typeof i.height === "number" ? i.height : undefined,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return out.length > 0 ? out : undefined;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
238
|
export class SessionStore {
|
|
267
239
|
path;
|
|
268
240
|
db;
|
|
@@ -1182,7 +1154,14 @@ export class SessionStore {
|
|
|
1182
1154
|
const createdAt = msg.createdAt ?? Date.now();
|
|
1183
1155
|
const eventsJsonl = msg.eventsJsonl ?? "";
|
|
1184
1156
|
const creditsUsed = msg.creditsUsed ?? null;
|
|
1185
|
-
|
|
1157
|
+
// Last line of defence: never persist an attachment that carries neither
|
|
1158
|
+
// a url nor data, and never persist megabytes of inline base64 — both
|
|
1159
|
+
// would bloat the DB and the history replay. Ingress paths reject these
|
|
1160
|
+
// earlier; here we only guarantee the invariant.
|
|
1161
|
+
const storedImages = coerceImages(msg.images, {
|
|
1162
|
+
onReject: (reason) => console.warn(`[spectral] warn: not persisting image for ${sessionId}: ${reason}`),
|
|
1163
|
+
});
|
|
1164
|
+
const imagesJson = storedImages && storedImages.length > 0 ? JSON.stringify(storedImages) : "";
|
|
1186
1165
|
let sequence = 0;
|
|
1187
1166
|
const tx = this.db.transaction(() => {
|
|
1188
1167
|
// Idempotent sequence assignment. A rewrite of the same message id must
|
|
@@ -1203,7 +1182,7 @@ export class SessionStore {
|
|
|
1203
1182
|
events: eventsJsonl,
|
|
1204
1183
|
createdAt,
|
|
1205
1184
|
sequence,
|
|
1206
|
-
...(
|
|
1185
|
+
...(storedImages && storedImages.length > 0 ? { images: storedImages } : {}),
|
|
1207
1186
|
...(creditsUsed != null ? { creditsUsed } : {}),
|
|
1208
1187
|
};
|
|
1209
1188
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-native permission confirmations for `spectral serve`.
|
|
3
|
+
*
|
|
4
|
+
* In serve mode there is no TUI, but extensions (notably desktop-control)
|
|
5
|
+
* still call `ctx.ui.confirm(...)` to gate potentially harmful actions. The
|
|
6
|
+
* user is chatting via the web UI, while `spectral serve` itself runs in a
|
|
7
|
+
* real terminal — so the confirmation is rendered there, on serve's stdout,
|
|
8
|
+
* and answered with a single keypress.
|
|
9
|
+
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - Non-TTY stdin: resolves `false` immediately (honest headless deny).
|
|
12
|
+
* - The prompt is a boxed render with a key legend: Enter/y = Allow,
|
|
13
|
+
* n/Esc/q = Deny, Ctrl+C = Deny (consumed as data in raw mode — it must
|
|
14
|
+
* never kill the serve process).
|
|
15
|
+
* - Stdin is put into raw mode for the duration of the prompt and always
|
|
16
|
+
* restored afterwards.
|
|
17
|
+
* - Title and message are sanitized before rendering: ANSI escape sequences
|
|
18
|
+
* and control characters are stripped (newlines collapse to spaces) so a
|
|
19
|
+
* malicious extension cannot inject terminal escapes.
|
|
20
|
+
* - Concurrent confirmations (multiple sessions share one terminal) are
|
|
21
|
+
* serialized through a module-level queue; queued requests print a short
|
|
22
|
+
* "waiting" notice.
|
|
23
|
+
* - Timeouts: `SPECTRAL_CONFIRM_TIMEOUT_MS` env (default 5 min) or the
|
|
24
|
+
* per-dialog `opts.timeout`. On timeout the confirmation resolves `false`.
|
|
25
|
+
* - `onOutcome` reports how the dialog ended exactly once, letting callers
|
|
26
|
+
* (e.g. desktop-control) distinguish an explicit user denial from
|
|
27
|
+
* auto-deny causes (timeout, non-TTY, abort) that must not lock sessions.
|
|
28
|
+
*
|
|
29
|
+
* The prompt logic is pure and takes injectable IO (`write` + `readKey`) so
|
|
30
|
+
* tests run without a TTY.
|
|
31
|
+
*/
|
|
32
|
+
/** Injectable terminal IO for the confirmation prompt. */
|
|
33
|
+
export interface ConfirmPromptIO {
|
|
34
|
+
/** Write text to the terminal (serve's stdout). */
|
|
35
|
+
write(text: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Start listening for the next keypress and pass raw stdin data to
|
|
38
|
+
* `handler`. Returns a cancel function that must detach the listener and
|
|
39
|
+
* restore any terminal state (raw mode).
|
|
40
|
+
*/
|
|
41
|
+
readKey(handler: (data: string) => void): () => void;
|
|
42
|
+
}
|
|
43
|
+
/** Minimal shape of a stdin stream the default IO implementation needs. */
|
|
44
|
+
export interface ConfirmStdin {
|
|
45
|
+
readonly isTTY?: boolean;
|
|
46
|
+
readonly isRaw?: boolean;
|
|
47
|
+
setRawMode?(mode: boolean): unknown;
|
|
48
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
49
|
+
off(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
50
|
+
resume(): unknown;
|
|
51
|
+
}
|
|
52
|
+
export type ConfirmKeyAction = "allow" | "deny" | "ignore";
|
|
53
|
+
/**
|
|
54
|
+
* How a confirmation ended. `deny` is an explicit human decision (deny key);
|
|
55
|
+
* `timeout`, `no_tty` and `cancelled` mean the dialog ended without a user
|
|
56
|
+
* answer. Same values as `ExtensionConfirmOutcome`.
|
|
57
|
+
*/
|
|
58
|
+
export type TerminalConfirmOutcome = "allow" | "deny" | "timeout" | "no_tty" | "cancelled";
|
|
59
|
+
/** Default confirmation timeout: 5 minutes. */
|
|
60
|
+
export declare const DEFAULT_CONFIRM_TIMEOUT_MS = 300000;
|
|
61
|
+
/**
|
|
62
|
+
* Classify a raw keypress. Only single-byte keys are acted on; multi-byte
|
|
63
|
+
* sequences (arrow keys, pastes) are ignored so they can't accidentally
|
|
64
|
+
* answer the prompt.
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyConfirmKey(data: string): ConfirmKeyAction;
|
|
67
|
+
/**
|
|
68
|
+
* Strip ANSI escape sequences and control characters from extension-provided
|
|
69
|
+
* text. Newlines become spaces (the box renders one wrapped block); every
|
|
70
|
+
* other C0 control character (incl. \r and tab), DEL and C1 are removed, as
|
|
71
|
+
* is any stray ESC left over from an unterminated sequence. Keeps the prompt
|
|
72
|
+
* box and result lines safe against terminal escape injection.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sanitizeConfirmText(text: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Render the confirmation prompt as a boxed block of text (with trailing
|
|
77
|
+
* newline). Pure — no IO.
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderConfirmBox(title: string, message: string): string;
|
|
80
|
+
/** Read the confirmation timeout from the environment (ms). */
|
|
81
|
+
export declare function parseConfirmTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
82
|
+
export interface TerminalConfirmOptions {
|
|
83
|
+
title: string;
|
|
84
|
+
message: string;
|
|
85
|
+
io: ConfirmPromptIO;
|
|
86
|
+
/** Whether stdin is an interactive TTY. Defaults to `process.stdin.isTTY`. */
|
|
87
|
+
isTTY?: boolean;
|
|
88
|
+
/** Timeout in ms. Defaults to `parseConfirmTimeoutMs()` (env-driven). */
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
/** AbortSignal to programmatically deny the dialog. */
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
/** Called when the prompt is actually rendered (not while queued). */
|
|
93
|
+
onShown?: () => void;
|
|
94
|
+
/**
|
|
95
|
+
* Called exactly once with the final outcome, before the promise settles.
|
|
96
|
+
* `deny` means an explicit human denial; `timeout`/`no_tty`/`cancelled`
|
|
97
|
+
* are auto-deny causes (no user decision was made).
|
|
98
|
+
*/
|
|
99
|
+
onOutcome?: (outcome: TerminalConfirmOutcome) => void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Run a single terminal confirmation. Resolves `true` (allow) or `false`
|
|
103
|
+
* (deny / timeout / abort / non-TTY). Never throws.
|
|
104
|
+
*/
|
|
105
|
+
export declare function runTerminalConfirm(options: TerminalConfirmOptions): Promise<boolean>;
|
|
106
|
+
/**
|
|
107
|
+
* Request a terminal confirmation, serialized with all other pending
|
|
108
|
+
* confirmations in this process. Queued requests print a short notice.
|
|
109
|
+
*/
|
|
110
|
+
export declare function requestTerminalConfirm(options: TerminalConfirmOptions): Promise<boolean>;
|
|
111
|
+
/**
|
|
112
|
+
* Default IO bound to the serve process's real terminal: writes to stdout,
|
|
113
|
+
* reads raw keypresses from stdin (entering raw mode for the duration and
|
|
114
|
+
* restoring the previous mode on cancel).
|
|
115
|
+
*/
|
|
116
|
+
export declare function createStdinConfirmIO(stdin?: ConfirmStdin): ConfirmPromptIO;
|
|
117
|
+
/** Wire event shape emitted when a confirm prompt is actually shown. */
|
|
118
|
+
export interface BridgeConfirmNotifyEvent {
|
|
119
|
+
type: "agent_notification";
|
|
120
|
+
message: string;
|
|
121
|
+
level: "info" | "warning" | "error";
|
|
122
|
+
}
|
|
123
|
+
/** Per-dialog options as passed by extensions (`ExtensionUIDialogOptions`). */
|
|
124
|
+
export interface BridgeConfirmDialogOptions {
|
|
125
|
+
/** AbortSignal to programmatically deny the dialog. */
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
/** Per-dialog timeout in ms (auto-deny when it fires). */
|
|
128
|
+
timeout?: number;
|
|
129
|
+
/** Outcome side-channel, forwarded to `runTerminalConfirm.onOutcome`. */
|
|
130
|
+
onOutcome?: (outcome: TerminalConfirmOutcome) => void;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build the `confirm(title, message, opts?)` implementation used by the
|
|
134
|
+
* serve-mode headless UI context: routes through `requestTerminalConfirm`
|
|
135
|
+
* (cross-session queue), maps `opts.timeout` → `timeoutMs` and
|
|
136
|
+
* `opts.signal` → `signal`, forwards `opts.onOutcome` (auto-deny vs explicit
|
|
137
|
+
* user denial side-channel), and emits an `agent_notification` (level
|
|
138
|
+
* "warning") once the prompt is rendered so web users know to look at the
|
|
139
|
+
* terminal. `io` defaults to the real stdin IO; `isTTY` overrides the TTY
|
|
140
|
+
* probe (defaults to `process.stdin.isTTY`, i.e. headless-safe).
|
|
141
|
+
*/
|
|
142
|
+
export declare function createBridgeConfirm(emit: (event: BridgeConfirmNotifyEvent) => void, io?: ConfirmPromptIO, opts?: {
|
|
143
|
+
isTTY?: boolean;
|
|
144
|
+
}): (title: string, message: string, dialog?: BridgeConfirmDialogOptions) => Promise<boolean>;
|
|
145
|
+
//# sourceMappingURL=terminal-confirm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-confirm.d.ts","sourceRoot":"","sources":["../../../src/server/ui/terminal-confirm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-native permission confirmations for `spectral serve`.
|
|
3
|
+
*
|
|
4
|
+
* In serve mode there is no TUI, but extensions (notably desktop-control)
|
|
5
|
+
* still call `ctx.ui.confirm(...)` to gate potentially harmful actions. The
|
|
6
|
+
* user is chatting via the web UI, while `spectral serve` itself runs in a
|
|
7
|
+
* real terminal — so the confirmation is rendered there, on serve's stdout,
|
|
8
|
+
* and answered with a single keypress.
|
|
9
|
+
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - Non-TTY stdin: resolves `false` immediately (honest headless deny).
|
|
12
|
+
* - The prompt is a boxed render with a key legend: Enter/y = Allow,
|
|
13
|
+
* n/Esc/q = Deny, Ctrl+C = Deny (consumed as data in raw mode — it must
|
|
14
|
+
* never kill the serve process).
|
|
15
|
+
* - Stdin is put into raw mode for the duration of the prompt and always
|
|
16
|
+
* restored afterwards.
|
|
17
|
+
* - Title and message are sanitized before rendering: ANSI escape sequences
|
|
18
|
+
* and control characters are stripped (newlines collapse to spaces) so a
|
|
19
|
+
* malicious extension cannot inject terminal escapes.
|
|
20
|
+
* - Concurrent confirmations (multiple sessions share one terminal) are
|
|
21
|
+
* serialized through a module-level queue; queued requests print a short
|
|
22
|
+
* "waiting" notice.
|
|
23
|
+
* - Timeouts: `SPECTRAL_CONFIRM_TIMEOUT_MS` env (default 5 min) or the
|
|
24
|
+
* per-dialog `opts.timeout`. On timeout the confirmation resolves `false`.
|
|
25
|
+
* - `onOutcome` reports how the dialog ended exactly once, letting callers
|
|
26
|
+
* (e.g. desktop-control) distinguish an explicit user denial from
|
|
27
|
+
* auto-deny causes (timeout, non-TTY, abort) that must not lock sessions.
|
|
28
|
+
*
|
|
29
|
+
* The prompt logic is pure and takes injectable IO (`write` + `readKey`) so
|
|
30
|
+
* tests run without a TTY.
|
|
31
|
+
*/
|
|
32
|
+
/** Default confirmation timeout: 5 minutes. */
|
|
33
|
+
export const DEFAULT_CONFIRM_TIMEOUT_MS = 300_000;
|
|
34
|
+
/**
|
|
35
|
+
* Classify a raw keypress. Only single-byte keys are acted on; multi-byte
|
|
36
|
+
* sequences (arrow keys, pastes) are ignored so they can't accidentally
|
|
37
|
+
* answer the prompt.
|
|
38
|
+
*/
|
|
39
|
+
export function classifyConfirmKey(data) {
|
|
40
|
+
if (data.length !== 1)
|
|
41
|
+
return "ignore";
|
|
42
|
+
if (data === "\r" || data === "\n" || data === "y" || data === "Y") {
|
|
43
|
+
return "allow";
|
|
44
|
+
}
|
|
45
|
+
if (data === "n" ||
|
|
46
|
+
data === "N" ||
|
|
47
|
+
data === "q" ||
|
|
48
|
+
data === "Q" ||
|
|
49
|
+
data === "\x1b" /* Esc */ ||
|
|
50
|
+
data === "\x03" /* Ctrl+C */) {
|
|
51
|
+
return "deny";
|
|
52
|
+
}
|
|
53
|
+
return "ignore";
|
|
54
|
+
}
|
|
55
|
+
const BOX_MAX_WIDTH = 72;
|
|
56
|
+
/**
|
|
57
|
+
* Complete ANSI escape sequences: CSI (…final byte), OSC (BEL- or ST-
|
|
58
|
+
* terminated) and generic two-byte ESC sequences (e.g. ESC c = reset).
|
|
59
|
+
* CSI/OSC are tried first so their full payload is consumed.
|
|
60
|
+
*/
|
|
61
|
+
const ANSI_ESCAPE_RE = /\x1b(?:\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-~])/g;
|
|
62
|
+
/**
|
|
63
|
+
* Strip ANSI escape sequences and control characters from extension-provided
|
|
64
|
+
* text. Newlines become spaces (the box renders one wrapped block); every
|
|
65
|
+
* other C0 control character (incl. \r and tab), DEL and C1 are removed, as
|
|
66
|
+
* is any stray ESC left over from an unterminated sequence. Keeps the prompt
|
|
67
|
+
* box and result lines safe against terminal escape injection.
|
|
68
|
+
*/
|
|
69
|
+
export function sanitizeConfirmText(text) {
|
|
70
|
+
return text
|
|
71
|
+
.replace(ANSI_ESCAPE_RE, "")
|
|
72
|
+
.replace(/\x1b/g, "")
|
|
73
|
+
.replace(/\n/g, " ")
|
|
74
|
+
.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f]/g, "");
|
|
75
|
+
}
|
|
76
|
+
/** Greedy word-wrap for a single line, fitting `width` columns. */
|
|
77
|
+
function wrapLine(line, width) {
|
|
78
|
+
if (line.length <= width)
|
|
79
|
+
return [line];
|
|
80
|
+
const words = line.split(" ");
|
|
81
|
+
const out = [];
|
|
82
|
+
let current = "";
|
|
83
|
+
for (const word of words) {
|
|
84
|
+
if (current.length === 0) {
|
|
85
|
+
current = word.slice(0, width);
|
|
86
|
+
}
|
|
87
|
+
else if (current.length + 1 + word.length <= width) {
|
|
88
|
+
current += ` ${word}`;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
out.push(current);
|
|
92
|
+
current = word.slice(0, width);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (current.length > 0)
|
|
96
|
+
out.push(current);
|
|
97
|
+
return out.length > 0 ? out : [""];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Render the confirmation prompt as a boxed block of text (with trailing
|
|
101
|
+
* newline). Pure — no IO.
|
|
102
|
+
*/
|
|
103
|
+
export function renderConfirmBox(title, message) {
|
|
104
|
+
const cleanTitle = sanitizeConfirmText(title);
|
|
105
|
+
const cleanMessage = sanitizeConfirmText(message);
|
|
106
|
+
const legend = "Enter/y = Allow n/Esc/q = Deny Ctrl+C = Deny";
|
|
107
|
+
const header = `⚠ Confirmation required · ${cleanTitle}`;
|
|
108
|
+
const innerWidth = Math.min(BOX_MAX_WIDTH, Math.max(header.length, legend.length, 40));
|
|
109
|
+
const lines = [header, ""];
|
|
110
|
+
for (const raw of cleanMessage.split(/\r?\n/)) {
|
|
111
|
+
lines.push(...wrapLine(raw, innerWidth));
|
|
112
|
+
}
|
|
113
|
+
lines.push("", legend);
|
|
114
|
+
const pad = (line) => line + " ".repeat(Math.max(0, innerWidth - line.length));
|
|
115
|
+
const border = "─".repeat(innerWidth + 2);
|
|
116
|
+
const body = lines.map((line) => `│ ${pad(line)} │`).join("\n");
|
|
117
|
+
return `┌${border}┐\n${body}\n└${border}┘\n`;
|
|
118
|
+
}
|
|
119
|
+
/** Read the confirmation timeout from the environment (ms). */
|
|
120
|
+
export function parseConfirmTimeoutMs(env = process.env) {
|
|
121
|
+
const raw = env.SPECTRAL_CONFIRM_TIMEOUT_MS;
|
|
122
|
+
if (raw === undefined || raw === "")
|
|
123
|
+
return DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
124
|
+
const parsed = Number(raw);
|
|
125
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
126
|
+
return DEFAULT_CONFIRM_TIMEOUT_MS;
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Run a single terminal confirmation. Resolves `true` (allow) or `false`
|
|
132
|
+
* (deny / timeout / abort / non-TTY). Never throws.
|
|
133
|
+
*/
|
|
134
|
+
export async function runTerminalConfirm(options) {
|
|
135
|
+
const { title, message, io } = options;
|
|
136
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY === true;
|
|
137
|
+
if (!isTTY) {
|
|
138
|
+
options.onOutcome?.("no_tty");
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (options.signal?.aborted) {
|
|
142
|
+
options.onOutcome?.("cancelled");
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// Fail-closed: a broken terminal must deny the request instead of throwing
|
|
146
|
+
// (callers and the cross-session queue rely on this resolving).
|
|
147
|
+
try {
|
|
148
|
+
io.write(renderConfirmBox(title, message));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
options.onShown?.();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* onShown is a side-channel (e.g. wire emit) — never block the prompt. */
|
|
158
|
+
}
|
|
159
|
+
const safeTitle = sanitizeConfirmText(title);
|
|
160
|
+
return await new Promise((resolve) => {
|
|
161
|
+
let settled = false;
|
|
162
|
+
let cancelRead;
|
|
163
|
+
const timeoutMs = options.timeoutMs ?? parseConfirmTimeoutMs();
|
|
164
|
+
const finish = (result, notice, outcome) => {
|
|
165
|
+
if (settled)
|
|
166
|
+
return;
|
|
167
|
+
settled = true;
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
170
|
+
cancelRead?.();
|
|
171
|
+
options.onOutcome?.(outcome ?? (result ? "allow" : "deny"));
|
|
172
|
+
try {
|
|
173
|
+
if (notice)
|
|
174
|
+
io.write(`${notice}\n`);
|
|
175
|
+
io.write(result ? `✓ Allowed: ${safeTitle}\n\n` : `✗ Denied: ${safeTitle}\n\n`);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* terminal may be gone — the promise must still settle */
|
|
179
|
+
}
|
|
180
|
+
resolve(result);
|
|
181
|
+
};
|
|
182
|
+
const onKey = (data) => {
|
|
183
|
+
const action = classifyConfirmKey(data);
|
|
184
|
+
if (action === "allow")
|
|
185
|
+
finish(true);
|
|
186
|
+
else if (action === "deny")
|
|
187
|
+
finish(false);
|
|
188
|
+
// "ignore": keep waiting for a meaningful key.
|
|
189
|
+
};
|
|
190
|
+
const onAbort = () => finish(false, "Confirmation aborted — denied.", "cancelled");
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
finish(false, `⏰ Confirmation timed out after ${timeoutMs}ms — denied by default.`, "timeout");
|
|
193
|
+
}, timeoutMs);
|
|
194
|
+
// A pending confirmation must not keep the serve process alive on its own.
|
|
195
|
+
if (typeof timer.unref === "function") {
|
|
196
|
+
timer.unref();
|
|
197
|
+
}
|
|
198
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
199
|
+
try {
|
|
200
|
+
cancelRead = io.readKey(onKey);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Could not attach a key listener: deny now instead of hanging until
|
|
204
|
+
// the timeout with no way for the user to answer.
|
|
205
|
+
finish(false, undefined, "cancelled");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Cross-session serialization
|
|
211
|
+
//
|
|
212
|
+
// All sessions in one serve process share a single terminal, so confirmations
|
|
213
|
+
// are queued behind each other at the module level.
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
let queueTail = Promise.resolve();
|
|
216
|
+
let pendingConfirmations = 0;
|
|
217
|
+
const QUEUED_NOTICE = "⏳ Another confirmation is pending in the terminal — request queued.\n";
|
|
218
|
+
/**
|
|
219
|
+
* Request a terminal confirmation, serialized with all other pending
|
|
220
|
+
* confirmations in this process. Queued requests print a short notice.
|
|
221
|
+
*/
|
|
222
|
+
export function requestTerminalConfirm(options) {
|
|
223
|
+
const position = pendingConfirmations;
|
|
224
|
+
pendingConfirmations++;
|
|
225
|
+
// `run` is always chained onto the queue before any side-effectful work
|
|
226
|
+
// (like the queued notice), so a throwing write can never leave the
|
|
227
|
+
// counter inflated. It never rejects: fail-closed deny.
|
|
228
|
+
const run = async () => {
|
|
229
|
+
try {
|
|
230
|
+
return await runTerminalConfirm(options);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
pendingConfirmations--;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const result = queueTail.then(run, run);
|
|
240
|
+
queueTail = result.then(() => undefined, () => undefined);
|
|
241
|
+
if (position > 0) {
|
|
242
|
+
try {
|
|
243
|
+
options.io.write(QUEUED_NOTICE);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* cosmetic notice — the queued confirm still runs */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Default IO bound to the serve process's real terminal: writes to stdout,
|
|
253
|
+
* reads raw keypresses from stdin (entering raw mode for the duration and
|
|
254
|
+
* restoring the previous mode on cancel).
|
|
255
|
+
*/
|
|
256
|
+
export function createStdinConfirmIO(stdin = process.stdin) {
|
|
257
|
+
return {
|
|
258
|
+
write: (text) => {
|
|
259
|
+
process.stdout.write(text);
|
|
260
|
+
},
|
|
261
|
+
readKey(handler) {
|
|
262
|
+
const wasRaw = stdin.isRaw === true;
|
|
263
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
264
|
+
try {
|
|
265
|
+
stdin.setRawMode(true);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Raw mode is what turns Ctrl+C into a plain deny key instead of
|
|
269
|
+
// SIGINT. Without it the "Ctrl+C = Deny" legend is a lie — say so.
|
|
270
|
+
process.stderr.write("⚠ Terminal confirm: could not enable raw mode — Ctrl+C may terminate the process instead of denying.\n");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const onData = (chunk) => {
|
|
274
|
+
handler(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
275
|
+
};
|
|
276
|
+
const cancel = () => {
|
|
277
|
+
stdin.off("data", onData);
|
|
278
|
+
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
279
|
+
try {
|
|
280
|
+
stdin.setRawMode(wasRaw);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
/* best-effort restore */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
stdin.on("data", onData);
|
|
289
|
+
stdin.resume();
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Attach/resume failed (e.g. stdin closed mid-flight): detach the
|
|
293
|
+
// listener and restore raw mode right away, but still hand back the
|
|
294
|
+
// cancel function so callers can never leak.
|
|
295
|
+
cancel();
|
|
296
|
+
}
|
|
297
|
+
return cancel;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Build the `confirm(title, message, opts?)` implementation used by the
|
|
303
|
+
* serve-mode headless UI context: routes through `requestTerminalConfirm`
|
|
304
|
+
* (cross-session queue), maps `opts.timeout` → `timeoutMs` and
|
|
305
|
+
* `opts.signal` → `signal`, forwards `opts.onOutcome` (auto-deny vs explicit
|
|
306
|
+
* user denial side-channel), and emits an `agent_notification` (level
|
|
307
|
+
* "warning") once the prompt is rendered so web users know to look at the
|
|
308
|
+
* terminal. `io` defaults to the real stdin IO; `isTTY` overrides the TTY
|
|
309
|
+
* probe (defaults to `process.stdin.isTTY`, i.e. headless-safe).
|
|
310
|
+
*/
|
|
311
|
+
export function createBridgeConfirm(emit, io = createStdinConfirmIO(), opts) {
|
|
312
|
+
return (title, message, dialog) => requestTerminalConfirm({
|
|
313
|
+
title,
|
|
314
|
+
message,
|
|
315
|
+
io,
|
|
316
|
+
isTTY: opts?.isTTY,
|
|
317
|
+
timeoutMs: dialog?.timeout,
|
|
318
|
+
signal: dialog?.signal,
|
|
319
|
+
onOutcome: dialog?.onOutcome,
|
|
320
|
+
onShown: () => {
|
|
321
|
+
try {
|
|
322
|
+
emit({
|
|
323
|
+
type: "agent_notification",
|
|
324
|
+
message: `Approval required in terminal: ${title}`,
|
|
325
|
+
level: "warning",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
/* best-effort — the prompt must still be shown */
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
package/dist/server/wire.d.ts
CHANGED
|
@@ -9,10 +9,35 @@
|
|
|
9
9
|
* Naming: client → server messages are `ClientMessage`, server → client
|
|
10
10
|
* frames are `ServerEvent`. JSON-encoded one-per-frame on the wire.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Image attached to a user message.
|
|
14
|
+
*
|
|
15
|
+
* Two carriers, in order of preference:
|
|
16
|
+
*
|
|
17
|
+
* 1. `url` — the image was uploaded to backend storage (S3) and only the
|
|
18
|
+
* pointer travels over the relay. **Preferred**: a screenshot is worth
|
|
19
|
+
* megabytes of base64, a url is ~40 characters.
|
|
20
|
+
* 2. `data` — inline base64 (no data URI prefix). Kept for backward
|
|
21
|
+
* compatibility with small/legacy attachments; rejected above
|
|
22
|
+
* `MAX_INLINE_IMAGE_BASE64_CHARS` (measured on the base64 string, i.e.
|
|
23
|
+
* on the wire payload) so the relay never carries megabytes.
|
|
24
|
+
*
|
|
25
|
+
* At least one of `data` / `url` must be present.
|
|
26
|
+
*/
|
|
13
27
|
export interface ImageAttachment {
|
|
14
|
-
/**
|
|
15
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Raw base64 image data (no data URI prefix). Legacy/small images only —
|
|
30
|
+
* hard-capped at `MAX_INLINE_IMAGE_BASE64_CHARS` (768 KiB of base64) at
|
|
31
|
+
* the wire boundary.
|
|
32
|
+
*/
|
|
33
|
+
data?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Hosted image url: backend-relative (`/generated-images/<id>`) or
|
|
36
|
+
* absolute. Preferred over `data` — when both are set the url wins.
|
|
37
|
+
*/
|
|
38
|
+
url?: string;
|
|
39
|
+
/** Backend image id, when the attachment came from `uploadImage()`. */
|
|
40
|
+
imageId?: string;
|
|
16
41
|
/** MIME type, e.g. "image/jpeg" or "image/png". */
|
|
17
42
|
mimeType: string;
|
|
18
43
|
/** Compressed width in pixels (optional). */
|
|
@@ -30,7 +55,7 @@ export interface WireMessage {
|
|
|
30
55
|
createdAt: number;
|
|
31
56
|
/** Stable per-session insertion sequence used for server-authoritative ordering. */
|
|
32
57
|
sequence?: number;
|
|
33
|
-
/**
|
|
58
|
+
/** Image attachments on user messages (hosted `url` or inline base64; empty for assistant/system). */
|
|
34
59
|
images?: ImageAttachment[];
|
|
35
60
|
/** Credits consumed by this message, persisted independently of the events blob (assistant only). */
|
|
36
61
|
creditsUsed?: number | null;
|
|
@@ -285,7 +310,7 @@ export type ClientMessage = {
|
|
|
285
310
|
content: string;
|
|
286
311
|
/** Enable autonomous iterative loop for this message (Ralph Wiggum pattern). */
|
|
287
312
|
loop?: boolean;
|
|
288
|
-
/** Optional
|
|
313
|
+
/** Optional image attachments (hosted `url` preferred, inline base64 legacy). */
|
|
289
314
|
images?: ImageAttachment[];
|
|
290
315
|
/** Optional loop max iterations (server caps at MAX_LOOP_ITERATIONS). */
|
|
291
316
|
loopMaxIterations?: number;
|