@oh-my-pi/pi-coding-agent 16.4.2 → 16.4.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/CHANGELOG.md +42 -0
- package/dist/cli.js +17514 -17799
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +10 -22
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +3 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/task/executor.d.ts +31 -0
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +16 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/vibe.d.ts +161 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/dist/types/vibe/runtime.d.ts +124 -0
- package/dist/types/vibe/state.d.ts +4 -0
- package/dist/types/web/search/provider.d.ts +2 -0
- package/dist/types/web/search/providers/bing.d.ts +14 -0
- package/dist/types/web/search/providers/browser-headers.d.ts +9 -0
- package/dist/types/web/search/providers/browser-page.d.ts +32 -0
- package/dist/types/web/search/providers/ecosia.d.ts +14 -0
- package/dist/types/web/search/providers/google.d.ts +13 -0
- package/dist/types/web/search/providers/mojeek.d.ts +14 -0
- package/dist/types/web/search/providers/public.d.ts +37 -0
- package/dist/types/web/search/providers/startpage.d.ts +14 -0
- package/dist/types/web/search/providers/yahoo.d.ts +14 -0
- package/dist/types/web/search/types.d.ts +28 -0
- package/package.json +15 -18
- package/scripts/build-binary.ts +56 -73
- package/scripts/bundle-dist.ts +36 -40
- package/scripts/compile-binary.ts +68 -0
- package/scripts/generate-docs-index.ts +3 -93
- package/scripts/legacy-pi-virtual-module.ts +192 -0
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/edit/index.ts +10 -76
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +328 -162
- package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
- package/src/internal-urls/docs-index.ts +2 -1
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +65 -1
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/status-line/component.ts +6 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/interactive-mode.ts +117 -1
- package/src/modes/types.ts +2 -0
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -2
- package/src/prompts/system/vibe-mode-active.md +23 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/vibe-kill.md +3 -0
- package/src/prompts/tools/vibe-list.md +3 -0
- package/src/prompts/tools/vibe-send.md +9 -0
- package/src/prompts/tools/vibe-spawn.md +10 -0
- package/src/prompts/tools/vibe-turn-result.md +19 -0
- package/src/prompts/tools/vibe-wait.md +8 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +103 -13
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/slash-commands/builtin-registry.ts +24 -8
- package/src/task/agents.ts +0 -2
- package/src/task/executor.ts +105 -0
- package/src/tools/__tests__/vibe-render.test.ts +210 -0
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +46 -48
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +264 -129
- package/src/tools/glob.ts +20 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +17 -9
- package/src/tools/renderers.ts +6 -0
- package/src/tools/vibe.ts +608 -0
- package/src/tools/write.ts +15 -2
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +70 -7
- package/src/vibe/runtime.ts +710 -0
- package/src/vibe/state.ts +4 -0
- package/src/web/search/index.ts +14 -6
- package/src/web/search/provider.ts +37 -1
- package/src/web/search/providers/bing.ts +197 -0
- package/src/web/search/providers/browser-headers.ts +92 -0
- package/src/web/search/providers/browser-page.ts +123 -0
- package/src/web/search/providers/duckduckgo.ts +13 -34
- package/src/web/search/providers/ecosia.ts +178 -0
- package/src/web/search/providers/google.ts +193 -0
- package/src/web/search/providers/mojeek.ts +206 -0
- package/src/web/search/providers/public.ts +201 -0
- package/src/web/search/providers/startpage.ts +213 -0
- package/src/web/search/providers/yahoo.ts +179 -0
- package/src/web/search/types.ts +35 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +0 -10
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +0 -10
- package/scripts/generate-legacy-pi-bundled-registry.ts +0 -420
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +0 -1011
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +0 -3430
- package/src/internal-urls/docs-index.generated.txt +0 -2
- package/src/prompts/agents/plan.md +0 -47
package/scripts/bundle-dist.ts
CHANGED
|
@@ -3,17 +3,24 @@
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import {
|
|
6
|
+
import { buildDocsIndexPayload } from "./generate-docs-index";
|
|
7
7
|
|
|
8
8
|
const packageDir = path.join(import.meta.dir, "..");
|
|
9
9
|
const outDir = path.join(packageDir, "dist");
|
|
10
10
|
const cliPath = path.join(outDir, "cli.js");
|
|
11
11
|
const shebang = "#!/usr/bin/env bun\n";
|
|
12
12
|
|
|
13
|
-
// Native / optional / platform-specific deps
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
const ALWAYS_EXTERNAL = [
|
|
13
|
+
// Native / optional / platform-specific deps are loaded from installed files.
|
|
14
|
+
// `omp-legacy-pi-modules` exists only in compiled binaries via the build plugin;
|
|
15
|
+
// the npm bundle never executes that `isCompiledBinary()` branch.
|
|
16
|
+
const ALWAYS_EXTERNAL = [
|
|
17
|
+
"mupdf",
|
|
18
|
+
"@oh-my-pi/pi-natives",
|
|
19
|
+
"@huggingface/transformers",
|
|
20
|
+
"fastembed",
|
|
21
|
+
"onnxruntime-node",
|
|
22
|
+
"omp-legacy-pi-modules",
|
|
23
|
+
];
|
|
17
24
|
|
|
18
25
|
// Heavy, lazily-used third-party leaf deps. Each is a declared `dependency`, so the
|
|
19
26
|
// published package resolves it from node_modules at runtime; bundling only embeds a
|
|
@@ -73,48 +80,37 @@ async function cleanBundleOutputs(): Promise<void> {
|
|
|
73
80
|
);
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
async function assertDocsEmbedPopulated(): Promise<void> {
|
|
77
|
-
// bundle-dist runs from prepack (which calls `gen:docs` first) or directly.
|
|
78
|
-
// Direct invocations must fail — the tarball ships src/, and an empty embed
|
|
79
|
-
// would make src/internal-urls/docs-index.ts fall through to the missing
|
|
80
|
-
// repo `docs/` tree at runtime in published packages (codex review, PR #3941).
|
|
81
|
-
const embedPath = path.join(packageDir, "src/internal-urls/docs-index.generated.txt");
|
|
82
|
-
const embed = await Bun.file(embedPath).text();
|
|
83
|
-
if (embed.length === 0) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"docs-index embed is empty. Run `bun run gen:docs` before `bun run gen:bundle`, or use `bun pm pack` which runs the prepack chain.",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
const expected = await buildDocsIndexPayload();
|
|
89
|
-
assertDocsIndexFresh(embed, expected);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
83
|
async function main(): Promise<void> {
|
|
93
84
|
const start = Bun.nanoseconds();
|
|
94
85
|
await cleanBundleOutputs();
|
|
95
|
-
await assertDocsEmbedPopulated();
|
|
96
86
|
// The npm bundle ships no stats dashboard sources, so embed the dashboard
|
|
97
87
|
// archive the same way compiled binaries do (scripts/build-binary.ts). Reset
|
|
98
|
-
// afterwards to keep the checked-in placeholder empty.
|
|
99
|
-
// populated on disk — postpack owns its reset so `bun pm pack` can pack a
|
|
100
|
-
// tarball whose src copy is still valid for subpath imports.
|
|
88
|
+
// afterwards to keep the checked-in placeholder empty.
|
|
101
89
|
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
|
|
102
90
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
91
|
+
// Build in-process: the docs embed payload is far larger than Linux's
|
|
92
|
+
// 128KiB per-argv-string cap, so it can never be passed as a CLI
|
|
93
|
+
// `--define` (posix_spawn fails with E2BIG).
|
|
94
|
+
const output = await Bun.build({
|
|
95
|
+
entrypoints: [path.join(packageDir, "src/cli.ts")],
|
|
96
|
+
outdir: outDir,
|
|
97
|
+
target: "bun",
|
|
98
|
+
external: [...ALWAYS_EXTERNAL, ...RUNTIME_EXTERNAL],
|
|
99
|
+
define: {
|
|
100
|
+
"process.env.PI_BUNDLED": JSON.stringify("true"),
|
|
101
|
+
"process.env.PI_DOCS_EMBED": JSON.stringify((await buildDocsIndexPayload()).payload),
|
|
102
|
+
},
|
|
103
|
+
minify: {
|
|
104
|
+
whitespace: true,
|
|
105
|
+
syntax: true,
|
|
106
|
+
identifiers: true,
|
|
107
|
+
keepNames: true,
|
|
108
|
+
},
|
|
109
|
+
throw: false,
|
|
110
|
+
});
|
|
111
|
+
if (!output.success) {
|
|
112
|
+
throw new Error(`CLI bundle failed:\n${output.logs.map(log => log.message).join("\n")}`);
|
|
113
|
+
}
|
|
118
114
|
} finally {
|
|
119
115
|
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
|
|
120
116
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildDocsIndexPayload } from "./generate-docs-index";
|
|
2
|
+
import { createLegacyPiVirtualModulePlugin } from "./legacy-pi-virtual-module";
|
|
3
|
+
|
|
4
|
+
/** Inputs shared by local and release coding-agent binary builds. */
|
|
5
|
+
export interface CodingAgentCompileOptions {
|
|
6
|
+
/** Absolute repository root used for package resolution. */
|
|
7
|
+
readonly repoRoot: string;
|
|
8
|
+
/** Absolute CLI entrypoint. */
|
|
9
|
+
readonly entrypoint: string;
|
|
10
|
+
/** Absolute standalone executable output path. */
|
|
11
|
+
readonly outfile: string;
|
|
12
|
+
/** Concrete Transformers.js version baked into the tiny-model worker. */
|
|
13
|
+
readonly transformersVersion: string;
|
|
14
|
+
/** Optional cross-compilation runtime target. */
|
|
15
|
+
readonly target?: Bun.Build.CompileTarget;
|
|
16
|
+
/** Dependencies intentionally resolved from the runtime filesystem. */
|
|
17
|
+
readonly external?: readonly string[];
|
|
18
|
+
/** Match release builds that minify identifiers while retaining names. */
|
|
19
|
+
readonly minifyIdentifiers?: boolean;
|
|
20
|
+
/** Disable Bun's built-in Darwin signing before the caller re-signs. */
|
|
21
|
+
readonly skipBuiltinCodesign?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compile the coding-agent executable with its legacy Pi compatibility module
|
|
26
|
+
* graph supplied by an in-memory build plugin rather than generated files.
|
|
27
|
+
*/
|
|
28
|
+
export async function compileCodingAgent(options: CodingAgentCompileOptions): Promise<void> {
|
|
29
|
+
const previousCodesignSetting = Bun.env.BUN_NO_CODESIGN_MACHO_BINARY;
|
|
30
|
+
if (options.skipBuiltinCodesign) {
|
|
31
|
+
Bun.env.BUN_NO_CODESIGN_MACHO_BINARY = "1";
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const output = await Bun.build({
|
|
35
|
+
entrypoints: [options.entrypoint],
|
|
36
|
+
root: options.repoRoot,
|
|
37
|
+
external: options.external ? [...options.external] : undefined,
|
|
38
|
+
define: {
|
|
39
|
+
"process.env.PI_COMPILED": JSON.stringify("true"),
|
|
40
|
+
"process.env.PI_TINY_TRANSFORMERS_VERSION": JSON.stringify(options.transformersVersion),
|
|
41
|
+
"process.env.PI_DOCS_EMBED": JSON.stringify((await buildDocsIndexPayload()).payload),
|
|
42
|
+
},
|
|
43
|
+
minify: {
|
|
44
|
+
identifiers: options.minifyIdentifiers ?? false,
|
|
45
|
+
keepNames: true,
|
|
46
|
+
},
|
|
47
|
+
plugins: [await createLegacyPiVirtualModulePlugin()],
|
|
48
|
+
compile: {
|
|
49
|
+
...(options.target ? { target: options.target } : {}),
|
|
50
|
+
outfile: options.outfile,
|
|
51
|
+
autoloadBunfig: false,
|
|
52
|
+
autoloadDotenv: false,
|
|
53
|
+
autoloadTsconfig: false,
|
|
54
|
+
autoloadPackageJson: false,
|
|
55
|
+
},
|
|
56
|
+
throw: false,
|
|
57
|
+
});
|
|
58
|
+
if (!output.success) {
|
|
59
|
+
throw new Error(`Coding-agent binary bundle failed:\n${output.logs.map(log => log.message).join("\n")}`);
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
if (previousCodesignSetting === undefined) {
|
|
63
|
+
delete Bun.env.BUN_NO_CODESIGN_MACHO_BINARY;
|
|
64
|
+
} else {
|
|
65
|
+
Bun.env.BUN_NO_CODESIGN_MACHO_BINARY = previousCodesignSetting;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,38 +1,18 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Populate, check, 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[]`). `--check` rebuilds
|
|
9
|
-
* that payload from the real docs corpus and compares it to the embed when
|
|
10
|
-
* present; the checked-in empty placeholder is accepted after verifying that a
|
|
11
|
-
* fresh generated payload round-trips. `--reset` restores the placeholder so the
|
|
12
|
-
* dev tree reads `docs/` from disk. Mirrors the stats / model-catalog embeds.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
1
|
import * as path from "node:path";
|
|
16
2
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
17
3
|
import { Glob } from "bun";
|
|
18
4
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const GENERATE_FLAG = "--generate";
|
|
22
|
-
const RESET_FLAG = "--reset";
|
|
23
|
-
const CHECK_FLAG = "--check";
|
|
5
|
+
const packageDir = path.resolve(import.meta.dir, "..");
|
|
6
|
+
const docsDir = path.resolve(packageDir, "../../docs");
|
|
24
7
|
|
|
25
8
|
export interface DocsIndexPayload {
|
|
26
|
-
/** Sorted `docs/**\/*.md` file names plus index-aligned bodies and embed text. */
|
|
27
9
|
readonly files: readonly string[];
|
|
28
10
|
readonly bodies: readonly string[];
|
|
29
11
|
readonly payload: string;
|
|
30
12
|
}
|
|
31
13
|
|
|
32
14
|
export interface DecodedDocsIndexPayload {
|
|
33
|
-
/** Sorted `docs/**\/*.md` file names decoded from an embed payload. */
|
|
34
15
|
readonly files: readonly string[];
|
|
35
|
-
/** Index-aligned Markdown bodies decoded from an embed payload. */
|
|
36
16
|
readonly bodies: readonly string[];
|
|
37
17
|
}
|
|
38
18
|
|
|
@@ -40,7 +20,7 @@ function isStringArray(value: unknown): value is string[] {
|
|
|
40
20
|
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
41
21
|
}
|
|
42
22
|
|
|
43
|
-
/** Build the exact two-line `omp://` docs embed from the source `docs
|
|
23
|
+
/** Build the exact two-line `omp://` docs embed from the source `docs` Markdown corpus. */
|
|
44
24
|
export async function buildDocsIndexPayload(): Promise<DocsIndexPayload> {
|
|
45
25
|
const glob = new Glob("**/*.md");
|
|
46
26
|
const files: string[] = [];
|
|
@@ -76,73 +56,3 @@ export function decodeDocsIndexPayload(embed: string): DecodedDocsIndexPayload |
|
|
|
76
56
|
|
|
77
57
|
return { files: filenames, bodies };
|
|
78
58
|
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Assert that an embed payload is fresh against the current source docs payload.
|
|
82
|
-
* An empty placeholder is accepted by round-tripping the expected payload (the
|
|
83
|
-
* dev tree and post-build reset state both checked-in placeholders).
|
|
84
|
-
*/
|
|
85
|
-
export function assertDocsIndexFresh(embed: string, expected: DecodedDocsIndexPayload): void {
|
|
86
|
-
const source =
|
|
87
|
-
embed.length > 0
|
|
88
|
-
? embed
|
|
89
|
-
: `${JSON.stringify(expected.files)}\n${Buffer.from(gzipSync(Buffer.from(JSON.stringify(expected.bodies)), { level: 9 })).toString("base64")}`;
|
|
90
|
-
const decoded = decodeDocsIndexPayload(source);
|
|
91
|
-
if (decoded === null) {
|
|
92
|
-
throw new Error("Embedded docs index is malformed: missing newline separator.");
|
|
93
|
-
}
|
|
94
|
-
if (decoded.files.length !== expected.files.length) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (decoded.bodies.length !== expected.bodies.length) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
for (let i = 0; i < expected.files.length; i++) {
|
|
105
|
-
if (decoded.files[i] !== expected.files[i]) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`Embedded docs index filename mismatch at ${i}: ${decoded.files[i] ?? "<missing>"} !== ${expected.files[i]}.`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
if (decoded.bodies[i] !== expected.bodies[i]) {
|
|
111
|
-
throw new Error(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function main(): Promise<void> {
|
|
117
|
-
const rel = path.relative(process.cwd(), outputPath);
|
|
118
|
-
|
|
119
|
-
if (process.argv.includes(RESET_FLAG)) {
|
|
120
|
-
await Bun.write(outputPath, "");
|
|
121
|
-
process.stdout.write(`Reset ${rel}\n`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (process.argv.includes(CHECK_FLAG)) {
|
|
126
|
-
const current = await buildDocsIndexPayload();
|
|
127
|
-
const embed = await Bun.file(outputPath).text();
|
|
128
|
-
assertDocsIndexFresh(embed, current);
|
|
129
|
-
process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!process.argv.includes(GENERATE_FLAG)) {
|
|
134
|
-
process.stdout.write(
|
|
135
|
-
`Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)\n`,
|
|
136
|
-
);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const current = await buildDocsIndexPayload();
|
|
141
|
-
assertDocsIndexFresh(current.payload, current);
|
|
142
|
-
await Bun.write(outputPath, current.payload);
|
|
143
|
-
process.stdout.write(`Generated ${rel} (${current.files.length} docs, ${current.payload.length} bytes)\n`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (import.meta.main) {
|
|
147
|
-
await main();
|
|
148
|
-
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isEnoent } from "@oh-my-pi/pi-utils/fs-error";
|
|
3
|
+
|
|
4
|
+
/** Build-time specifier resolved to bundled legacy Pi module namespaces. */
|
|
5
|
+
export const LEGACY_PI_MODULES_SPECIFIER = "omp-legacy-pi-modules";
|
|
6
|
+
|
|
7
|
+
const VIRTUAL_NAMESPACE = "omp-legacy-pi-modules-build";
|
|
8
|
+
const packageDir = path.resolve(import.meta.dir, "..");
|
|
9
|
+
const repoRoot = path.resolve(packageDir, "..", "..");
|
|
10
|
+
|
|
11
|
+
interface BundledPackage {
|
|
12
|
+
readonly dir: string;
|
|
13
|
+
readonly identifier: string;
|
|
14
|
+
readonly rootShim: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const BUNDLED_PACKAGES: readonly BundledPackage[] = [
|
|
18
|
+
{ dir: "agent", identifier: "PiAgentCore", rootShim: null },
|
|
19
|
+
{ dir: "ai", identifier: "PiAi", rootShim: "legacy-pi-ai-shim.ts" },
|
|
20
|
+
{ dir: "coding-agent", identifier: "PiCodingAgent", rootShim: "legacy-pi-coding-agent-shim.ts" },
|
|
21
|
+
{ dir: "natives", identifier: "PiNatives", rootShim: null },
|
|
22
|
+
{ dir: "tui", identifier: "PiTui", rootShim: null },
|
|
23
|
+
{ dir: "utils", identifier: "PiUtils", rootShim: null },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const TYPEBOX_MODULE_KEY = "typebox";
|
|
27
|
+
const TYPEBOX_SHIM = "typebox.ts";
|
|
28
|
+
const SKIPPED_WILDCARD_BASENAMES = new Set(["index"]);
|
|
29
|
+
const MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES = new Set(["worker-entry"]);
|
|
30
|
+
|
|
31
|
+
/** One namespace module the binary must retain for legacy extension imports. */
|
|
32
|
+
export interface BundledPiEntry {
|
|
33
|
+
/** Canonical import key exposed to extensions. */
|
|
34
|
+
readonly key: string;
|
|
35
|
+
/** Unique identifier used by the virtual module's generated import. */
|
|
36
|
+
readonly binding: string;
|
|
37
|
+
/** Package or absolute source specifier compiled into the binary. */
|
|
38
|
+
readonly importSpecifier: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WildcardPattern {
|
|
42
|
+
readonly exportPrefix: string;
|
|
43
|
+
readonly exportSuffix: string;
|
|
44
|
+
readonly sourcePrefix: string;
|
|
45
|
+
readonly sourceSuffix: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function bindingForSubpath(identifier: string, subpath: string): string {
|
|
53
|
+
const segments = subpath
|
|
54
|
+
.split("/")
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map(segment =>
|
|
57
|
+
segment
|
|
58
|
+
.split(/[-_]/)
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
61
|
+
.join(""),
|
|
62
|
+
);
|
|
63
|
+
return `bundled${identifier}${segments.join("")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isSafeWildcardBasename(basename: string): boolean {
|
|
67
|
+
if (!basename || basename.startsWith(".") || basename.startsWith("_")) return false;
|
|
68
|
+
if (SKIPPED_WILDCARD_BASENAMES.has(basename)) return false;
|
|
69
|
+
if (MAIN_THREAD_UNSAFE_WILDCARD_BASENAMES.has(basename)) return false;
|
|
70
|
+
return !/\.(test|spec|d|generated|bench)$/.test(basename);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseWildcardPattern(exportKey: string, sourcePattern: string): WildcardPattern | null {
|
|
74
|
+
const exportStar = exportKey.indexOf("*");
|
|
75
|
+
const sourceStar = sourcePattern.indexOf("*");
|
|
76
|
+
if (exportStar === -1 || sourceStar === -1) return null;
|
|
77
|
+
if (exportKey.indexOf("*", exportStar + 1) !== -1) return null;
|
|
78
|
+
if (sourcePattern.indexOf("*", sourceStar + 1) !== -1) return null;
|
|
79
|
+
if (!sourcePattern.startsWith("./")) return null;
|
|
80
|
+
return {
|
|
81
|
+
exportPrefix: exportKey.slice(2, exportStar),
|
|
82
|
+
exportSuffix: exportKey.slice(exportStar + 1),
|
|
83
|
+
sourcePrefix: sourcePattern.slice(2, sourceStar),
|
|
84
|
+
sourceSuffix: sourcePattern.slice(sourceStar + 1),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function exportImportTarget(value: unknown): string | null {
|
|
89
|
+
if (typeof value === "string") return value;
|
|
90
|
+
if (isRecord(value) && typeof value.import === "string") return value.import;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function shimSpecifier(file: string): string {
|
|
95
|
+
return path.join(packageDir, "src", "extensibility", file);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Derive the bundled legacy Pi module surface from current package exports.
|
|
100
|
+
* Named wildcard exports are expanded from source; root catch-alls stay out to
|
|
101
|
+
* avoid importing CLI entrypoints and other non-extension surfaces.
|
|
102
|
+
*/
|
|
103
|
+
export async function collectBundledPiEntries(): Promise<BundledPiEntry[]> {
|
|
104
|
+
const entries: BundledPiEntry[] = [];
|
|
105
|
+
const seenKeys = new Set<string>();
|
|
106
|
+
const seenBindings = new Set<string>();
|
|
107
|
+
function addEntry(key: string, binding: string, importSpecifier: string): void {
|
|
108
|
+
if (seenKeys.has(key)) return;
|
|
109
|
+
if (seenBindings.has(binding)) {
|
|
110
|
+
throw new Error(`Duplicate bundled Pi binding ${binding} for ${key}`);
|
|
111
|
+
}
|
|
112
|
+
seenKeys.add(key);
|
|
113
|
+
seenBindings.add(binding);
|
|
114
|
+
entries.push({ key, binding, importSpecifier });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const pkg of BUNDLED_PACKAGES) {
|
|
118
|
+
const packageRoot = path.join(repoRoot, "packages", pkg.dir);
|
|
119
|
+
const manifestPath = path.join(packageRoot, "package.json");
|
|
120
|
+
const manifest: unknown = await Bun.file(manifestPath).json();
|
|
121
|
+
if (!isRecord(manifest) || typeof manifest.name !== "string") {
|
|
122
|
+
throw new Error(`Bundled Pi package manifest has no name: ${manifestPath}`);
|
|
123
|
+
}
|
|
124
|
+
const exportsField = isRecord(manifest.exports) ? manifest.exports : {};
|
|
125
|
+
const rootSpecifier = pkg.rootShim ? shimSpecifier(pkg.rootShim) : manifest.name;
|
|
126
|
+
addEntry(manifest.name, `bundled${pkg.identifier}`, rootSpecifier);
|
|
127
|
+
|
|
128
|
+
for (const exportKey in exportsField) {
|
|
129
|
+
if (!exportKey.startsWith("./") || exportKey === "." || exportKey.includes("*")) continue;
|
|
130
|
+
const subpath = exportKey.slice(2);
|
|
131
|
+
const key = `${manifest.name}/${subpath}`;
|
|
132
|
+
addEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const exportKey in exportsField) {
|
|
136
|
+
if (!exportKey.startsWith("./") || exportKey === "." || !exportKey.includes("*")) continue;
|
|
137
|
+
const sourcePattern = exportImportTarget(exportsField[exportKey]);
|
|
138
|
+
if (!sourcePattern) continue;
|
|
139
|
+
const pattern = parseWildcardPattern(exportKey, sourcePattern);
|
|
140
|
+
if (!pattern || !/\.(ts|tsx|mts|cts|js|mjs|cjs|jsx)$/.test(pattern.sourceSuffix)) continue;
|
|
141
|
+
if (pattern.exportPrefix === "" || pattern.exportPrefix === "/") continue;
|
|
142
|
+
|
|
143
|
+
const sourceDir = path.join(packageRoot, pattern.sourcePrefix);
|
|
144
|
+
try {
|
|
145
|
+
const glob = new Bun.Glob(`*${pattern.sourceSuffix}`);
|
|
146
|
+
const matches: string[] = [];
|
|
147
|
+
for await (const match of glob.scan({ cwd: sourceDir, onlyFiles: true })) {
|
|
148
|
+
matches.push(match);
|
|
149
|
+
}
|
|
150
|
+
matches.sort();
|
|
151
|
+
for (const match of matches) {
|
|
152
|
+
if (!match.endsWith(pattern.sourceSuffix)) continue;
|
|
153
|
+
const basename = match.slice(0, match.length - pattern.sourceSuffix.length);
|
|
154
|
+
if (!isSafeWildcardBasename(basename) || basename.includes("/")) continue;
|
|
155
|
+
const subpath = `${pattern.exportPrefix}${basename}${pattern.exportSuffix}`;
|
|
156
|
+
const key = `${manifest.name}/${subpath}`;
|
|
157
|
+
addEntry(key, bindingForSubpath(pkg.identifier, subpath), key);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!isEnoent(error)) throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
addEntry(TYPEBOX_MODULE_KEY, "bundledTypeBoxShim", shimSpecifier(TYPEBOX_SHIM));
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderVirtualModule(entries: readonly BundledPiEntry[]): string {
|
|
170
|
+
const imports = entries.map(entry => `import * as ${entry.binding} from ${JSON.stringify(entry.importSpecifier)};`);
|
|
171
|
+
const modules = entries.map(entry => `\t${JSON.stringify(entry.key)}: ${entry.binding},`);
|
|
172
|
+
return [...imports, "", "export const BUNDLED_PI_MODULES = {", ...modules, "};", ""].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build plugin that materializes the legacy Pi module graph entirely in
|
|
177
|
+
* memory. Bun still needs static import edges at compile time, but no generated
|
|
178
|
+
* source or key-list file is written to the repository.
|
|
179
|
+
*/
|
|
180
|
+
export async function createLegacyPiVirtualModulePlugin(): Promise<Bun.BunPlugin> {
|
|
181
|
+
const source = renderVirtualModule(await collectBundledPiEntries());
|
|
182
|
+
return {
|
|
183
|
+
name: "omp:legacy-pi-modules",
|
|
184
|
+
setup(build) {
|
|
185
|
+
build.onResolve({ filter: /^omp-legacy-pi-modules$/ }, () => ({
|
|
186
|
+
path: LEGACY_PI_MODULES_SPECIFIER,
|
|
187
|
+
namespace: VIRTUAL_NAMESPACE,
|
|
188
|
+
}));
|
|
189
|
+
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NAMESPACE }, () => ({ contents: source, loader: "ts" }));
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -1827,6 +1827,19 @@ describe("advisor", () => {
|
|
|
1827
1827
|
expect(text).toContain("read, grep, glob (default)");
|
|
1828
1828
|
});
|
|
1829
1829
|
|
|
1830
|
+
it("renders an explicit no-tools advisor distinctly from the omitted default", async () => {
|
|
1831
|
+
const uiTheme = await getThemeByName("dark");
|
|
1832
|
+
if (!uiTheme) throw new Error("theme unavailable");
|
|
1833
|
+
setThemeInstance(uiTheme);
|
|
1834
|
+
const overlay = make({
|
|
1835
|
+
advisors: [{ name: "Blank", tools: [] }],
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
const text = strip(overlay.render(200));
|
|
1839
|
+
expect(text.toLowerCase()).toContain("no tools");
|
|
1840
|
+
expect(text).not.toContain("read, grep, glob (default)");
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1830
1843
|
it("moves the preview with keyboard selection and preserves an explicit tool set", async () => {
|
|
1831
1844
|
const uiTheme = await getThemeByName("dark");
|
|
1832
1845
|
if (!uiTheme) throw new Error("theme unavailable");
|
|
@@ -56,6 +56,27 @@ describe("discoverAdvisorConfigs", () => {
|
|
|
56
56
|
expect(sharedInstructions).toBe("Shared baseline for all advisors.");
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it("distinguishes omitted tools, explicit no-tools, and invalid-only lists", async () => {
|
|
60
|
+
const yaml = [
|
|
61
|
+
"advisors:",
|
|
62
|
+
" - name: No Tools",
|
|
63
|
+
" tools: []",
|
|
64
|
+
" - name: Default Tools",
|
|
65
|
+
" - name: Invalid Only",
|
|
66
|
+
" tools: [reed]",
|
|
67
|
+
].join("\n");
|
|
68
|
+
await Bun.write(path.join(tmp, "WATCHDOG.yml"), yaml);
|
|
69
|
+
|
|
70
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, agentDir);
|
|
71
|
+
const noTools = advisors.find(a => a.name === "No Tools");
|
|
72
|
+
const defaultTools = advisors.find(a => a.name === "Default Tools");
|
|
73
|
+
const invalidOnly = advisors.find(a => a.name === "Invalid Only");
|
|
74
|
+
|
|
75
|
+
expect(noTools?.tools).toEqual([]);
|
|
76
|
+
expect(defaultTools?.tools).toBeUndefined();
|
|
77
|
+
expect(invalidOnly?.tools).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
59
80
|
it("ignores a malformed YAML file without throwing", async () => {
|
|
60
81
|
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: [unclosed bracket");
|
|
61
82
|
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
@@ -206,6 +227,21 @@ describe("WATCHDOG.yml file round-trip", () => {
|
|
|
206
227
|
expect(sharedInstructions).toContain("Shared baseline.");
|
|
207
228
|
});
|
|
208
229
|
|
|
230
|
+
it("round-trips an explicit empty tools list without collapsing it into the default", async () => {
|
|
231
|
+
const file = path.join(tmp, "WATCHDOG.yml");
|
|
232
|
+
const explicitNoToolsDoc: WatchdogConfigDoc = {
|
|
233
|
+
advisors: [{ name: "No Tools", tools: [] }, { name: "Default Tools" }],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
await saveWatchdogConfigFile(file, explicitNoToolsDoc);
|
|
237
|
+
const serializedDoc = await loadWatchdogConfigFile(file);
|
|
238
|
+
expect(serializedDoc).toEqual(explicitNoToolsDoc);
|
|
239
|
+
|
|
240
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, tmp);
|
|
241
|
+
expect(advisors.find(a => a.name === "No Tools")?.tools).toEqual([]);
|
|
242
|
+
expect(advisors.find(a => a.name === "Default Tools")?.tools).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
209
245
|
it("removes the file when the doc is empty so legacy discovery resumes", async () => {
|
|
210
246
|
const file = path.join(tmp, "WATCHDOG.yml");
|
|
211
247
|
await saveWatchdogConfigFile(file, doc);
|
package/src/advisor/config.ts
CHANGED
|
@@ -12,9 +12,10 @@ import { collectConfigCandidates } from "./watchdog";
|
|
|
12
12
|
* with an optional `:level` thinking suffix (e.g. `x-ai/grok-code-fast:high`),
|
|
13
13
|
* resolved exactly like any other model override; `tools` is a subset of
|
|
14
14
|
* `BUILTIN_TOOL_NAMES` — any built-in name, including mutating tools such as
|
|
15
|
-
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted
|
|
16
|
-
*
|
|
17
|
-
* advisor's specialization, appended to the shared
|
|
15
|
+
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted falls back to
|
|
16
|
+
* the default `read`/`grep`/`glob` subset; an explicit empty list grants no
|
|
17
|
+
* tools. `instructions` is the advisor's specialization, appended to the shared
|
|
18
|
+
* baseline.
|
|
18
19
|
*/
|
|
19
20
|
export interface AdvisorConfig {
|
|
20
21
|
name: string;
|
|
@@ -93,11 +94,13 @@ const KNOWN_TOOL_NAMES = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
|
93
94
|
/**
|
|
94
95
|
* Keep only valid tool names from an advisor's `tools` list, dropping unknowns
|
|
95
96
|
* with a warning. The advisor is a full agent, so any built tool may be granted;
|
|
96
|
-
* the runtime further filters to what's actually available this session.
|
|
97
|
-
*
|
|
97
|
+
* the runtime further filters to what's actually available this session.
|
|
98
|
+
* `undefined` means "use the default subset" (read/grep/glob); only an explicit
|
|
99
|
+
* raw empty list means "no tools".
|
|
98
100
|
*/
|
|
99
101
|
function filterAdvisorTools(tools: string[] | undefined, sourcePath: string): string[] | undefined {
|
|
100
|
-
if (
|
|
102
|
+
if (tools === undefined) return undefined;
|
|
103
|
+
if (tools.length === 0) return [];
|
|
101
104
|
// Normalize legacy aliases (search→grep, find→glob) and dedupe before validating.
|
|
102
105
|
const filtered = normalizeToolNames(tools).filter(name => {
|
|
103
106
|
if (KNOWN_TOOL_NAMES.has(name)) return true;
|
|
@@ -238,7 +241,7 @@ export async function loadWatchdogConfigFile(filePath: string): Promise<Watchdog
|
|
|
238
241
|
advisors: (result.advisors ?? []).map(a => ({
|
|
239
242
|
name: a.name,
|
|
240
243
|
model: a.model?.trim() || undefined,
|
|
241
|
-
tools: a.tools
|
|
244
|
+
tools: a.tools === undefined ? undefined : [...a.tools],
|
|
242
245
|
instructions: a.instructions?.trim() ? a.instructions : undefined,
|
|
243
246
|
})),
|
|
244
247
|
};
|
|
@@ -257,7 +260,7 @@ export function serializeWatchdogConfig(doc: WatchdogConfigDoc): string {
|
|
|
257
260
|
out.advisors = doc.advisors.map(a => {
|
|
258
261
|
const entry: AdvisorConfig = { name: a.name };
|
|
259
262
|
if (a.model?.trim()) entry.model = a.model;
|
|
260
|
-
if (a.tools
|
|
263
|
+
if (a.tools !== undefined) entry.tools = [...a.tools];
|
|
261
264
|
if (a.instructions?.trim()) entry.instructions = a.instructions;
|
|
262
265
|
return entry;
|
|
263
266
|
});
|
|
@@ -29,6 +29,7 @@ export interface CommitAgentInput {
|
|
|
29
29
|
requireChangelog: boolean;
|
|
30
30
|
diffText?: string;
|
|
31
31
|
existingChangelogEntries?: ExistingChangelogEntries[];
|
|
32
|
+
onComplete?: (state: CommitAgentState) => Promise<void> | void;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface ExistingChangelogEntries {
|
|
@@ -175,6 +176,9 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
if (input.onComplete) {
|
|
180
|
+
await input.onComplete(state);
|
|
181
|
+
}
|
|
178
182
|
return state;
|
|
179
183
|
} finally {
|
|
180
184
|
unsubscribe();
|