@oh-my-pi/pi-coding-agent 16.0.7 → 16.0.8

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli.js +4752 -12462
  3. package/dist/types/cli/update-cli.d.ts +11 -0
  4. package/dist/types/debug/remote-debugger.d.ts +45 -0
  5. package/dist/types/internal-urls/docs-index.d.ts +19 -0
  6. package/dist/types/markit/converters/docx.d.ts +6 -0
  7. package/dist/types/markit/converters/epub.d.ts +15 -0
  8. package/dist/types/markit/converters/pdf/columns.d.ts +35 -0
  9. package/dist/types/markit/converters/pdf/extract.d.ts +10 -0
  10. package/dist/types/markit/converters/pdf/grid.d.ts +25 -0
  11. package/dist/types/markit/converters/pdf/headers.d.ts +24 -0
  12. package/dist/types/markit/converters/pdf/index.d.ts +6 -0
  13. package/dist/types/markit/converters/pdf/render.d.ts +24 -0
  14. package/dist/types/markit/converters/pdf/types.d.ts +75 -0
  15. package/dist/types/markit/converters/pptx.d.ts +57 -0
  16. package/dist/types/markit/converters/xlsx.d.ts +25 -0
  17. package/dist/types/markit/index.d.ts +2 -0
  18. package/dist/types/markit/registry.d.ts +16 -0
  19. package/dist/types/markit/types.d.ts +30 -0
  20. package/dist/types/session/agent-session.d.ts +7 -8
  21. package/dist/types/session/auth-storage.d.ts +3 -2
  22. package/dist/types/session/yield-queue.d.ts +3 -1
  23. package/dist/types/tools/browser/attach.d.ts +1 -1
  24. package/dist/types/utils/markit.d.ts +0 -8
  25. package/dist/types/utils/mupdf-wasm-embed.d.ts +1 -0
  26. package/dist/types/utils/turndown.d.ts +15 -0
  27. package/dist/types/utils/zip.d.ts +119 -0
  28. package/package.json +20 -18
  29. package/scripts/build-binary.ts +7 -3
  30. package/scripts/bundle-dist.ts +28 -12
  31. package/scripts/embed-mupdf-wasm.ts +67 -0
  32. package/scripts/generate-docs-index.ts +48 -32
  33. package/scripts/omp +1 -1
  34. package/src/advisor/__tests__/advisor.test.ts +83 -0
  35. package/src/advisor/runtime.ts +16 -1
  36. package/src/cli/auth-broker-cli.ts +1 -3
  37. package/src/cli/auth-gateway-cli.ts +2 -5
  38. package/src/cli/update-cli.ts +63 -3
  39. package/src/config/model-discovery.ts +20 -8
  40. package/src/config/models-config-schema.ts +8 -1
  41. package/src/debug/index.ts +44 -0
  42. package/src/debug/remote-debugger.ts +151 -0
  43. package/src/debug/report-bundle.ts +2 -1
  44. package/src/internal-urls/docs-index.generated.txt +2 -0
  45. package/src/internal-urls/docs-index.ts +102 -0
  46. package/src/internal-urls/omp-protocol.ts +10 -9
  47. package/src/markit/NOTICE +32 -0
  48. package/src/markit/converters/docx.ts +56 -0
  49. package/src/markit/converters/epub.ts +136 -0
  50. package/src/markit/converters/mammoth.d.ts +24 -0
  51. package/src/markit/converters/pdf/columns.ts +103 -0
  52. package/src/markit/converters/pdf/extract.ts +574 -0
  53. package/src/markit/converters/pdf/grid.ts +780 -0
  54. package/src/markit/converters/pdf/headers.ts +106 -0
  55. package/src/markit/converters/pdf/index.ts +146 -0
  56. package/src/markit/converters/pdf/render.ts +501 -0
  57. package/src/markit/converters/pdf/types.ts +84 -0
  58. package/src/markit/converters/pptx.ts +325 -0
  59. package/src/markit/converters/xlsx.ts +173 -0
  60. package/src/markit/index.ts +2 -0
  61. package/src/markit/registry.ts +59 -0
  62. package/src/markit/types.ts +35 -0
  63. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -7
  64. package/src/modes/components/snapcompact-shape-preview.ts +2 -2
  65. package/src/modes/controllers/input-controller.ts +29 -8
  66. package/src/modes/interactive-mode.ts +26 -9
  67. package/src/prompts/advisor/system.md +1 -0
  68. package/src/sdk.ts +5 -9
  69. package/src/session/agent-session.ts +62 -40
  70. package/src/session/auth-storage.ts +2 -11
  71. package/src/session/yield-queue.ts +7 -1
  72. package/src/tools/browser/attach.ts +2 -2
  73. package/src/tools/fetch.ts +25 -60
  74. package/src/tools/read.ts +1 -1
  75. package/src/tools/search.ts +1 -6
  76. package/src/tools/write.ts +25 -65
  77. package/src/utils/markit.ts +25 -9
  78. package/src/utils/mupdf-wasm-embed.ts +12 -0
  79. package/src/utils/tools-manager.ts +2 -11
  80. package/src/utils/turndown.ts +83 -0
  81. package/src/{tools/archive-reader.ts → utils/zip.ts} +453 -83
  82. package/src/web/scrapers/types.ts +3 -46
  83. package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
  84. package/dist/types/tools/archive-reader.d.ts +0 -49
  85. package/src/internal-urls/docs-index.generated.ts +0 -120
