@oh-my-pi/pi-coding-agent 17.3.5 → 17.3.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.
- package/CHANGELOG.md +80 -0
- package/dist/{CHANGELOG-tt9k4jpr.md → CHANGELOG-vr9cckb4.md} +80 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +23 -4
- package/dist/types/extensibility/extensions/types.d.ts +58 -0
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/stats-cli.ts +6 -72
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commands/stats.ts +7 -4
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/runner.ts +118 -5
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/extensions/wrapper.ts +10 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
package/src/cli/stats-cli.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
|
|
8
|
-
import {
|
|
8
|
+
import { formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
10
10
|
import { openPath } from "../utils/open";
|
|
11
11
|
|
|
@@ -57,45 +57,11 @@ function shortenSessionFile(p: string): string {
|
|
|
57
57
|
|
|
58
58
|
export interface StatsCommandArgs {
|
|
59
59
|
port: number;
|
|
60
|
+
host: string;
|
|
60
61
|
json: boolean;
|
|
61
62
|
summary: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// =============================================================================
|
|
65
|
-
// Argument Parser
|
|
66
|
-
// =============================================================================
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Parse stats subcommand arguments.
|
|
70
|
-
* Returns undefined if not a stats command.
|
|
71
|
-
*/
|
|
72
|
-
export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
|
|
73
|
-
if (args.length === 0 || args[0] !== "stats") {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result: StatsCommandArgs = {
|
|
78
|
-
port: 3847,
|
|
79
|
-
json: false,
|
|
80
|
-
summary: false,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
for (let i = 1; i < args.length; i++) {
|
|
84
|
-
const arg = args[i];
|
|
85
|
-
if (arg === "--json" || arg === "-j") {
|
|
86
|
-
result.json = true;
|
|
87
|
-
} else if (arg === "--summary" || arg === "-s") {
|
|
88
|
-
result.summary = true;
|
|
89
|
-
} else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
|
|
90
|
-
result.port = parseInt(args[++i], 10);
|
|
91
|
-
} else if (arg.startsWith("--port=")) {
|
|
92
|
-
result.port = parseInt(arg.split("=")[1], 10);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
65
|
function formatCost(n: number): string {
|
|
100
66
|
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
101
67
|
if (n < 1) return `$${n.toFixed(3)}`;
|
|
@@ -112,9 +78,8 @@ function normalizePremiumRequests(n: number): number {
|
|
|
112
78
|
|
|
113
79
|
export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
|
|
114
80
|
// Lazy import to avoid loading stats module when not needed
|
|
115
|
-
const {
|
|
116
|
-
"@oh-my-pi/omp-stats"
|
|
117
|
-
);
|
|
81
|
+
const { closeDb, formatStatsDashboardUrl, getDashboardStats, getTotalMessageCount, startServer, syncAllSessions } =
|
|
82
|
+
await import("@oh-my-pi/omp-stats");
|
|
118
83
|
|
|
119
84
|
// Sync session files first
|
|
120
85
|
const progress = createSyncProgressReporter();
|
|
@@ -136,8 +101,8 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
|
|
|
136
101
|
}
|
|
137
102
|
|
|
138
103
|
// Start the dashboard server
|
|
139
|
-
const { hostname, port } = await startServer(cmd.port);
|
|
140
|
-
const url =
|
|
104
|
+
const { hostname, port } = await startServer(cmd.port, cmd.host);
|
|
105
|
+
const url = formatStatsDashboardUrl(hostname, port);
|
|
141
106
|
console.log(chalk.green(`Dashboard available at: ${url}`));
|
|
142
107
|
|
|
143
108
|
// Open browser
|
|
@@ -197,34 +162,3 @@ async function printStatsSummary(): Promise<void> {
|
|
|
197
162
|
|
|
198
163
|
console.log("");
|
|
199
164
|
}
|
|
200
|
-
|
|
201
|
-
// =============================================================================
|
|
202
|
-
// Help
|
|
203
|
-
// =============================================================================
|
|
204
|
-
|
|
205
|
-
export function printStatsHelp(): void {
|
|
206
|
-
console.log(`${chalk.bold(`${APP_NAME} stats`)} - AI Usage Statistics Dashboard
|
|
207
|
-
|
|
208
|
-
${chalk.bold("Usage:")}
|
|
209
|
-
${APP_NAME} stats [options]
|
|
210
|
-
|
|
211
|
-
${chalk.bold("Options:")}
|
|
212
|
-
-p, --port <port> Port for the dashboard server (default: 3847)
|
|
213
|
-
-j, --json Output stats as JSON and exit
|
|
214
|
-
-s, --summary Print summary to console and exit
|
|
215
|
-
-h, --help Show this help message
|
|
216
|
-
|
|
217
|
-
${chalk.bold("Examples:")}
|
|
218
|
-
${APP_NAME} stats # Start dashboard server
|
|
219
|
-
${APP_NAME} stats --json # Print stats as JSON
|
|
220
|
-
${APP_NAME} stats --summary # Print summary to console
|
|
221
|
-
${APP_NAME} stats --port 8080 # Start on custom port
|
|
222
|
-
|
|
223
|
-
${chalk.bold("Metrics:")}
|
|
224
|
-
- Total requests and error rate
|
|
225
|
-
- Token usage (input, output, cache)
|
|
226
|
-
- Cost breakdown
|
|
227
|
-
- Average duration and time to first token (TTFT)
|
|
228
|
-
- Tokens per second throughput
|
|
229
|
-
`);
|
|
230
|
-
}
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -15,7 +15,12 @@ import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
|
15
15
|
import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
|
|
16
16
|
import { $ } from "bun";
|
|
17
17
|
import { theme } from "../modes/theme/theme";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isTimeoutError,
|
|
20
|
+
isUnsupportedProxyError,
|
|
21
|
+
unsupportedProxyMessage,
|
|
22
|
+
withTimeoutSignal,
|
|
23
|
+
} from "../utils/fetch-timeout";
|
|
19
24
|
|
|
20
25
|
const REPO = "can1357/oh-my-pi";
|
|
21
26
|
const PACKAGE = "@oh-my-pi/pi-coding-agent";
|
|
@@ -248,6 +253,7 @@ async function getReleaseBinaryAsset(
|
|
|
248
253
|
if (isTimeoutError(err)) {
|
|
249
254
|
throw new Error("Timed out fetching GitHub release metadata after 30s", { cause: err });
|
|
250
255
|
}
|
|
256
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
251
257
|
throw err;
|
|
252
258
|
}
|
|
253
259
|
if ((response.status === 403 && !githubToken) || response.status === 429) {
|
|
@@ -287,6 +293,7 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
|
|
|
287
293
|
if (isTimeoutError(err)) {
|
|
288
294
|
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
289
295
|
}
|
|
296
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
290
297
|
throw err;
|
|
291
298
|
}
|
|
292
299
|
if (!response.ok || !response.body) {
|
|
@@ -326,6 +333,7 @@ export async function downloadVerifiedBinary(options: VerifiedBinaryDownloadOpti
|
|
|
326
333
|
if (isTimeoutError(err)) {
|
|
327
334
|
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
328
335
|
}
|
|
336
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
329
337
|
throw err;
|
|
330
338
|
}
|
|
331
339
|
}
|
|
@@ -441,6 +449,14 @@ function tryRealpath(p: string): string | undefined {
|
|
|
441
449
|
}
|
|
442
450
|
}
|
|
443
451
|
|
|
452
|
+
function isSymlinkPath(p: string): boolean {
|
|
453
|
+
try {
|
|
454
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
444
460
|
function isPathInDirectoryLexical(filePath: string, directoryPath: string): boolean {
|
|
445
461
|
const normalizedPath = normalizePathForComparison(path.resolve(filePath));
|
|
446
462
|
const normalizedDirectory = normalizePathForComparison(path.resolve(directoryPath));
|
|
@@ -509,6 +525,14 @@ interface UpdateMethodResolutionOptions {
|
|
|
509
525
|
* preserves a global package symlink instead of resolving into its checkout.
|
|
510
526
|
*/
|
|
511
527
|
ompLinkTarget?: string;
|
|
528
|
+
/**
|
|
529
|
+
* Whether package-manager routing (bun/npm) is permitted. Binary-only
|
|
530
|
+
* releases pass `false`: a manager launcher then resolves to `"binary"` and
|
|
531
|
+
* is taken over in place rather than reinstalled through its manager. Defaults
|
|
532
|
+
* to `true` in {@link resolveUpdateMethod} so callers that only classify need
|
|
533
|
+
* not set it.
|
|
534
|
+
*/
|
|
535
|
+
allowPackageManagers?: boolean;
|
|
512
536
|
}
|
|
513
537
|
|
|
514
538
|
type UpdateTarget =
|
|
@@ -525,6 +549,7 @@ function resolveUpdateMethod(
|
|
|
525
549
|
options: UpdateMethodResolutionOptions = {},
|
|
526
550
|
): UpdateMethod {
|
|
527
551
|
const {
|
|
552
|
+
allowPackageManagers = true,
|
|
528
553
|
bunGlobalDir,
|
|
529
554
|
homebrewPrefix,
|
|
530
555
|
miseBinDirs = [],
|
|
@@ -555,6 +580,7 @@ function resolveUpdateMethod(
|
|
|
555
580
|
globalBinDir: bunBinDir,
|
|
556
581
|
});
|
|
557
582
|
if (
|
|
583
|
+
allowPackageManagers &&
|
|
558
584
|
bunBinDir &&
|
|
559
585
|
isPathInDirectory(ompPath, bunBinDir) &&
|
|
560
586
|
!isStandaloneRegularFile &&
|
|
@@ -564,6 +590,7 @@ function resolveUpdateMethod(
|
|
|
564
590
|
}
|
|
565
591
|
const npmNodeModulesDir = resolveNpmGlobalNodeModulesDir(npmBinDir);
|
|
566
592
|
if (
|
|
593
|
+
allowPackageManagers &&
|
|
567
594
|
npmBinDir &&
|
|
568
595
|
isPathInDirectory(ompPath, npmBinDir) &&
|
|
569
596
|
!isStandaloneRegularFile &&
|
|
@@ -611,10 +638,25 @@ export function resolveUpdateTargetFromPath(
|
|
|
611
638
|
ompLinkTarget,
|
|
612
639
|
});
|
|
613
640
|
if (method === "binary") {
|
|
614
|
-
// A
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
641
|
+
// A symlinked launcher created by bun/npm is taken over in place on a
|
|
642
|
+
// binary-only release: routing through the manager is impossible, so the
|
|
643
|
+
// standalone binary replaces the launcher and keeps the PATH entry live.
|
|
644
|
+
// Every other symlink — a foreign alias, or an admin symlink into a
|
|
645
|
+
// shared install — is self-healing: update the real binary it resolves
|
|
646
|
+
// to and leave the launcher untouched, in every distribution channel.
|
|
647
|
+
// The old channel gate clobbered these foreign launchers on binary-only
|
|
648
|
+
// releases (EACCES on a root-owned link dir, or a stale split-brain copy
|
|
649
|
+
// of the binary shadowing the shared install).
|
|
650
|
+
const managerLauncher =
|
|
651
|
+
ompIsSymlink &&
|
|
652
|
+
!options.allowPackageManagers &&
|
|
653
|
+
resolveUpdateMethod(ompPath, bunBinDir, {
|
|
654
|
+
...options,
|
|
655
|
+
allowPackageManagers: true,
|
|
656
|
+
ompIsRegularFile,
|
|
657
|
+
ompLinkTarget,
|
|
658
|
+
}) !== "binary";
|
|
659
|
+
const binaryPath = ompIsSymlink && !managerLauncher ? (ompRealpath ?? ompPath) : ompPath;
|
|
618
660
|
return { method, path: binaryPath, replacesSymlink: ompIsSymlink && binaryPath === ompPath };
|
|
619
661
|
}
|
|
620
662
|
if (method === "bun" || method === "npm") return { method, path: ompPath };
|
|
@@ -623,25 +665,34 @@ export function resolveUpdateTargetFromPath(
|
|
|
623
665
|
/**
|
|
624
666
|
* Resolve how the running install should be updated.
|
|
625
667
|
*
|
|
626
|
-
* `allowPackageManagers: false`
|
|
627
|
-
*
|
|
628
|
-
*
|
|
668
|
+
* `allowPackageManagers: false` disables bun/npm routing — used for
|
|
669
|
+
* binary-only releases, where reinstalling through a package manager is never
|
|
670
|
+
* valid. The `bun pm bin -g` / `npm prefix -g` probes are then skipped unless
|
|
671
|
+
* the launcher is a symlink, whose bin dirs distinguish a manager launcher
|
|
672
|
+
* (taken over in place) from a foreign symlink (resolved to its real binary).
|
|
629
673
|
* Homebrew/mise detection always runs: both managers install GitHub release
|
|
630
674
|
* binaries and stay valid regardless of how the release is distributed.
|
|
631
675
|
*/
|
|
632
676
|
async function resolveUpdateTarget(options: { allowPackageManagers: boolean }): Promise<UpdateTarget> {
|
|
633
|
-
const bunBinDir = options.allowPackageManagers ? await getBunGlobalBinDir() : undefined;
|
|
634
|
-
const npmBinDir = options.allowPackageManagers ? await getNpmGlobalBinDir() : undefined;
|
|
635
677
|
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
|
636
678
|
const miseAvailable = $which("mise") !== undefined;
|
|
637
679
|
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
|
638
680
|
const miseDataDir = miseAvailable ? getMiseDataDir() : undefined;
|
|
639
681
|
const ompPath = resolveOmpPath();
|
|
640
682
|
|
|
683
|
+
// Binary-only releases skip package-manager routing, but a symlinked
|
|
684
|
+
// launcher still needs the manager bin dirs to tell a bun/npm launcher
|
|
685
|
+
// (taken over in place) from a foreign symlink (resolved to its real
|
|
686
|
+
// binary). A plain-file install never needs the distinction, so the common
|
|
687
|
+
// case stays probe-free.
|
|
688
|
+
const probeManagers = options.allowPackageManagers || (ompPath !== undefined && isSymlinkPath(ompPath));
|
|
689
|
+
const bunBinDir = probeManagers ? await getBunGlobalBinDir() : undefined;
|
|
690
|
+
const npmBinDir = probeManagers ? await getNpmGlobalBinDir() : undefined;
|
|
691
|
+
|
|
641
692
|
if (ompPath) {
|
|
642
693
|
return resolveUpdateTargetFromPath(ompPath, bunBinDir, {
|
|
643
694
|
allowPackageManagers: options.allowPackageManagers,
|
|
644
|
-
bunGlobalDir:
|
|
695
|
+
bunGlobalDir: probeManagers ? process.env.BUN_INSTALL_GLOBAL_DIR : undefined,
|
|
645
696
|
homebrewPrefix,
|
|
646
697
|
miseBinDirs,
|
|
647
698
|
miseDataDir,
|
|
@@ -672,6 +723,7 @@ async function fetchLatestManifest(
|
|
|
672
723
|
cause: err,
|
|
673
724
|
});
|
|
674
725
|
}
|
|
726
|
+
if (isUnsupportedProxyError(err)) throw new Error(unsupportedProxyMessage(), { cause: err });
|
|
675
727
|
throw err;
|
|
676
728
|
}
|
|
677
729
|
if (!response.ok) {
|
package/src/cli-commands.ts
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { CommandEntry } from "@oh-my-pi/pi-utils/cli";
|
|
12
12
|
import * as commandHelp from "./cli/command-help";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS,
|
|
15
|
+
flagConsumesValue,
|
|
16
|
+
OPTIONAL_VALUE_FLAGS,
|
|
17
|
+
STRING_VALUE_FLAGS,
|
|
18
|
+
VALUELESS_FLAGS,
|
|
19
|
+
} from "./cli/flag-tables";
|
|
14
20
|
import { launchHelp } from "./commands/launch-help";
|
|
15
21
|
|
|
16
22
|
export const commands: CommandEntry[] = [
|
|
@@ -285,12 +291,54 @@ function leadingSubcommandIndex(argv: string[]): number {
|
|
|
285
291
|
return -1;
|
|
286
292
|
}
|
|
287
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Subcommands that share the launch flag surface, so leading global flags
|
|
296
|
+
* (`--cwd`, `--model`, `--approval-mode`, …) placed before them are meaningful
|
|
297
|
+
* and must be forwarded ({@link resolveCliArgv}, #2970). Every other subcommand
|
|
298
|
+
* parses only its own flags.
|
|
299
|
+
*/
|
|
300
|
+
export const LAUNCH_FLAG_COMMANDS: Record<string, true> = { launch: true, acp: true };
|
|
301
|
+
|
|
302
|
+
/** Whether `arg` names a flag from the launch surface (bare or `--flag=value`). */
|
|
303
|
+
function isLaunchGlobalFlag(arg: string): boolean {
|
|
304
|
+
const eq = arg.indexOf("=");
|
|
305
|
+
const name = arg.startsWith("--") && eq !== -1 ? arg.slice(0, eq) : arg;
|
|
306
|
+
return (
|
|
307
|
+
STRING_VALUE_FLAGS.has(name) ||
|
|
308
|
+
OPTIONAL_VALUE_FLAGS.has(name) ||
|
|
309
|
+
VALUELESS_FLAGS.has(name) ||
|
|
310
|
+
EXTENSION_SHADOWABLE_STRING_FLAGS.has(name)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Drop recognized launch-global flags (and any value they consume) from the
|
|
316
|
+
* leading segment before a hoisted non-launch subcommand. `--cwd` and friends
|
|
317
|
+
* belong to the launch surface and mean nothing to a subcommand like `update`,
|
|
318
|
+
* whose strict parser would otherwise reject them with a cryptic
|
|
319
|
+
* `node:util.parseArgs` error (#8891). Tokens the launch tables don't recognize
|
|
320
|
+
* are kept, so a subcommand's own leading flags still reach it.
|
|
321
|
+
*/
|
|
322
|
+
function stripLaunchGlobalFlags(leading: readonly string[]): string[] {
|
|
323
|
+
const kept: string[] = [];
|
|
324
|
+
for (let index = 0; index < leading.length; index += 1) {
|
|
325
|
+
const arg = leading[index];
|
|
326
|
+
if (isLaunchGlobalFlag(arg)) {
|
|
327
|
+
if (flagConsumesValue(arg, leading[index + 1])) index += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
kept.push(arg);
|
|
331
|
+
}
|
|
332
|
+
return kept;
|
|
333
|
+
}
|
|
334
|
+
|
|
288
335
|
/**
|
|
289
336
|
* Decide what the CLI runner should do with raw argv: reject bare reserved
|
|
290
337
|
* management words, pass help/version through untouched, route a recognized
|
|
291
338
|
* subcommand (even behind leading global flags like `--approval-mode=yolo`) to
|
|
292
|
-
* that command
|
|
293
|
-
*
|
|
339
|
+
* that command, and forward everything else to `launch` (#2970). Leading
|
|
340
|
+
* launch-global flags are forwarded to launch-shaped commands but stripped for
|
|
341
|
+
* other subcommands that cannot parse them (#8891).
|
|
294
342
|
*/
|
|
295
343
|
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
296
344
|
const first = argv[0];
|
|
@@ -302,12 +350,18 @@ export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
|
302
350
|
if (isSubcommand(first)) return { argv };
|
|
303
351
|
// A subcommand can hide behind leading global option flags
|
|
304
352
|
// (`omp --approval-mode=yolo acp`). `run` dispatches strictly on argv[0], so
|
|
305
|
-
// hoist the subcommand to the front
|
|
306
|
-
//
|
|
307
|
-
//
|
|
353
|
+
// hoist the subcommand to the front. Launch-shaped commands share the launch
|
|
354
|
+
// flag surface, so their leading flags are forwarded and applied; every other
|
|
355
|
+
// subcommand parses only its own flags, so launch-global flags placed before
|
|
356
|
+
// it (`omp --cwd <dir> update`) are stripped rather than forwarded into a
|
|
357
|
+
// crash (#8891). Genuine launch prompts (no trailing subcommand) are untouched.
|
|
308
358
|
const subIndex = leadingSubcommandIndex(argv);
|
|
309
359
|
if (subIndex >= 0) {
|
|
310
|
-
|
|
360
|
+
const sub = argv[subIndex];
|
|
361
|
+
const leading = argv.slice(0, subIndex);
|
|
362
|
+
const trailing = argv.slice(subIndex + 1);
|
|
363
|
+
const forwardedLeading = LAUNCH_FLAG_COMMANDS[sub] === true ? leading : stripLaunchGlobalFlags(leading);
|
|
364
|
+
return { argv: [sub, ...forwardedLeading, ...trailing] };
|
|
311
365
|
}
|
|
312
366
|
return { argv: ["launch", ...argv] };
|
|
313
367
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { APP_NAME, postmortem, VERSION } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { Args, type CliConfig, Command, type CommandCtor } from "@oh-my-pi/pi-utils/cli";
|
|
10
10
|
import { completionsHelp as commandHelp } from "../cli/command-help";
|
|
11
11
|
import { buildSpec, generateCompletion, type Shell } from "../cli/completion-gen";
|
|
@@ -55,6 +55,7 @@ export default class Completions extends Command {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
await Bun.write(Bun.stdout, await generateLiveCompletion(shell));
|
|
58
|
+
await postmortem.quit(0);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
package/src/commands/stats.ts
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
6
6
|
import { statsHelp as commandHelp } from "../cli/command-help";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
7
|
+
import type { StatsCommandArgs } from "../cli/stats-cli";
|
|
8
|
+
import * as statsCli from "../cli/stats-cli";
|
|
9
|
+
import * as theme from "../modes/theme/theme";
|
|
9
10
|
|
|
10
11
|
export default class Stats extends Command {
|
|
11
12
|
static description = commandHelp.description;
|
|
12
13
|
static flags = {
|
|
13
14
|
port: Flags.integer({ char: "p", description: "Port for the dashboard server", default: 3847 }),
|
|
15
|
+
host: Flags.string({ description: "Host to bind", default: "127.0.0.1" }),
|
|
14
16
|
json: Flags.boolean({ char: "j", description: "Output stats as JSON", default: false }),
|
|
15
17
|
summary: Flags.boolean({ char: "s", description: "Print summary to console", default: false }),
|
|
16
18
|
};
|
|
@@ -20,11 +22,12 @@ export default class Stats extends Command {
|
|
|
20
22
|
|
|
21
23
|
const cmd: StatsCommandArgs = {
|
|
22
24
|
port: flags.port,
|
|
25
|
+
host: flags.host ?? "127.0.0.1",
|
|
23
26
|
json: flags.json,
|
|
24
27
|
summary: flags.summary,
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
await initTheme();
|
|
28
|
-
await runStatsCommand(cmd);
|
|
30
|
+
await theme.initTheme();
|
|
31
|
+
await statsCli.runStatsCommand(cmd);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -11,7 +11,7 @@ import { ModelRegistry } from "../../config/model-registry";
|
|
|
11
11
|
import { Settings } from "../../config/settings";
|
|
12
12
|
import { discoverAuthStorage, discoverContextFiles, loadCliExtensionProviders } from "../../sdk";
|
|
13
13
|
import * as git from "../../utils/git";
|
|
14
|
-
import { abortOnGitFailure, pushOrAbort } from "../execute";
|
|
14
|
+
import { abortOnGitFailure, CommitAbortedError, pushOrAbort } from "../execute";
|
|
15
15
|
import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
|
|
16
16
|
import { generateFallbackProposal } from "./fallback";
|
|
17
17
|
import { assignLockFilesToPlan } from "./lock-files";
|
|
@@ -303,7 +303,20 @@ async function runSplitCommit(
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
process.stdout.write("● Creating split commits...\n");
|
|
306
|
-
|
|
306
|
+
let stagedDiff: string;
|
|
307
|
+
try {
|
|
308
|
+
stagedDiff = await git.diff(ctx.cwd, { cached: true, binary: true, requireComplete: true });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof git.GitOutputTruncatedError) {
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`✗ Cannot create split commits: ${error.message}\n` +
|
|
313
|
+
" A large or binary file makes the staged diff too big to slice safely.\n" +
|
|
314
|
+
" Commit the large file(s) on their own, then re-run for the rest.\n",
|
|
315
|
+
);
|
|
316
|
+
throw new CommitAbortedError();
|
|
317
|
+
}
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
307
320
|
await git.stage.reset(ctx.cwd);
|
|
308
321
|
for (const [position, commitIndex] of order.entries()) {
|
|
309
322
|
const commit = plan.commits[commitIndex];
|
package/src/commit/git/diff.ts
CHANGED
|
@@ -21,9 +21,13 @@ export function parseNumstat(output: string): NumstatEntry[] {
|
|
|
21
21
|
|
|
22
22
|
export function parseFileDiffs(diff: string): FileDiff[] {
|
|
23
23
|
const sections: FileDiff[] = [];
|
|
24
|
-
|
|
24
|
+
// Split on a line-start lookahead so each block keeps its terminating
|
|
25
|
+
// newline(s) verbatim. Consuming the delimiter would drop the blank line
|
|
26
|
+
// that terminates a `GIT binary patch` block, corrupting binary diffs on
|
|
27
|
+
// rebuild (issue #8899).
|
|
28
|
+
const parts = diff.split(/^(?=diff --git )/m);
|
|
25
29
|
for (let index = 0; index < parts.length; index += 1) {
|
|
26
|
-
const part =
|
|
30
|
+
const part = parts[index];
|
|
27
31
|
if (!part.trim()) continue;
|
|
28
32
|
const lines = part.split("\n");
|
|
29
33
|
const header = lines[0] ?? "";
|
|
@@ -22,7 +22,7 @@ import { modelMatchesHost } from "@oh-my-pi/pi-catalog/hosts";
|
|
|
22
22
|
import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
|
|
23
23
|
import { stripThinkingVariantToken } from "@oh-my-pi/pi-catalog/identity/family";
|
|
24
24
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
25
|
-
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
25
|
+
import { type GeneratedProvider, getBundledModels, modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
26
26
|
import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models";
|
|
27
27
|
import { resolveBareVariantAlias, resolveVariantAlias } from "@oh-my-pi/pi-catalog/variant-collapse";
|
|
28
28
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
@@ -595,6 +595,41 @@ function includeSyntheticAllowedModels(available: Model<Api>[], allowedModels: I
|
|
|
595
595
|
return result;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Provider-lock a raw-id cross match.
|
|
600
|
+
*
|
|
601
|
+
* A slash-prefixed selector like `anthropic/claude-opus-5` is ambiguous: it is
|
|
602
|
+
* both the `anthropic` provider's canonical selector and — verbatim — an
|
|
603
|
+
* OpenRouter aggregator model id. When the named provider genuinely carries
|
|
604
|
+
* that model id in the bundled catalog but the model is missing from the
|
|
605
|
+
* candidate set (provider disabled, no credentials, or filtered out), letting
|
|
606
|
+
* the raw-id fallback re-bind the request onto a different provider's
|
|
607
|
+
* same-named model is a silent, expensive surprise — typically the aggregator's
|
|
608
|
+
* copy (OpenRouter bills published per-token Claude prices). Such a reference
|
|
609
|
+
* is provider-locked: it must fail rather than shadow.
|
|
610
|
+
*
|
|
611
|
+
* The lock only applies when the named provider carries the exact id in the
|
|
612
|
+
* bundled catalog. An aggregator raw id the named provider does NOT carry
|
|
613
|
+
* (e.g. `openai/gpt-4o:extended` — `openai` bundles `gpt-4o`, not the
|
|
614
|
+
* `:extended` variant) is legitimately an aggregator id and keeps resolving
|
|
615
|
+
* through the raw-id fallback, so bare aggregator selectors keep working.
|
|
616
|
+
*/
|
|
617
|
+
function isProviderLockedCrossMatch(pattern: string, matchedModel: Model<Api>): boolean {
|
|
618
|
+
const slashIdx = pattern.indexOf("/");
|
|
619
|
+
if (slashIdx <= 0) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const provider = pattern.slice(0, slashIdx).toLowerCase();
|
|
623
|
+
const modelId = pattern.slice(slashIdx + 1).toLowerCase();
|
|
624
|
+
if (matchedModel.provider.toLowerCase() === provider) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
// Case-insensitive on both halves: the surrounding matcher lowercases the
|
|
628
|
+
// selector before comparing ids, so the lock must not evaporate on case
|
|
629
|
+
// variance (catalog provider keys are lowercase; model ids may not be).
|
|
630
|
+
return getBundledModels(provider as GeneratedProvider).some(m => m.id.toLowerCase() === modelId);
|
|
631
|
+
}
|
|
632
|
+
|
|
598
633
|
/**
|
|
599
634
|
* Find an exact explicit provider/model match.
|
|
600
635
|
*/
|
|
@@ -644,11 +679,17 @@ function matchModel(
|
|
|
644
679
|
// Exact ID match (case-insensitive) — this must happen before provider-scoped
|
|
645
680
|
// fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
|
|
646
681
|
// IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
|
|
647
|
-
// misread as a provider-qualified selector.
|
|
682
|
+
// misread as a provider-qualified selector. A provider-qualified pattern
|
|
683
|
+
// whose named provider carries the id stays locked to that provider when its
|
|
684
|
+
// only exact-id matches live on a different provider (isProviderLockedCrossMatch).
|
|
648
685
|
const lowerPattern = modelPattern.toLowerCase();
|
|
649
686
|
const exactMatches = availableModels.filter(m => m.id.toLowerCase() === lowerPattern);
|
|
650
687
|
if (exactMatches.length > 0) {
|
|
651
|
-
|
|
688
|
+
const unlockedMatches = exactMatches.filter(m => !isProviderLockedCrossMatch(modelPattern, m));
|
|
689
|
+
if (unlockedMatches.length > 0) {
|
|
690
|
+
return pickPreferredModel(unlockedMatches, context);
|
|
691
|
+
}
|
|
692
|
+
return undefined;
|
|
652
693
|
}
|
|
653
694
|
|
|
654
695
|
const bedrockInferenceProfile = resolveBedrockInferenceProfileModelId(modelPattern, availableModels);
|
|
@@ -1710,11 +1751,14 @@ function findExactCliModel(
|
|
|
1710
1751
|
// Flat-id (or full-selector-string) matches prefer authenticated providers,
|
|
1711
1752
|
// then fall back to catalog order. This covers aggregator-style flat ids
|
|
1712
1753
|
// that merely look provider-qualified (e.g. "openai/gpt-oss-120b" hosted on
|
|
1713
|
-
// OpenRouter), where the provider/id decomposition above found nothing.
|
|
1754
|
+
// OpenRouter), where the provider/id decomposition above found nothing. A
|
|
1755
|
+
// provider-qualified selector whose named provider carries the id must not
|
|
1756
|
+
// re-bind onto another provider's same-named flat id
|
|
1757
|
+
// (isProviderLockedCrossMatch); it stays provider-locked and fails instead.
|
|
1714
1758
|
const lower = selector.toLowerCase();
|
|
1715
1759
|
const isFlatMatch = (model: Model<Api>) =>
|
|
1716
1760
|
model.id.toLowerCase() === lower || formatModelString(model).toLowerCase() === lower;
|
|
1717
|
-
const preferred = availableModels.find(isFlatMatch);
|
|
1761
|
+
const preferred = availableModels.find(m => isFlatMatch(m) && !isProviderLockedCrossMatch(selector, m));
|
|
1718
1762
|
if (preferred) return preferred;
|
|
1719
1763
|
// The unauthenticated catalog fallback is a weak match: a bare id like
|
|
1720
1764
|
// `default` collides with the bundled `cursor/default` model, which must not
|
|
@@ -1723,7 +1767,9 @@ function findExactCliModel(
|
|
|
1723
1767
|
// role gets a chance first; the deferred fuzzy fallback below still recovers
|
|
1724
1768
|
// the catalog id when no role matches.
|
|
1725
1769
|
if (options?.catalogFallback === false) return undefined;
|
|
1726
|
-
return availableModels === allModels
|
|
1770
|
+
return availableModels === allModels
|
|
1771
|
+
? undefined
|
|
1772
|
+
: allModels.find(m => isFlatMatch(m) && !isProviderLockedCrossMatch(selector, m));
|
|
1727
1773
|
}
|
|
1728
1774
|
|
|
1729
1775
|
export interface ResolveCliModelResult {
|
|
@@ -66,12 +66,12 @@ export function validateProviderConfiguration(
|
|
|
66
66
|
const requiresAuth =
|
|
67
67
|
mode === "runtime-register"
|
|
68
68
|
? !config.apiKey && !config.oauthConfigured
|
|
69
|
-
: !config.apiKey && (config.auth ?? "apiKey") !== "none";
|
|
69
|
+
: !config.apiKey && (config.auth ?? "apiKey") !== "none" && (config.auth ?? "apiKey") !== "oauth";
|
|
70
70
|
if (requiresAuth) {
|
|
71
71
|
throw new Error(
|
|
72
72
|
mode === "runtime-register"
|
|
73
73
|
? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
|
|
74
|
-
: `Provider ${providerName}: "apiKey" is required when defining custom models unless auth is "none".`,
|
|
74
|
+
: `Provider ${providerName}: "apiKey" is required when defining custom models unless auth is "none" or "oauth".`,
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -5254,6 +5254,39 @@ export const SETTINGS_SCHEMA = {
|
|
|
5254
5254
|
},
|
|
5255
5255
|
},
|
|
5256
5256
|
|
|
5257
|
+
"providers.cacheRetention": {
|
|
5258
|
+
type: "enum",
|
|
5259
|
+
values: ["auto", "short", "long", "none"] as const,
|
|
5260
|
+
default: "auto",
|
|
5261
|
+
ui: {
|
|
5262
|
+
tab: "providers",
|
|
5263
|
+
group: "Protocol",
|
|
5264
|
+
label: "Prompt Cache Retention",
|
|
5265
|
+
description:
|
|
5266
|
+
"Prompt-cache retention forwarded to providers that support it (Anthropic, Bedrock, OpenRouter, OpenAI)",
|
|
5267
|
+
options: [
|
|
5268
|
+
{
|
|
5269
|
+
value: "auto",
|
|
5270
|
+
label: "Auto",
|
|
5271
|
+
description:
|
|
5272
|
+
"Provider default — Anthropic uses 5m entries kept warm by idle keep-alive refreshes; PI_CACHE_RETENTION still applies",
|
|
5273
|
+
},
|
|
5274
|
+
{
|
|
5275
|
+
value: "short",
|
|
5276
|
+
label: "Short (5m)",
|
|
5277
|
+
description:
|
|
5278
|
+
"Cheapest cache writes; Anthropic keeps the entry warm with bounded keep-alive refreshes while idle",
|
|
5279
|
+
},
|
|
5280
|
+
{
|
|
5281
|
+
value: "long",
|
|
5282
|
+
label: "Long (1h)",
|
|
5283
|
+
description: "1h TTL where the provider supports it; pricier writes, no keep-alive refresh requests",
|
|
5284
|
+
},
|
|
5285
|
+
{ value: "none", label: "Off", description: "Disable prompt caching and cache-affinity routing" },
|
|
5286
|
+
],
|
|
5287
|
+
},
|
|
5288
|
+
},
|
|
5289
|
+
|
|
5257
5290
|
"providers.streamFirstEventTimeoutSeconds": {
|
|
5258
5291
|
type: "number",
|
|
5259
5292
|
default: -1,
|