@gajae-code/coding-agent 0.15.2 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +13 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +13 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +403 -56
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask.ts +8 -0
- package/src/utils/clipboard.ts +37 -28
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { sanitizeDisplayLine, sanitizeText } from "@gajae-code/utils";
|
|
5
|
+
import { type ExecOptions, type ExecResult, execCommand } from "../../exec/exec";
|
|
6
|
+
import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
|
|
7
|
+
import { commandConsumed, errorMessage, parseSubcommand, usage } from "./parse";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Aside CLI discovery and `/aside` dispatch.
|
|
11
|
+
*
|
|
12
|
+
* Probe-only: never runs the installer. Missing-CLI handling is the caller's
|
|
13
|
+
* decision. This is an explicit composer frontend, not a GJC browser backend.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const ASIDE_INSTALL_URL = "https://releases.aside.com/install.sh";
|
|
17
|
+
export const ASIDE_INSTALL_COMMAND = "curl -fsSL https://releases.aside.com/install.sh | bash";
|
|
18
|
+
|
|
19
|
+
const ASIDE_HELP_VERBS = new Set(["help", "-h", "--help"]);
|
|
20
|
+
|
|
21
|
+
const ASIDE_STATUS_TIMEOUT_MS = 15_000;
|
|
22
|
+
const ASIDE_EXEC_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
export const ASIDE_USAGE = [
|
|
25
|
+
"Aside CLI (explicit opt-in; does not enable GJC browser-control by default)",
|
|
26
|
+
" /aside Show CLI path and usage",
|
|
27
|
+
" /aside <prompt> Run `aside exec <prompt>`",
|
|
28
|
+
" /aside exec [args] Pass argv through to `aside exec`",
|
|
29
|
+
" /aside repl Not available inside GJC; run `aside repl` in a terminal",
|
|
30
|
+
" /aside mcp Print MCP registration using the resolved CLI path",
|
|
31
|
+
" /aside account [args] Run `aside account` (list/status/use)",
|
|
32
|
+
" /aside help Show this help",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
export type AsideCliProbe =
|
|
36
|
+
| { ok: true; path: string }
|
|
37
|
+
| { ok: false; searched: string[]; manualInstallCommand: string; url: string };
|
|
38
|
+
|
|
39
|
+
export type AsideWhich = (command: string) => string | null | undefined;
|
|
40
|
+
export type AsideIsExecutable = (filePath: string) => boolean;
|
|
41
|
+
export type AsideExec = (command: string, args: string[], cwd: string, options?: ExecOptions) => Promise<ExecResult>;
|
|
42
|
+
|
|
43
|
+
export interface AsideHandlerOptions {
|
|
44
|
+
homedir?: () => string;
|
|
45
|
+
which?: AsideWhich;
|
|
46
|
+
isExecutable?: AsideIsExecutable;
|
|
47
|
+
exec?: AsideExec;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Candidate absolute paths for the `aside` CLI, in priority order. */
|
|
51
|
+
export function asideCliCandidates(home = os.homedir()): string[] {
|
|
52
|
+
return [
|
|
53
|
+
path.join(home, ".local", "bin", "aside"),
|
|
54
|
+
path.join(home, ".aside", "cli", "Aside CLI.app", "Contents", "MacOS", "aside"),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultIsExecutable(filePath: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const st = fs.statSync(filePath);
|
|
61
|
+
if (!st.isFile()) return false;
|
|
62
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultWhich(command: string): string | null {
|
|
70
|
+
return Bun.which(command) ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSafeAsideCliPath(value: string): boolean {
|
|
74
|
+
return sanitizeText(value) === value && !/[\r\n]/u.test(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolve the first executable Aside CLI path, or null when none is found. */
|
|
78
|
+
export function resolveAsideCliPath(
|
|
79
|
+
home = os.homedir(),
|
|
80
|
+
options: Pick<AsideHandlerOptions, "which" | "isExecutable"> = {},
|
|
81
|
+
): string | null {
|
|
82
|
+
const isExecutable = options.isExecutable ?? defaultIsExecutable;
|
|
83
|
+
const which = options.which ?? defaultWhich;
|
|
84
|
+
for (const candidate of asideCliCandidates(home)) {
|
|
85
|
+
if (isExecutable(candidate) && isSafeAsideCliPath(candidate)) return candidate;
|
|
86
|
+
}
|
|
87
|
+
const fromPath = which("aside");
|
|
88
|
+
if (fromPath && isExecutable(fromPath) && isSafeAsideCliPath(fromPath)) return fromPath;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Structured probe result for callers that need to guide installation. */
|
|
93
|
+
export function probeAsideCli(
|
|
94
|
+
home = os.homedir(),
|
|
95
|
+
options: Pick<AsideHandlerOptions, "which" | "isExecutable"> = {},
|
|
96
|
+
): AsideCliProbe {
|
|
97
|
+
const searched = [...asideCliCandidates(home), "PATH (aside)"];
|
|
98
|
+
const found = resolveAsideCliPath(home, options);
|
|
99
|
+
if (found) return { ok: true, path: found };
|
|
100
|
+
return { ok: false, searched, manualInstallCommand: ASIDE_INSTALL_COMMAND, url: ASIDE_INSTALL_URL };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function posixQuote(value: string): string {
|
|
104
|
+
if (value.length === 0) return "''";
|
|
105
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) return value;
|
|
106
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Quote a path for the PowerShell command shown on native Windows. */
|
|
110
|
+
export function windowsPowerShellQuote(value: string): string {
|
|
111
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function quoteAsidePath(value: string, platform = process.platform): string {
|
|
115
|
+
return platform === "win32" ? windowsPowerShellQuote(value) : posixQuote(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type AsideArgvResult = { ok: true; args: string[] } | { ok: false; error: string };
|
|
119
|
+
|
|
120
|
+
/** Parse user-provided Aside argv without silently accepting malformed quoting. */
|
|
121
|
+
export function parseAsideArgv(input: string): AsideArgvResult {
|
|
122
|
+
const args: string[] = [];
|
|
123
|
+
let current = "";
|
|
124
|
+
let tokenStarted = false;
|
|
125
|
+
let quote: "'" | '"' | null = null;
|
|
126
|
+
let escaped = false;
|
|
127
|
+
|
|
128
|
+
for (const char of input) {
|
|
129
|
+
if (escaped) {
|
|
130
|
+
current += char;
|
|
131
|
+
tokenStarted = true;
|
|
132
|
+
escaped = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (quote) {
|
|
136
|
+
if (char === quote) {
|
|
137
|
+
quote = null;
|
|
138
|
+
} else if (char === "\\" && quote === '"') {
|
|
139
|
+
escaped = true;
|
|
140
|
+
} else {
|
|
141
|
+
current += char;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (char === "'" || char === '"') {
|
|
146
|
+
quote = char;
|
|
147
|
+
tokenStarted = true;
|
|
148
|
+
} else if (char === "\\") {
|
|
149
|
+
escaped = true;
|
|
150
|
+
tokenStarted = true;
|
|
151
|
+
} else if (char === " " || char === "\t") {
|
|
152
|
+
if (tokenStarted) {
|
|
153
|
+
args.push(current);
|
|
154
|
+
current = "";
|
|
155
|
+
tokenStarted = false;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
current += char;
|
|
159
|
+
tokenStarted = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (escaped) return { ok: false, error: "unfinished escape" };
|
|
164
|
+
if (quote) return { ok: false, error: "unterminated quote" };
|
|
165
|
+
if (tokenStarted) args.push(current);
|
|
166
|
+
return { ok: true, args };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function formatAsideMissingCli(probe: Extract<AsideCliProbe, { ok: false }>): string {
|
|
170
|
+
const installLabel = process.platform === "win32" ? "Install with in WSL or Git Bash:" : "Install with:";
|
|
171
|
+
return [
|
|
172
|
+
"Aside CLI was not found.",
|
|
173
|
+
"Looked in:",
|
|
174
|
+
...probe.searched.map(entry => ` - ${entry}`),
|
|
175
|
+
installLabel,
|
|
176
|
+
` ${probe.manualInstallCommand}`,
|
|
177
|
+
`Docs: ${probe.url}`,
|
|
178
|
+
"Then retry /aside, or register MCP with the concrete binary path.",
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function formatAsideMcpRegistration(cliPath: string, platform = process.platform): string {
|
|
183
|
+
const safeCliPath = sanitizeText(cliPath).replace(/[\r\n]/gu, " ");
|
|
184
|
+
const quotedCliPath = quoteAsidePath(safeCliPath, platform);
|
|
185
|
+
return [
|
|
186
|
+
`Aside CLI: ${safeCliPath}`,
|
|
187
|
+
"`/aside mcp` does not start a stdio server inside GJC.",
|
|
188
|
+
"Register the user-owned MCP definition with the resolved binary:",
|
|
189
|
+
` gjc mcp add aside ${quotedCliPath} mcp --project`,
|
|
190
|
+
"Inspect the redacted record with `gjc mcp list --json`.",
|
|
191
|
+
"Do not paste cookies, screenshots, or private Aside profile paths into issues or PRs.",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatExecOutput(result: ExecResult): string {
|
|
196
|
+
const body = [sanitizeText(result.stdout).trim(), sanitizeText(result.stderr).trim()].filter(Boolean).join("\n");
|
|
197
|
+
if (result.killed) {
|
|
198
|
+
return body ? `${body}\nAside CLI timed out or was cancelled.` : "Aside CLI timed out or was cancelled.";
|
|
199
|
+
}
|
|
200
|
+
if (!body) {
|
|
201
|
+
return result.code === 0
|
|
202
|
+
? "Aside CLI exited 0 with no output."
|
|
203
|
+
: `Aside CLI exited ${result.code} with no output.`;
|
|
204
|
+
}
|
|
205
|
+
if (result.code !== 0) return `${body}\n(exit ${result.code})`;
|
|
206
|
+
return body;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createAsideHandler(options: AsideHandlerOptions = {}) {
|
|
210
|
+
const homedir = options.homedir ?? os.homedir;
|
|
211
|
+
const exec = options.exec ?? execCommand;
|
|
212
|
+
const probeOptions = { which: options.which, isExecutable: options.isExecutable };
|
|
213
|
+
|
|
214
|
+
async function runCli(
|
|
215
|
+
runtime: SlashCommandRuntime,
|
|
216
|
+
cliPath: string,
|
|
217
|
+
args: string[],
|
|
218
|
+
timeout: number,
|
|
219
|
+
): Promise<SlashCommandResult> {
|
|
220
|
+
try {
|
|
221
|
+
const result = await exec(cliPath, args, runtime.cwd, { timeout });
|
|
222
|
+
await runtime.output(formatExecOutput(result));
|
|
223
|
+
return result.code === 0 && !result.killed
|
|
224
|
+
? commandConsumed()
|
|
225
|
+
: { consumed: true, exitCode: result.code || 1 };
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return usage(`Aside CLI failed: ${errorMessage(error)}`, runtime);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return async function handleAsideAcp(
|
|
232
|
+
command: ParsedSlashCommand,
|
|
233
|
+
runtime: SlashCommandRuntime,
|
|
234
|
+
): Promise<SlashCommandResult> {
|
|
235
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
236
|
+
if (ASIDE_HELP_VERBS.has(verb)) return usage(ASIDE_USAGE, runtime);
|
|
237
|
+
|
|
238
|
+
const probe = probeAsideCli(homedir(), probeOptions);
|
|
239
|
+
|
|
240
|
+
if (verb === "mcp") {
|
|
241
|
+
if (rest) return usage("Usage: /aside mcp", runtime);
|
|
242
|
+
if (!probe.ok) return usage(formatAsideMissingCli(probe), runtime);
|
|
243
|
+
return usage(formatAsideMcpRegistration(probe.path), runtime);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (verb === "repl") {
|
|
247
|
+
if (rest) return usage("Usage: /aside repl", runtime);
|
|
248
|
+
if (!probe.ok) return usage(formatAsideMissingCli(probe), runtime);
|
|
249
|
+
return usage(
|
|
250
|
+
[
|
|
251
|
+
"`/aside repl` needs a real terminal TTY.",
|
|
252
|
+
`Run this outside GJC: ${quoteAsidePath(probe.path)} repl`,
|
|
253
|
+
].join("\n"),
|
|
254
|
+
runtime,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!verb) {
|
|
259
|
+
if (!probe.ok) return usage(`${formatAsideMissingCli(probe)}\n\n${ASIDE_USAGE}`, runtime);
|
|
260
|
+
const lines = [`Aside CLI: ${probe.path}`];
|
|
261
|
+
try {
|
|
262
|
+
const version = await exec(probe.path, ["--version"], runtime.cwd, { timeout: ASIDE_STATUS_TIMEOUT_MS });
|
|
263
|
+
const label = sanitizeDisplayLine(version.stdout).trim() || sanitizeDisplayLine(version.stderr).trim();
|
|
264
|
+
if (version.code === 0 && label) lines.push(`Version: ${label.split("\n")[0]}`);
|
|
265
|
+
} catch {
|
|
266
|
+
// Status should still print when `--version` is unavailable.
|
|
267
|
+
}
|
|
268
|
+
lines.push(ASIDE_USAGE);
|
|
269
|
+
await runtime.output(lines.join("\n"));
|
|
270
|
+
return commandConsumed();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!probe.ok) return usage(formatAsideMissingCli(probe), runtime);
|
|
274
|
+
|
|
275
|
+
if (verb === "account") {
|
|
276
|
+
const parsed = parseAsideArgv(rest);
|
|
277
|
+
if (!parsed.ok) return usage(`Invalid Aside arguments: ${parsed.error}.`, runtime);
|
|
278
|
+
return runCli(runtime, probe.path, ["account", ...parsed.args], ASIDE_STATUS_TIMEOUT_MS);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (verb === "exec") {
|
|
282
|
+
const parsed = parseAsideArgv(rest);
|
|
283
|
+
if (!parsed.ok) return usage(`Invalid Aside arguments: ${parsed.error}.`, runtime);
|
|
284
|
+
const argv = parsed.args;
|
|
285
|
+
if (argv.length === 0) return usage("Usage: /aside exec [args] <prompt>", runtime);
|
|
286
|
+
return runCli(runtime, probe.path, ["exec", ...argv], ASIDE_EXEC_TIMEOUT_MS);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (verb.startsWith("-")) {
|
|
290
|
+
const parsed = parseAsideArgv(command.args);
|
|
291
|
+
if (!parsed.ok) return usage(`Invalid Aside arguments: ${parsed.error}.`, runtime);
|
|
292
|
+
return runCli(runtime, probe.path, ["exec", ...parsed.args], ASIDE_EXEC_TIMEOUT_MS);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return runCli(runtime, probe.path, ["exec", command.args.trim()], ASIDE_EXEC_TIMEOUT_MS);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const handleAsideAcp = createAsideHandler();
|
package/src/tools/ask.ts
CHANGED
|
@@ -733,6 +733,14 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
733
733
|
readonly rawArgumentValidation = (arguments_: Record<string, unknown>): RawArgumentValidationResult =>
|
|
734
734
|
recoverRoundZeroIntentContract(arguments_, this.session.getDeepInterviewAskStage?.());
|
|
735
735
|
readonly strict = true;
|
|
736
|
+
/**
|
|
737
|
+
* Display-only fields: question text and option labels render to the user
|
|
738
|
+
* and carry no executable or persisted meaning, so `\uXXXX`-escaped
|
|
739
|
+
* non-ASCII text inside them degrades to a warning instead of failing the
|
|
740
|
+
* run (issue #4983). Ids, workflow-gate metadata, and deep-interview
|
|
741
|
+
* records stay load-bearing and keep the fail-closed rejection.
|
|
742
|
+
*/
|
|
743
|
+
readonly displaySafeEscapedArgFields = ["questions.question", "questions.options.label"] as const;
|
|
736
744
|
readonly loadMode = "discoverable";
|
|
737
745
|
constructor(private readonly session: ToolSession) {
|
|
738
746
|
this.description = prompt.render(askDescription);
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -34,6 +34,10 @@ export class ClipboardTransportError extends Error {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export type ClipboardDeliveryOutcome =
|
|
38
|
+
| { status: "verified"; transport: "ssh" }
|
|
39
|
+
| { status: "attempted"; transport: "auto" | "native" | "osc52"; reason: string };
|
|
40
|
+
|
|
37
41
|
const SSH_HOST_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
38
42
|
const SSH_OPERATION_TIMEOUT_MS = 5000;
|
|
39
43
|
const SSH_CONNECT_TIMEOUT_S = 3;
|
|
@@ -261,41 +265,32 @@ function getClipboardTransportConfig(): { transport: "auto" | "native" | "osc52"
|
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
267
|
|
|
264
|
-
function emitOsc52(text: string):
|
|
265
|
-
if (!process.stdout.isTTY) return;
|
|
266
|
-
|
|
268
|
+
function emitOsc52(text: string): boolean {
|
|
269
|
+
if (!process.stdout.isTTY) return false;
|
|
270
|
+
let delivered = false;
|
|
271
|
+
const onError = (_err: unknown) => {
|
|
267
272
|
process.stdout.off("error", onError);
|
|
268
|
-
// Prevent unhandled 'error' from crashing the process when stdout is a closed pipe.
|
|
269
|
-
if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
273
|
};
|
|
273
274
|
try {
|
|
274
275
|
const encoded = Buffer.from(text).toString("base64");
|
|
275
276
|
const osc52 = `\x1b]52;c;${encoded}\x07`;
|
|
276
277
|
process.stdout.on("error", onError);
|
|
277
|
-
process.stdout.write(osc52,
|
|
278
|
+
process.stdout.write(osc52, _err => {
|
|
278
279
|
process.stdout.off("error", onError);
|
|
279
|
-
// If stdout is closed (e.g. piped to a process that exits early),
|
|
280
|
-
// ignore EPIPE and proceed with native clipboard best-effort.
|
|
281
|
-
if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
280
|
});
|
|
285
|
-
|
|
281
|
+
delivered = true;
|
|
282
|
+
} catch (_err) {
|
|
286
283
|
process.stdout.off("error", onError);
|
|
287
|
-
if ((err as NodeJS.ErrnoException | null | undefined)?.code !== "EPIPE") {
|
|
288
|
-
// Ignore all write failures (OSC 52 is best-effort).
|
|
289
|
-
}
|
|
290
284
|
}
|
|
285
|
+
return delivered;
|
|
291
286
|
}
|
|
292
287
|
|
|
293
|
-
async function copyToClipboardNative(text: string): Promise<
|
|
288
|
+
async function copyToClipboardNative(text: string): Promise<boolean> {
|
|
294
289
|
try {
|
|
295
290
|
if (process.env.TERMUX_VERSION) {
|
|
296
291
|
try {
|
|
297
292
|
execSync("termux-clipboard-set", { input: text, timeout: 5000 });
|
|
298
|
-
return;
|
|
293
|
+
return true;
|
|
299
294
|
} catch {
|
|
300
295
|
// Fall through to native
|
|
301
296
|
}
|
|
@@ -305,8 +300,9 @@ async function copyToClipboardNative(text: string): Promise<void> {
|
|
|
305
300
|
// unaffected by the ssh/osc52 explicit-transport contract below, which
|
|
306
301
|
// never reaches this function.
|
|
307
302
|
nativeClipboard().copyToClipboard(text);
|
|
303
|
+
return true;
|
|
308
304
|
} catch {
|
|
309
|
-
|
|
305
|
+
return false;
|
|
310
306
|
}
|
|
311
307
|
}
|
|
312
308
|
|
|
@@ -324,27 +320,40 @@ async function copyToClipboardNative(text: string): Promise<void> {
|
|
|
324
320
|
*
|
|
325
321
|
* @param text - UTF-8 text to place on the clipboard.
|
|
326
322
|
*/
|
|
327
|
-
export async function copyToClipboard(text: string): Promise<
|
|
323
|
+
export async function copyToClipboard(text: string): Promise<ClipboardDeliveryOutcome> {
|
|
328
324
|
const { transport, sshHost } = getClipboardTransportConfig();
|
|
329
325
|
|
|
330
326
|
if (transport === "ssh") {
|
|
331
327
|
await copyToClipboardViaSsh(sshHost, text);
|
|
332
|
-
return;
|
|
328
|
+
return { status: "verified", transport: "ssh" };
|
|
333
329
|
}
|
|
334
330
|
|
|
335
331
|
if (transport === "osc52") {
|
|
336
|
-
emitOsc52(text);
|
|
337
|
-
return
|
|
332
|
+
const delivered = emitOsc52(text);
|
|
333
|
+
return {
|
|
334
|
+
status: "attempted",
|
|
335
|
+
transport: "osc52",
|
|
336
|
+
reason: delivered ? "terminal accepted write; no read-back" : "terminal rejected write",
|
|
337
|
+
};
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
if (transport === "native") {
|
|
341
|
-
await copyToClipboardNative(text);
|
|
342
|
-
return
|
|
341
|
+
const delivered = await copyToClipboardNative(text);
|
|
342
|
+
return {
|
|
343
|
+
status: "attempted",
|
|
344
|
+
transport: "native",
|
|
345
|
+
reason: delivered ? "native API provides no read-back verification" : "native clipboard API failed",
|
|
346
|
+
};
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
// auto: prior best-effort dual-path behavior.
|
|
346
|
-
emitOsc52(text);
|
|
347
|
-
await copyToClipboardNative(text);
|
|
350
|
+
const osc52Delivered = emitOsc52(text);
|
|
351
|
+
const nativeDelivered = await copyToClipboardNative(text);
|
|
352
|
+
return {
|
|
353
|
+
status: "attempted",
|
|
354
|
+
transport: "auto",
|
|
355
|
+
reason: `OSC52 ${osc52Delivered ? "accepted" : "failed"}; native ${nativeDelivered ? "accepted" : "failed"}; no read-back verification`,
|
|
356
|
+
};
|
|
348
357
|
}
|
|
349
358
|
|
|
350
359
|
/**
|