agent-yes 1.72.4 → 1.73.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-yes.config.schema.json +32 -8
- package/default.config.yaml +154 -0
- package/dist/SUPPORTED_CLIS-C7sGMdKJ.js +10 -0
- package/dist/{agent-yes.config-CtQprJrA.js → agent-yes.config-CyP5iRZf.js} +75 -119
- package/dist/cli.js +147 -16
- package/dist/index.js +3 -2
- package/dist/logger-B9h0djqx.js +51 -0
- package/dist/package-DpfHTSW2.js +7 -0
- package/dist/pidStore-B4yDm3TL.js +4 -0
- package/dist/pidStore-CPrgJSJi.js +319 -0
- package/dist/{runningLock-BBI_URhR.js → runningLock-DQWJSptq.js} +3 -3
- package/dist/{tray-CPpdxTV-.js → tray-Bzb1owBN.js} +4 -4
- package/dist/{SUPPORTED_CLIS-Bqw9gxey.js → ts-CsdLrLod.js} +146 -362
- package/package.json +9 -3
- package/ts/cli.ts +16 -9
- package/ts/configLoader.spec.ts +19 -0
- package/ts/configLoader.ts +8 -2
- package/ts/configShared.spec.ts +97 -0
- package/ts/configShared.ts +158 -0
- package/ts/index.ts +93 -102
- package/ts/logger.spec.ts +27 -0
- package/ts/logger.ts +63 -19
- package/ts/parseCliArgs.spec.ts +88 -0
- package/ts/parseCliArgs.ts +48 -10
- package/ts/rustBinary.ts +68 -0
- package/ts/versionChecker.spec.ts +48 -0
- package/ts/versionChecker.ts +67 -10
- package/ts/xterm-proxy.ts +130 -0
- package/dist/logger-CX77vJDA.js +0 -16
package/ts/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import DIE from "phpdie";
|
|
6
6
|
import sflow from "sflow";
|
|
7
|
-
import {
|
|
7
|
+
import { XtermProxy } from "./xterm-proxy.ts";
|
|
8
8
|
import {
|
|
9
9
|
extractSessionId,
|
|
10
10
|
getSessionForCwd,
|
|
@@ -16,7 +16,6 @@ import { acquireLock, releaseLock, shouldUseLock } from "./runningLock.ts";
|
|
|
16
16
|
import { logger } from "./logger.ts";
|
|
17
17
|
import { createFifoStream } from "./beta/fifo.ts";
|
|
18
18
|
import { PidStore } from "./pidStore.ts";
|
|
19
|
-
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
20
19
|
import { sendEnter, sendMessage } from "./core/messaging.ts";
|
|
21
20
|
import {
|
|
22
21
|
initializeLogPaths,
|
|
@@ -41,11 +40,16 @@ export type AgentCliConfig = {
|
|
|
41
40
|
version?: string; // hint user for version command to check if installed
|
|
42
41
|
binary?: string; // actual binary name if different from cli, e.g. cursor -> cursor-agent
|
|
43
42
|
defaultArgs?: string[]; // function to ensure certain args are present
|
|
43
|
+
help?: string; // documentation/help URL for the CLI
|
|
44
|
+
bunx?: boolean; // metadata for bunx-based launches
|
|
45
|
+
systemPrompt?: string; // flag name for system prompt injection
|
|
46
|
+
system?: string; // system prompt content to inject
|
|
44
47
|
|
|
45
48
|
// status detect, and actions
|
|
46
49
|
ready?: RegExp[]; // regex matcher for stdin ready, or line index for gemini. Set to empty array [] to disable ready check entirely.
|
|
47
50
|
fatal?: RegExp[]; // array of regex to match for fatal errors
|
|
48
51
|
working?: RegExp[]; // regex matcher for working status
|
|
52
|
+
updateAvailable?: RegExp[]; // regex matcher for update available banners
|
|
49
53
|
exitCommands?: string[]; // commands to exit the cli gracefully
|
|
50
54
|
promptArg?: (string & {}) | "first-arg" | "last-arg"; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
|
|
51
55
|
|
|
@@ -121,7 +125,7 @@ export default async function agentYes({
|
|
|
121
125
|
autoYes = true,
|
|
122
126
|
idleAction,
|
|
123
127
|
}: {
|
|
124
|
-
cli:
|
|
128
|
+
cli: keyof typeof CLIS_CONFIG;
|
|
125
129
|
cliArgs?: string[];
|
|
126
130
|
prompt?: string;
|
|
127
131
|
robust?: boolean;
|
|
@@ -185,9 +189,14 @@ export default async function agentYes({
|
|
|
185
189
|
process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
|
|
186
190
|
if (verbose) logger.debug(`[stdin] Raw mode set, isRaw: ${(process.stdin as any).isRaw}`);
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
// XtermProxy: headless xterm emulator that auto-responds to all terminal
|
|
193
|
+
// queries (DSR, DA, etc.) so the spawned CLI never blocks waiting for replies.
|
|
194
|
+
// writeToPty is set after shell spawn (see below).
|
|
195
|
+
let shellWrite: (data: string) => void = () => {};
|
|
196
|
+
const xtermProxy = new XtermProxy({
|
|
197
|
+
...getTerminalDimensions(),
|
|
198
|
+
writeToPty: (data) => shellWrite(data),
|
|
199
|
+
});
|
|
191
200
|
|
|
192
201
|
logger.debug(`Using ${ptyPackage} for pseudo terminal management.`);
|
|
193
202
|
|
|
@@ -329,11 +338,14 @@ export default async function agentYes({
|
|
|
329
338
|
ptyOptions,
|
|
330
339
|
});
|
|
331
340
|
|
|
341
|
+
// Wire up the xterm proxy to write back to the PTY
|
|
342
|
+
shellWrite = (data: string) => shell.write(data);
|
|
343
|
+
|
|
332
344
|
// Attach data handler IMMEDIATELY after spawn to avoid losing early PTY output.
|
|
333
345
|
// node-pty emits 'data' events eagerly — if no listener is attached, events are lost.
|
|
334
346
|
function onData(data: string) {
|
|
335
347
|
const currentPid = shell.pid;
|
|
336
|
-
|
|
348
|
+
xtermProxy.write(data);
|
|
337
349
|
globalAgentRegistry.appendStdout(currentPid, data);
|
|
338
350
|
}
|
|
339
351
|
shell.onData(onData);
|
|
@@ -348,7 +360,7 @@ export default async function agentYes({
|
|
|
348
360
|
|
|
349
361
|
// Initialize log paths (independent of registration)
|
|
350
362
|
const logPaths = await initializeLogPaths(pidStore, shell.pid);
|
|
351
|
-
setupDebugLogging(logPaths.debuggingLogsPath);
|
|
363
|
+
await setupDebugLogging(logPaths.debuggingLogsPath);
|
|
352
364
|
|
|
353
365
|
// Create agent context
|
|
354
366
|
const ctx = new AgentContext({
|
|
@@ -447,6 +459,7 @@ export default async function agentYes({
|
|
|
447
459
|
env: ptyEnv,
|
|
448
460
|
};
|
|
449
461
|
shell = pty.spawn(bin!, args, restartPtyOptions);
|
|
462
|
+
shellWrite = (data: string) => shell.write(data);
|
|
450
463
|
// Register process in pidStore (non-blocking)
|
|
451
464
|
try {
|
|
452
465
|
await pidStore.registerProcess({ pid: shell.pid, cli, args, prompt, cwd: workingDir });
|
|
@@ -548,6 +561,7 @@ export default async function agentYes({
|
|
|
548
561
|
env: ptyEnv,
|
|
549
562
|
};
|
|
550
563
|
shell = pty.spawn(cli, restoreArgs, restorePtyOptions);
|
|
564
|
+
shellWrite = (data: string) => shell.write(data);
|
|
551
565
|
// Register process in pidStore (non-blocking)
|
|
552
566
|
try {
|
|
553
567
|
await pidStore.registerProcess({
|
|
@@ -601,14 +615,15 @@ export default async function agentYes({
|
|
|
601
615
|
return pendingExitCode.resolve(exitCode);
|
|
602
616
|
});
|
|
603
617
|
|
|
604
|
-
// when current tty resized, resize
|
|
618
|
+
// when current tty resized, resize both pty and xterm proxy
|
|
605
619
|
process.stdout.on("resize", () => {
|
|
606
620
|
const { cols, rows } = getTerminalDimensions();
|
|
607
621
|
shell.resize(cols, rows);
|
|
622
|
+
xtermProxy.resize(cols, rows);
|
|
608
623
|
});
|
|
609
624
|
|
|
610
625
|
const isStillWorkingQ = () => {
|
|
611
|
-
const rendered =
|
|
626
|
+
const rendered = xtermProxy.tail(24).replace(/\s+/g, " ");
|
|
612
627
|
return conf.working?.some((rgx) => rgx.test(rendered));
|
|
613
628
|
};
|
|
614
629
|
|
|
@@ -617,7 +632,7 @@ export default async function agentYes({
|
|
|
617
632
|
let lastHeartbeatRendered = "";
|
|
618
633
|
const heartbeatInterval = setInterval(async () => {
|
|
619
634
|
try {
|
|
620
|
-
const rendered = removeControlCharacters(
|
|
635
|
+
const rendered = removeControlCharacters(xtermProxy.tail(12));
|
|
621
636
|
|
|
622
637
|
// Skip if output hasn't changed since last heartbeat
|
|
623
638
|
if (rendered === lastHeartbeatRendered) return;
|
|
@@ -895,7 +910,7 @@ export default async function agentYes({
|
|
|
895
910
|
shell.write(data);
|
|
896
911
|
},
|
|
897
912
|
}),
|
|
898
|
-
readable:
|
|
913
|
+
readable: xtermProxy.readable,
|
|
899
914
|
})
|
|
900
915
|
|
|
901
916
|
.forEach(() => {
|
|
@@ -926,97 +941,72 @@ export default async function agentYes({
|
|
|
926
941
|
// if (cli === "codex" && !process.stdin.isTTY) shell.write(`\u001b[1;1R`); // send cursor position response when stdin is not tty
|
|
927
942
|
|
|
928
943
|
let lastRendered = "";
|
|
929
|
-
return
|
|
930
|
-
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
944
|
+
return (
|
|
945
|
+
e
|
|
946
|
+
// Terminal query responses (DA, DSR, etc.) are handled automatically
|
|
947
|
+
// by XtermProxy via @xterm/headless — no ad-hoc interception needed.
|
|
948
|
+
.forEach(async (line, lineIndex) => {
|
|
949
|
+
// ============ respond on rendered screen
|
|
950
|
+
const rendered = xtermProxy.tail(24);
|
|
951
|
+
// Skip processing if output hasn't changed
|
|
952
|
+
if (rendered === lastRendered) return;
|
|
953
|
+
lastRendered = rendered;
|
|
954
|
+
|
|
955
|
+
logger.debug(`stdout|${line}`);
|
|
956
|
+
|
|
957
|
+
// ready matcher: if matched, mark stdin ready
|
|
958
|
+
if (conf.ready?.some((rx: RegExp) => line.match(rx))) {
|
|
959
|
+
logger.debug(`ready |${line}`);
|
|
960
|
+
if (cli === "gemini" && lineIndex <= 80) return; // gemini initial noise, only after many lines
|
|
961
|
+
ctx.stdinReady.ready();
|
|
962
|
+
ctx.stdinFirstReady.ready();
|
|
943
963
|
}
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Only handle cursor position when stdin is not tty, because tty already handled this
|
|
948
|
-
if (process.stdin.isTTY) return;
|
|
949
|
-
|
|
950
|
-
// Handle cursor position request - ESC[6n
|
|
951
|
-
if (!chunk.includes("\u001b[6n")) return;
|
|
952
|
-
|
|
953
|
-
// xterm replies CSI row; column R if asked cursor position
|
|
954
|
-
// https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
|
|
955
|
-
const { col, row } = terminalRender.getCursorPosition();
|
|
956
|
-
shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
|
|
957
|
-
|
|
958
|
-
logger.debug(`cursor|respond position: row=${String(row)}, col=${String(col)}`);
|
|
959
|
-
})
|
|
960
|
-
.forEach(async (line, lineIndex) => {
|
|
961
|
-
// ============ respond on rendered screen
|
|
962
|
-
const rendered = terminalRender.tail(24);
|
|
963
|
-
// Skip processing if output hasn't changed
|
|
964
|
-
if (rendered === lastRendered) return;
|
|
965
|
-
|
|
966
|
-
logger.debug(`stdout|${line}`);
|
|
967
|
-
|
|
968
|
-
// ready matcher: if matched, mark stdin ready
|
|
969
|
-
if (conf.ready?.some((rx: RegExp) => line.match(rx))) {
|
|
970
|
-
logger.debug(`ready |${line}`);
|
|
971
|
-
if (cli === "gemini" && lineIndex <= 80) return; // gemini initial noise, only after many lines
|
|
972
|
-
ctx.stdinReady.ready();
|
|
973
|
-
ctx.stdinFirstReady.ready();
|
|
974
|
-
}
|
|
975
964
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
965
|
+
// enter matchers: send Enter when any enter regex matches
|
|
966
|
+
if (conf.enter?.some((rx: RegExp) => line.match(rx))) {
|
|
967
|
+
logger.debug(`sendEnter matched|${line}`);
|
|
968
|
+
return await sendEnter(ctx.messageContext, 400); // wait for idle for a short while and then send Enter
|
|
969
|
+
}
|
|
981
970
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
971
|
+
// typingRespond matcher: if matched, send the specified message
|
|
972
|
+
const typeingRespondMatched = Object.entries(conf.typingRespond ?? {}).filter(
|
|
973
|
+
([_sendString, onThePatterns]) => onThePatterns.some((rx) => line.match(rx)),
|
|
974
|
+
);
|
|
975
|
+
const typingResponded =
|
|
976
|
+
typeingRespondMatched.length &&
|
|
977
|
+
(await sflow(typeingRespondMatched)
|
|
978
|
+
.map(
|
|
979
|
+
async ([sendString]) =>
|
|
980
|
+
await sendMessage(ctx.messageContext, sendString, { waitForReady: false }),
|
|
981
|
+
)
|
|
982
|
+
.toCount());
|
|
983
|
+
if (typingResponded) return;
|
|
984
|
+
|
|
985
|
+
// fatal matchers: set isFatal flag when matched
|
|
986
|
+
if (conf.fatal?.some((rx: RegExp) => line.match(rx))) {
|
|
987
|
+
logger.debug(`fatal |${line}`);
|
|
988
|
+
ctx.isFatal = true;
|
|
989
|
+
await exitAgent();
|
|
990
|
+
}
|
|
1002
991
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
992
|
+
// restartWithoutContinueArg matchers: set flag to restart without continue args
|
|
993
|
+
if (conf.restartWithoutContinueArg?.some((rx: RegExp) => line.match(rx))) {
|
|
994
|
+
logger.debug(`restart-without-continue|${line}`);
|
|
995
|
+
ctx.shouldRestartWithoutContinue = true;
|
|
996
|
+
ctx.isFatal = true; // also set fatal to trigger exit
|
|
997
|
+
await exitAgent();
|
|
998
|
+
}
|
|
1010
999
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1000
|
+
// session ID capture for codex
|
|
1001
|
+
if (cli === "codex") {
|
|
1002
|
+
const sessionId = extractSessionId(line);
|
|
1003
|
+
if (sessionId) {
|
|
1004
|
+
logger.debug(`session|captured session ID: ${sessionId}`);
|
|
1005
|
+
await storeSessionForCwd(workingDir, sessionId);
|
|
1006
|
+
}
|
|
1017
1007
|
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1020
1010
|
})
|
|
1021
1011
|
|
|
1022
1012
|
// auto-response
|
|
@@ -1043,7 +1033,7 @@ export default async function agentYes({
|
|
|
1043
1033
|
.by(createTerminatorStream(pendingExitCode.promise))
|
|
1044
1034
|
.to(fromWritable(process.stdout));
|
|
1045
1035
|
|
|
1046
|
-
await saveLogFile(ctx.logPaths.logPath,
|
|
1036
|
+
await saveLogFile(ctx.logPaths.logPath, xtermProxy.render());
|
|
1047
1037
|
|
|
1048
1038
|
// and then get its exitcode
|
|
1049
1039
|
const exitCode = await pendingExitCode.promise;
|
|
@@ -1052,13 +1042,14 @@ export default async function agentYes({
|
|
|
1052
1042
|
// Final pidStore cleanup
|
|
1053
1043
|
await pidStore.close();
|
|
1054
1044
|
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1045
|
+
// Capture final render before disposing xterm proxy
|
|
1046
|
+
const finalRender = xtermProxy.render();
|
|
1047
|
+
xtermProxy.dispose();
|
|
1057
1048
|
|
|
1058
1049
|
// deprecated logFile option, we have logPath now, but keep for backward compatibility
|
|
1059
|
-
await saveDeprecatedLogFile(logFile,
|
|
1050
|
+
await saveDeprecatedLogFile(logFile, finalRender, verbose);
|
|
1060
1051
|
|
|
1061
|
-
return { exitCode, logs:
|
|
1052
|
+
return { exitCode, logs: finalRender };
|
|
1062
1053
|
|
|
1063
1054
|
async function exitAgent() {
|
|
1064
1055
|
ctx.robust = false; // disable robust to avoid auto restart
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Reset module state between tests by re-importing fresh
|
|
4
|
+
describe("logger", () => {
|
|
5
|
+
it("queues messages before winston loads and flushes them", async () => {
|
|
6
|
+
const { logger, flushLogger } = await import("./logger.ts");
|
|
7
|
+
const logs: string[] = [];
|
|
8
|
+
logger.info("queued message");
|
|
9
|
+
await flushLogger();
|
|
10
|
+
// If we reach here without throwing, the lazy init succeeded
|
|
11
|
+
expect(true).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("logs directly when already initialized", async () => {
|
|
15
|
+
const { logger } = await import("./logger.ts");
|
|
16
|
+
// Second call — _inner should be set, no queue
|
|
17
|
+
expect(() => logger.info("direct message")).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("addTransport adds a transport after init", async () => {
|
|
21
|
+
const { addTransport, flushLogger } = await import("./logger.ts");
|
|
22
|
+
await flushLogger(); // ensure initialized
|
|
23
|
+
const winston = (await import("winston")).default;
|
|
24
|
+
const transport = new winston.transports.Console({ silent: true });
|
|
25
|
+
await expect(addTransport(transport)).resolves.toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
package/ts/logger.ts
CHANGED
|
@@ -1,21 +1,65 @@
|
|
|
1
|
-
|
|
1
|
+
type LogLevel = "error" | "warn" | "info" | "http" | "verbose" | "debug" | "silly";
|
|
2
|
+
type WinstonLike = Record<LogLevel, (msg: string, ...meta: unknown[]) => void>;
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
7
|
-
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
8
|
-
return `${timestamp} [${level}]: ${message}${metaStr}`;
|
|
9
|
-
}),
|
|
10
|
-
);
|
|
4
|
+
let _inner: WinstonLike | null = null;
|
|
5
|
+
let _initPromise: Promise<void> | null = null;
|
|
6
|
+
const _queue: Array<{ level: LogLevel; msg: string; meta: unknown[] }> = [];
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
function init(): Promise<void> {
|
|
9
|
+
if (_initPromise) return _initPromise;
|
|
10
|
+
_initPromise = import("winston").then(({ default: winston }) => {
|
|
11
|
+
const logFormat = winston.format.combine(
|
|
12
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
13
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
14
|
+
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
15
|
+
return `${timestamp} [${level}]: ${message}${metaStr}`;
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
_inner = winston.createLogger({
|
|
19
|
+
level: process.env.VERBOSE ? "debug" : "info",
|
|
20
|
+
format: logFormat,
|
|
21
|
+
transports: [
|
|
22
|
+
new winston.transports.Console({
|
|
23
|
+
format: winston.format.combine(winston.format.colorize(), logFormat),
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
silent: false,
|
|
27
|
+
}) as unknown as WinstonLike;
|
|
28
|
+
for (const { level, msg, meta } of _queue.splice(0)) {
|
|
29
|
+
_inner[level](msg, ...meta);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return _initPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeMethod(level: LogLevel) {
|
|
36
|
+
return (msg: string, ...meta: unknown[]) => {
|
|
37
|
+
if (_inner) {
|
|
38
|
+
_inner[level](msg, ...meta);
|
|
39
|
+
} else {
|
|
40
|
+
_queue.push({ level, msg, meta });
|
|
41
|
+
init().catch((e) => console.error("[logger] Failed to load winston:", e));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Wait for all queued log messages to be flushed. Call before process.exit when needed. */
|
|
47
|
+
export async function flushLogger(): Promise<void> {
|
|
48
|
+
await init();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Add a winston transport. Awaits logger initialization first. */
|
|
52
|
+
export async function addTransport(transport: unknown): Promise<void> {
|
|
53
|
+
await init();
|
|
54
|
+
(_inner as unknown as { add(t: unknown): void }).add(transport);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const logger = {
|
|
58
|
+
error: makeMethod("error"),
|
|
59
|
+
warn: makeMethod("warn"),
|
|
60
|
+
info: makeMethod("info"),
|
|
61
|
+
http: makeMethod("http"),
|
|
62
|
+
verbose: makeMethod("verbose"),
|
|
63
|
+
debug: makeMethod("debug"),
|
|
64
|
+
silly: makeMethod("silly"),
|
|
65
|
+
};
|
package/ts/parseCliArgs.spec.ts
CHANGED
|
@@ -333,4 +333,92 @@ describe("CLI argument parsing", () => {
|
|
|
333
333
|
expect(result.cli).toBe("claude");
|
|
334
334
|
expect(result.autoYes).toBe(true);
|
|
335
335
|
});
|
|
336
|
+
|
|
337
|
+
it("should parse bare positional words as prompt (cy arg1 arg2)", () => {
|
|
338
|
+
const result = parseCliArgs(["node", "/usr/local/bin/cy", "arg1", "arg2"]);
|
|
339
|
+
|
|
340
|
+
expect(result.cli).toBe("claude");
|
|
341
|
+
expect(result.prompt).toBe("arg1 arg2");
|
|
342
|
+
expect(result.cliArgs).toEqual([]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should parse bare positional words as prompt with explicit CLI name", () => {
|
|
346
|
+
const result = parseCliArgs(["node", "/path/to/cli", "claude", "fix", "this", "bug"]);
|
|
347
|
+
|
|
348
|
+
expect(result.cli).toBe("claude");
|
|
349
|
+
expect(result.prompt).toBe("fix this bug");
|
|
350
|
+
expect(result.cliArgs).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should parse bare words as prompt with claude-yes script name", () => {
|
|
354
|
+
const result = parseCliArgs(["node", "/usr/local/bin/claude-yes", "solve", "all", "todos"]);
|
|
355
|
+
|
|
356
|
+
expect(result.cli).toBe("claude");
|
|
357
|
+
expect(result.prompt).toBe("solve all todos");
|
|
358
|
+
expect(result.cliArgs).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should separate CLI flags from bare words as prompt", () => {
|
|
362
|
+
const result = parseCliArgs([
|
|
363
|
+
"node",
|
|
364
|
+
"/usr/local/bin/claude-yes",
|
|
365
|
+
"--some-flag",
|
|
366
|
+
"value",
|
|
367
|
+
"fix",
|
|
368
|
+
"the",
|
|
369
|
+
"bug",
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
expect(result.cli).toBe("claude");
|
|
373
|
+
expect(result.prompt).toBe("fix the bug");
|
|
374
|
+
expect(result.cliArgs).toContain("--some-flag");
|
|
375
|
+
expect(result.cliArgs).toContain("value");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should combine --prompt flag with bare positional words", () => {
|
|
379
|
+
const result = parseCliArgs([
|
|
380
|
+
"node",
|
|
381
|
+
"/usr/local/bin/claude-yes",
|
|
382
|
+
"--prompt",
|
|
383
|
+
"prefix:",
|
|
384
|
+
"solve",
|
|
385
|
+
"this",
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
expect(result.prompt).toBe("prefix: solve this");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should combine bare positional words with dash prompt", () => {
|
|
392
|
+
const result = parseCliArgs([
|
|
393
|
+
"node",
|
|
394
|
+
"/usr/local/bin/claude-yes",
|
|
395
|
+
"solve",
|
|
396
|
+
"--",
|
|
397
|
+
"this",
|
|
398
|
+
"too",
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
expect(result.prompt).toBe("solve this too");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should not pass --no-rust to the target CLI", () => {
|
|
405
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-rust"]);
|
|
406
|
+
|
|
407
|
+
expect(result.useRust).toBe(false);
|
|
408
|
+
expect(result.cliArgs).not.toContain("--no-rust");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should not pass --no-robust to the target CLI", () => {
|
|
412
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-robust"]);
|
|
413
|
+
|
|
414
|
+
expect(result.robust).toBe(false);
|
|
415
|
+
expect(result.cliArgs).not.toContain("--no-robust");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should not pass --no-queue to the target CLI", () => {
|
|
419
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-queue"]);
|
|
420
|
+
|
|
421
|
+
expect(result.queue).toBe(false);
|
|
422
|
+
expect(result.cliArgs).not.toContain("--no-queue");
|
|
423
|
+
});
|
|
336
424
|
});
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
|
-
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
5
4
|
/**
|
|
6
5
|
* Parse CLI arguments the same way cli.ts does
|
|
7
6
|
* This is a test helper that mirrors the parsing logic in cli.ts
|
|
8
7
|
*/
|
|
9
|
-
export function parseCliArgs(argv: string[]) {
|
|
8
|
+
export function parseCliArgs(argv: string[], supportedClis?: readonly string[]) {
|
|
10
9
|
// Detect cli name from script name (same logic as cli.ts:10-14)
|
|
11
10
|
const scriptBaseName =
|
|
12
11
|
argv[1]
|
|
@@ -171,7 +170,7 @@ export function parseCliArgs(argv: string[]) {
|
|
|
171
170
|
.positional("cli", {
|
|
172
171
|
describe: "The AI CLI to run, e.g., claude, codex, copilot, cursor, gemini",
|
|
173
172
|
type: "string",
|
|
174
|
-
choices:
|
|
173
|
+
choices: supportedClis as string[] | undefined,
|
|
175
174
|
demandOption: false,
|
|
176
175
|
default: cliName,
|
|
177
176
|
})
|
|
@@ -211,12 +210,33 @@ export function parseCliArgs(argv: string[]) {
|
|
|
211
210
|
}
|
|
212
211
|
});
|
|
213
212
|
|
|
213
|
+
// Collect bare positional words as prompt text (e.g., `cy arg1 arg2` → prompt = "arg1 arg2")
|
|
214
|
+
const positionalPromptWords: string[] = [];
|
|
215
|
+
|
|
214
216
|
const cliArgsForSpawn = (() => {
|
|
215
217
|
if (parsedArgv._[0] && !cliName) {
|
|
216
|
-
// Explicit CLI name provided as positional arg
|
|
217
|
-
|
|
218
|
+
// Explicit CLI name provided as positional arg — separate flags from bare words
|
|
219
|
+
const allAfterCli = rawArgs.slice((cliArgIndex ?? 0) + 1, dashIndex ?? undefined);
|
|
220
|
+
const result: string[] = [];
|
|
221
|
+
for (let i = 0; i < allAfterCli.length; i++) {
|
|
222
|
+
const arg = allAfterCli[i]!;
|
|
223
|
+
if (arg.startsWith("-")) {
|
|
224
|
+
result.push(arg);
|
|
225
|
+
// Consume the next arg as the flag's value if separate (--flag value)
|
|
226
|
+
if (!arg.includes("=") && i + 1 < allAfterCli.length) {
|
|
227
|
+
const nextArg = allAfterCli[i + 1];
|
|
228
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
229
|
+
result.push(nextArg);
|
|
230
|
+
i++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
positionalPromptWords.push(arg);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
218
238
|
} else if (cliName) {
|
|
219
|
-
// CLI name from script, filter out
|
|
239
|
+
// CLI name from script, filter out what yargs consumed; bare words become prompt
|
|
220
240
|
const result: string[] = [];
|
|
221
241
|
const argsToCheck = rawArgs.slice(0, dashIndex ?? undefined);
|
|
222
242
|
|
|
@@ -226,7 +246,11 @@ export function parseCliArgs(argv: string[]) {
|
|
|
226
246
|
|
|
227
247
|
const [flag] = arg.split("=");
|
|
228
248
|
|
|
229
|
-
|
|
249
|
+
// Check both the flag itself and its --no- negation (yargs stores --no-x as key "x")
|
|
250
|
+
const isConsumed =
|
|
251
|
+
(flag && yargsConsumed.has(flag)) ||
|
|
252
|
+
(flag?.startsWith("--no-") && yargsConsumed.has(`--${flag.slice(5)}`));
|
|
253
|
+
if (isConsumed) {
|
|
230
254
|
// Skip consumed flag and its value if separate
|
|
231
255
|
if (!arg.includes("=") && i + 1 < argsToCheck.length) {
|
|
232
256
|
const nextArg = argsToCheck[i + 1];
|
|
@@ -234,14 +258,27 @@ export function parseCliArgs(argv: string[]) {
|
|
|
234
258
|
i++; // Skip value
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
|
-
} else {
|
|
261
|
+
} else if (arg.startsWith("-")) {
|
|
262
|
+
// Non-consumed flag → pass to target CLI
|
|
238
263
|
result.push(arg);
|
|
264
|
+
// Consume the next arg as the flag's value if separate
|
|
265
|
+
if (!arg.includes("=") && i + 1 < argsToCheck.length) {
|
|
266
|
+
const nextArg = argsToCheck[i + 1];
|
|
267
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
268
|
+
result.push(nextArg);
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Bare word → treat as prompt text
|
|
274
|
+
positionalPromptWords.push(arg);
|
|
239
275
|
}
|
|
240
276
|
}
|
|
241
277
|
return result;
|
|
242
278
|
}
|
|
243
279
|
return [];
|
|
244
280
|
})();
|
|
281
|
+
const positionalPrompt = positionalPromptWords.join(" ") || undefined;
|
|
245
282
|
const dashPrompt: string | undefined =
|
|
246
283
|
dashIndex === undefined ? undefined : rawArgs.slice(dashIndex + 1).join(" ");
|
|
247
284
|
|
|
@@ -260,9 +297,10 @@ export function parseCliArgs(argv: string[]) {
|
|
|
260
297
|
parsedArgv.cli ||
|
|
261
298
|
(dashIndex !== 0
|
|
262
299
|
? parsedArgv._[0]?.toString()?.replace?.(/-yes$/, "")
|
|
263
|
-
: undefined)) as
|
|
300
|
+
: undefined)) as string,
|
|
264
301
|
cliArgs: [...cliArgsForSpawn, ...(parsedArgv.yes ? ["--dangerously-skip-permissions"] : [])],
|
|
265
|
-
prompt:
|
|
302
|
+
prompt:
|
|
303
|
+
[parsedArgv.prompt, positionalPrompt, dashPrompt].filter(Boolean).join(" ") || undefined,
|
|
266
304
|
install: parsedArgv.install,
|
|
267
305
|
exitOnIdle: Number(
|
|
268
306
|
(parsedArgv.timeout || parsedArgv.idle || parsedArgv.exitOnIdle)?.replace(/.*/, (e) =>
|