@oh-my-pi/pi-coding-agent 15.3.0 → 15.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +26 -0
- package/dist/types/modes/components/status-line.d.ts +6 -0
- package/dist/types/session/session-manager.d.ts +10 -0
- package/dist/types/task/types.d.ts +8 -0
- package/package.json +7 -7
- package/src/cli/auth-gateway-cli.ts +71 -2
- package/src/commands/auth-gateway.ts +2 -0
- package/src/config/model-registry.ts +46 -21
- package/src/extensibility/plugins/marketplace/manager.ts +20 -1
- package/src/hashline/parser.ts +44 -5
- package/src/internal-urls/docs-index.generated.ts +3 -1
- package/src/lsp/config.ts +87 -22
- package/src/modes/components/status-line.ts +124 -31
- package/src/modes/utils/context-usage.ts +18 -7
- package/src/sdk.ts +4 -1
- package/src/session/agent-session.ts +14 -2
- package/src/session/session-manager.ts +68 -3
- package/src/slash-commands/builtin-registry.ts +9 -4
- package/src/task/executor.ts +29 -0
- package/src/task/render.ts +53 -1
- package/src/task/types.ts +8 -0
- package/src/tools/jtd-to-json-schema.ts +5 -1
- package/src/tools/read.ts +3 -35
- package/src/utils/clipboard.ts +14 -3
- package/src/utils/image-resize.ts +28 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.3.2] - 2026-05-25
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added inline `|TEXT` payload support to `»` and `«` hashline insert operations, allowing single-line inserts on the op line and still supporting additional payload lines
|
|
9
|
+
- Added support for using inline payloads with BOF/EOF inserts so `|TEXT` is treated as inserted content at file boundaries
|
|
10
|
+
- Added live nested-`task` rendering: while a subagent is mid-flight, the parent UI now surfaces both completed nested `task` sub-calls and the in-flight nested snapshot (forwarded from `tool_execution_update`), matching the finished-result tree
|
|
11
|
+
- Added `omp auth-gateway check` (and matching `GET /v1/credentials/check` endpoint) — probes each broker-supplied credential against its provider's auth-verifying usage endpoint and prints per-credential health, so when a multi-account pool starts returning 401s you can identify which row in the broker is the bad one. The existing `/v1/usage` endpoint silently drops failed credentials, which is the wrong shape for diagnosing auth — the new endpoint captures errors and surfaces the credential's id, provider, type, email/accountId, and the upstream error string. CLI groups results per-provider, exits non-zero when any credential failed, and supports `--json` for scripting. The probe also exercises OAuth refresh on expired tokens, so a working refresh + working access reports as `ok` and a revoked refresh token reports as `oauth refresh failed: …` instead of being masked by the cached expired access token.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed parsing of inline `|TEXT` payloads containing whitespace on `»` and `«` inserts, which previously failed with unrecognized-op errors
|
|
16
|
+
- Fixed anchored insert handling so an inline `|TEXT` body matching the anchor line is treated as anchor decoration and no longer inserted as a duplicate
|
|
17
|
+
|
|
5
18
|
## [15.3.0] - 2026-05-25
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -9,6 +22,8 @@
|
|
|
9
22
|
- Added `OMP_NO_WEBP` environment variable to disable WebP encoding in image resize, fixing HTTP 400 errors when attaching browser snapshots to vision models running on local llama.cpp (which uses STB library that lacks WebP support)
|
|
10
23
|
- Fixed loop mode submitting the next prompt while a background async-job delivery turn (idle flush) was still pending, which could cause the job result to be silently dropped and make the session appear to keep firing while work was ongoing ([#1294](https://github.com/can1357/oh-my-pi/issues/1294))
|
|
11
24
|
- Fixed clipboard image paste (Ctrl+V) silently failing on WSL2 by routing image reads through a `powershell.exe` bridge when WSL interop is detected, since `arboard` returns `ContentNotAvailable` under WSLg ([#1280](https://github.com/can1357/oh-my-pi/issues/1280))
|
|
25
|
+
- Fixed config-only marketplace LSP plugins such as `csharp-lsp` not registering servers with the CLI when the plugin cache has only marketplace metadata and no package code ([#1352](https://github.com/can1357/oh-my-pi/issues/1352)).
|
|
26
|
+
- Fixed JTD-to-JSON-Schema conversion treating user-named properties as nested JTD forms when their keys collided with JTD keywords like `ref`, which broke the built-in explore agent's output validator with `schema_violation: files.0.ref: must not be present` ([#1345](https://github.com/can1357/oh-my-pi/issues/1345))
|
|
12
27
|
- Fixed extension `ctx.ui.notify()` messages emitted during `session_start` being cleared before the first interactive render ([#1316](https://github.com/can1357/oh-my-pi/issues/1316)).
|
|
13
28
|
- Fixed append-only context mode not being recomputed after model switches — the mode was frozen at session construction time using the initial model's provider, so `provider.appendOnlyContext=auto` left append-only enabled after switching away from DeepSeek (or disabled after switching to DeepSeek) for the rest of the session
|
|
14
29
|
|
|
@@ -213,6 +213,32 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
213
213
|
exclude?: string[] | undefined;
|
|
214
214
|
} | undefined;
|
|
215
215
|
}>;
|
|
216
|
+
/** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
|
|
217
|
+
interface ProviderOverride {
|
|
218
|
+
baseUrl?: string;
|
|
219
|
+
headers?: Record<string, string>;
|
|
220
|
+
apiKey?: string;
|
|
221
|
+
authHeader?: boolean;
|
|
222
|
+
compat?: Model<Api>["compat"];
|
|
223
|
+
transport?: Model<Api>["transport"];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Merge a freshly discovered model with the matching bundled/configured entry
|
|
227
|
+
* (or a runtime provider override when no bundled entry exists).
|
|
228
|
+
*
|
|
229
|
+
* `baseUrl` resolution priority:
|
|
230
|
+
* 1. User-set `providerOverride.baseUrl` (explicit override in models.json)
|
|
231
|
+
* 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to
|
|
232
|
+
* `token-plan-sgp.xiaomimimo.com` at discovery time)
|
|
233
|
+
* 3. Existing bundled baseUrl (the host baked into `models.json`)
|
|
234
|
+
*
|
|
235
|
+
* Without (1), the user's override would lose to discovery; without (2)
|
|
236
|
+
* preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
|
|
237
|
+
* tp- token-plan host and produce 401s on the first stream call.
|
|
238
|
+
* See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
|
|
239
|
+
* regression in `model-registry.test.ts`.
|
|
240
|
+
*/
|
|
241
|
+
export declare function mergeDiscoveredModel<TApi extends Api>(model: Model<TApi>, existing: Model<Api> | undefined, providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">): Model<TApi>;
|
|
216
242
|
export type ProviderDiscoveryStatus = "idle" | "ok" | "empty" | "cached" | "unavailable" | "unauthenticated";
|
|
217
243
|
export interface ProviderDiscoveryState {
|
|
218
244
|
provider: string;
|
|
@@ -53,6 +53,12 @@ export declare class StatusLineComponent implements Component {
|
|
|
53
53
|
watchBranch(onBranchChange: () => void): void;
|
|
54
54
|
dispose(): void;
|
|
55
55
|
invalidate(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Background-refresh the Anthropic OAuth quota report. Guarded by a 5-min
|
|
58
|
+
* TTL on both success (cache lifetime) and error (backoff). Exposed
|
|
59
|
+
* (non-private) so unit tests can verify the backoff invariant.
|
|
60
|
+
*/
|
|
61
|
+
refreshUsageInBackground(): void;
|
|
56
62
|
/**
|
|
57
63
|
* Compute the (cached) used-tokens / context-window totals for the
|
|
58
64
|
* status-line context% segment. Exposed (non-private) so unit tests can
|
|
@@ -214,6 +214,16 @@ declare class RecentSessionInfo {
|
|
|
214
214
|
/** Human-readable relative time (e.g., "2 hours ago") */
|
|
215
215
|
get timeAgo(): string;
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Promote orphaned `<basename>.jsonl.<snowflake>.bak` backups created by
|
|
219
|
+
* `#replaceSessionFileAfterEperm` back to their primary path when the primary
|
|
220
|
+
* is missing. This runs once per session-dir scan, before the main `*.jsonl`
|
|
221
|
+
* glob, so a crash between the two renames in the EPERM-rewrite path does not
|
|
222
|
+
* leave the user's last good state stranded outside the loader's view.
|
|
223
|
+
*
|
|
224
|
+
* Exported for testing.
|
|
225
|
+
*/
|
|
226
|
+
export declare function recoverOrphanedBackups(sessionDir: string, storage: SessionStorage): Promise<void>;
|
|
217
227
|
/** Exported for testing */
|
|
218
228
|
export declare function findMostRecentSession(sessionDir: string, storage?: SessionStorage): Promise<string | null>;
|
|
219
229
|
/** Get recent sessions for display in welcome screen */
|
|
@@ -214,6 +214,14 @@ export interface AgentProgress {
|
|
|
214
214
|
attempt: number;
|
|
215
215
|
errorMessage: string;
|
|
216
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* Snapshot of the most recent `task` tool call's in-flight `TaskToolDetails`,
|
|
219
|
+
* captured from `tool_execution_update`. Lets the parent UI surface live
|
|
220
|
+
* nested-subagent progress while this agent is still inside its own `task`
|
|
221
|
+
* call. Cleared when the call ends — finalized data lives in
|
|
222
|
+
* `extractedToolData.task` after that.
|
|
223
|
+
*/
|
|
224
|
+
inflightTaskDetails?: TaskToolDetails;
|
|
217
225
|
}
|
|
218
226
|
/** Result from a single agent execution */
|
|
219
227
|
export interface SingleResult {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.3.
|
|
4
|
+
"version": "15.3.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.3.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.3.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.3.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.3.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.3.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.3.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.3.2",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.3.2",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.3.2",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.3.2",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.3.2",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.3.2",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -32,7 +32,7 @@ import { getConfigRootDir, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
|
|
32
32
|
import chalk from "chalk";
|
|
33
33
|
import { type AuthBrokerClientConfig, resolveAuthBrokerConfig } from "../session/auth-broker-config";
|
|
34
34
|
|
|
35
|
-
export type AuthGatewayAction = "serve" | "token" | "status";
|
|
35
|
+
export type AuthGatewayAction = "serve" | "token" | "status" | "check";
|
|
36
36
|
|
|
37
37
|
export interface AuthGatewayCommandArgs {
|
|
38
38
|
action: AuthGatewayAction;
|
|
@@ -49,7 +49,7 @@ export interface AuthGatewayCommandArgs {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const ACTIONS: readonly AuthGatewayAction[] = ["serve", "token", "status"];
|
|
52
|
+
const ACTIONS: readonly AuthGatewayAction[] = ["serve", "token", "status", "check"];
|
|
53
53
|
|
|
54
54
|
function getTokenFilePath(): string {
|
|
55
55
|
return path.join(getConfigRootDir(), "auth-gateway.token");
|
|
@@ -332,6 +332,9 @@ export async function runAuthGatewayCommand(cmd: AuthGatewayCommandArgs): Promis
|
|
|
332
332
|
case "status":
|
|
333
333
|
await runStatus(cmd.flags);
|
|
334
334
|
return;
|
|
335
|
+
case "check":
|
|
336
|
+
await runCheck(cmd.flags);
|
|
337
|
+
return;
|
|
335
338
|
default: {
|
|
336
339
|
const _exhaustive: never = cmd.action;
|
|
337
340
|
throw new Error(`Unknown auth-gateway action: ${String(_exhaustive)}`);
|
|
@@ -339,4 +342,70 @@ export async function runAuthGatewayCommand(cmd: AuthGatewayCommandArgs): Promis
|
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
344
|
|
|
345
|
+
/**
|
|
346
|
+
* `omp auth-gateway check` — probe each broker-supplied credential and print
|
|
347
|
+
* per-credential auth health. Use this when the gateway is returning 401s and
|
|
348
|
+
* you need to find which row in a multi-account pool is the bad one. The
|
|
349
|
+
* aggregate `/v1/usage` endpoint silently drops failed credentials, so a
|
|
350
|
+
* dedicated diagnostic is the only way to see which credentials failed.
|
|
351
|
+
*/
|
|
352
|
+
async function runCheck(flags: AuthGatewayCommandArgs["flags"]): Promise<void> {
|
|
353
|
+
const brokerConfig = await resolveAuthBrokerConfig();
|
|
354
|
+
if (!brokerConfig) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
"`omp auth-gateway check` requires OMP_AUTH_BROKER_URL (or `auth.broker.url`/`auth.broker.token` in config.yml). It probes the same credentials the gateway would serve.",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const client = createBrokerClient(brokerConfig);
|
|
361
|
+
const initialSnapshot = await fetchBrokerSnapshot(client);
|
|
362
|
+
const store = new RemoteAuthCredentialStore({ client, initialSnapshot });
|
|
363
|
+
const storage = new AuthStorage(store, { sourceLabel: `broker ${brokerConfig.url}` });
|
|
364
|
+
try {
|
|
365
|
+
await storage.reload();
|
|
366
|
+
const results = await storage.checkCredentials();
|
|
367
|
+
|
|
368
|
+
if (flags.json) {
|
|
369
|
+
process.stdout.write(`${JSON.stringify({ broker: brokerConfig.url, credentials: results }, null, 2)}\n`);
|
|
370
|
+
} else {
|
|
371
|
+
const grouped = new Map<string, typeof results>();
|
|
372
|
+
for (const row of results) {
|
|
373
|
+
const list = grouped.get(row.provider) ?? [];
|
|
374
|
+
list.push(row);
|
|
375
|
+
grouped.set(row.provider, list);
|
|
376
|
+
}
|
|
377
|
+
const providers = [...grouped.keys()].sort();
|
|
378
|
+
process.stdout.write(`broker: ${brokerConfig.url}\n`);
|
|
379
|
+
for (const provider of providers) {
|
|
380
|
+
const rows = grouped.get(provider) ?? [];
|
|
381
|
+
process.stdout.write(`\n${chalk.bold(provider)} (${rows.length})\n`);
|
|
382
|
+
for (const row of rows) {
|
|
383
|
+
const status =
|
|
384
|
+
row.ok === true
|
|
385
|
+
? chalk.green("ok ")
|
|
386
|
+
: row.ok === false
|
|
387
|
+
? chalk.red("FAIL ")
|
|
388
|
+
: chalk.yellow("unknown ");
|
|
389
|
+
const identity =
|
|
390
|
+
row.email ?? row.accountId ?? (row.type === "api_key" ? "(api key)" : "(no identity on credential)");
|
|
391
|
+
const remote = row.remoteRefresh ? chalk.dim(" [remote-refresh]") : "";
|
|
392
|
+
const reason = row.reason ? chalk.dim(` — ${row.reason}`) : "";
|
|
393
|
+
process.stdout.write(
|
|
394
|
+
` ${status} id=${row.id.toString().padStart(3)} ${row.type.padEnd(7)} ${identity}${remote}${reason}\n`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const failed = results.filter(row => row.ok === false).length;
|
|
399
|
+
const unverifiable = results.filter(row => row.ok === null).length;
|
|
400
|
+
const passing = results.filter(row => row.ok === true).length;
|
|
401
|
+
process.stdout.write(
|
|
402
|
+
`\n${chalk.green(`${passing} ok`)}, ${chalk.red(`${failed} failed`)}, ${chalk.yellow(`${unverifiable} unverifiable`)}, ${results.length} total\n`,
|
|
403
|
+
);
|
|
404
|
+
if (failed > 0) process.exitCode = 1;
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
storage.close();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
342
411
|
export { ACTIONS as AUTH_GATEWAY_ACTIONS };
|
|
@@ -38,6 +38,8 @@ export default class AuthGateway extends Command {
|
|
|
38
38
|
"# Rotate the gateway bearer token\n omp auth-gateway token --regenerate",
|
|
39
39
|
"# Run on loopback without any bearer (anyone on this host can call)\n omp auth-gateway serve --no-auth",
|
|
40
40
|
"# Show local gateway + broker config status\n omp auth-gateway status",
|
|
41
|
+
"# Probe each broker credential to see which one is producing 401s\n omp auth-gateway check",
|
|
42
|
+
"# Same, machine-readable for scripts\n omp auth-gateway check --json",
|
|
41
43
|
];
|
|
42
44
|
|
|
43
45
|
async run(): Promise<void> {
|
|
@@ -252,6 +252,45 @@ interface ProviderOverride {
|
|
|
252
252
|
transport?: Model<Api>["transport"];
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Merge a freshly discovered model with the matching bundled/configured entry
|
|
257
|
+
* (or a runtime provider override when no bundled entry exists).
|
|
258
|
+
*
|
|
259
|
+
* `baseUrl` resolution priority:
|
|
260
|
+
* 1. User-set `providerOverride.baseUrl` (explicit override in models.json)
|
|
261
|
+
* 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to
|
|
262
|
+
* `token-plan-sgp.xiaomimimo.com` at discovery time)
|
|
263
|
+
* 3. Existing bundled baseUrl (the host baked into `models.json`)
|
|
264
|
+
*
|
|
265
|
+
* Without (1), the user's override would lose to discovery; without (2)
|
|
266
|
+
* preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
|
|
267
|
+
* tp- token-plan host and produce 401s on the first stream call.
|
|
268
|
+
* See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
|
|
269
|
+
* regression in `model-registry.test.ts`.
|
|
270
|
+
*/
|
|
271
|
+
export function mergeDiscoveredModel<TApi extends Api>(
|
|
272
|
+
model: Model<TApi>,
|
|
273
|
+
existing: Model<Api> | undefined,
|
|
274
|
+
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
|
|
275
|
+
): Model<TApi> {
|
|
276
|
+
if (existing) {
|
|
277
|
+
return {
|
|
278
|
+
...model,
|
|
279
|
+
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
280
|
+
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (providerOverride) {
|
|
284
|
+
return {
|
|
285
|
+
...model,
|
|
286
|
+
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
287
|
+
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
|
288
|
+
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return model;
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
interface DiscoveryProviderConfig {
|
|
256
295
|
provider: string;
|
|
257
296
|
api: Api;
|
|
@@ -1182,27 +1221,13 @@ export class ModelRegistry {
|
|
|
1182
1221
|
return;
|
|
1183
1222
|
}
|
|
1184
1223
|
const discoveredModels = this.#applyHardcodedModelPolicies(
|
|
1185
|
-
discovered.map(model =>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
const providerOverride = this.#providerOverrides.get(model.provider);
|
|
1195
|
-
return providerOverride
|
|
1196
|
-
? {
|
|
1197
|
-
...model,
|
|
1198
|
-
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
1199
|
-
headers: providerOverride.headers
|
|
1200
|
-
? { ...model.headers, ...providerOverride.headers }
|
|
1201
|
-
: model.headers,
|
|
1202
|
-
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
1203
|
-
}
|
|
1204
|
-
: model;
|
|
1205
|
-
}),
|
|
1224
|
+
discovered.map(model =>
|
|
1225
|
+
mergeDiscoveredModel(
|
|
1226
|
+
model,
|
|
1227
|
+
this.find(model.provider, model.id),
|
|
1228
|
+
this.#providerOverrides.get(model.provider),
|
|
1229
|
+
),
|
|
1230
|
+
),
|
|
1206
1231
|
);
|
|
1207
1232
|
const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
|
|
1208
1233
|
const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
|
|
@@ -10,7 +10,7 @@ import * as fs from "node:fs/promises";
|
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
|
|
13
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { isEnoent, logger, pathIsWithin } from "@oh-my-pi/pi-utils";
|
|
14
14
|
|
|
15
15
|
import { cachePlugin } from "./cache";
|
|
16
16
|
import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
|
|
@@ -290,6 +290,7 @@ export class MarketplaceManager {
|
|
|
290
290
|
try {
|
|
291
291
|
version = await this.#resolvePluginVersion(pluginEntry, sourcePath);
|
|
292
292
|
cachePath = await cachePlugin(sourcePath, this.#opts.pluginsCacheDir, marketplace, name, version);
|
|
293
|
+
await this.#writeEmbeddedLspConfig(pluginEntry, cachePath);
|
|
293
294
|
} finally {
|
|
294
295
|
// Clean up temp clone dirs created by resolvePluginSource; leave user-supplied local dirs alone
|
|
295
296
|
if (tempCloneRoot) {
|
|
@@ -342,6 +343,24 @@ export class MarketplaceManager {
|
|
|
342
343
|
return installedEntry;
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
async #writeEmbeddedLspConfig(entry: MarketplacePluginEntry, cachePath: string): Promise<void> {
|
|
347
|
+
const lspServers = entry.lspServers;
|
|
348
|
+
if (!lspServers) return;
|
|
349
|
+
|
|
350
|
+
const targetPath = path.join(cachePath, ".lsp.json");
|
|
351
|
+
if (typeof lspServers === "string") {
|
|
352
|
+
const sourcePath = path.resolve(cachePath, lspServers);
|
|
353
|
+
if (!pathIsWithin(cachePath, sourcePath)) {
|
|
354
|
+
throw new Error(`Plugin "${entry.name}" lspServers path escapes the plugin directory`);
|
|
355
|
+
}
|
|
356
|
+
const content = await Bun.file(sourcePath).text();
|
|
357
|
+
await Bun.write(targetPath, content);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await Bun.write(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\n`);
|
|
362
|
+
}
|
|
363
|
+
|
|
345
364
|
/**
|
|
346
365
|
* Resolve plugin version from multiple sources:
|
|
347
366
|
* 1. Catalog entry version (if set)
|
package/src/hashline/parser.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ABORT_MARKER, ABORT_WARNING, BEGIN_PATCH_MARKER, END_PATCH_MARKER, RANGE_INTERIOR_HASH } from "./constants";
|
|
2
2
|
import {
|
|
3
|
+
computeLineHash,
|
|
3
4
|
describeAnchorExamples,
|
|
5
|
+
HL_BODY_SEP_RE_RAW,
|
|
4
6
|
HL_FILE_PREFIX,
|
|
5
7
|
HL_HASH_CAPTURE_RE_RAW,
|
|
6
8
|
HL_OP_CHARS,
|
|
@@ -10,7 +12,12 @@ import {
|
|
|
10
12
|
} from "./hash";
|
|
11
13
|
import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
// Leniently accept anchors copied from read/search output:
|
|
16
|
+
// - optional leading line-marker decoration (`*`, `>`, `+`, `-`)
|
|
17
|
+
// - the required `LINE+HASH`
|
|
18
|
+
// - an optional trailing `|TEXT` body (or anything after the hash) so users
|
|
19
|
+
// can paste a full `LINE+HASH|TEXT` line verbatim.
|
|
20
|
+
const LID_CAPTURE_RE = new RegExp(`^\\s*[>+\\-*]*\\s*${HL_HASH_CAPTURE_RE_RAW}(?:\\|.*)?\\s*$`);
|
|
14
21
|
const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
22
|
|
|
16
23
|
function parseLid(raw: string, lineNum: number): Anchor {
|
|
@@ -69,8 +76,36 @@ function parseInsertTarget(raw: string, lineNum: number, kind: "before" | "after
|
|
|
69
76
|
return { kind: cursorKind, anchor: parseLid(raw, lineNum) };
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Decide how to interpret the optional `|TEXT` body captured on an insert
|
|
81
|
+
* op line:
|
|
82
|
+
* - For BOF/EOF cursors the body is always treated as an inline payload
|
|
83
|
+
* line (there's no anchor hash to compare against).
|
|
84
|
+
* - For anchored cursors, compute the hash of TEXT at the anchor's line
|
|
85
|
+
* number. If it matches the anchor's hash, the body is just a verbatim
|
|
86
|
+
* copy of the anchored line — discard it, payload must come from the
|
|
87
|
+
* following lines as usual.
|
|
88
|
+
* - Otherwise the body is the first (or only) payload line for this op.
|
|
89
|
+
*/
|
|
90
|
+
function resolveInlineInsertBody(cursor: HashlineCursor, body: string | undefined): string | undefined {
|
|
91
|
+
if (body === undefined) return undefined;
|
|
92
|
+
if (cursor.kind !== "before_anchor" && cursor.kind !== "after_anchor") return body;
|
|
93
|
+
const { line, hash } = cursor.anchor;
|
|
94
|
+
if (computeLineHash(line, body) === hash) return undefined;
|
|
95
|
+
return body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Insert ops leniently accept a trailing `|TEXT` body on the op line itself
|
|
99
|
+
// (e.g. `»502zk|\tconst foo = ...`). The anchor token excludes `|` so the body
|
|
100
|
+
// is captured separately; resolveInlineInsertBody decides whether to treat the
|
|
101
|
+
// captured text as a verbatim anchor decoration (when its hash matches the
|
|
102
|
+
// anchor's) or as an inline payload line.
|
|
103
|
+
const INSERT_BEFORE_OP_RE = new RegExp(
|
|
104
|
+
`^${regexEscape(HL_OP_INSERT_BEFORE)}\\s*([^|\\s]+)(?:${HL_BODY_SEP_RE_RAW}(.*))?\\s*$`,
|
|
105
|
+
);
|
|
106
|
+
const INSERT_AFTER_OP_RE = new RegExp(
|
|
107
|
+
`^${regexEscape(HL_OP_INSERT_AFTER)}\\s*([^|\\s]+)(?:${HL_BODY_SEP_RE_RAW}(.*))?\\s*$`,
|
|
108
|
+
);
|
|
74
109
|
const REPLACE_OP_RE = new RegExp(`^${regexEscape(HL_OP_REPLACE)}\\s*([^\\s+<\\-=]\\S*)\\s*$`);
|
|
75
110
|
|
|
76
111
|
function isEnvelopeOrAbortMarkerLine(line: string): boolean {
|
|
@@ -153,7 +188,9 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
153
188
|
const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line);
|
|
154
189
|
if (insertBeforeMatch) {
|
|
155
190
|
const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
|
|
156
|
-
const
|
|
191
|
+
const inlineBody = resolveInlineInsertBody(cursor, insertBeforeMatch[2]);
|
|
192
|
+
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, inlineBody === undefined);
|
|
193
|
+
if (inlineBody !== undefined) pushInsert(cursor, inlineBody, lineNum);
|
|
157
194
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
158
195
|
i = nextIndex;
|
|
159
196
|
continue;
|
|
@@ -162,7 +199,9 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
162
199
|
const insertAfterMatch = INSERT_AFTER_OP_RE.exec(line);
|
|
163
200
|
if (insertAfterMatch) {
|
|
164
201
|
const cursor = parseInsertTarget(insertAfterMatch[1], lineNum, "after");
|
|
165
|
-
const
|
|
202
|
+
const inlineBody = resolveInlineInsertBody(cursor, insertAfterMatch[2]);
|
|
203
|
+
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, inlineBody === undefined);
|
|
204
|
+
if (inlineBody !== undefined) pushInsert(cursor, inlineBody, lineNum);
|
|
166
205
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
167
206
|
i = nextIndex;
|
|
168
207
|
continue;
|