@@ -0,0 +1,119 @@
1
+ /** A ZIP archive decoded to a `path → bytes` map of its file members. */
2
+ export type Unzipped = Record<string, Uint8Array>;
3
+ /** Read a single ZIP entry as UTF-8 text, or `undefined` when the entry is absent. */
4
+ export declare function unzipText(entries: Unzipped, entryPath: string): string | undefined;
5
+ /**
6
+ * Decode an in-memory ZIP archive into a `path → bytes` map of its file members
7
+ * (directory entries and `..`-escaping names are dropped). Shares the
8
+ * central-directory record parser with the lazy, file-backed reader.
9
+ */
10
+ export declare function unzip(bytes: Uint8Array): Unzipped;
11
+ export type ArchiveFormat = "zip" | "tar" | "tar.gz";
12
+ /**
13
+ * Where to read an archive from: a filesystem path (format inferred from the
14
+ * extension; ZIP is read lazily via ranged central-directory access) or
15
+ * in-memory bytes with an explicit format.
16
+ */
17
+ export type ArchiveSource = string | {
18
+ bytes: Uint8Array;
19
+ format: ArchiveFormat;
20
+ };
21
+ /** Content for a member when packing or extracting an archive. */
22
+ export type ArchiveMemberContent = string | Uint8Array | Blob;
23
+ export interface ArchivePathCandidate {
24
+ archivePath: string;
25
+ subPath: string;
26
+ }
27
+ export interface ArchiveNode {
28
+ path: string;
29
+ isDirectory: boolean;
30
+ size: number;
31
+ mtimeMs?: number;
32
+ }
33
+ export interface ArchiveDirectoryEntry extends ArchiveNode {
34
+ name: string;
35
+ }
36
+ export interface ExtractedArchiveFile extends ArchiveNode {
37
+ bytes: Uint8Array;
38
+ }
39
+ /** A byte window into an archive — file-backed (lazy) or in-memory. */
40
+ interface ByteSource {
41
+ readonly size: number;
42
+ read(start: number, end: number): Promise<Uint8Array>;
43
+ }
44
+ interface TarStorage {
45
+ type: "tar";
46
+ file: File;
47
+ }
48
+ interface ZipStorage {
49
+ type: "zip";
50
+ source: ByteSource;
51
+ compressedSize: number;
52
+ compression: number;
53
+ flags: number;
54
+ localHeaderOffset: number;
55
+ }
56
+ type EntryStorage = TarStorage | ZipStorage;
57
+ interface ArchiveIndexEntry extends ArchiveNode {
58
+ storage?: EntryStorage;
59
+ }
60
+ /** Infer an archive format from a filesystem path's extension. */
61
+ export declare function archiveFormatFromPath(filePath: string): ArchiveFormat | undefined;
62
+ export declare function formatArchiveEntryLines(entries: readonly ArchiveDirectoryEntry[]): string[];
63
+ export declare function sniffArchiveFormat(bytes: Uint8Array): ArchiveFormat | undefined;
64
+ /**
65
+ * Split an `archive.ext:inner/path` reference into every plausible
66
+ * `{ archivePath, subPath }` pair, longest archive prefix first. A path may
67
+ * contain more than one archive extension, so each candidate is a guess at
68
+ * where the archive ends and the member portion begins.
69
+ */
70
+ export declare function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[];
71
+ /**
72
+ * An indexed, read-only view over a single archive. ZIP archives are indexed
73
+ * from the central directory and members are inflated on demand; tar archives
74
+ * are fully materialized by `Bun.Archive` up front.
75
+ */
76
+ export declare class ArchiveReader {
77
+ #private;
78
+ readonly format: ArchiveFormat;
79
+ constructor(format: ArchiveFormat, entries: ArchiveIndexEntry[]);
80
+ getNode(subPath?: string): ArchiveNode | undefined;
81
+ listDirectory(subPath?: string): ArchiveDirectoryEntry[];
82
+ readFile(subPath: string): Promise<ExtractedArchiveFile>;
83
+ }
84
+ /**
85
+ * Open an archive for reading. ZIP archives opened from a path are indexed
86
+ * lazily via ranged central-directory reads (members inflate on demand); tar
87
+ * archives and in-memory ZIPs are read from a single buffer.
88
+ */
89
+ export declare function openArchive(source: ArchiveSource): Promise<ArchiveReader>;
90
+ /** Render the top-level entries of an in-memory archive as one line each. */
91
+ export declare function listArchiveRoot(bytes: Uint8Array, format: ArchiveFormat, opts?: {
92
+ limit?: number;
93
+ }): Promise<string>;
94
+ /**
95
+ * Fully materialize every file member into a `path → content` map: ZIP members
96
+ * are inflated in memory, tar members are returned as lazy `File`s. Use this
97
+ * when you need every entry (rewrite, extract); for browsing or single-member
98
+ * reads prefer `openArchive`, which is lazy for ZIP.
99
+ */
100
+ export declare function readArchiveEntries(source: ArchiveSource): Promise<Map<string, ArchiveMemberContent>>;
101
+ /**
102
+ * Serialize `entries` into an archive of `format` and write it to `destPath`.
103
+ * ZIP is framed in memory, tar / tar.gz via `Bun.Archive` (gzip for tar.gz).
104
+ * String members are encoded as UTF-8.
105
+ */
106
+ export declare function writeArchive(destPath: string, format: ArchiveFormat, entries: Iterable<readonly [string, ArchiveMemberContent]>): Promise<void>;
107
+ /**
108
+ * Extract every file member to `destDir`, creating parent directories as
109
+ * needed. Entries that would escape `destDir` (via `..` or an absolute path)
110
+ * are rejected. Returns the number of files written.
111
+ */
112
+ export declare function extractArchive(source: ArchiveSource, destDir: string): Promise<number>;
113
+ /**
114
+ * Frame a `path → bytes` map into a ZIP archive in memory. Each member is raw
115
+ * DEFLATE unless that would not shrink it, in which case it is stored. ZIP64 is
116
+ * not emitted; archives beyond the 32-bit limits throw rather than corrupt.
117
+ */
118
+ export declare function zip(entries: Unzipped): Uint8Array;
119
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.0.7",
4
+ "version": "16.0.8",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,29 +35,30 @@
35
35
  "check": "biome check . && bun run check:types",
