@oh-my-pi/pi-coding-agent 11.10.0 → 11.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.10.2] - 2026-02-10
6
+
7
+ ### Added
8
+
9
+ - Exported `streamHashLinesFromUtf8` and `streamHashLinesFromLines` functions for streaming hashline-formatted output with configurable chunking
10
+ - Added `HashlineStreamOptions` interface to control streaming behavior (startLine, maxChunkLines, maxChunkBytes)
11
+ - Added `streamHashLinesFromUtf8` function to incrementally format content with hash lines from a UTF-8 byte stream
12
+ - Added `streamHashLinesFromLines` function to incrementally format content with hash lines from an iterable of lines
13
+
14
+ ### Changed
15
+
16
+ - Updated hashline format to use 2-character hex hashes instead of 4-character hashes for more compact line references
17
+ - Modified `computeLineHash` to normalize whitespace in line content and removed line number from hash seed for consistency
18
+ - Improved CLI argument parsing to explicitly handle `--help`, `--version`, and subcommand detection instead of prefix-based routing
19
+
20
+ ### Removed
21
+
22
+ - Removed `@types/diff` dev dependency
23
+ - Removed AggregateError unwrapping from console.warn in CLI initialization
24
+
25
+ ## [11.10.1] - 2026-02-10
26
+ ### Changed
27
+
28
+ - Migrated CLI framework from oclif to lightweight pi-utils CLI runner
29
+ - Replaced oclif command registration with explicit command entries in cli.ts
30
+ - Changed default root command name from 'index' to 'launch'
31
+ - Updated all command imports to use @oh-my-pi/pi-utils/cli instead of @oclif/core
32
+
33
+ ### Removed
34
+
35
+ - Removed @oclif/core and @oclif/plugin-autocomplete dependencies
36
+ - Removed oclif configuration from package.json
37
+ - Removed custom oclif help renderer (oclif-help.ts)
38
+
5
39
  ## [11.10.0] - 2026-02-10
6
40
  ### Breaking Changes
7
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.10.0",
3
+ "version": "11.10.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -10,14 +10,6 @@
10
10
  "bin": {
11
11
  "omp": "src/cli.ts"
12
12
  },
13
- "oclif": {
14
- "bin": "omp",
15
- "commands": "./src/commands",
16
- "helpClass": "./src/cli/oclif-help.ts",
17
- "plugins": [
18
- "@oclif/plugin-autocomplete"
19
- ]
20
- },
21
13
  "main": "./src/index.ts",
22
14
  "types": "./src/index.ts",
23
15
  "exports": {
@@ -88,14 +80,12 @@
88
80
  },
89
81
  "dependencies": {
90
82
  "@mozilla/readability": "0.6.0",
91
- "@oclif/core": "^4.8.0",
92
- "@oclif/plugin-autocomplete": "^3.2.40",
93
- "@oh-my-pi/omp-stats": "11.10.0",
94
- "@oh-my-pi/pi-agent-core": "11.10.0",
95
- "@oh-my-pi/pi-ai": "11.10.0",
96
- "@oh-my-pi/pi-natives": "11.10.0",
97
- "@oh-my-pi/pi-tui": "11.10.0",
98
- "@oh-my-pi/pi-utils": "11.10.0",
83
+ "@oh-my-pi/omp-stats": "11.10.2",
84
+ "@oh-my-pi/pi-agent-core": "11.10.2",
85
+ "@oh-my-pi/pi-ai": "11.10.2",
86
+ "@oh-my-pi/pi-natives": "11.10.2",
87
+ "@oh-my-pi/pi-tui": "11.10.2",
88
+ "@oh-my-pi/pi-utils": "11.10.2",
99
89
  "@sinclair/typebox": "^0.34.48",
100
90
  "ajv": "^8.17.1",
101
91
  "chalk": "^5.6.2",
@@ -112,7 +102,6 @@
112
102
  "zod": "^4.3.6"
113
103
  },
