@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.1
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 +26 -0
- package/dist/cli.js +3336 -3318
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +830 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +288 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/chat-transcript-builder.ts +22 -1
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/selector-controller.ts +15 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +33 -3
- package/src/session/agent-session.ts +239 -17
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
|
@@ -214,8 +214,15 @@ function extractAccount(access: {
|
|
|
214
214
|
accountId?: string;
|
|
215
215
|
projectId?: string;
|
|
216
216
|
enterpriseUrl?: string;
|
|
217
|
+
orgId?: string;
|
|
218
|
+
orgName?: string;
|
|
217
219
|
}): string {
|
|
218
|
-
|
|
220
|
+
const base =
|
|
221
|
+
access.email ?? access.accountId ?? access.projectId ?? access.enterpriseUrl ?? "(unknown oauth account)";
|
|
222
|
+
// Two subscriptions (orgs) can share one email — name the org so per-account
|
|
223
|
+
// bench rows stay tellable apart.
|
|
224
|
+
const org = access.orgName ?? access.orgId;
|
|
225
|
+
return org ? `${base} (${org})` : base;
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
function getBenchTargetKey(access: {
|
|
@@ -225,15 +232,18 @@ function getBenchTargetKey(access: {
|
|
|
225
232
|
projectId?: string;
|
|
226
233
|
enterpriseUrl?: string;
|
|
227
234
|
accessToken?: string;
|
|
235
|
+
orgId?: string;
|
|
228
236
|
}): string {
|
|
229
|
-
|
|
237
|
+
const base =
|
|
230
238
|
access.email ??
|
|
231
239
|
access.accountId ??
|
|
232
240
|
access.projectId ??
|
|
233
241
|
access.enterpriseUrl ??
|
|
234
242
|
(access.credentialId === undefined ? access.accessToken : `credential:${access.credentialId}`) ??
|
|
235
|
-
"(unknown oauth account)"
|
|
236
|
-
|
|
243
|
+
"(unknown oauth account)";
|
|
244
|
+
// Org-qualify: two org-scoped credentials under one email are two distinct
|
|
245
|
+
// benchmark targets, not duplicates.
|
|
246
|
+
return access.orgId ? `${base}|org:${access.orgId}` : base;
|
|
237
247
|
}
|
|
238
248
|
|
|
239
249
|
function sanitizeBenchText(text: string, width: number): string {
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
34
34
|
import type { Args } from "./args";
|
|
35
|
+
import { CliUsageError } from "./usage-error";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Runtime dependencies injected into setters that need to validate input or
|
|
@@ -85,6 +86,24 @@ const setResume: OptionalSetter = (result, value) => {
|
|
|
85
86
|
result.resume = value !== undefined ? value : true;
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
const MAX_TIME_DURATION_RE = /^(\d+(?:\.\d+)?)([smh])$/;
|
|
90
|
+
|
|
91
|
+
function maxTimeMultiplier(unit: string | undefined): number {
|
|
92
|
+
if (unit === "h") return 3600;
|
|
93
|
+
if (unit === "m") return 60;
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseMaxTimeSeconds(value: string): number {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
const duration = MAX_TIME_DURATION_RE.exec(trimmed);
|
|
100
|
+
const seconds = duration ? Number(duration[1]) * maxTimeMultiplier(duration[2]) : Number(trimmed);
|
|
101
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
102
|
+
throw new CliUsageError(
|
|
103
|
+
`Invalid --max-time value: ${JSON.stringify(value)}. Expected a positive number of seconds or duration like "5s", "10m", "1h".`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
/**
|
|
89
108
|
* Setters for flags with string values. Most built-ins consume the next argv
|
|
90
109
|
* token even when it starts with `-`; flags listed in
|
|
@@ -127,13 +146,8 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
127
146
|
"--plan-yolo-into": (result, value) => {
|
|
128
147
|
result.planYoloInto = value;
|
|
129
148
|
},
|
|
130
|
-
"--max-time": (result, value
|
|
131
|
-
|
|
132
|
-
if (Number.isFinite(seconds) && seconds > 0) {
|
|
133
|
-
result.maxTime = seconds;
|
|
134
|
-
} else {
|
|
135
|
-
deps.logger.warn("Invalid seconds passed to --max-time", { value });
|
|
136
|
-
}
|
|
149
|
+
"--max-time": (result, value) => {
|
|
150
|
+
result.maxTime = parseMaxTimeSeconds(value);
|
|
137
151
|
},
|
|
138
152
|
"--api-key": (result, value) => {
|
|
139
153
|
result.apiKey = value;
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -109,6 +109,19 @@ async function getBunGlobalBinDir(): Promise<string | undefined> {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async function getNpmGlobalBinDir(): Promise<string | undefined> {
|
|
113
|
+
if (!$which("npm")) return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const result = await $`npm prefix -g`.quiet().nothrow();
|
|
116
|
+
if (result.exitCode !== 0) return undefined;
|
|
117
|
+
const prefix = result.text().trim();
|
|
118
|
+
if (prefix.length === 0) return undefined;
|
|
119
|
+
return process.platform === "win32" ? prefix : path.join(prefix, "bin");
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
async function getHomebrewFormulaPrefix(): Promise<string | undefined> {
|
|
113
126
|
if (!$which("brew")) return undefined;
|
|
114
127
|
for (const formula of [HOMEBREW_FORMULA, APP_NAME]) {
|
|
@@ -189,26 +202,36 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
189
202
|
return isPathInDirectoryLexical(resolvedFile, dirReal);
|
|
190
203
|
}
|
|
191
204
|
|
|
192
|
-
type UpdateMethod = "brew" | "mise" | "bun" | "binary";
|
|
205
|
+
type UpdateMethod = "brew" | "mise" | "bun" | "npm" | "binary";
|
|
193
206
|
|
|
194
207
|
interface UpdateMethodResolutionOptions {
|
|
195
208
|
homebrewPrefix?: string;
|
|
196
209
|
miseBinDirs?: readonly string[];
|
|
197
210
|
miseDataDir?: string;
|
|
211
|
+
npmBinDir?: string;
|
|
198
212
|
}
|
|
199
213
|
|
|
200
|
-
type UpdateTarget =
|
|
214
|
+
type UpdateTarget =
|
|
215
|
+
| { method: "brew" }
|
|
216
|
+
| { method: "mise" }
|
|
217
|
+
| { method: "bun" }
|
|
218
|
+
| { method: "npm" }
|
|
219
|
+
| { method: "binary"; path: string };
|
|
201
220
|
|
|
202
221
|
function resolveUpdateMethod(
|
|
203
222
|
ompPath: string,
|
|
204
223
|
bunBinDir: string | undefined,
|
|
205
224
|
options: UpdateMethodResolutionOptions = {},
|
|
206
225
|
): UpdateMethod {
|
|
207
|
-
const { homebrewPrefix, miseBinDirs = [], miseDataDir } = options;
|
|
226
|
+
const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
|
|
227
|
+
const launcherExtension = path.extname(ompPath).toLowerCase();
|
|
228
|
+
const isWindowsScriptLauncher =
|
|
229
|
+
launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
|
|
208
230
|
if (homebrewPrefix && isPathInDirectory(ompPath, path.join(homebrewPrefix, "bin"))) return "brew";
|
|
209
231
|
if (miseBinDirs.some(dir => isPathInDirectory(ompPath, dir))) return "mise";
|
|
210
232
|
if (miseDataDir && isPathInDirectory(ompPath, path.join(miseDataDir, "shims"))) return "mise";
|
|
211
233
|
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
|
|
234
|
+
if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
|
|
212
235
|
return "binary";
|
|
213
236
|
}
|
|
214
237
|
|
|
@@ -221,6 +244,7 @@ export function resolveUpdateMethodForTest(
|
|
|
221
244
|
}
|
|
222
245
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
223
246
|
const bunBinDir = await getBunGlobalBinDir();
|
|
247
|
+
const npmBinDir = await getNpmGlobalBinDir();
|
|
224
248
|
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
|
225
249
|
const miseAvailable = $which("mise") !== undefined;
|
|
226
250
|
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
|
@@ -228,7 +252,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
228
252
|
const ompPath = resolveOmpPath();
|
|
229
253
|
|
|
230
254
|
if (ompPath) {
|
|
231
|
-
const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir });
|
|
255
|
+
const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
|
|
232
256
|
if (method === "binary") return { method, path: ompPath };
|
|
233
257
|
return { method };
|
|
234
258
|
}
|
|
@@ -715,6 +739,14 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
715
739
|
}
|
|
716
740
|
}
|
|
717
741
|
|
|
742
|
+
function buildVersionedPackageInstallArgs(expectedVersion: string, nativeTag: string): string[] {
|
|
743
|
+
const args = [`${PACKAGE}@${expectedVersion}`, `${NATIVES_PACKAGE}@${expectedVersion}`];
|
|
744
|
+
if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
|
|
745
|
+
args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
|
|
746
|
+
}
|
|
747
|
+
return args;
|
|
748
|
+
}
|
|
749
|
+
|
|
718
750
|
/**
|
|
719
751
|
* Build the bun argv used to globally install a specific omp version.
|
|
720
752
|
*
|
|
@@ -746,17 +778,23 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
746
778
|
* See #1824.
|
|
747
779
|
*/
|
|
748
780
|
export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
|
749
|
-
|
|
781
|
+
return [
|
|
750
782
|
"install",
|
|
751
783
|
"-g",
|
|
752
784
|
"--no-cache",
|
|
753
785
|
`--registry=${NPM_REGISTRY}`,
|
|
754
|
-
|
|
755
|
-
|
|
786
|
+
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Build the npm argv used to update npm-managed global installs. */
|
|
791
|
+
export function buildNpmInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
|
792
|
+
const args = [
|
|
793
|
+
"install",
|
|
794
|
+
"-g",
|
|
795
|
+
`--registry=${NPM_REGISTRY}`,
|
|
796
|
+
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
|
756
797
|
];
|
|
757
|
-
if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
|
|
758
|
-
args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
|
|
759
|
-
}
|
|
760
798
|
return args;
|
|
761
799
|
}
|
|
762
800
|
|
|
@@ -773,7 +811,7 @@ export function buildMiseForceInstallArgs(expectedVersion: string): string[] {
|
|
|
773
811
|
}
|
|
774
812
|
|
|
775
813
|
/**
|
|
776
|
-
* Update via
|
|
814
|
+
* Update via package manager.
|
|
777
815
|
*/
|
|
778
816
|
async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
779
817
|
console.log(chalk.dim("Updating via bun..."));
|
|
@@ -794,6 +832,17 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
794
832
|
}
|
|
795
833
|
}
|
|
796
834
|
|
|
835
|
+
async function updateViaNpm(expectedVersion: string): Promise<void> {
|
|
836
|
+
console.log(chalk.dim("Updating via npm..."));
|
|
837
|
+
const args = buildNpmInstallArgs(expectedVersion);
|
|
838
|
+
const result = await $`npm ${args}`.nothrow();
|
|
839
|
+
if (result.exitCode !== 0) {
|
|
840
|
+
throw new Error(`npm install failed with exit code ${result.exitCode}`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
await printVerification(expectedVersion);
|
|
844
|
+
}
|
|
845
|
+
|
|
797
846
|
async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
|
|
798
847
|
console.log(chalk.dim("Updating Homebrew formulae..."));
|
|
799
848
|
const update = await $`brew update`.nothrow();
|
|
@@ -920,6 +969,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
920
969
|
await updateViaMise(release.version, opts.force);
|
|
921
970
|
} else if (target.method === "bun") {
|
|
922
971
|
await updateViaBun(release.version);
|
|
972
|
+
} else if (target.method === "npm") {
|
|
973
|
+
await updateViaNpm(release.version);
|
|
923
974
|
} else {
|
|
924
975
|
await updateViaBinaryAt(target.path, release.version);
|
|
925
976
|
}
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -41,6 +41,9 @@ export interface UsageAccountIdentity {
|
|
|
41
41
|
accountId?: string;
|
|
42
42
|
projectId?: string;
|
|
43
43
|
enterpriseUrl?: string;
|
|
44
|
+
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
45
|
+
orgId?: string;
|
|
46
|
+
orgName?: string;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -134,6 +137,7 @@ function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountId
|
|
|
134
137
|
add(meta.accountId);
|
|
135
138
|
add(meta.projectId);
|
|
136
139
|
add(meta.orgId);
|
|
140
|
+
add(meta.orgName);
|
|
137
141
|
for (const limit of report.limits) {
|
|
138
142
|
add(limit.scope.accountId);
|
|
139
143
|
add(limit.scope.projectId);
|
|
@@ -144,6 +148,8 @@ function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountId
|
|
|
144
148
|
add(account.email);
|
|
145
149
|
add(account.accountId);
|
|
146
150
|
add(account.projectId);
|
|
151
|
+
add(account.orgId);
|
|
152
|
+
add(account.orgName);
|
|
147
153
|
add(account.enterpriseUrl);
|
|
148
154
|
}
|
|
149
155
|
return values;
|
|
@@ -293,9 +299,38 @@ export function collectUnreportedAccounts(
|
|
|
293
299
|
const providerReports = byProvider.get(account.provider) ?? [];
|
|
294
300
|
if (providerReports.length === 0) return true;
|
|
295
301
|
if (account.type === "api_key") return false;
|
|
302
|
+
// Org-decisive attribution when EITHER side carries an org (Anthropic
|
|
303
|
+
// multi-subscription): two orgs share every other identifier, so an
|
|
304
|
+
// org-scoped account is covered only by its own org's report, and an
|
|
305
|
+
// org-less legacy account is never covered by an org-attributed sibling
|
|
306
|
+
// report — its own fetch failing must surface as "no usage data". The
|
|
307
|
+
// shared org is a GATE, not a match: two Team members share the org id
|
|
308
|
+
// while drawing on per-user pools, so coverage also requires the
|
|
309
|
+
// account's own base identity inside the same-org subset (an org-only
|
|
310
|
+
// account, with no base identifiers, is covered by any same-org
|
|
311
|
+
// report). The email/account fallback below applies only when both
|
|
312
|
+
// sides are org-less.
|
|
313
|
+
const accountOrg = account.orgId?.toLowerCase();
|
|
296
314
|
const ids = [account.email, account.accountId, account.projectId]
|
|
297
315
|
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
298
316
|
.map(value => value.toLowerCase());
|
|
317
|
+
const sameOrgReports: UsageReport[] = [];
|
|
318
|
+
let sawReportOrg = false;
|
|
319
|
+
for (const report of providerReports) {
|
|
320
|
+
const metaOrg = report.metadata?.orgId;
|
|
321
|
+
if (typeof metaOrg === "string" && metaOrg) {
|
|
322
|
+
sawReportOrg = true;
|
|
323
|
+
if (accountOrg !== undefined && metaOrg.toLowerCase() === accountOrg) sameOrgReports.push(report);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (accountOrg || sawReportOrg) {
|
|
327
|
+
if (!accountOrg || sameOrgReports.length === 0) return true;
|
|
328
|
+
if (ids.length === 0) return false;
|
|
329
|
+
return !sameOrgReports.some(report => {
|
|
330
|
+
const identifiers = reportIdentifiers(report);
|
|
331
|
+
return ids.some(id => identifiers.has(id));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
299
334
|
if (ids.length === 0) return false;
|
|
300
335
|
const reported = new Set<string>();
|
|
301
336
|
let anyIdentified = false;
|
|
@@ -309,9 +344,17 @@ export function collectUnreportedAccounts(
|
|
|
309
344
|
});
|
|
310
345
|
}
|
|
311
346
|
|
|
312
|
-
|
|
347
|
+
/** Compose the account label from parts, masking each part individually so `--redact` cannot be bypassed by the composite string. */
|
|
348
|
+
function accountIdentityLabel(account: UsageAccountIdentity, redaction?: Map<string, string>): string {
|
|
313
349
|
if (account.type === "api_key") return "API key";
|
|
314
|
-
|
|
350
|
+
const base = account.email ?? account.accountId ?? account.projectId ?? account.enterpriseUrl ?? "OAuth account";
|
|
351
|
+
const masked = redaction?.get(base) ?? base;
|
|
352
|
+
// orgId fallback: the uuid is the actual scoped identity; a token response
|
|
353
|
+
// can carry it without a display name, and two same-email rows must still
|
|
354
|
+
// be tellable apart.
|
|
355
|
+
const org = account.orgName ?? account.orgId;
|
|
356
|
+
if (!org || org === base) return masked;
|
|
357
|
+
return `${masked} · ${redaction?.get(org) ?? org}`;
|
|
315
358
|
}
|
|
316
359
|
|
|
317
360
|
function formatAccountHeader(
|
|
@@ -324,6 +367,12 @@ function formatAccountHeader(
|
|
|
324
367
|
const icon = STATUS_COLOR[status]("●");
|
|
325
368
|
const label = reportAccountLabel(report, index);
|
|
326
369
|
let header = `${icon} ${chalk.bold(redaction?.get(label) ?? label)}`;
|
|
370
|
+
const metaOrgName = report.metadata?.orgName;
|
|
371
|
+
const metaOrgId = report.metadata?.orgId;
|
|
372
|
+
const org = typeof metaOrgName === "string" && metaOrgName ? metaOrgName : metaOrgId;
|
|
373
|
+
if (typeof org === "string" && org && org !== label) {
|
|
374
|
+
header += chalk.dim(` · ${redaction?.get(org) ?? org}`);
|
|
375
|
+
}
|
|
327
376
|
const planType = report.metadata?.planType;
|
|
328
377
|
if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
|
|
329
378
|
const savedResets = report.resetCredits?.availableCount ?? 0;
|
|
@@ -520,8 +569,8 @@ export function formatUsageBreakdown(
|
|
|
520
569
|
});
|
|
521
570
|
|
|
522
571
|
for (const account of providerUnreported) {
|
|
523
|
-
const label = accountIdentityLabel(account);
|
|
524
|
-
lines.push(` ${chalk.dim("○")} ${chalk.dim(`${
|
|
572
|
+
const label = accountIdentityLabel(account, redaction);
|
|
573
|
+
lines.push(` ${chalk.dim("○")} ${chalk.dim(`${label} — no usage data`)}`);
|
|
525
574
|
}
|
|
526
575
|
|
|
527
576
|
const stats = computeProviderWindowStats(providerReports);
|
|
@@ -690,6 +739,8 @@ function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[]
|
|
|
690
739
|
accountId: credential.accountId,
|
|
691
740
|
projectId: credential.projectId,
|
|
692
741
|
enterpriseUrl: credential.enterpriseUrl,
|
|
742
|
+
orgId: credential.orgId,
|
|
743
|
+
orgName: credential.orgName,
|
|
693
744
|
});
|
|
694
745
|
} else {
|
|
695
746
|
accounts.push({ provider, type: "api_key" });
|
|
@@ -726,7 +777,7 @@ function maskIdentity(redaction: Map<string, string>, value: string | undefined)
|
|
|
726
777
|
return value === undefined ? undefined : (redaction.get(value) ?? value);
|
|
727
778
|
}
|
|
728
779
|
|
|
729
|
-
const IDENTITY_METADATA_KEYS = ["email", "accountId", "projectId", "orgId"] as const;
|
|
780
|
+
const IDENTITY_METADATA_KEYS = ["email", "accountId", "projectId", "orgId", "orgName"] as const;
|
|
730
781
|
|
|
731
782
|
/** Mask identity fields in a raw-stripped report for `--redact --json`. */
|
|
732
783
|
function redactReportForJson(
|
|
@@ -830,6 +881,8 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
830
881
|
accountId: maskIdentity(redaction, account.accountId),
|
|
831
882
|
projectId: maskIdentity(redaction, account.projectId),
|
|
832
883
|
enterpriseUrl: maskIdentity(redaction, account.enterpriseUrl),
|
|
884
|
+
orgId: maskIdentity(redaction, account.orgId),
|
|
885
|
+
orgName: maskIdentity(redaction, account.orgName),
|
|
833
886
|
}));
|
|
834
887
|
}
|
|
835
888
|
const capacity: Record<string, ProviderWindowStat[]> = {};
|
package/src/cli.ts
CHANGED
|
@@ -108,6 +108,7 @@ const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
|
|
|
108
108
|
const STATS_SYNC_WORKER_ARG = "__omp_worker_stats_sync";
|
|
109
109
|
const TAB_WORKER_ARG = "__omp_worker_tab";
|
|
110
110
|
const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
|
|
111
|
+
const JS_EVAL_PROCESS_ARG = "__omp_worker_js_eval_process";
|
|
111
112
|
const STT_WORKER_ARG = "__omp_worker_stt";
|
|
112
113
|
const TTS_WORKER_ARG = "__omp_worker_tts";
|
|
113
114
|
const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
|
|
@@ -156,6 +157,14 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
156
157
|
await import("./eval/js/worker-entry");
|
|
157
158
|
return true;
|
|
158
159
|
}
|
|
160
|
+
if (arg === JS_EVAL_PROCESS_ARG) {
|
|
161
|
+
const { startJsEvalProcess } = await import("./eval/js/process-entry");
|
|
162
|
+
// The JS evaluator forwards user-controlled payloads (tool-call args,
|
|
163
|
+
// display outputs); a non-serializable one must fail that cell, not
|
|
164
|
+
// SIGKILL the kernel and erase the eval session's state.
|
|
165
|
+
await runIpcSubprocessWorker(startJsEvalProcess, { rethrowConnectedSendErrors: true });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
159
168
|
if (arg === STT_WORKER_ARG) {
|
|
160
169
|
const { startSttWorker } = await import("./stt/asr-worker");
|
|
161
170
|
await runIpcSubprocessWorker(startSttWorker);
|
|
@@ -196,6 +205,18 @@ async function runIpcSubprocessWorker<In, Out>(
|
|
|
196
205
|
sendAndFlush(message: Out): Promise<void>;
|
|
197
206
|
onMessage(handler: (message: In) => void): () => void;
|
|
198
207
|
}) => void,
|
|
208
|
+
options?: {
|
|
209
|
+
/**
|
|
210
|
+
* Rethrow send failures while the IPC channel is still connected instead
|
|
211
|
+
* of shutting down. A connected-channel failure means this particular
|
|
212
|
+
* message could not be serialized (e.g. a JS eval cell passed a function
|
|
213
|
+
* into tool args, a DataCloneError under advanced serialization) — the
|
|
214
|
+
* caller must see that error, exactly as Worker `postMessage` would
|
|
215
|
+
* deliver it, rather than losing the whole worker and its state.
|
|
216
|
+
* Channel-gone failures still shut down.
|
|
217
|
+
*/
|
|
218
|
+
rethrowConnectedSendErrors?: boolean;
|
|
219
|
+
},
|
|
199
220
|
): Promise<void> {
|
|
200
221
|
const { promise: shuttingDown, resolve: shutdown } = Promise.withResolvers<void>();
|
|
201
222
|
type IpcSend = (this: NodeJS.Process, message: unknown, callback?: (error: Error | null) => void) => boolean;
|
|
@@ -211,7 +232,8 @@ async function runIpcSubprocessWorker<In, Out>(
|
|
|
211
232
|
}
|
|
212
233
|
try {
|
|
213
234
|
sender.call(process, message);
|
|
214
|
-
} catch {
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (options?.rethrowConnectedSendErrors && process.connected) throw error;
|
|
215
237
|
shutdown();
|
|
216
238
|
}
|
|
217
239
|
};
|
package/src/commands/acp.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ACP terminal-auth flag asks the same command to open the interactive TUI.
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from "@oh-my-pi/pi-utils/cli";
|
|
8
|
-
import { parseArgs } from "../cli/args";
|
|
8
|
+
import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
|
|
9
9
|
import { runRootCommand } from "../main";
|
|
10
10
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
11
11
|
|
|
@@ -15,7 +15,16 @@ export default class Acp extends Command {
|
|
|
15
15
|
|
|
16
16
|
async run(): Promise<void> {
|
|
17
17
|
const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
|
|
18
|
-
|
|
18
|
+
let parsed: ParsedArgs;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseArgs(args);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (reportCliUsageError(error)) {
|
|
23
|
+
process.exitCode = 2;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
19
28
|
if (!terminalAuth) {
|
|
20
29
|
parsed.mode = "acp";
|
|
21
30
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
7
|
-
import { parseArgs } from "../cli/args";
|
|
7
|
+
import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
|
|
8
8
|
import { runRootCommand } from "../main";
|
|
9
9
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
10
10
|
import { CLI_THINKING_LEVELS } from "../thinking";
|
|
@@ -157,7 +157,7 @@ export default class Index extends Command {
|
|
|
157
157
|
description: "Include thinking blocks in print mode text output",
|
|
158
158
|
}),
|
|
159
159
|
"max-time": Flags.string({
|
|
160
|
-
description: "Stop the session after this
|
|
160
|
+
description: "Stop the session after this duration (e.g., 600, 10m, 1h)",
|
|
161
161
|
}),
|
|
162
162
|
// `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
|
|
163
163
|
// Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
|
|
@@ -193,7 +193,16 @@ export default class Index extends Command {
|
|
|
193
193
|
|
|
194
194
|
async run(): Promise<void> {
|
|
195
195
|
const { args } = prepareAcpTerminalAuthArgs(this.argv);
|
|
196
|
-
|
|
196
|
+
let parsed: ParsedArgs;
|
|
197
|
+
try {
|
|
198
|
+
parsed = parseArgs(args);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (reportCliUsageError(error)) {
|
|
201
|
+
process.exitCode = 2;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
197
206
|
await runRootCommand(parsed, args);
|
|
198
207
|
}
|
|
199
208
|
}
|
package/src/commands/token.ts
CHANGED
|
@@ -64,12 +64,14 @@ export default class Token extends Command {
|
|
|
64
64
|
}
|
|
65
65
|
if (flags.list) {
|
|
66
66
|
for (const acct of accounts) {
|
|
67
|
-
const
|
|
67
|
+
const base =
|
|
68
68
|
acct.email ??
|
|
69
69
|
acct.accountId ??
|
|
70
70
|
acct.projectId ??
|
|
71
71
|
acct.enterpriseUrl ??
|
|
72
72
|
`credential #${acct.credentialId}`;
|
|
73
|
+
const org = acct.orgName ?? acct.orgId;
|
|
74
|
+
const label = org && org !== base ? `${base} (${org})` : base;
|
|
73
75
|
process.stdout.write(`${acct.position + 1}. ${label}\n`);
|
|
74
76
|
}
|
|
75
77
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Api, type ApiKeyResolver, type AuthStorage, isUsageLimitOutcome, type Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
2
3
|
|
|
3
4
|
/** Model slice accepted by the model-form `resolver(model, sessionId)` overload. */
|
|
4
5
|
export type ApiKeyResolverModel = Pick<Model<Api>, "provider" | "baseUrl" | "id">;
|
|
@@ -27,7 +28,7 @@ export interface ApiKeyResolverRegistry {
|
|
|
27
28
|
/**
|
|
28
29
|
* Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
|
|
29
30
|
* policy: initial → resolve; step (b) → force-refresh same account; step (c)
|
|
30
|
-
* → rotate to a sibling
|
|
31
|
+
* → rotate to a sibling and re-resolve, unless quota exhaustion has no sibling.
|
|
31
32
|
*
|
|
32
33
|
* Two call forms: `resolver(provider, options?)` for provider-scoped keys,
|
|
33
34
|
* and `resolver(model, sessionId?)` which derives `baseUrl`/`modelId` from
|
|
@@ -59,12 +60,20 @@ export function createApiKeyResolver(
|
|
|
59
60
|
// sibling exists we switch immediately; the precise no-sibling backoff
|
|
60
61
|
// is owned by `markUsageLimitReached` (default + server usage-report
|
|
61
62
|
// reset) and the outer whole-turn retry layer.
|
|
62
|
-
await registry.authStorage.rotateSessionCredential(provider, sessionId, {
|
|
63
|
+
const switched = await registry.authStorage.rotateSessionCredential(provider, sessionId, {
|
|
63
64
|
error,
|
|
64
65
|
modelId,
|
|
65
66
|
signal,
|
|
66
67
|
apiKey: previousKey,
|
|
67
68
|
});
|
|
69
|
+
if (!switched) {
|
|
70
|
+
const status = AIError.status(error);
|
|
71
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
72
|
+
// No sibling for an account-quota failure: stop so the outer
|
|
73
|
+
// whole-turn retry layer can honor the recorded backoff. A hard
|
|
74
|
+
// auth decline can instead mean a peer refreshed the bearer.
|
|
75
|
+
if (AIError.isUsageLimit(error) || isUsageLimitOutcome(status, message)) return undefined;
|
|
76
|
+
}
|
|
68
77
|
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
|
|
69
78
|
}
|
|
70
79
|
return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });
|
|
@@ -11,9 +11,9 @@ interface ConfigSchemaError {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Module-private cache of
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Module-private cache of JSON → YAML migrations this process already ran.
|
|
15
|
+
* Prevents `ConfigFile.relocate()` / repeated `tryLoad()` calls from re-running
|
|
16
|
+
* the migration over and over on the boot path.
|
|
17
17
|
*/
|
|
18
18
|
const migratedPaths = new Set<string>();
|
|
19
19
|
|
|
@@ -123,6 +123,7 @@ export type LoadResult<T> =
|
|
|
123
123
|
|
|
124
124
|
export class ConfigFile<T> implements IConfigFile<T> {
|
|
125
125
|
readonly #basePath: string;
|
|
126
|
+
readonly #yamlFallbackPath: string | null;
|
|
126
127
|
readonly #jsonMigrationPath: string | null;
|
|
127
128
|
#cache?: LoadResult<T>;
|
|
128
129
|
#auxValidate?: (value: T) => void;
|
|
@@ -134,13 +135,17 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
134
135
|
) {
|
|
135
136
|
this.#basePath = configPath;
|
|
136
137
|
if (configPath.endsWith(".yml")) {
|
|
138
|
+
this.#yamlFallbackPath = `${configPath.slice(0, -4)}.yaml`;
|
|
137
139
|
this.#jsonMigrationPath = `${configPath.slice(0, -4)}.json`;
|
|
138
140
|
} else if (configPath.endsWith(".yaml")) {
|
|
141
|
+
this.#yamlFallbackPath = null;
|
|
139
142
|
this.#jsonMigrationPath = `${configPath.slice(0, -5)}.json`;
|
|
140
143
|
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
144
|
+
this.#yamlFallbackPath = null;
|
|
141
145
|
// JSON configs are still supported without migration.
|
|
142
146
|
this.#jsonMigrationPath = null;
|
|
143
147
|
} else {
|
|
148
|
+
this.#yamlFallbackPath = null;
|
|
144
149
|
throw new Error(`Invalid config file path: ${configPath}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -150,9 +155,11 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
150
155
|
* Sync callers (tests, settings init) hit this implicitly via {@link tryLoad}.
|
|
151
156
|
*/
|
|
152
157
|
#ensureMigrated(): void {
|
|
153
|
-
if (this.#jsonMigrationPath)
|
|
154
|
-
|
|
158
|
+
if (!this.#jsonMigrationPath) return;
|
|
159
|
+
if (this.#yamlFallbackPath && !fs.existsSync(this.#basePath) && fs.existsSync(this.#yamlFallbackPath)) {
|
|
160
|
+
return;
|
|
155
161
|
}
|
|
162
|
+
migrateJsonToYml(this.#jsonMigrationPath, this.#basePath);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
relocate(configPath?: string): ConfigFile<T> {
|
|
@@ -163,9 +170,19 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
163
170
|
return result;
|
|
164
171
|
}
|
|
165
172
|
|
|
173
|
+
#resolveReadPath(): string {
|
|
174
|
+
if (fs.existsSync(this.#basePath)) {
|
|
175
|
+
return this.#basePath;
|
|
176
|
+
}
|
|
177
|
+
if (this.#yamlFallbackPath && fs.existsSync(this.#yamlFallbackPath)) {
|
|
178
|
+
return this.#yamlFallbackPath;
|
|
179
|
+
}
|
|
180
|
+
return this.#basePath;
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
getMtimeMs(): number | null {
|
|
167
184
|
try {
|
|
168
|
-
return fs.statSync(this
|
|
185
|
+
return fs.statSync(this.#resolveReadPath()).mtimeMs;
|
|
169
186
|
} catch (err) {
|
|
170
187
|
if (isEnoent(err)) return null;
|
|
171
188
|
throw err;
|
|
@@ -211,12 +228,13 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
211
228
|
#parseContent(content: string): LoadResult<T> {
|
|
212
229
|
try {
|
|
213
230
|
let parsed: unknown;
|
|
214
|
-
|
|
231
|
+
const readPath = this.#resolveReadPath();
|
|
232
|
+
if (readPath.endsWith(".json") || readPath.endsWith(".jsonc")) {
|
|
215
233
|
parsed = JSONC.parse(content);
|
|
216
|
-
} else if (
|
|
234
|
+
} else if (readPath.endsWith(".yml") || readPath.endsWith(".yaml")) {
|
|
217
235
|
parsed = YAML.parse(content);
|
|
218
236
|
} else {
|
|
219
|
-
throw new Error(`Invalid config file path: ${
|
|
237
|
+
throw new Error(`Invalid config file path: ${readPath}`);
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
const checked = this.schema(parsed);
|
|
@@ -255,7 +273,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
255
273
|
|
|
256
274
|
let content: string;
|
|
257
275
|
try {
|
|
258
|
-
content = fs.readFileSync(this
|
|
276
|
+
content = fs.readFileSync(this.#resolveReadPath(), "utf-8").trim();
|
|
259
277
|
} catch (error) {
|
|
260
278
|
if (isEnoent(error)) {
|
|
261
279
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -275,7 +293,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
275
293
|
|
|
276
294
|
let content: string;
|
|
277
295
|
try {
|
|
278
|
-
content = (await Bun.file(this
|
|
296
|
+
content = (await Bun.file(this.#resolveReadPath()).text()).trim();
|
|
279
297
|
} catch (error) {
|
|
280
298
|
if (isEnoent(error)) {
|
|
281
299
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -306,7 +324,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
306
324
|
}
|
|
307
325
|
|
|
308
326
|
path(): string {
|
|
309
|
-
return this.#
|
|
327
|
+
return this.#resolveReadPath();
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
invalidate() {
|