36
36
  "check:types": "tsgo -p tsconfig.json --noEmit",
37
37
  "lint": "biome lint .",
38
- "test": "bun test --parallel=4",
39
- "fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index",
38
+ "test": "bun test --parallel=4 test src",
39
+ "fix": "biome check --write --unsafe . && bun run format-prompts",
40
40
  "fmt": "biome format --write . && bun run format-prompts",
41
41
  "format-prompts": "bun scripts/format-prompts.ts",
42
- "generate-docs-index": "bun scripts/generate-docs-index.ts",
43
- "prepack": "bun scripts/generate-docs-index.ts && bun --cwd=../collab-web run build:tool-views && bun scripts/bundle-dist.ts",
42
+ "generate-docs-index": "bun scripts/generate-docs-index.ts --generate",
43
+ "prepack": "bun scripts/generate-docs-index.ts --generate && bun --cwd=../collab-web run build:tool-views && bun scripts/bundle-dist.ts || ( bun scripts/generate-docs-index.ts --reset; exit 1 )",
44
+ "postpack": "bun scripts/generate-docs-index.ts --reset",
44
45
  "bench:guard": "bun scripts/bench-guard.ts"
45
46
  },
46
47
  "dependencies": {
47
48
  "@agentclientprotocol/sdk": "0.25.0",
48
49
  "@babel/parser": "^7.29.7",
49
50
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "16.0.7",
51
- "@oh-my-pi/omp-stats": "16.0.7",
52
- "@oh-my-pi/pi-agent-core": "16.0.7",
53
- "@oh-my-pi/pi-ai": "16.0.7",
54
- "@oh-my-pi/pi-catalog": "16.0.7",
55
- "@oh-my-pi/pi-mnemopi": "16.0.7",
56
- "@oh-my-pi/pi-natives": "16.0.7",
57
- "@oh-my-pi/pi-tui": "16.0.7",
58
- "@oh-my-pi/pi-utils": "16.0.7",
59
- "@oh-my-pi/pi-wire": "16.0.7",
60
- "@oh-my-pi/snapcompact": "16.0.7",
51
+ "@oh-my-pi/hashline": "16.0.8",
52
+ "@oh-my-pi/omp-stats": "16.0.8",
53
+ "@oh-my-pi/pi-agent-core": "16.0.8",
54
+ "@oh-my-pi/pi-ai": "16.0.8",
55
+ "@oh-my-pi/pi-catalog": "16.0.8",
56
+ "@oh-my-pi/pi-mnemopi": "16.0.8",
57
+ "@oh-my-pi/pi-natives": "16.0.8",
58
+ "@oh-my-pi/pi-tui": "16.0.8",
59
+ "@oh-my-pi/pi-utils": "16.0.8",
60
+ "@oh-my-pi/pi-wire": "16.0.8",
61
+ "@oh-my-pi/snapcompact": "16.0.8",
61
62
  "@opentelemetry/api": "^1.9.1",
62
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
63
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -70,11 +71,12 @@
70
71
  "arktype": "^2.2.0",
71
72
  "chalk": "^5.6.2",
72
73
  "diff": "^9.0.0",
73
- "fflate": "0.8.3",
74
+ "fast-xml-parser": "^5.9.0",
74
75
  "handlebars": "^4.7.9",
75
76
  "linkedom": "^0.18.12",
76
77
  "lru-cache": "11.5.1",
77
- "markit-ai": "0.5.3",
78
+ "mammoth": "^1.12.0",
79
+ "mupdf": "^1.27.0",
78
80
  "puppeteer-core": "^25.1.0",
79
81
  "turndown": "7.2.4",
80
82
  "turndown-plugin-gfm": "1.0.2",
@@ -38,9 +38,13 @@ async function runCommand(
38
38
  }
39
39
 
40
40
  async function main(): Promise<void> {
41
- await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
41
+ // Generate inside the try so the finally always restores the empty checked-in
42
+ // placeholders (stats client archive, docs index) even on failure.
42
43
  try {
44
+ await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
45
+ await runCommand(["bun", "scripts/generate-docs-index.ts", "--generate"]);
43
46
  await runCommand(["bun", "--cwd=../natives", "run", "embed:native"]);
47
+ await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--generate"]);
44
48
  try {
45
49
  const buildEnv = shouldAdhocSignDarwinBinary() ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env;
46
50
  await runCommand(
@@ -58,8 +62,6 @@ async function main(): Promise<void> {
58
62
  "--define",
59
63
  `process.env.PI_TINY_TRANSFORMERS_VERSION=${JSON.stringify(transformersVersion)}`,
60
64
  "--external",
61
- "mupdf",
62
- "--external",
63
65
  "fastembed",
64
66
  "--external",
65
67
  "onnxruntime-node",
@@ -94,10 +96,12 @@ async function main(): Promise<void> {
94
96
  await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
95
97
  }
96
98
  } finally {
99
+ await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--reset"]);
97
100
  await runCommand(["bun", "--cwd=../natives", "run", "embed:native", "--reset"]);
98
101
  }
99
102
  } finally {
100
103
  await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--reset"]);
104
+ await runCommand(["bun", "scripts/generate-docs-index.ts", "--reset"]);
101
105
  }
102
106
  }
103
107
 
@@ -9,6 +9,30 @@ const outDir = path.join(packageDir, "dist");
9
9
  const cliPath = path.join(outDir, "cli.js");
10
10
  const shebang = "#!/usr/bin/env bun\n";
11
11
 
12
+ // Native / optional / platform-specific deps that are never bundled — installed on
13
+ // demand (transformers/fastembed/onnxruntime) or shipped as their own artifact
14
+ // (native addon, mupdf).
15
+ const ALWAYS_EXTERNAL = ["mupdf", "@oh-my-pi/pi-natives", "@huggingface/transformers", "fastembed", "onnxruntime-node"];
16
+
17
+ // Heavy, lazily-used third-party leaf deps. Each is a declared `dependency`, so the
18
+ // published package resolves it from node_modules at runtime; bundling only embeds a
19
+ // redundant copy that bloats dist/cli.js. NEVER add a patched dependency here — the
20
+ // bundle is where a root `patchedDependencies` patch is baked in, so an externalized
21
+ // import would load the unpatched npm package in users' installs (currently
22
+ // @ark/schema is patched, so it — and arktype, which pulls @ark/schema — stay
23
+ // bundled).
24
+ const RUNTIME_EXTERNAL = [
25
+ "puppeteer-core",
26
+ "@puppeteer/browsers",
27
+ "@babel/parser",
28
+ "@xterm/headless",
29
+ "turndown",
30
+ "turndown-plugin-gfm",
31
+ "@mozilla/readability",
32
+ "linkedom",
33
+ "@agentclientprotocol/sdk",
34
+ ];
35
+
12
36
  async function runCommand(command: string[]): Promise<void> {
13
37
  const proc = Bun.spawn(command, {
14
38
  cwd: packageDir,
@@ -63,19 +87,11 @@ async function main(): Promise<void> {
63
87
  "--target=bun",
64
88
  "--outdir",
65
89
  "dist",
66
- "--minify-whitespace",
67
- "--minify-syntax",
90
+ // Full minify (whitespace + syntax + identifiers); --keep-names retains
91
+ // fn/class .name where code depends on it.
92
+ "--minify",
68
93
  "--keep-names",
69
- "--external",
70
- "mupdf",
71
- "--external",
72
- "@oh-my-pi/pi-natives",
73
- "--external",
74
- "@huggingface/transformers",
75
- "--external",
76
- "fastembed",
77
- "--external",
78
- "onnxruntime-node",
94
+ ...[...ALWAYS_EXTERNAL, ...RUNTIME_EXTERNAL].flatMap(dep => ["--external", dep]),
79
95
  "--define",
80
96
  'process.env.PI_BUNDLED="true"',
81
97
  "./src/cli.ts",
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Embeds mupdf's `mupdf-wasm.wasm` into the compiled single-file binary.
4
+ //
5
+ // mupdf loads its wasm by reading the `mupdf-wasm.wasm` sibling of its own
6
+ // module via `new URL(..., import.meta.url)` + `readFileSync`. A `bun --compile`
7
+ // binary has no node_modules, so that read fails (`ENOENT .../mupdf-wasm.wasm`),
8
+ // and marking mupdf `--external` instead makes `bun --compile` eagerly fail to
9
+ // resolve the package at startup (the static `import * as mupdf` lives in a lazy
10
+ // chunk but is hoisted). So the binary build bundles mupdf and embeds the wasm
11
+ // bytes here, handing them to the WASM module as `$libmupdf_wasm_Module.wasmBinary`
12
+ // (see src/utils/markit.ts).
13
+ //
14
+ // `--generate` copies the wasm next to src/utils/mupdf-wasm-embed.ts and rewrites
15
+ // that module to import it via `with { type: "file" }`; `--reset` restores the
16
+ // checked-in placeholder and removes the copy. The npm `dist/cli.js` bundle never
17
+ // runs this — it keeps mupdf external and loads the wasm from node_modules.
18
+
19
+ import * as fs from "node:fs/promises";
20
+ import { createRequire } from "node:module";
21
+ import * as path from "node:path";
22
+
23
+ const utilsDir = path.join(import.meta.dir, "..", "src", "utils");
24
+ const helperPath = path.join(utilsDir, "mupdf-wasm-embed.ts");
25
+ const wasmCopyPath = path.join(utilsDir, "mupdf-wasm.wasm");
26
+
27
+ const placeholder = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit by hand.
28
+ //
29
+ // Compiled single-file binaries cannot let mupdf resolve its \`mupdf-wasm.wasm\`
30
+ // sibling from the read-only bunfs, so the binary build (scripts/build-binary.ts
31
+ // and scripts/ci-release-build-binaries.ts) regenerates this module to embed the
32
+ // wasm bytes via \`with { type: "file" }\` and copies the wasm next to it. Source
33
+ // checkouts, \`bun test\`, and the npm \`dist/cli.js\` bundle keep mupdf external and
34
+ // load the wasm from node_modules, so this placeholder returns undefined and the
35
+ // build resets back to it afterward.
36
+ export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
37
+ \treturn undefined;
38
+ }
39
+ `;
40
+
41
+ const generated = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit or commit.
42
+ import { readFileSync } from "node:fs";
43
+ import wasmPath from "./mupdf-wasm.wasm" with { type: "file" };
44
+
45
+ export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
46
+ \treturn readFileSync(wasmPath);
47
+ }
48
+ `;
49
+
50
+ if (process.argv.includes("--reset")) {
51
+ await Bun.write(helperPath, placeholder);
52
+ try {
53
+ await fs.unlink(wasmCopyPath);
54
+ } catch (err) {
55
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
56
+ }
57
+ process.exit(0);
58
+ }
59
+
60
+ const wasmSource = path.join(path.dirname(createRequire(import.meta.url).resolve("mupdf")), "mupdf-wasm.wasm");
61
+ const wasmFile = Bun.file(wasmSource);
62
+ if (!(await wasmFile.exists())) {
63
+ throw new Error(`mupdf wasm not found at ${wasmSource}; run \`bun install\` first.`);
64
+ }
65
+ await Bun.write(wasmCopyPath, wasmFile);
66
+ await Bun.write(helperPath, generated);
67
+ console.log(`Embedded mupdf wasm (${wasmFile.size} bytes) into ${path.relative(process.cwd(), wasmCopyPath)}`);
@@ -1,40 +1,56 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ /**
4
+ * Populate (or reset) the embedded harness documentation index for `omp://`.
5
+ *
6
+ * `--generate` writes `src/internal-urls/docs-index.generated.txt` as two lines:
7
+ * a plain JSON array of the sorted `docs/**\/*.md` file names, then a base64
8
+ * gzip blob of the index-aligned doc bodies (`string[]`). Keeping the filename
9
+ * list out of the blob lets the loader list docs without inflating it.
10
+ * Compiled binaries and the prepacked npm bundle inline this (~0.5MB) instead of
11
+ * the ~1.6MB raw map; `--reset` restores the checked-in empty placeholder so the
12
+ * dev tree reads `docs/` from disk. Mirrors the stats / model-catalog embeds.
13
+ */
14
+
3
15
  import * as path from "node:path";
16
+ import { gzipSync } from "node:zlib";
4
17
  import { Glob } from "bun";
5
18
 
6
19
  const docsDir = path.resolve(import.meta.dir, "../../../docs");
7
- const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
20
+ const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.txt");
21
+ const GENERATE_FLAG = "--generate";
22
+ const RESET_FLAG = "--reset";
23
+
24
+ async function main(): Promise<void> {
25
+ const rel = path.relative(process.cwd(), outputPath);
26
+
27
+ if (process.argv.includes(RESET_FLAG)) {
28
+ await Bun.write(outputPath, "");
29
+ console.log(`Reset ${rel}`);
30
+ return;
31
+ }
32
+
33
+ if (!process.argv.includes(GENERATE_FLAG)) {
34
+ console.log(`Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)`);
35
+ return;
36
+ }
8
37
 
9
- const glob = new Glob("**/*.md");
10
- const entries: string[] = [];
11
- for await (const relativePath of glob.scan(docsDir)) {
12
- entries.push(relativePath.split(path.sep).join("/"));
38
+ const glob = new Glob("**/*.md");
39
+ const files: string[] = [];
40
+ for await (const relativePath of glob.scan(docsDir)) {
41
+ files.push(relativePath.split(path.sep).join("/"));
42
+ }
43
+ files.sort();
44
+
45
+ // Index-aligned bodies (Promise.all preserves order), kept separate from the
46
+ // filename list so the loader can list docs without inflating the blob.
47
+ const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
48
+
49
+ const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(bodies)), { level: 9 })).toString("base64");
50
+ // Two lines: plain filename array, then the base64 gzip blob.
51
+ const payload = `${JSON.stringify(files)}\n${bodiesB64}`;
52
+ await Bun.write(outputPath, payload);
53
+ console.log(`Generated ${rel} (${files.length} docs, ${payload.length} bytes)`);
13
54
  }
14
- entries.sort();
15
-
16
- const docsWithContent = await Promise.all(
17
- entries.map(async relativePath => ({
18
- relativePath,
19
- content: await Bun.file(path.join(docsDir, relativePath)).text(),
20
- })),
21
- );
22
-
23
- const filenamesLiteral = JSON.stringify(entries);
24
-
25
- const mapEntries = docsWithContent
26
- .map(({ relativePath, content }) => `\t${JSON.stringify(relativePath)}: ${JSON.stringify(content)},`)
27
- .join("\n");
28
- const output = [
29
- "// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT",
30
- "",
31
- `export const EMBEDDED_DOC_FILENAMES: readonly string[] = ${filenamesLiteral};`,
32
- "",
33
- `export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {`,
34
- `${mapEntries}`,
35
- `};`,
36
- "",
37
- ].join("\n");
38
-
39
- await Bun.write(outputPath, output);
40
- console.log(`Generated ${path.relative(process.cwd(), outputPath)} (${entries.length} docs)`);
55
+
56
+ await main();
package/scripts/omp CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/bin/sh
2
- # Dev launcher for the omp CLI.
2
+ # Dev launcher for the omp CLI, installed by `bun run setup`.
3
3
  #
4
4
  # Problem it solves: Bun reads `bunfig.toml` from the *current working
5
5
  # directory* at startup and evaluates its `preload` entries before running the
@@ -106,6 +106,32 @@ describe("advisor", () => {
106
106
  yq.enqueue("normal", { note: "b" });
107
107
  expect(scheduled).toBe(1);
108
108
  });
109
+
110
+ it("clear(kind) drops only that kind's queued entries", () => {
111
+ const yq = new YieldQueue({
112
+ isStreaming: () => false,
113
+ injectIdle: async () => {},
114
+ scheduleIdleFlush: () => {},
115
+ });
116
+ yq.register<{ note: string }>("advisor", {
117
+ build: entries => (entries.length === 0 ? null : ({ role: "custom", content: "x" } as AgentMessage)),
118
+ skipIdleFlush: true,
119
+ });
120
+ yq.register<{ note: string }>("normal", {
121
+ build: entries => (entries.length === 0 ? null : ({ role: "custom", content: "y" } as AgentMessage)),
122
+ });
123
+
124
+ yq.enqueue("advisor", { note: "stale advice" });
125
+ yq.enqueue("normal", { note: "keep me" });
126
+ expect(yq.has("advisor")).toBe(true);
127
+ expect(yq.has("normal")).toBe(true);
128
+
129
+ // Conversation-boundary cleanup must drop advisor deliveries without
130
+ // touching other kinds (IRC asides, async-job/diagnostic deliveries).
131
+ yq.clear("advisor");
132
+ expect(yq.has("advisor")).toBe(false);
133
+ expect(yq.has("normal")).toBe(true);
134
+ });
109
135
  });
110
136
 
111
137
  describe("AdviseTool", () => {
@@ -659,6 +685,63 @@ describe("advisor", () => {
659
685
  expect(promptInputs).toHaveLength(3);
660
686
  expect(runtime.backlog).toBe(0);
661
687
  });
688
+
689
+ it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
690
+ const promptInputs: string[] = [];
691
+ const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();
692
+ let rejectInFlight: ((err: unknown) => void) | undefined;
693
+ let promptCalls = 0;
694
+ const agent: AdvisorAgent = {
695
+ prompt: input => {
696
+ promptInputs.push(input);
697
+ promptCalls++;
698
+ if (promptCalls === 1) {
699
+ const { promise, reject } = Promise.withResolvers<void>();
700
+ rejectInFlight = reject;
701
+ startFirstPrompt();
702
+ return promise;
703
+ }
704
+ return Promise.resolve();
705
+ },
706
+ // AdvisorRuntime.reset() calls agent.reset() then agent.abort(); the real
707
+ // Agent.abort rejects the awaited prompt, so model that rejection here.
708
+ abort: () => rejectInFlight?.(new Error("advisor reset")),
709
+ reset: () => {},
710
+ state: { messages: [] },
711
+ };
712
+ const messages: AgentMessage[] = [{ role: "user", content: "old-conversation", timestamp: 1 } as AgentMessage];
713
+ const host: AdvisorRuntimeHost = {
714
+ snapshotMessages: () => messages,
715
+ enqueueAdvice: () => {},
716
+ };
717
+ const runtime = new AdvisorRuntime(agent, host, 0);
718
+
719
+ runtime.onTurnEnd(messages);
720
+ await firstPromptStarted;
721
+ expect(promptInputs).toHaveLength(1);
722
+ expect(promptInputs[0]).toContain("old-conversation");
723
+
724
+ // Conversation boundary (/new): transcript replaced and the runtime reset
725
+ // while the advisor prompt is still in flight. The abort that rejects the
726
+ // prompt is the reset itself — it must NOT be treated as a transient
727
+ // failure that requeues and re-sends the stale pre-reset batch.
728
+ messages.length = 0;
729
+ messages.push({ role: "user", content: "new-conversation", timestamp: 2 } as AgentMessage);
730
+ runtime.reset();
731
+ await Bun.sleep(0);
732
+ await Bun.sleep(0);
733
+
734
+ expect(promptInputs).toHaveLength(1);
735
+ expect(runtime.backlog).toBe(0);
736
+
737
+ // The runtime still works afterward: the next turn replays the new
738
+ // transcript only, never the dropped pre-reset content.
739
+ runtime.onTurnEnd(messages);
740
+ await Bun.sleep(0);
741
+ expect(promptInputs).toHaveLength(2);
742
+ expect(promptInputs[1]).toContain("new-conversation");
743
+ expect(promptInputs[1]).not.toContain("old-conversation");
744
+ });
662
745
  });