114
104
  "devDependencies": {
115
- "@types/diff": "^7.0.2",
116
105
  "@types/ms": "^2.1.0",
117
106
  "@types/bun": "^1.3.8",
118
107
  "ms": "^2.1.3",
package/src/cli.ts CHANGED
@@ -1,32 +1,58 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * CLI entry point for the refactored coding agent.
4
- * Uses main.ts with AgentSession and new mode modules.
5
- *
6
- * Test with: npx tsx src/cli-new.ts [args...]
3
+ * CLI entry point registers all commands explicitly and delegates to the
4
+ * lightweight CLI runner from pi-utils.
7
5
  */
8
- import { run } from "@oclif/core";
9
- import { APP_NAME } from "./config";
6
+ import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
7
+ import { APP_NAME, VERSION } from "./config";
10
8
 
11
- // oclif's warn() doesn't unwrap AggregateError — override to surface the real messages
12
- const originalWarn = console.warn;
13
- console.warn = (...args: unknown[]) => {
14
- for (const arg of args) {
15
- if (arg instanceof AggregateError) {
16
- for (const err of arg.errors) {
17
- originalWarn(err instanceof Error ? (err.stack ?? err.message) : String(err));
18
- }
19
- return;
20
- }
9
+ process.title = APP_NAME;
10
+
11
+ const commands: CommandEntry[] = [
12
+ { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
13
+ { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
14
+ { name: "config", load: () => import("./commands/config").then(m => m.default) },
15
+ { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
16
+ { name: "jupyter", load: () => import("./commands/jupyter").then(m => m.default) },
17
+ { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
18
+ { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
19
+ { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
20
+ { name: "stats", load: () => import("./commands/stats").then(m => m.default) },
21
+ { name: "update", load: () => import("./commands/update").then(m => m.default) },
22
+ { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
23
+ ];
24
+
25
+ async function showHelp(config: import("@oh-my-pi/pi-utils/cli").CliConfig): Promise<void> {
26
+ const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli");
27
+ const { getExtraHelpText } = await import("./cli/args");
28
+ renderRootHelp(config);
29
+ const extra = getExtraHelpText();
30
+ if (extra.trim().length > 0) {
31
+ process.stdout.write(`\n${extra}\n`);
21
32
  }
22
- originalWarn(...args);
23
- };
33
+ }
24
34
 
25
- process.title = APP_NAME;
26
- const argv = process.argv.slice(2);
27
- const runArgv = argv.length === 0 || argv[0]?.startsWith("-") ? ["index", ...argv] : argv;
28
- run(runArgv, import.meta.url).catch((error: unknown) => {
29
- const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
30
- process.stderr.write(`${message}\n`);
31
- process.exit(1);
32
- });
35
+ /**
36
+ * Determine whether argv[0] is a known subcommand name.
37
+ * If not, the entire argv is treated as args to the default "launch" command.
38
+ */
39
+ function isSubcommand(first: string | undefined): boolean {
40
+ if (!first || first.startsWith("-") || first.startsWith("@")) return false;
41
+ return commands.some(e => e.name === first || e.aliases?.includes(first));
42
+ }
43
+
44
+ /** Run the CLI with the given argv (no `process.argv` prefix). */
45
+ export function runCli(argv: string[]): Promise<void> {
46
+ // --help and --version are handled by run() directly, don't rewrite those.
47
+ // Everything else that isn't a known subcommand routes to "launch".
48
+ const first = argv[0];
49
+ const runArgv =
50
+ first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help"
51
+ ? argv
52
+ : isSubcommand(first)
53
+ ? argv
54
+ : ["launch", ...argv];
55
+ return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
56
+ }
57
+
58
+ await runCli(process.argv.slice(2));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Generate and optionally push a commit with changelog updates.
3
3
  */
4
- import { Command, Flags } from "@oclif/core";
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runCommitCommand } from "../commit";
6
6
  import type { CommitCommandArgs } from "../commit/types";
7
7
  import { initTheme } from "../modes/theme/theme";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Manage configuration settings.
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { type ConfigAction, type ConfigCommandArgs, runConfigCommand } from "../cli/config-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test grep tool.
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { type GrepCommandArgs, runGrepCommand } from "../cli/grep-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Manage the shared Jupyter gateway.
3
3
  */
4
- import { Args, Command } from "@oclif/core";
4
+ import { Args, Command } from "@oh-my-pi/pi-utils/cli";
5
5
  import { type JupyterAction, type JupyterCommandArgs, runJupyterCommand } from "../cli/jupyter-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
5
- import { parseArgs } from "../../cli/args";
6
- import { APP_NAME } from "../../config";
7
- import { runRootCommand } from "../../main";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { parseArgs } from "../cli/args";
6
+ import { APP_NAME } from "../config";
7
+ import { runRootCommand } from "../main";
8
8
 
9
9
  export default class Index extends Command {
10
10
  static description = "AI coding assistant";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Manage plugins (install, uninstall, list, etc.).
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { type PluginAction, type PluginCommandArgs, runPluginCommand } from "../cli/plugin-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Install dependencies for optional features.
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Interactive shell console.
3
3
  */
4
- import { Command, Flags } from "@oclif/core";
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runShellCommand, type ShellCommandArgs } from "../cli/shell-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * View usage statistics dashboard.
3
3
  */
4
- import { Command, Flags } from "@oclif/core";
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runStatsCommand, type StatsCommandArgs } from "../cli/stats-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -10,8 +10,8 @@ export default class Stats extends Command {
10
10
 
11
11
  static flags = {
12
12
  port: Flags.integer({ char: "p", description: "Port for the dashboard server", default: 3847 }),
13
- json: Flags.boolean({ char: "j", description: "Output stats as JSON" }),
14
- summary: Flags.boolean({ char: "s", description: "Print summary to console" }),
13
+ json: Flags.boolean({ char: "j", description: "Output stats as JSON", default: false }),
14
+ summary: Flags.boolean({ char: "s", description: "Print summary to console", default: false }),
15
15
  };
16
16
 
17
17
  async run(): Promise<void> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Check for and install updates.
3
3
  */
4
- import { Command, Flags } from "@oclif/core";
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runUpdateCommand } from "../cli/update-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
@@ -9,8 +9,8 @@ export default class Update extends Command {
9
9
  static description = "Check for and install updates";
10
10
 
11
11
  static flags = {
12
- force: Flags.boolean({ char: "f", description: "Force update" }),
13
- check: Flags.boolean({ char: "c", description: "Check for updates without installing" }),
12
+ force: Flags.boolean({ char: "f", description: "Force update", default: false }),
13
+ check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
14
14
  };
15
15
 
16
16
  async run(): Promise<void> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Test web search providers.
3
3
  */
4
- import { Args, Command, Flags } from "@oclif/core";
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runSearchCommand, type SearchCommandArgs } from "../cli/web-search-cli";
6
6
  import type { SearchProviderId } from "../web/search/types";
7
7
 
package/src/main.ts CHANGED
@@ -8,7 +8,6 @@ import * as fs from "node:fs/promises";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { createInterface } from "node:readline/promises";
11
- import { run } from "@oclif/core";
12
11
  import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
13
12
  import { $env, postmortem } from "@oh-my-pi/pi-utils";
14
13
  import chalk from "chalk";
@@ -713,6 +712,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
713
712
  }
714
713
 
715
714
  export async function main(args: string[]): Promise<void> {
716
- const argv = args.length === 0 ? ["index"] : args;
717
- await run(argv, import.meta.url);
715
+ const { runCli } = await import("./cli");
716
+ await runCli(args.length === 0 ? ["launch"] : args);
718
717
  }
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Hashline edit mode — a line-addressable edit format using content hashes.
3
3
  *
4
- * Each line in a file is identified by its 1-indexed line number and a 4-character
5
- * hex hash derived from the line content and the line number (xxHash64 with the
6
- * line number as seed, truncated to 4 hex chars).
4
+ * Each line in a file is identified by its 1-indexed line number and a short
5
+ * hex hash derived from the normalized line content (xxHash64, truncated to 2
6
+ * hex chars).
7
7
  * The combined `LINE:HASH` reference acts as both an address and a staleness check:
8
8
  * if the file has changed since the caller last read it, hash mismatches are caught
9
9
  * before any mutation occurs.
10
10
  *
11
11
  * Displayed format: `LINENUM:HASH| CONTENT`
12
- * Reference format: `"LINENUM:HASH"` (e.g. `"5:a3f2"`)
12
+ * Reference format: `"LINENUM:HASH"` (e.g. `"5:a3"`)
13
13
  */
14
14
 
15
15
  import type { HashlineEdit, HashMismatch, SrcSpec } from "./types";
@@ -305,11 +305,11 @@ const HASH_MASK = BigInt((1 << (HASH_LEN * 4)) - 1);
305
305
  const HEX_DICT = Array.from({ length: Number(HASH_MASK) + 1 }, (_, i) => i.toString(16).padStart(HASH_LEN, "0"));
306
306
 
307
307
  /**
308
- * Compute the 4-character hex hash of a single line.
308
+ * Compute a short hex hash of a single line.
309
309
  *
310
- * Uses xxHash64 truncated to the first 4 hex characters.
311
- * The line number is included as a seed so the same content on different lines
312
- * produces different hashes.
310
+ * Uses xxHash64 on a whitespace-normalized line, truncated to {@link HASH_LEN}
311
+ * hex characters. The `idx` parameter is accepted for compatibility with older
312
+ * call sites, but is not currently mixed into the hash.
313
313
  * The line input should not include a trailing newline.
314
314
  */
315
315
  export function computeLineHash(idx: number, line: string): string {
@@ -333,7 +333,7 @@ export function computeLineHash(idx: number, line: string): string {
333
333
  * @example
334
334
  * ```
335
335
  * formatHashLines("function hi() {\n return;\n}")
336
- * // "1:a3f2| function hi() {\n2:b1c0| return;\n3:de45| }"
336
+ * // "1:HH| function hi() {\n2:HH| return;\n3:HH| }"
337
337
  * ```
338
338
  */
339
339
  export function formatHashLines(content: string, startLine = 1): string {
@@ -347,6 +347,218 @@ export function formatHashLines(content: string, startLine = 1): string {
347
347
  .join("\n");
348
348
  }
349
349
 
350
+ // ═══════════════════════════════════════════════════════════════════════════
351
+ // Hashline streaming formatter
352
+ // ═══════════════════════════════════════════════════════════════════════════
353
+
354
+ export interface HashlineStreamOptions {
355
+ /** First line number to use when formatting (1-indexed). */
356
+ startLine?: number;
357
+ /** Maximum formatted lines per yielded chunk (default: 200). */
358
+ maxChunkLines?: number;
359
+ /** Maximum UTF-8 bytes per yielded chunk (default: 64 KiB). */
360
+ maxChunkBytes?: number;
361
+ }
362
+
363
+ function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
364
+ return (
365
+ typeof value === "object" &&
366
+ value !== null &&
367
+ "getReader" in value &&
368
+ typeof (value as { getReader?: unknown }).getReader === "function"
369
+ );
370
+ }
371
+
372
+ async function* bytesFromReadableStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
373
+ const reader = stream.getReader();
374
+ try {
375
+ while (true) {
376
+ const { done, value } = await reader.read();
377
+ if (done) return;
378
+ if (value) yield value;
379
+ }
380
+ } finally {
381
+ reader.releaseLock();
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Stream hashline-formatted output from a UTF-8 byte source.
387
+ *
388
+ * This is intended for large files where callers want incremental output
389
+ * (e.g. while reading from a file handle) rather than allocating a single
390
+ * large string.
391
+ */
392
+ export async function* streamHashLinesFromUtf8(
393
+ source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
394
+ options: HashlineStreamOptions = {},
395
+ ): AsyncGenerator<string> {
396
+ const startLine = options.startLine ?? 1;
397
+ const maxChunkLines = options.maxChunkLines ?? 200;
398
+ const maxChunkBytes = options.maxChunkBytes ?? 64 * 1024;
399
+ const decoder = new TextDecoder("utf-8");
400
+ const chunks = isReadableStream(source) ? bytesFromReadableStream(source) : source;
401
+ let lineNum = startLine;
402
+ let pending = "";
403
+ let sawAnyText = false;
404
+ let endedWithNewline = false;
405
+ let outLines: string[] = [];
406
+ let outBytes = 0;
407
+
408
+ const flush = (): string | undefined => {
409
+ if (outLines.length === 0) return undefined;
410
+ const chunk = outLines.join("\n");
411
+ outLines = [];
412
+ outBytes = 0;
413
+ return chunk;
414
+ };
415
+
416
+ const pushLine = (line: string): string[] => {
417
+ const formatted = `${lineNum}:${computeLineHash(lineNum, line)}| ${line}`;
418
+ lineNum++;
419
+
420
+ const chunksToYield: string[] = [];
421
+ const sepBytes = outLines.length === 0 ? 0 : 1; // "\n"
422
+ const lineBytes = Buffer.byteLength(formatted, "utf-8");
423
+
424
+ if (
425
+ outLines.length > 0 &&
426
+ (outLines.length >= maxChunkLines || outBytes + sepBytes + lineBytes > maxChunkBytes)
427
+ ) {
428
+ const flushed = flush();
429
+ if (flushed) chunksToYield.push(flushed);
430
+ }
431
+
432
+ outLines.push(formatted);
433
+ outBytes += (outLines.length === 1 ? 0 : 1) + lineBytes;
434
+
435
+ if (outLines.length >= maxChunkLines || outBytes >= maxChunkBytes) {
436
+ const flushed = flush();
437
+ if (flushed) chunksToYield.push(flushed);
438
+ }
439
+
440
+ return chunksToYield;
441
+ };
442
+
443
+ const consumeText = (text: string): string[] => {
444
+ if (text.length === 0) return [];
445
+ sawAnyText = true;
446
+ pending += text;
447
+ const chunksToYield: string[] = [];
448
+ while (true) {
449
+ const idx = pending.indexOf("\n");
450
+ if (idx === -1) break;
451
+ const line = pending.slice(0, idx);
452
+ pending = pending.slice(idx + 1);
453
+ endedWithNewline = true;
454
+ chunksToYield.push(...pushLine(line));
455
+ }
456
+ if (pending.length > 0) endedWithNewline = false;
457
+ return chunksToYield;
458
+ };
459
+ for await (const chunk of chunks) {
460
+ for (const out of consumeText(decoder.decode(chunk, { stream: true }))) {
461
+ yield out;
462
+ }
463
+ }
464
+
465
+ for (const out of consumeText(decoder.decode())) {
466
+ yield out;
467
+ }
468
+ if (!sawAnyText) {
469
+ // Mirror `"".split("\n")` behavior: one empty line.
470
+ for (const out of pushLine("")) {
471
+ yield out;
472
+ }
473
+ } else if (pending.length > 0 || endedWithNewline) {
474
+ // Emit the final line (may be empty if the file ended with a newline).
475
+ for (const out of pushLine(pending)) {
476
+ yield out;
477
+ }
478
+ }
479
+
480
+ const last = flush();
481
+ if (last) yield last;
482
+ }
483
+
484
+ /**
485
+ * Stream hashline-formatted output from an (async) iterable of lines.
486
+ *
487
+ * Each yielded chunk is a `\n`-joined string of one or more formatted lines.
488
+ */
489
+ export async function* streamHashLinesFromLines(
490
+ lines: Iterable<string> | AsyncIterable<string>,
491
+ options: HashlineStreamOptions = {},
492
+ ): AsyncGenerator<string> {
493
+ const startLine = options.startLine ?? 1;
494
+ const maxChunkLines = options.maxChunkLines ?? 200;
495
+ const maxChunkBytes = options.maxChunkBytes ?? 64 * 1024;
496
+
497
+ let lineNum = startLine;
498
+ let outLines: string[] = [];
499
+ let outBytes = 0;
500
+ let sawAnyLine = false;
501
+ const flush = (): string | undefined => {
502
+ if (outLines.length === 0) return undefined;
503
+ const chunk = outLines.join("\n");
504
+ outLines = [];
505
+ outBytes = 0;
506
+ return chunk;
507
+ };
508
+
509
+ const pushLine = (line: string): string[] => {
510
+ sawAnyLine = true;
511
+ const formatted = `${lineNum}:${computeLineHash(lineNum, line)}| ${line}`;
512
+ lineNum++;
513
+
514
+ const chunksToYield: string[] = [];
515
+ const sepBytes = outLines.length === 0 ? 0 : 1;
516
+ const lineBytes = Buffer.byteLength(formatted, "utf-8");
517
+
518
+ if (
519
+ outLines.length > 0 &&
520
+ (outLines.length >= maxChunkLines || outBytes + sepBytes + lineBytes > maxChunkBytes)
521
+ ) {
522
+ const flushed = flush();
523
+ if (flushed) chunksToYield.push(flushed);
524
+ }
525
+
526
+ outLines.push(formatted);
527
+ outBytes += (outLines.length === 1 ? 0 : 1) + lineBytes;
528
+
529
+ if (outLines.length >= maxChunkLines || outBytes >= maxChunkBytes) {
530
+ const flushed = flush();
531
+ if (flushed) chunksToYield.push(flushed);
532
+ }
533
+
534
+ return chunksToYield;
535
+ };
536
+
537
+ const asyncIterator = (lines as AsyncIterable<string>)[Symbol.asyncIterator];
538
+ if (typeof asyncIterator === "function") {
539
+ for await (const line of lines as AsyncIterable<string>) {
540
+ for (const out of pushLine(line)) {
541
+ yield out;
542
+ }
543
+ }
544
+ } else {
545
+ for (const line of lines as Iterable<string>) {
546
+ for (const out of pushLine(line)) {
547
+ yield out;
548
+ }
549
+ }
550
+ }
551
+ if (!sawAnyLine) {
552
+ // Mirror `"".split("\n")` behavior: one empty line.
553
+ for (const out of pushLine("")) {
554
+ yield out;
555
+ }
556
+ }
557
+
558
+ const last = flush();
559
+ if (last) yield last;
560
+ }
561
+
350
562
  /**
351
563
  * Parse a line reference string like `"5:abcd"` into structured form.
352
564
  *
@@ -360,7 +572,7 @@ export function parseLineRef(ref: string): { line: number; hash: string } {
360
572
  const prefixMatch = strictMatch ? null : cleaned.match(new RegExp(`^(\\d+):([0-9a-fA-F]{${HASH_LEN}})`));
361
573
  const match = strictMatch ?? prefixMatch;
362
574
  if (!match) {
363
- throw new Error(`Invalid line reference "${ref}". Expected format "LINE:HASH" (e.g. "5:a3f2").`);
575
+ throw new Error(`Invalid line reference "${ref}". Expected format "LINE:HASH" (e.g. "5:aa").`);
364
576
  }
365
577
  const line = Number.parseInt(match[1], 10);
366
578
  if (line < 1) {
@@ -55,6 +55,8 @@ export {
55
55
  formatHashLines,
56
56
  HashlineMismatchError,
57
57
  parseLineRef,
58
+ streamHashLinesFromLines,
59
+ streamHashLinesFromUtf8,
58
60
  validateLineRef,
59
61
  } from "./hashline";
60
62
  // Normalization
@@ -1,26 +0,0 @@
1
- /**
2
- * Custom help renderer for the coding agent CLI.
3
- */
4
- import { CommandHelp, Help } from "@oclif/core";
5
- import { getExtraHelpText } from "./args";
6
-
7
- export default class OclifHelp extends Help {
8
- async showRootHelp(): Promise<void> {
9
- await super.showRootHelp();
10
- const rootCommand = this.config.findCommand("index");
11
- if (rootCommand) {
12
- const rootHelp = new CommandHelp(rootCommand, this.config, {
13
- ...this.opts,
14
- sections: ["arguments", "flags", "examples"],
15
- });
16
- const output = rootHelp.generate();
17
- if (output.trim().length > 0) {
18
- process.stdout.write(`\n${output}\n`);
19
- }
20
- }
21
- const extra = getExtraHelpText();
22
- if (extra.trim().length > 0) {
23
- process.stdout.write(`\n${extra}\n`);
24
- }
25
- }
26
- }