agent-yes 1.73.2 → 1.75.0
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/SUPPORTED_CLIS-Dzf60ENT.js +11 -0
- package/dist/{agent-yes.config-CyP5iRZf.js → agent-yes.config-1LMoK18R.js} +1 -1
- package/dist/cli.js +12 -5
- package/dist/globalPidIndex-DNEh8a_O.js +103 -0
- package/dist/index.js +3 -2
- package/dist/{package-DpfHTSW2.js → package-CE0J-uFT.js} +2 -2
- package/dist/{pidStore-CPrgJSJi.js → pidStore-CHLHMBEM.js} +25 -4
- package/dist/pidStore-DR1yPY3t.js +5 -0
- package/dist/subcommands-DpOqSs-b.js +391 -0
- package/dist/{tray-Bzb1owBN.js → tray-D5deJPjk.js} +1 -1
- package/dist/{ts-CsdLrLod.js → ts-D_iRstH9.js} +4 -4
- package/package.json +1 -1
- package/ts/cli.ts +11 -0
- package/ts/globalPidIndex.spec.ts +166 -0
- package/ts/globalPidIndex.ts +143 -0
- package/ts/pidStore.ts +24 -0
- package/ts/subcommands.spec.ts +581 -0
- package/ts/subcommands.ts +529 -0
- package/dist/SUPPORTED_CLIS-C7sGMdKJ.js +0 -10
- package/dist/pidStore-B4yDm3TL.js +0 -4
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cy ls / read / cat / tail / head / send` subcommand implementations.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the principles of koho's `terminal-ws-lib.ts` (session list, render
|
|
5
|
+
* via @xterm/headless, keyword-keyed input) — but file-based instead of via
|
|
6
|
+
* a daemon. Reads ~/.agent-yes/pids.jsonl (cross-runtime global index, written
|
|
7
|
+
* by both the TS PidStore and the Rust pid_store::PidStore) and the per-pid
|
|
8
|
+
* raw log files.
|
|
9
|
+
*
|
|
10
|
+
* Returns null when argv[2] is not a known subcommand so cli.ts falls through
|
|
11
|
+
* to the normal agent-spawning flow.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile, stat } from "fs/promises";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { type GlobalPidRecord, readGlobalPids } from "./globalPidIndex.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the per-cwd TS PidStore JSONL and convert to the global record shape,
|
|
21
|
+
* so pre-existing TS agents that were spawned before the global-index mirror
|
|
22
|
+
* shipped still show up in `cy ls`. Merging is done in `mergeRecords`.
|
|
23
|
+
*/
|
|
24
|
+
async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
|
|
25
|
+
const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
|
|
26
|
+
let raw: string;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(jsonlPath, "utf-8");
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Same merge semantics as ts/JsonlStore.ts: last line per _id wins,
|
|
34
|
+
// tombstones (`$$deleted`) drop the entry.
|
|
35
|
+
const docs = new Map<string, any>();
|
|
36
|
+
for (const line of raw.split("\n")) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed) continue;
|
|
39
|
+
try {
|
|
40
|
+
const doc = JSON.parse(trimmed);
|
|
41
|
+
if (!doc._id) continue;
|
|
42
|
+
if (doc.$$deleted) {
|
|
43
|
+
docs.delete(doc._id);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const prev = docs.get(doc._id);
|
|
47
|
+
docs.set(doc._id, prev ? { ...prev, ...doc } : doc);
|
|
48
|
+
} catch {
|
|
49
|
+
// skip corrupt
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Array.from(docs.values()).map((d) => ({
|
|
54
|
+
pid: d.pid,
|
|
55
|
+
cli: d.cli,
|
|
56
|
+
prompt: d.prompt ?? null,
|
|
57
|
+
cwd: d.cwd,
|
|
58
|
+
log_file: d.logFile ?? null,
|
|
59
|
+
fifo_file: d.fifoFile ?? null,
|
|
60
|
+
status: d.status ?? "active",
|
|
61
|
+
exit_code: d.exitCode ?? null,
|
|
62
|
+
exit_reason: d.exitReason ?? null,
|
|
63
|
+
started_at: d.startedAt ?? 0,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Merge by pid; later entries (typically from the global file) win. */
|
|
68
|
+
function mergeRecords(...buckets: GlobalPidRecord[][]): GlobalPidRecord[] {
|
|
69
|
+
const out = new Map<number, GlobalPidRecord>();
|
|
70
|
+
for (const bucket of buckets) {
|
|
71
|
+
for (const r of bucket) {
|
|
72
|
+
const prev = out.get(r.pid);
|
|
73
|
+
out.set(r.pid, prev ? { ...prev, ...r } : r);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Array.from(out.values());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SUBCOMMANDS = new Set(["ls", "list", "ps", "read", "cat", "tail", "head", "send"]);
|
|
80
|
+
|
|
81
|
+
export function isSubcommand(name: string | undefined): boolean {
|
|
82
|
+
return !!name && SUBCOMMANDS.has(name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Top-level entry. Returns the desired process exit code, or null if argv
|
|
87
|
+
* is not a subcommand invocation.
|
|
88
|
+
*/
|
|
89
|
+
export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
90
|
+
const sub = argv[2];
|
|
91
|
+
if (!isSubcommand(sub)) return null;
|
|
92
|
+
|
|
93
|
+
const rest = argv.slice(3);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
switch (sub) {
|
|
97
|
+
case "ls":
|
|
98
|
+
case "list":
|
|
99
|
+
case "ps":
|
|
100
|
+
return await cmdLs(rest);
|
|
101
|
+
case "read":
|
|
102
|
+
case "cat":
|
|
103
|
+
return await cmdRead(rest, { mode: "cat" });
|
|
104
|
+
case "tail":
|
|
105
|
+
return await cmdRead(rest, { mode: "tail" });
|
|
106
|
+
case "head":
|
|
107
|
+
return await cmdRead(rest, { mode: "head" });
|
|
108
|
+
case "send":
|
|
109
|
+
return await cmdSend(rest);
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
process.stderr.write(`cy ${sub}: ${msg}\n`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// shared helpers
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
interface CommonOpts {
|
|
125
|
+
all: boolean;
|
|
126
|
+
cwdScope: string | null;
|
|
127
|
+
latest: boolean;
|
|
128
|
+
json: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ParsedArgs {
|
|
132
|
+
flags: Record<string, string | boolean>;
|
|
133
|
+
positional: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parseArgs(rest: string[]): ParsedArgs {
|
|
137
|
+
const flags: Record<string, string | boolean> = {};
|
|
138
|
+
const positional: string[] = [];
|
|
139
|
+
for (let i = 0; i < rest.length; i++) {
|
|
140
|
+
const arg = rest[i]!;
|
|
141
|
+
if (arg.startsWith("--")) {
|
|
142
|
+
const eq = arg.indexOf("=");
|
|
143
|
+
if (eq >= 0) {
|
|
144
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
145
|
+
} else {
|
|
146
|
+
const key = arg.slice(2);
|
|
147
|
+
const next = rest[i + 1];
|
|
148
|
+
// Boolean flags: --all, --json, --latest
|
|
149
|
+
if (["all", "json", "latest"].includes(key) || !next || next.startsWith("-")) {
|
|
150
|
+
flags[key] = true;
|
|
151
|
+
} else {
|
|
152
|
+
flags[key] = next;
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
157
|
+
// -n N short flag
|
|
158
|
+
if (arg === "-n") {
|
|
159
|
+
flags["n"] = rest[i + 1] ?? "";
|
|
160
|
+
i++;
|
|
161
|
+
} else {
|
|
162
|
+
flags[arg.slice(1)] = true;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
positional.push(arg);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { flags, positional };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function commonOpts(flags: Record<string, string | boolean>): CommonOpts {
|
|
172
|
+
return {
|
|
173
|
+
all: !!flags.all,
|
|
174
|
+
cwdScope:
|
|
175
|
+
typeof flags.cwd === "string"
|
|
176
|
+
? path.resolve(flags.cwd)
|
|
177
|
+
: flags.cwd === true
|
|
178
|
+
? process.cwd()
|
|
179
|
+
: null,
|
|
180
|
+
latest: !!flags.latest,
|
|
181
|
+
json: !!flags.json,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function matchKeyword(record: GlobalPidRecord, keyword: string): boolean {
|
|
186
|
+
if (!keyword) return true;
|
|
187
|
+
const kw = keyword.toLowerCase();
|
|
188
|
+
// 1. exact pid
|
|
189
|
+
if (/^\d+$/.test(keyword) && record.pid === Number(keyword)) return true;
|
|
190
|
+
// 2. cwd contains keyword
|
|
191
|
+
if (record.cwd.toLowerCase().includes(kw)) return true;
|
|
192
|
+
// 3. cli exact (lowercase)
|
|
193
|
+
if (record.cli.toLowerCase() === kw) return true;
|
|
194
|
+
// 4. prompt substring
|
|
195
|
+
if (record.prompt && record.prompt.toLowerCase().includes(kw)) return true;
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function listRecords(
|
|
200
|
+
keyword: string | undefined,
|
|
201
|
+
opts: CommonOpts,
|
|
202
|
+
): Promise<GlobalPidRecord[]> {
|
|
203
|
+
// Read both sources: global cross-runtime index (Rust + new TS) and the
|
|
204
|
+
// per-cwd TS file in process.cwd() (catches pre-existing TS agents that
|
|
205
|
+
// started before the global mirror shipped). Optional --cwd <dir> adds
|
|
206
|
+
// that directory's per-cwd file too.
|
|
207
|
+
const local = await readLocalTsPids(process.cwd());
|
|
208
|
+
const scopeLocal = opts.cwdScope ? await readLocalTsPids(opts.cwdScope) : [];
|
|
209
|
+
const global = await readGlobalPids(); // raw, will filter below
|
|
210
|
+
let records = mergeRecords(local, scopeLocal, global);
|
|
211
|
+
|
|
212
|
+
if (!opts.all) {
|
|
213
|
+
records = records.filter((r) => r.status !== "exited" && isPidAlive(r.pid));
|
|
214
|
+
}
|
|
215
|
+
if (opts.cwdScope) {
|
|
216
|
+
const scope = opts.cwdScope;
|
|
217
|
+
records = records.filter((r) => r.cwd === scope || r.cwd.startsWith(scope + path.sep));
|
|
218
|
+
}
|
|
219
|
+
if (keyword) records = records.filter((r) => matchKeyword(r, keyword));
|
|
220
|
+
// newest first
|
|
221
|
+
records.sort((a, b) => b.started_at - a.started_at);
|
|
222
|
+
return records;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isPidAlive(pid: number): boolean {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promise<GlobalPidRecord> {
|
|
235
|
+
if (!keyword) {
|
|
236
|
+
throw new Error("keyword required (pid, cwd substring, cli name, or prompt substring)");
|
|
237
|
+
}
|
|
238
|
+
const matches = await listRecords(keyword, opts);
|
|
239
|
+
if (matches.length === 0) {
|
|
240
|
+
throw new Error(`no running agent matched "${keyword}"`);
|
|
241
|
+
}
|
|
242
|
+
if (matches.length === 1) return matches[0]!;
|
|
243
|
+
if (opts.latest) return matches[0]!; // already sorted newest-first
|
|
244
|
+
const lines = matches
|
|
245
|
+
.slice(0, 10)
|
|
246
|
+
.map((r) => ` ${r.pid} ${r.cli} ${r.cwd}`)
|
|
247
|
+
.join("\n");
|
|
248
|
+
throw new Error(
|
|
249
|
+
`keyword "${keyword}" matched ${matches.length} agents — disambiguate by pid or pass --latest:\n${lines}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// cy ls
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
async function cmdLs(rest: string[]): Promise<number> {
|
|
258
|
+
const { flags, positional } = parseArgs(rest);
|
|
259
|
+
const opts = commonOpts(flags);
|
|
260
|
+
const keyword = positional[0];
|
|
261
|
+
const records = await listRecords(keyword, opts);
|
|
262
|
+
|
|
263
|
+
if (opts.json) {
|
|
264
|
+
process.stdout.write(JSON.stringify(records, null, 2) + "\n");
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (records.length === 0) {
|
|
269
|
+
process.stderr.write(
|
|
270
|
+
keyword ? `no running agents matched "${keyword}"\n` : "no running agents\n",
|
|
271
|
+
);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Budget the trailing PROMPT column to whatever space is left in the
|
|
276
|
+
// terminal after the fixed columns, so users on wide terminals see more
|
|
277
|
+
// context and users on narrow ones don't get an awkwardly-wrapped table.
|
|
278
|
+
const termWidth = (process.stdout as any).columns ?? 120;
|
|
279
|
+
|
|
280
|
+
const rawCwds = records.map((r) => shortenPath(r.cwd));
|
|
281
|
+
const widths = {
|
|
282
|
+
pid: Math.max(3, ...records.map((r) => String(r.pid).length)),
|
|
283
|
+
cli: Math.max(3, ...records.map((r) => r.cli.length)),
|
|
284
|
+
status: Math.max(6, ...records.map((r) => r.status.length)),
|
|
285
|
+
age: Math.max(3, ...records.map((r) => humanizeAge(Date.now() - r.started_at).length)),
|
|
286
|
+
cwd: Math.max(3, ...rawCwds.map((c) => c.length)),
|
|
287
|
+
};
|
|
288
|
+
const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 5 * 2; // 5 separators of " "
|
|
289
|
+
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
290
|
+
|
|
291
|
+
const rows = records.map((r) => ({
|
|
292
|
+
pid: String(r.pid),
|
|
293
|
+
cli: r.cli,
|
|
294
|
+
status: r.status,
|
|
295
|
+
age: humanizeAge(Date.now() - r.started_at),
|
|
296
|
+
cwd: shortenPath(r.cwd),
|
|
297
|
+
prompt: truncate(r.prompt ?? "", promptBudget),
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
const header =
|
|
301
|
+
[
|
|
302
|
+
"PID".padEnd(widths.pid),
|
|
303
|
+
"CLI".padEnd(widths.cli),
|
|
304
|
+
"STATUS".padEnd(widths.status),
|
|
305
|
+
"AGE".padEnd(widths.age),
|
|
306
|
+
"CWD".padEnd(widths.cwd),
|
|
307
|
+
"PROMPT",
|
|
308
|
+
].join(" ") + "\n";
|
|
309
|
+
process.stdout.write(header);
|
|
310
|
+
|
|
311
|
+
for (const r of rows) {
|
|
312
|
+
process.stdout.write(
|
|
313
|
+
[
|
|
314
|
+
r.pid.padEnd(widths.pid),
|
|
315
|
+
r.cli.padEnd(widths.cli),
|
|
316
|
+
r.status.padEnd(widths.status),
|
|
317
|
+
r.age.padEnd(widths.age),
|
|
318
|
+
r.cwd.padEnd(widths.cwd),
|
|
319
|
+
r.prompt,
|
|
320
|
+
].join(" ") + "\n",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function humanizeAge(ms: number): string {
|
|
328
|
+
if (ms < 1000) return "0s";
|
|
329
|
+
const s = Math.floor(ms / 1000);
|
|
330
|
+
if (s < 60) return `${s}s`;
|
|
331
|
+
const m = Math.floor(s / 60);
|
|
332
|
+
if (m < 60) return `${m}m`;
|
|
333
|
+
const h = Math.floor(m / 60);
|
|
334
|
+
if (h < 24) return `${h}h`;
|
|
335
|
+
const d = Math.floor(h / 24);
|
|
336
|
+
return `${d}d`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function shortenPath(p: string): string {
|
|
340
|
+
const home = homedir();
|
|
341
|
+
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function truncate(s: string, n: number): string {
|
|
345
|
+
if (s.length <= n) return s;
|
|
346
|
+
return s.slice(0, n - 1) + "…";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// cy read / cat / tail / head
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
interface ReadOpts {
|
|
354
|
+
mode: "cat" | "tail" | "head";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
358
|
+
const { flags, positional } = parseArgs(rest);
|
|
359
|
+
const opts = commonOpts(flags);
|
|
360
|
+
const keyword = positional[0];
|
|
361
|
+
|
|
362
|
+
const nFlag = typeof flags.n === "string" ? Number(flags.n) : undefined;
|
|
363
|
+
const n =
|
|
364
|
+
nFlag !== undefined && Number.isFinite(nFlag) && nFlag > 0
|
|
365
|
+
? Math.floor(nFlag)
|
|
366
|
+
: mode === "cat"
|
|
367
|
+
? 0
|
|
368
|
+
: 96;
|
|
369
|
+
|
|
370
|
+
const record = await resolveOne(keyword, opts);
|
|
371
|
+
const logPath = record.log_file;
|
|
372
|
+
if (!logPath) {
|
|
373
|
+
throw new Error(`pid ${record.pid}: no log_file recorded`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let stats;
|
|
377
|
+
try {
|
|
378
|
+
stats = await stat(logPath);
|
|
379
|
+
} catch {
|
|
380
|
+
throw new Error(`pid ${record.pid}: log file not found at ${logPath}`);
|
|
381
|
+
}
|
|
382
|
+
if (!stats.isFile()) {
|
|
383
|
+
throw new Error(`pid ${record.pid}: log path is not a file: ${logPath}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const buf = await readFile(logPath);
|
|
387
|
+
const rendered = await renderRawLog(buf, { mode, n });
|
|
388
|
+
process.stdout.write(rendered);
|
|
389
|
+
if (!rendered.endsWith("\n")) process.stdout.write("\n");
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Feed the raw PTY bytes through @xterm/headless and emit plain text.
|
|
395
|
+
* Same approach as koho's renderTerminalBuffer + agent-yes's XtermProxy.
|
|
396
|
+
*/
|
|
397
|
+
async function renderRawLog(
|
|
398
|
+
buf: Uint8Array,
|
|
399
|
+
{ mode, n }: { mode: "cat" | "tail" | "head"; n: number },
|
|
400
|
+
): Promise<string> {
|
|
401
|
+
// Default screen geometry — we don't know what the agent used, but
|
|
402
|
+
// 200x50 is a reasonable upper bound that won't truncate normal output.
|
|
403
|
+
const cols = 200;
|
|
404
|
+
const rows = 50;
|
|
405
|
+
// Scrollback must hold enough lines for the requested slice.
|
|
406
|
+
const scrollback = Math.max(50000, n + rows + 100);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const xtermPkg = await import("@xterm/headless");
|
|
410
|
+
const { Terminal } = xtermPkg;
|
|
411
|
+
const term = new Terminal({ cols, rows, scrollback, allowProposedApi: true });
|
|
412
|
+
await new Promise<void>((resolve) => term.write(buf, resolve));
|
|
413
|
+
const active = term.buffer.active;
|
|
414
|
+
const lines: string[] = [];
|
|
415
|
+
for (let i = 0; i < active.length; i++) {
|
|
416
|
+
const line = active.getLine(i);
|
|
417
|
+
lines.push(line ? line.translateToString(false).trimEnd() : "");
|
|
418
|
+
}
|
|
419
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
420
|
+
|
|
421
|
+
if (mode === "cat") return lines.join("\n");
|
|
422
|
+
if (mode === "tail") return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
423
|
+
return lines.slice(0, n).join("\n");
|
|
424
|
+
} catch {
|
|
425
|
+
// Fallback: regex strip ANSI
|
|
426
|
+
let text = new TextDecoder().decode(buf);
|
|
427
|
+
// oxlint-disable-next-line no-control-regex -- intentional: strip ANSI
|
|
428
|
+
const ansi = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
|
|
429
|
+
// oxlint-disable-next-line no-control-regex -- intentional: strip control
|
|
430
|
+
const ctrl = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
431
|
+
text = text.replace(ansi, "").replace(ctrl, "");
|
|
432
|
+
const lines = text.split("\n");
|
|
433
|
+
if (mode === "cat") return lines.join("\n");
|
|
434
|
+
if (mode === "tail") return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
435
|
+
return lines.slice(0, n).join("\n");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// cy send
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
async function cmdSend(rest: string[]): Promise<number> {
|
|
444
|
+
const { flags, positional } = parseArgs(rest);
|
|
445
|
+
const opts = commonOpts(flags);
|
|
446
|
+
const keyword = positional[0];
|
|
447
|
+
const message = positional.slice(1).join(" ");
|
|
448
|
+
|
|
449
|
+
if (!keyword)
|
|
450
|
+
throw new Error("usage: cy send <keyword> <msg> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
451
|
+
|
|
452
|
+
const codeName = typeof flags.code === "string" ? flags.code.toLowerCase() : "enter";
|
|
453
|
+
const trailing = controlCodeFromName(codeName);
|
|
454
|
+
|
|
455
|
+
const record = await resolveOne(keyword, opts);
|
|
456
|
+
const fifoPath = record.fifo_file;
|
|
457
|
+
if (!fifoPath) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`pid ${record.pid}: no fifo_file recorded — agent was not started with --stdpush (or was spawned by Rust which doesn't yet support FIFO IPC; see ROADMAP item 10)`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const payload = (message ?? "") + trailing;
|
|
464
|
+
await writeToIpc(fifoPath, payload);
|
|
465
|
+
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function controlCodeFromName(name: string): string {
|
|
470
|
+
switch (name) {
|
|
471
|
+
case "enter":
|
|
472
|
+
case "cr":
|
|
473
|
+
case "return":
|
|
474
|
+
return "\r";
|
|
475
|
+
case "esc":
|
|
476
|
+
case "escape":
|
|
477
|
+
return "\x1b";
|
|
478
|
+
case "ctrl-c":
|
|
479
|
+
case "ctrlc":
|
|
480
|
+
return "\x03";
|
|
481
|
+
case "ctrl-y":
|
|
482
|
+
case "ctrly":
|
|
483
|
+
return "\x19";
|
|
484
|
+
case "ctrl-d":
|
|
485
|
+
case "ctrld":
|
|
486
|
+
return "\x04";
|
|
487
|
+
case "tab":
|
|
488
|
+
return "\t";
|
|
489
|
+
case "none":
|
|
490
|
+
case "":
|
|
491
|
+
return "";
|
|
492
|
+
default:
|
|
493
|
+
// raw:0xNN form
|
|
494
|
+
const m = /^raw:0x([0-9a-f]+)$/i.exec(name);
|
|
495
|
+
if (m) return String.fromCharCode(parseInt(m[1]!, 16));
|
|
496
|
+
throw new Error(`unknown --code=${name}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
501
|
+
if (process.platform === "win32") {
|
|
502
|
+
const { connect } = await import("net");
|
|
503
|
+
await new Promise<void>((resolve, reject) => {
|
|
504
|
+
const client = connect(ipcPath);
|
|
505
|
+
const timer = setTimeout(() => {
|
|
506
|
+
client.destroy();
|
|
507
|
+
reject(new Error("named pipe connect timeout"));
|
|
508
|
+
}, 5000);
|
|
509
|
+
client.on("connect", () => {
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
client.write(payload);
|
|
512
|
+
client.end();
|
|
513
|
+
resolve();
|
|
514
|
+
});
|
|
515
|
+
client.on("error", (err) => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
reject(err);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
const { openSync, writeFileSync, closeSync } = await import("fs");
|
|
522
|
+
const fd = openSync(ipcPath, "w");
|
|
523
|
+
try {
|
|
524
|
+
writeFileSync(fd, payload);
|
|
525
|
+
} finally {
|
|
526
|
+
closeSync(fd);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-CsdLrLod.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./pidStore-CPrgJSJi.js";
|
|
4
|
-
|
|
5
|
-
//#region ts/SUPPORTED_CLIS.ts
|
|
6
|
-
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
9
|
-
export { SUPPORTED_CLIS };
|
|
10
|
-
//# sourceMappingURL=SUPPORTED_CLIS-C7sGMdKJ.js.map
|