663
746
 
664
747
  describe("read-only tool allowlist", () => {
@@ -48,6 +48,11 @@ export class AdvisorRuntime {
48
48
  #consecutiveFailures = 0;
49
49
  #latestMessages?: AgentMessage[];
50
50
  #waiters: CatchupWaiter[] = [];
51
+ /** Bumped by every external {@link reset}/{@link dispose}. A drain iteration
52
+ * captures it before its awaits; a mismatch on resume means a reset aborted
53
+ * the in-flight advisor prompt, so the stale batch is dropped instead of
54
+ * being retried/requeued into the post-reset conversation. */
55
+ #epoch = 0;
51
56
  disposed = false;
52
57
 
53
58
  constructor(
@@ -95,6 +100,7 @@ export class AdvisorRuntime {
95
100
 
96
101
  dispose(): void {
97
102
  this.disposed = true;
103
+ this.#epoch++;
98
104
  this.#pending = [];
99
105
  this.#backlog = 0;
100
106
  this.#consecutiveFailures = 0;
@@ -130,6 +136,7 @@ export class AdvisorRuntime {
130
136
  * leaving it blind to everything before the rewrite.
131
137
  */
132
138
  reset(): void {
139
+ this.#epoch++;
133
140
  this.#resetAdvisorContext(true, true);
134
141
  }
135
142
 
@@ -187,6 +194,7 @@ export class AdvisorRuntime {
187
194
  try {
188
195
  while (!this.disposed && this.#pending.length) {
189
196
  const popped = this.#pending.splice(0);
197
+ const epoch = this.#epoch;
190
198
  // Each delta already opens with a `### Session update` heading, so
191
199
  // join with a blank line rather than a `---` rule.
192
200
  const candidateBatch = popped.map(b => b.text).join("\n\n");
@@ -205,6 +213,8 @@ export class AdvisorRuntime {
205
213
  logger.debug("advisor context maintenance failed", { err: String(err) });
206
214
  }
207
215
  }
216
+ // A reset/dispose during context maintenance invalidates this batch.
217
+ if (this.#epoch !== epoch) continue;
208
218
 
209
219
  let batch: string | null;
210
220
  let finalTurns: number;
@@ -231,6 +241,11 @@ export class AdvisorRuntime {
231
241
  success = true;
232
242
  this.#consecutiveFailures = 0;
233
243
  } catch (err) {
244
+ // reset()/dispose() aborts the in-flight prompt; the rejection is the
245
+ // reset itself, not a transient advisor failure. Drop the stale batch
246
+ // (reset already cleared #pending and rewound the cursor) instead of
247
+ // requeuing it into the post-reset conversation.
248
+ if (this.#epoch !== epoch) continue;
234
249
  logger.debug("advisor turn failed", { err: String(err) });
235
250
  this.#consecutiveFailures++;
236
251
  if (this.#consecutiveFailures >= 3) {
@@ -243,7 +258,7 @@ export class AdvisorRuntime {
243
258
  }
244
259
  }
245
260
 
246
- if (success) {
261
+ if (success && this.#epoch === epoch) {
247
262
  this.#backlog = Math.max(0, this.#backlog - finalTurns);
248
263
  this.#notifyWaiters();
249
264
  }
@@ -19,11 +19,9 @@ import * as os from "node:os";
19
19
  import * as path from "node:path";
20
20
  import * as readline from "node:readline";
21
21
  import {
22
- AuthBrokerClient,
23
22
  type AuthCredential,
24
23
  AuthStorage,
25
24
  type CredentialDisabledEvent,
26
- DEFAULT_AUTH_BROKER_BIND,
27
25
  getEnvApiKey,
28
26
  getOAuthProviders,
29
27
  listProvidersWithEnvKey,
@@ -32,8 +30,8 @@ import {
32
30
  type OAuthProviderInfo,
33
31
  PROVIDER_REGISTRY,
34
32
  SqliteAuthCredentialStore,
35
- startAuthBroker,
36
33
  } from "@oh-my-pi/pi-ai";
34
+ import { AuthBrokerClient, DEFAULT_AUTH_BROKER_BIND, startAuthBroker } from "@oh-my-pi/pi-ai/auth-broker";
37
35
  import { $which, APP_NAME, getAgentDbPath, getConfigRootDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
38
36
  import { setTransports as setLoggerTransports } from "@oh-my-pi/pi-utils/logger";
39
37
  import { $ } from "bun";
@@ -17,18 +17,15 @@ import * as fs from "node:fs/promises";
17
17
  import * as path from "node:path";
18
18
  import {
19
19
  type Api,
20
- AuthBrokerClient,
21
20
  AuthStorage,
22
21
  type CompletionProbe,
23
22
  type CompletionProbeInput,
24
23
  type CredentialCompletionResult,
25
24
  completeSimple,
26
- DEFAULT_AUTH_GATEWAY_BIND,
27
25
  type Model,
28
- RemoteAuthCredentialStore,
29
- type SnapshotResponse,
30
- startAuthGateway,
31
26
  } from "@oh-my-pi/pi-ai";
27
+ import { AuthBrokerClient, RemoteAuthCredentialStore, type SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker";
28
+ import { DEFAULT_AUTH_GATEWAY_BIND, startAuthGateway } from "@oh-my-pi/pi-ai/auth-gateway";
32
29
  import { type GeneratedProvider, getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/models";
33
30
  import { getConfigRootDir, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
34
31
  import chalk from "chalk";