pi-rlm 0.1.0 → 0.1.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/bin/pi-rlm.mjs ADDED
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { promises as fs } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { createInterface } from "node:readline";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import process from "node:process";
9
+ const defaults = {
10
+ backend: "sdk",
11
+ mode: "auto",
12
+ cwd: process.cwd(),
13
+ toolsProfile: "coding",
14
+ maxDepth: 2,
15
+ maxNodes: 24,
16
+ maxBranching: 3,
17
+ concurrency: 2,
18
+ timeoutMs: 180000,
19
+ json: false,
20
+ live: false,
21
+ liveRefreshMs: 250,
22
+ piBin: "pi",
23
+ tmuxUseCurrentSession: false
24
+ };
25
+ const allowedBackends = new Set(["sdk", "cli", "tmux"]);
26
+ const allowedModes = new Set(["auto", "solve", "decompose"]);
27
+ const allowedProfiles = new Set(["coding", "read-only"]);
28
+ async function main() {
29
+ try {
30
+ const opts = parseArgs(process.argv.slice(2));
31
+ if (opts.help) {
32
+ printHelp();
33
+ process.exit(0);
34
+ }
35
+ if (opts.version) {
36
+ process.stdout.write("pi-rlm-cli/0.1.0\n");
37
+ process.exit(0);
38
+ }
39
+ if (!opts.task || !opts.task.trim()) {
40
+ fail('Missing task. Pass --task "..." or a positional task string.');
41
+ }
42
+ if (opts.live && opts.json) {
43
+ fail("--live and --json cannot be used together.");
44
+ }
45
+ if (opts.live && !process.stdout.isTTY) {
46
+ fail("--live requires a TTY terminal.");
47
+ }
48
+ const __dirname = dirname(fileURLToPath(import.meta.url));
49
+ const extensionPath = resolve(__dirname, "..", "index.ts");
50
+ const toolParams = {
51
+ op: "start",
52
+ task: opts.task,
53
+ backend: opts.backend,
54
+ mode: opts.mode,
55
+ cwd: opts.cwd,
56
+ toolsProfile: opts.toolsProfile,
57
+ maxDepth: opts.maxDepth,
58
+ maxNodes: opts.maxNodes,
59
+ maxBranching: opts.maxBranching,
60
+ concurrency: opts.concurrency,
61
+ timeoutMs: opts.timeoutMs,
62
+ async: false,
63
+ tmuxUseCurrentSession: opts.tmuxUseCurrentSession,
64
+ ...(opts.model ? { model: opts.model } : {})
65
+ };
66
+ const prompt = createPrompt(toolParams);
67
+ const args = createPiArgs(extensionPath, prompt);
68
+ const run = opts.live
69
+ ? await runPiLive(opts.piBin, args, toolParams, opts.liveRefreshMs)
70
+ : await runPi(opts.piBin, args, toolParams);
71
+ if (!run.toolResult) {
72
+ const stderr = run.stderr.trim();
73
+ if (stderr) {
74
+ fail(`Failed to capture rlm tool result.\n${stderr}`);
75
+ }
76
+ fail("Failed to capture rlm tool result.");
77
+ }
78
+ if (!run.toolArgsMatch) {
79
+ fail("The underlying pi agent did not execute rlm with the exact requested arguments.");
80
+ }
81
+ if (opts.json) {
82
+ process.stdout.write(`${JSON.stringify({
83
+ ok: !run.toolError,
84
+ params: toolParams,
85
+ result: run.toolResult
86
+ }, null, 2)}\n`);
87
+ }
88
+ else {
89
+ if (opts.live) {
90
+ process.stdout.write("\n");
91
+ }
92
+ const text = extractText(run.toolResult.content);
93
+ if (text) {
94
+ process.stdout.write(`${text}\n`);
95
+ }
96
+ else {
97
+ process.stdout.write(`${JSON.stringify(run.toolResult, null, 2)}\n`);
98
+ }
99
+ }
100
+ const failed = run.toolError || run.code !== 0;
101
+ process.exit(failed ? 1 : 0);
102
+ }
103
+ catch (error) {
104
+ fail(error instanceof Error ? error.message : String(error));
105
+ }
106
+ }
107
+ function createPrompt(toolParams) {
108
+ return [
109
+ "You MUST call the rlm tool exactly once using these exact arguments.",
110
+ "Do not modify any value. Do not call any other tool.",
111
+ "After the tool call, respond with exactly: __PI_RLM_DONE__",
112
+ "Arguments JSON:",
113
+ JSON.stringify(toolParams)
114
+ ].join("\n");
115
+ }
116
+ function createPiArgs(extensionPath, prompt) {
117
+ return [
118
+ "-p",
119
+ "--no-session",
120
+ "--no-extensions",
121
+ "--no-tools",
122
+ "-e",
123
+ extensionPath,
124
+ "--mode",
125
+ "json",
126
+ prompt
127
+ ];
128
+ }
129
+ function parseArgs(argv) {
130
+ const opts = {
131
+ ...defaults,
132
+ task: "",
133
+ help: false,
134
+ version: false
135
+ };
136
+ const positional = [];
137
+ for (let i = 0; i < argv.length; i += 1) {
138
+ const arg = argv[i];
139
+ if (arg === "--help" || arg === "-h") {
140
+ opts.help = true;
141
+ continue;
142
+ }
143
+ if (arg === "--version" || arg === "-v") {
144
+ opts.version = true;
145
+ continue;
146
+ }
147
+ if (arg === "--json") {
148
+ opts.json = true;
149
+ continue;
150
+ }
151
+ if (arg === "--live") {
152
+ opts.live = true;
153
+ continue;
154
+ }
155
+ if (arg === "--tmux-current-session") {
156
+ opts.tmuxUseCurrentSession = true;
157
+ continue;
158
+ }
159
+ if (arg === "--task") {
160
+ opts.task = expectValue(argv, ++i, "--task");
161
+ continue;
162
+ }
163
+ if (arg.startsWith("--task=")) {
164
+ opts.task = arg.slice("--task=".length);
165
+ continue;
166
+ }
167
+ if (arg === "--backend") {
168
+ opts.backend = expectValue(argv, ++i, "--backend");
169
+ continue;
170
+ }
171
+ if (arg.startsWith("--backend=")) {
172
+ opts.backend = arg.slice("--backend=".length);
173
+ continue;
174
+ }
175
+ if (arg === "--mode") {
176
+ opts.mode = expectValue(argv, ++i, "--mode");
177
+ continue;
178
+ }
179
+ if (arg.startsWith("--mode=")) {
180
+ opts.mode = arg.slice("--mode=".length);
181
+ continue;
182
+ }
183
+ if (arg === "--model") {
184
+ opts.model = expectValue(argv, ++i, "--model");
185
+ continue;
186
+ }
187
+ if (arg.startsWith("--model=")) {
188
+ opts.model = arg.slice("--model=".length);
189
+ continue;
190
+ }
191
+ if (arg === "--cwd") {
192
+ opts.cwd = expectValue(argv, ++i, "--cwd");
193
+ continue;
194
+ }
195
+ if (arg.startsWith("--cwd=")) {
196
+ opts.cwd = arg.slice("--cwd=".length);
197
+ continue;
198
+ }
199
+ if (arg === "--tools-profile") {
200
+ opts.toolsProfile = expectValue(argv, ++i, "--tools-profile");
201
+ continue;
202
+ }
203
+ if (arg.startsWith("--tools-profile=")) {
204
+ opts.toolsProfile = arg.slice("--tools-profile=".length);
205
+ continue;
206
+ }
207
+ if (arg === "--max-depth") {
208
+ opts.maxDepth = parseIntArg(expectValue(argv, ++i, "--max-depth"), "--max-depth");
209
+ continue;
210
+ }
211
+ if (arg.startsWith("--max-depth=")) {
212
+ opts.maxDepth = parseIntArg(arg.slice("--max-depth=".length), "--max-depth");
213
+ continue;
214
+ }
215
+ if (arg === "--max-nodes") {
216
+ opts.maxNodes = parseIntArg(expectValue(argv, ++i, "--max-nodes"), "--max-nodes");
217
+ continue;
218
+ }
219
+ if (arg.startsWith("--max-nodes=")) {
220
+ opts.maxNodes = parseIntArg(arg.slice("--max-nodes=".length), "--max-nodes");
221
+ continue;
222
+ }
223
+ if (arg === "--max-branching") {
224
+ opts.maxBranching = parseIntArg(expectValue(argv, ++i, "--max-branching"), "--max-branching");
225
+ continue;
226
+ }
227
+ if (arg.startsWith("--max-branching=")) {
228
+ opts.maxBranching = parseIntArg(arg.slice("--max-branching=".length), "--max-branching");
229
+ continue;
230
+ }
231
+ if (arg === "--concurrency") {
232
+ opts.concurrency = parseIntArg(expectValue(argv, ++i, "--concurrency"), "--concurrency");
233
+ continue;
234
+ }
235
+ if (arg.startsWith("--concurrency=")) {
236
+ opts.concurrency = parseIntArg(arg.slice("--concurrency=".length), "--concurrency");
237
+ continue;
238
+ }
239
+ if (arg === "--timeout-ms") {
240
+ opts.timeoutMs = parseIntArg(expectValue(argv, ++i, "--timeout-ms"), "--timeout-ms");
241
+ continue;
242
+ }
243
+ if (arg.startsWith("--timeout-ms=")) {
244
+ opts.timeoutMs = parseIntArg(arg.slice("--timeout-ms=".length), "--timeout-ms");
245
+ continue;
246
+ }
247
+ if (arg === "--live-refresh-ms") {
248
+ opts.liveRefreshMs = parseIntArg(expectValue(argv, ++i, "--live-refresh-ms"), "--live-refresh-ms");
249
+ continue;
250
+ }
251
+ if (arg.startsWith("--live-refresh-ms=")) {
252
+ opts.liveRefreshMs = parseIntArg(arg.slice("--live-refresh-ms=".length), "--live-refresh-ms");
253
+ continue;
254
+ }
255
+ if (arg === "--pi-bin") {
256
+ opts.piBin = expectValue(argv, ++i, "--pi-bin");
257
+ continue;
258
+ }
259
+ if (arg.startsWith("--pi-bin=")) {
260
+ opts.piBin = arg.slice("--pi-bin=".length);
261
+ continue;
262
+ }
263
+ if (arg.startsWith("-")) {
264
+ fail(`Unknown argument: ${arg}`);
265
+ }
266
+ positional.push(arg);
267
+ }
268
+ if (!opts.task && positional.length > 0) {
269
+ opts.task = positional.join(" ");
270
+ }
271
+ if (!allowedBackends.has(opts.backend)) {
272
+ fail(`Invalid --backend '${opts.backend}'. Expected one of: sdk, cli, tmux`);
273
+ }
274
+ if (!allowedModes.has(opts.mode)) {
275
+ fail(`Invalid --mode '${opts.mode}'. Expected one of: auto, solve, decompose`);
276
+ }
277
+ if (!allowedProfiles.has(opts.toolsProfile)) {
278
+ fail(`Invalid --tools-profile '${opts.toolsProfile}'. Expected one of: coding, read-only`);
279
+ }
280
+ ensureRange(opts.maxDepth, 0, 8, "--max-depth");
281
+ ensureRange(opts.maxNodes, 1, 300, "--max-nodes");
282
+ ensureRange(opts.maxBranching, 1, 8, "--max-branching");
283
+ ensureRange(opts.concurrency, 1, 8, "--concurrency");
284
+ ensureRange(opts.timeoutMs, 1000, 3600000, "--timeout-ms");
285
+ ensureRange(opts.liveRefreshMs, 100, 5000, "--live-refresh-ms");
286
+ return opts;
287
+ }
288
+ function expectValue(argv, index, flag) {
289
+ const value = argv[index];
290
+ if (!value || value.startsWith("-")) {
291
+ fail(`Missing value for ${flag}`);
292
+ }
293
+ return value;
294
+ }
295
+ function parseIntArg(value, flag) {
296
+ const parsed = Number.parseInt(value, 10);
297
+ if (!Number.isFinite(parsed)) {
298
+ fail(`Invalid integer for ${flag}: '${value}'`);
299
+ }
300
+ return parsed;
301
+ }
302
+ function ensureRange(value, min, max, flag) {
303
+ if (value < min || value > max) {
304
+ fail(`Invalid ${flag} value '${value}'. Expected ${min}..${max}.`);
305
+ }
306
+ }
307
+ async function runPi(command, args, expectedToolParams) {
308
+ const child = spawn(command, args, {
309
+ stdio: ["ignore", "pipe", "pipe"],
310
+ env: process.env
311
+ });
312
+ const state = {
313
+ stderr: "",
314
+ toolResult: undefined,
315
+ toolError: false,
316
+ toolArgsMatch: false
317
+ };
318
+ const rl = createInterface({ input: child.stdout });
319
+ rl.on("line", (line) => {
320
+ const event = parseJsonLine(line);
321
+ if (!event)
322
+ return;
323
+ if (event.type === "tool_execution_start" && event.toolName === "rlm") {
324
+ state.toolArgsMatch = deepEqual(event.args, expectedToolParams);
325
+ }
326
+ if (event.type === "tool_execution_end" && event.toolName === "rlm" && event.result) {
327
+ state.toolResult = event.result;
328
+ state.toolError = Boolean(event.isError);
329
+ }
330
+ });
331
+ child.stderr.on("data", (chunk) => {
332
+ state.stderr += chunk.toString();
333
+ });
334
+ const code = await waitForChild(child);
335
+ rl.close();
336
+ return {
337
+ code,
338
+ ...state
339
+ };
340
+ }
341
+ async function runPiLive(command, args, expectedToolParams, refreshMs) {
342
+ const child = spawn(command, args, {
343
+ stdio: ["ignore", "pipe", "pipe"],
344
+ env: process.env
345
+ });
346
+ const state = {
347
+ stderr: "",
348
+ toolResult: undefined,
349
+ toolError: false,
350
+ toolArgsMatch: false,
351
+ runId: undefined,
352
+ liveMonitor: createLiveMonitor(refreshMs)
353
+ };
354
+ const rl = createInterface({ input: child.stdout });
355
+ rl.on("line", (line) => {
356
+ const event = parseJsonLine(line);
357
+ if (!event)
358
+ return;
359
+ if (event.type === "tool_execution_start" && event.toolName === "rlm") {
360
+ state.toolArgsMatch = deepEqual(event.args, expectedToolParams);
361
+ }
362
+ if (event.type === "tool_execution_update" && event.toolName === "rlm") {
363
+ const runIdFromUpdate = extractRunIdFromUpdateEvent(event);
364
+ if (!state.runId && runIdFromUpdate) {
365
+ state.runId = runIdFromUpdate;
366
+ startLiveMonitor(state.liveMonitor, runIdFromUpdate);
367
+ }
368
+ }
369
+ if (event.type === "tool_execution_end" && event.toolName === "rlm" && event.result) {
370
+ state.toolResult = event.result;
371
+ state.toolError = Boolean(event.isError);
372
+ state.liveMonitor.toolError = state.toolError;
373
+ if (!state.runId) {
374
+ const runIdFromResult = extractRunIdFromToolResult(event.result);
375
+ if (runIdFromResult) {
376
+ state.runId = runIdFromResult;
377
+ startLiveMonitor(state.liveMonitor, runIdFromResult);
378
+ }
379
+ }
380
+ }
381
+ });
382
+ child.stderr.on("data", (chunk) => {
383
+ state.stderr += chunk.toString();
384
+ });
385
+ const code = await waitForChild(child);
386
+ rl.close();
387
+ await stopLiveMonitor(state.liveMonitor);
388
+ return {
389
+ code,
390
+ stderr: state.stderr,
391
+ toolResult: state.toolResult,
392
+ toolError: state.toolError,
393
+ toolArgsMatch: state.toolArgsMatch
394
+ };
395
+ }
396
+ async function waitForChild(child) {
397
+ return new Promise((resolve, reject) => {
398
+ child.on("error", reject);
399
+ child.on("close", resolve);
400
+ });
401
+ }
402
+ function parseJsonLine(line) {
403
+ const trimmed = line.trim();
404
+ if (!trimmed)
405
+ return undefined;
406
+ try {
407
+ const parsed = JSON.parse(trimmed);
408
+ if (!isRecord(parsed))
409
+ return undefined;
410
+ return parsed;
411
+ }
412
+ catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ function extractRunIdFromUpdateEvent(event) {
417
+ const partialResult = isRecord(event.partialResult) ? event.partialResult : undefined;
418
+ const text = extractText(partialResult?.content);
419
+ if (!text)
420
+ return undefined;
421
+ const match = text.match(/RLM run\s+([a-f0-9]{8})\s+started/i);
422
+ return match?.[1];
423
+ }
424
+ function extractRunIdFromToolResult(result) {
425
+ const details = isRecord(result?.details) ? result.details : undefined;
426
+ if (typeof details?.run_id === "string") {
427
+ return details.run_id;
428
+ }
429
+ const text = extractText(result?.content);
430
+ if (!text)
431
+ return undefined;
432
+ const match = text.match(/run_id:\s*([a-f0-9]{8})/i);
433
+ return match?.[1];
434
+ }
435
+ function createLiveMonitor(refreshMs) {
436
+ return {
437
+ refreshMs,
438
+ runId: undefined,
439
+ eventsPath: undefined,
440
+ offset: 0,
441
+ remainder: "",
442
+ polling: false,
443
+ timer: undefined,
444
+ nodes: new Map(),
445
+ nextOrder: 1,
446
+ runMeta: undefined,
447
+ runEnd: undefined,
448
+ toolError: false,
449
+ lastFrame: ""
450
+ };
451
+ }
452
+ function startLiveMonitor(monitor, runId) {
453
+ if (monitor.timer)
454
+ return;
455
+ monitor.runId = runId;
456
+ monitor.eventsPath = join(tmpdir(), "pi-rlm-runs", runId, "events.jsonl");
457
+ renderLiveMonitor(monitor, true);
458
+ void pollLiveEvents(monitor);
459
+ monitor.timer = setInterval(() => {
460
+ void pollLiveEvents(monitor);
461
+ }, monitor.refreshMs);
462
+ }
463
+ async function stopLiveMonitor(monitor) {
464
+ if (monitor.timer) {
465
+ clearInterval(monitor.timer);
466
+ monitor.timer = undefined;
467
+ }
468
+ await pollLiveEvents(monitor);
469
+ renderLiveMonitor(monitor, true);
470
+ }
471
+ async function pollLiveEvents(monitor) {
472
+ if (!monitor.eventsPath || monitor.polling)
473
+ return;
474
+ monitor.polling = true;
475
+ try {
476
+ let stat;
477
+ try {
478
+ stat = await fs.stat(monitor.eventsPath);
479
+ }
480
+ catch {
481
+ return;
482
+ }
483
+ if (stat.size < monitor.offset) {
484
+ monitor.offset = 0;
485
+ monitor.remainder = "";
486
+ }
487
+ if (stat.size === monitor.offset) {
488
+ return;
489
+ }
490
+ const length = stat.size - monitor.offset;
491
+ const handle = await fs.open(monitor.eventsPath, "r");
492
+ let chunk = "";
493
+ try {
494
+ const buffer = Buffer.alloc(length);
495
+ await handle.read(buffer, 0, length, monitor.offset);
496
+ monitor.offset = stat.size;
497
+ chunk = buffer.toString("utf8");
498
+ }
499
+ finally {
500
+ await handle.close();
501
+ }
502
+ applyLiveChunk(monitor, chunk);
503
+ renderLiveMonitor(monitor);
504
+ }
505
+ finally {
506
+ monitor.polling = false;
507
+ }
508
+ }
509
+ function applyLiveChunk(monitor, chunk) {
510
+ const data = `${monitor.remainder}${chunk}`;
511
+ const lines = data.split("\n");
512
+ monitor.remainder = lines.pop() ?? "";
513
+ for (const line of lines) {
514
+ const event = parseJsonLine(line);
515
+ if (!event)
516
+ continue;
517
+ applyLiveEvent(monitor, event);
518
+ }
519
+ }
520
+ function applyLiveEvent(monitor, event) {
521
+ if (event.type === "run_start") {
522
+ monitor.runMeta = {
523
+ backend: asString(event.backend),
524
+ mode: asString(event.mode),
525
+ maxDepth: asNumber(event.maxDepth),
526
+ maxNodes: asNumber(event.maxNodes),
527
+ maxBranching: asNumber(event.maxBranching),
528
+ concurrency: asNumber(event.concurrency)
529
+ };
530
+ return;
531
+ }
532
+ if (event.type === "run_end") {
533
+ monitor.runEnd = {
534
+ durationMs: asNumber(event.durationMs),
535
+ nodesVisited: asNumber(event.nodesVisited),
536
+ maxDepthSeen: asNumber(event.maxDepthSeen),
537
+ finalChars: asNumber(event.finalChars)
538
+ };
539
+ return;
540
+ }
541
+ const nodeId = asString(event.nodeId);
542
+ if (!nodeId) {
543
+ return;
544
+ }
545
+ const node = getOrCreateLiveNode(monitor, nodeId);
546
+ const depth = asNumber(event.depth);
547
+ if (typeof depth === "number") {
548
+ node.depth = depth;
549
+ }
550
+ const task = asString(event.task);
551
+ if (task) {
552
+ node.task = task;
553
+ }
554
+ if (Object.prototype.hasOwnProperty.call(event, "parentId")) {
555
+ linkLiveParent(monitor, node, normalizeParentId(event.parentId));
556
+ }
557
+ switch (event.type) {
558
+ case "node_start":
559
+ node.status = "running";
560
+ break;
561
+ case "node_decompose":
562
+ node.action = "decompose";
563
+ if (typeof event.reason === "string")
564
+ node.reason = event.reason;
565
+ break;
566
+ case "node_end":
567
+ node.status = "completed";
568
+ if (typeof event.action === "string")
569
+ node.action = event.action;
570
+ if (typeof event.reason === "string")
571
+ node.reason = event.reason;
572
+ break;
573
+ case "node_cancelled":
574
+ node.status = "cancelled";
575
+ if (typeof event.error === "string") {
576
+ node.error = event.error;
577
+ node.reason = node.reason || event.error;
578
+ }
579
+ break;
580
+ case "node_error":
581
+ node.status = "failed";
582
+ if (typeof event.error === "string") {
583
+ node.error = event.error;
584
+ }
585
+ break;
586
+ case "node_skipped":
587
+ node.status = "cancelled";
588
+ if (typeof event.reason === "string") {
589
+ node.reason = event.reason;
590
+ }
591
+ break;
592
+ default:
593
+ break;
594
+ }
595
+ }
596
+ function getOrCreateLiveNode(monitor, nodeId) {
597
+ let node = monitor.nodes.get(nodeId);
598
+ if (node)
599
+ return node;
600
+ node = {
601
+ id: nodeId,
602
+ depth: 0,
603
+ task: "",
604
+ status: "pending",
605
+ action: undefined,
606
+ reason: undefined,
607
+ error: undefined,
608
+ parentId: undefined,
609
+ children: [],
610
+ order: monitor.nextOrder++
611
+ };
612
+ monitor.nodes.set(nodeId, node);
613
+ return node;
614
+ }
615
+ function normalizeParentId(parentId) {
616
+ if (parentId === null || parentId === undefined)
617
+ return undefined;
618
+ return String(parentId);
619
+ }
620
+ function linkLiveParent(monitor, node, parentId) {
621
+ if (node.parentId === parentId)
622
+ return;
623
+ if (node.parentId) {
624
+ const previousParent = monitor.nodes.get(node.parentId);
625
+ if (previousParent) {
626
+ previousParent.children = previousParent.children.filter((childId) => childId !== node.id);
627
+ }
628
+ }
629
+ node.parentId = parentId;
630
+ if (!parentId)
631
+ return;
632
+ const parent = getOrCreateLiveNode(monitor, parentId);
633
+ if (!parent.children.includes(node.id)) {
634
+ parent.children.push(node.id);
635
+ }
636
+ }
637
+ function renderLiveMonitor(monitor, force = false) {
638
+ const frame = buildLiveFrame(monitor);
639
+ if (!force && frame === monitor.lastFrame)
640
+ return;
641
+ monitor.lastFrame = frame;
642
+ process.stdout.write("\x1b[2J\x1b[H");
643
+ process.stdout.write(frame);
644
+ }
645
+ function buildLiveFrame(monitor) {
646
+ const lines = [];
647
+ lines.push(`pi-rlm live tree | run_id: ${monitor.runId ?? "(waiting...)"}`);
648
+ if (monitor.runMeta) {
649
+ lines.push(`backend=${monitor.runMeta.backend} mode=${monitor.runMeta.mode} depth<=${monitor.runMeta.maxDepth} nodes<=${monitor.runMeta.maxNodes}`);
650
+ }
651
+ if (monitor.runEnd) {
652
+ const finalStatus = monitor.toolError ? "failed" : "completed";
653
+ lines.push(`status=${finalStatus} nodes=${monitor.runEnd.nodesVisited} maxDepthSeen=${monitor.runEnd.maxDepthSeen} durationMs=${monitor.runEnd.durationMs}`);
654
+ }
655
+ else {
656
+ lines.push(`status=running observed_nodes=${monitor.nodes.size}`);
657
+ }
658
+ lines.push("");
659
+ const roots = Array.from(monitor.nodes.values())
660
+ .filter((node) => !node.parentId)
661
+ .sort((a, b) => a.order - b.order);
662
+ if (roots.length === 0) {
663
+ lines.push("(waiting for node events...)");
664
+ lines.push("\nLegend: [status] (action) task");
665
+ return `${lines.join("\n")}\n`;
666
+ }
667
+ const visited = new Set();
668
+ roots.forEach((root, idx) => {
669
+ renderLiveNode(monitor, root.id, "", idx === roots.length - 1, visited, lines);
670
+ });
671
+ lines.push("");
672
+ lines.push("Legend: [status] (action) task");
673
+ return `${lines.join("\n")}\n`;
674
+ }
675
+ function renderLiveNode(monitor, nodeId, prefix, isLast, visited, lines) {
676
+ const node = monitor.nodes.get(nodeId);
677
+ if (!node)
678
+ return;
679
+ const connector = isLast ? "└─" : "├─";
680
+ const action = node.action ? ` (${node.action})` : "";
681
+ const line = `${prefix}${connector} ${node.id} [d=${node.depth}] [${node.status}]${action} ${short(node.task || "(task pending)", 90)}`;
682
+ lines.push(line);
683
+ const childPrefix = `${prefix}${isLast ? " " : "│ "}`;
684
+ if (node.reason) {
685
+ lines.push(`${childPrefix}reason: ${short(node.reason, 76)}`);
686
+ }
687
+ if (node.error && node.error !== node.reason) {
688
+ lines.push(`${childPrefix}error: ${short(node.error, 76)}`);
689
+ }
690
+ if (visited.has(node.id)) {
691
+ lines.push(`${childPrefix}(cycle detected in live event graph)`);
692
+ return;
693
+ }
694
+ visited.add(node.id);
695
+ const children = node.children
696
+ .map((childId) => monitor.nodes.get(childId))
697
+ .filter((child) => Boolean(child))
698
+ .sort((a, b) => a.order - b.order);
699
+ children.forEach((child, idx) => {
700
+ renderLiveNode(monitor, child.id, childPrefix, idx === children.length - 1, visited, lines);
701
+ });
702
+ visited.delete(node.id);
703
+ }
704
+ function short(value, maxChars = 80) {
705
+ const text = String(value).replace(/\s+/g, " ").trim();
706
+ if (text.length <= maxChars)
707
+ return text;
708
+ return `${text.slice(0, maxChars - 1)}…`;
709
+ }
710
+ function extractText(content) {
711
+ if (!Array.isArray(content))
712
+ return "";
713
+ return content
714
+ .filter((item) => item && item.type === "text" && typeof item.text === "string")
715
+ .map((item) => item.text)
716
+ .join("\n")
717
+ .trim();
718
+ }
719
+ function isRecord(value) {
720
+ return typeof value === "object" && value !== null;
721
+ }
722
+ function asString(value) {
723
+ return typeof value === "string" ? value : undefined;
724
+ }
725
+ function asNumber(value) {
726
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
727
+ }
728
+ function deepEqual(a, b) {
729
+ if (Object.is(a, b))
730
+ return true;
731
+ if (typeof a !== typeof b)
732
+ return false;
733
+ if (typeof a !== "object" || a === null || b === null)
734
+ return false;
735
+ if (Array.isArray(a) !== Array.isArray(b))
736
+ return false;
737
+ if (Array.isArray(a)) {
738
+ const arrA = a;
739
+ const arrB = b;
740
+ if (arrA.length !== arrB.length)
741
+ return false;
742
+ for (let i = 0; i < arrA.length; i += 1) {
743
+ if (!deepEqual(arrA[i], arrB[i]))
744
+ return false;
745
+ }
746
+ return true;
747
+ }
748
+ const objA = a;
749
+ const objB = b;
750
+ const aKeys = Object.keys(objA);
751
+ const bKeys = Object.keys(objB);
752
+ if (aKeys.length !== bKeys.length)
753
+ return false;
754
+ for (const key of aKeys) {
755
+ if (!Object.prototype.hasOwnProperty.call(objB, key))
756
+ return false;
757
+ if (!deepEqual(objA[key], objB[key]))
758
+ return false;
759
+ }
760
+ return true;
761
+ }
762
+ function printHelp() {
763
+ process.stdout.write("pi-rlm - run Recursive Language Model tasks directly\n\n");
764
+ process.stdout.write("Usage:\n");
765
+ process.stdout.write(" pi-rlm --task \"Analyze src architecture\" [options]\n");
766
+ process.stdout.write(" pi-rlm \"Analyze src architecture\" [options]\n\n");
767
+ process.stdout.write("Options:\n");
768
+ process.stdout.write(" --backend <sdk|cli|tmux> Backend for subcalls (default: sdk)\n");
769
+ process.stdout.write(" --mode <auto|solve|decompose> Recursion mode (default: auto)\n");
770
+ process.stdout.write(" --model <provider/model[:thinking]> Optional model override\n");
771
+ process.stdout.write(" --cwd <path> Working directory for subcalls (default: current dir)\n");
772
+ process.stdout.write(" --tools-profile <coding|read-only> Tool profile (default: coding)\n");
773
+ process.stdout.write(" --max-depth <n> Max recursion depth (default: 2)\n");
774
+ process.stdout.write(" --max-nodes <n> Max total nodes (default: 24)\n");
775
+ process.stdout.write(" --max-branching <n> Max subtasks per decomposition (default: 3)\n");
776
+ process.stdout.write(" --concurrency <n> Child concurrency (default: 2)\n");
777
+ process.stdout.write(" --timeout-ms <n> Timeout per model call (default: 180000)\n");
778
+ process.stdout.write(" --live Show live tree visualization (TTY only)\n");
779
+ process.stdout.write(" --live-refresh-ms <n> Live refresh interval in ms (default: 250)\n");
780
+ process.stdout.write(" --tmux-current-session For backend=tmux, use current tmux session windows\n");
781
+ process.stdout.write(" --json Print machine-readable JSON\n");
782
+ process.stdout.write(" --pi-bin <path> Override pi binary path (default: pi)\n");
783
+ process.stdout.write(" -h, --help Show help\n");
784
+ process.stdout.write(" -v, --version Show version\n\n");
785
+ process.stdout.write("Notes:\n");
786
+ process.stdout.write(" - This wrapper runs a single synchronous rlm start operation.\n");
787
+ process.stdout.write(" - It shells out to the installed 'pi' CLI and loads this extension.\n");
788
+ process.stdout.write(" - --live reads /tmp/pi-rlm-runs/<runId>/events.jsonl for real-time tree updates.\n");
789
+ }
790
+ function fail(message) {
791
+ process.stderr.write(`pi-rlm: ${message}\n`);
792
+ process.exit(1);
793
+ }
794
+ void main();