@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.10
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 +23 -0
- package/dist/cli.js +2822 -2872
- package/dist/types/collab/host.d.ts +2 -2
- package/dist/types/collab/protocol.d.ts +4 -5
- package/dist/types/config/model-resolver.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +12 -2
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
- package/dist/types/tools/index.d.ts +9 -1
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/dist/types/utils/qrcode.d.ts +48 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -1
- package/src/collab/host.ts +4 -4
- package/src/collab/protocol.ts +48 -15
- package/src/config/config-file.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-registry.ts +16 -4
- package/src/config/model-resolver.ts +193 -35
- package/src/config/settings-schema.ts +14 -2
- package/src/config/settings.ts +3 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +2 -2
- package/src/modes/components/oauth-selector.ts +31 -2
- package/src/prompts/tools/inspect-image.md +1 -1
- package/src/sdk.ts +26 -7
- package/src/session/agent-session.ts +93 -14
- package/src/slash-commands/builtin-registry.ts +29 -11
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/thinking.ts +25 -5
- package/src/tools/index.ts +10 -1
- package/src/tools/inspect-image.ts +72 -9
- package/src/utils/file-mentions.ts +5 -2
- package/src/utils/image-loading.ts +58 -0
- package/src/utils/qrcode.ts +535 -0
|
@@ -16,14 +16,14 @@ export declare class CollabHost {
|
|
|
16
16
|
#private;
|
|
17
17
|
constructor(ctx: InteractiveModeContext);
|
|
18
18
|
get link(): string;
|
|
19
|
-
/** Browser deep link
|
|
19
|
+
/** Browser deep link for the configured collab web UI. */
|
|
20
20
|
get webLink(): string;
|
|
21
21
|
/** Read-only variant of {@link link}: bare room key, no write token. */
|
|
22
22
|
get viewLink(): string;
|
|
23
23
|
/** Read-only variant of {@link webLink}. */
|
|
24
24
|
get webViewLink(): string;
|
|
25
25
|
get participants(): CollabParticipant[];
|
|
26
|
-
start(relayUrl: string): Promise<void>;
|
|
26
|
+
start(relayUrl: string, webUrl?: string): Promise<void>;
|
|
27
27
|
/** Broadcast a goodbye, detach all taps, and close the socket. */
|
|
28
28
|
stop(reason: string): Promise<void>;
|
|
29
29
|
}
|
|
@@ -108,12 +108,11 @@ export declare function generateRoomId(): string;
|
|
|
108
108
|
*/
|
|
109
109
|
export declare function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Array, writeToken?: Uint8Array): string;
|
|
110
110
|
/**
|
|
111
|
-
* Render the browser deep link
|
|
112
|
-
* relay
|
|
113
|
-
* room
|
|
114
|
-
* Terminals auto-link the https form, making it click-to-join.
|
|
111
|
+
* Render the browser deep link. The browser UI may be hosted separately from
|
|
112
|
+
* the relay; the fragment always carries the relay-specific collab link, so
|
|
113
|
+
* room secrets stay out of HTTP path and query bytes.
|
|
115
114
|
*/
|
|
116
|
-
export declare function formatCollabWebLink(relayUrl: string, roomId: string, key: Uint8Array, writeToken?: Uint8Array): string;
|
|
115
|
+
export declare function formatCollabWebLink(relayUrl: string, roomId: string, key: Uint8Array, writeToken?: Uint8Array, webUrl?: string): string;
|
|
117
116
|
export declare function parseCollabLink(link: string): ParsedCollabLink | {
|
|
118
117
|
error: string;
|
|
119
118
|
};
|
|
@@ -32,11 +32,17 @@ export interface ScopedModel {
|
|
|
32
32
|
thinkingLevel?: ThinkingLevel;
|
|
33
33
|
explicitThinkingLevel: boolean;
|
|
34
34
|
}
|
|
35
|
+
interface ThinkingSuffixOptions {
|
|
36
|
+
allowMaxAlias?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface ModelStringParseOptions extends ThinkingSuffixOptions {
|
|
39
|
+
isLiteralModelId?: (provider: string, id: string) => boolean;
|
|
40
|
+
}
|
|
35
41
|
/**
|
|
36
42
|
* Parse a model string in "provider/modelId" format.
|
|
37
43
|
* Returns undefined if the format is invalid.
|
|
38
44
|
*/
|
|
39
|
-
export declare function parseModelString(modelStr: string): {
|
|
45
|
+
export declare function parseModelString(modelStr: string, options?: ModelStringParseOptions): {
|
|
40
46
|
provider: string;
|
|
41
47
|
id: string;
|
|
42
48
|
thinkingLevel?: ThinkingLevel;
|
|
@@ -115,7 +121,10 @@ export declare function resolveModelRoleValue(roleValue: string | undefined, ava
|
|
|
115
121
|
matchPreferences?: ModelMatchPreferences;
|
|
116
122
|
modelRegistry?: CanonicalModelRegistry;
|
|
117
123
|
}): ResolvedModelRoleValue;
|
|
118
|
-
|
|
124
|
+
interface ExplicitThinkingSelectorOptions {
|
|
125
|
+
isLiteralModelId?: (provider: string, id: string) => boolean;
|
|
126
|
+
}
|
|
127
|
+
export declare function extractExplicitThinkingSelector(value: string | undefined, settings?: Settings, options?: ExplicitThinkingSelectorOptions): ThinkingLevel | undefined;
|
|
119
128
|
/**
|
|
120
129
|
* Resolve a model identifier or pattern to a Model instance.
|
|
121
130
|
*/
|
|
@@ -835,7 +835,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
835
835
|
};
|
|
836
836
|
readonly defaultThinkingLevel: {
|
|
837
837
|
readonly type: "enum";
|
|
838
|
-
readonly values: readonly [...import("@oh-my-pi/pi-catalog").Effort[], "auto"];
|
|
838
|
+
readonly values: readonly [...import("@oh-my-pi/pi-catalog").Effort[], "auto", "max"];
|
|
839
839
|
readonly default: "high";
|
|
840
840
|
readonly ui: {
|
|
841
841
|
readonly tab: "model";
|
|
@@ -1579,6 +1579,16 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
1579
1579
|
readonly description: "Relay used by /collab (wss://host[:port])";
|
|
1580
1580
|
};
|
|
1581
1581
|
};
|
|
1582
|
+
readonly "collab.webUrl": {
|
|
1583
|
+
readonly type: "string";
|
|
1584
|
+
readonly default: "";
|
|
1585
|
+
readonly ui: {
|
|
1586
|
+
readonly tab: "interaction";
|
|
1587
|
+
readonly group: "Collab";
|
|
1588
|
+
readonly label: "Web UI URL";
|
|
1589
|
+
readonly description: "Browser UI used by /collab links; empty derives from collab.relayUrl; explicit http:// is localhost-only";
|
|
1590
|
+
};
|
|
1591
|
+
};
|
|
1582
1592
|
readonly "collab.displayName": {
|
|
1583
1593
|
readonly type: "string";
|
|
1584
1594
|
readonly default: "";
|
|
@@ -2012,7 +2022,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2012
2022
|
}, {
|
|
2013
2023
|
readonly value: "pi";
|
|
2014
2024
|
readonly label: "Pi";
|
|
2015
|
-
readonly description: "Use the Pi owned dialect.";
|
|
2025
|
+
readonly description: "Use the Pi owned dialect (compact sigil-delimited tool calls).";
|
|
2016
2026
|
}, {
|
|
2017
2027
|
readonly value: "qwen3";
|
|
2018
2028
|
readonly label: "Qwen3";
|
|
@@ -237,6 +237,13 @@ export interface AgentSessionConfig {
|
|
|
237
237
|
advisorReadOnlyTools?: AgentTool[];
|
|
238
238
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
239
239
|
advisorWatchdogPrompt?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Disconnect this session's OWNED MCP manager on dispose. Provided only when
|
|
242
|
+
* the session created the manager (top-level sessions); subagents reuse a
|
|
243
|
+
* parent's manager via `options.mcpManager` and omit this so a child's
|
|
244
|
+
* teardown never tears down the shared servers.
|
|
245
|
+
*/
|
|
246
|
+
disconnectOwnedMcpManager?: () => Promise<void>;
|
|
240
247
|
}
|
|
241
248
|
/** Options for AgentSession.prompt() */
|
|
242
249
|
export interface PromptOptions {
|
|
@@ -552,6 +559,12 @@ export declare class AgentSession {
|
|
|
552
559
|
get hasPostPromptWork(): boolean;
|
|
553
560
|
/** All messages including custom types like BashExecutionMessage */
|
|
554
561
|
get messages(): AgentMessage[];
|
|
562
|
+
/** Latest image attachments addressable by tools as `Image #N` or `attachment://N`. */
|
|
563
|
+
getImageAttachments(): {
|
|
564
|
+
label: string;
|
|
565
|
+
uri: string;
|
|
566
|
+
image: ImageContent;
|
|
567
|
+
}[];
|
|
555
568
|
buildDisplaySessionContext(): SessionContext;
|
|
556
569
|
/**
|
|
557
570
|
* Full-history transcript for TUI display: every path entry in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { BuiltinSlashCommand, ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime, SlashCommandSpec, TuiSlashCommandRuntime } from "./types";
|
|
3
3
|
export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
4
4
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Component } from "@oh-my-pi/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* One-shot transcript block that prints a collab browser-join URL as a
|
|
4
|
+
* scannable QR code. The symbol is encoded once at construction (byte mode,
|
|
5
|
+
* EC level M) and rendered as ANSI half-blocks; on terminals too narrow for
|
|
6
|
+
* the symbol it degrades to a one-line hint pointing at the printed URL.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CollabQrCodeComponent implements Component {
|
|
9
|
+
#private;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
constructor(url: string);
|
|
12
|
+
render(width: number): readonly string[];
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { FetchImpl, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { FetchImpl, ImageContent, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
5
5
|
import type { Rule } from "../capability/rule";
|
|
6
6
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
@@ -71,6 +71,12 @@ export type ContextFileEntry = {
|
|
|
71
71
|
content: string;
|
|
72
72
|
depth?: number;
|
|
73
73
|
};
|
|
74
|
+
/** Image attachment handle exposed to tools for user-facing labels such as `Image #1`. */
|
|
75
|
+
export type ImageAttachmentEntry = {
|
|
76
|
+
label: string;
|
|
77
|
+
uri: string;
|
|
78
|
+
image: ImageContent;
|
|
79
|
+
};
|
|
74
80
|
export type { DiscoverableTool, DiscoverableToolSearchIndex, DiscoverableToolSearchResult, DiscoverableToolSource, } from "../tool-discovery/tool-index";
|
|
75
81
|
/**
|
|
76
82
|
* A late LSP diagnostics result that arrived after the edit/write tool already
|
|
@@ -309,6 +315,8 @@ export interface ToolSession {
|
|
|
309
315
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
310
316
|
* the parent's tracer/hooks with the subagent's own identity stamped. */
|
|
311
317
|
getTelemetry?: () => AgentTelemetryConfig | undefined;
|
|
318
|
+
/** Return image attachments visible to tools for resolving labels such as `Image #1`. */
|
|
319
|
+
getImageAttachments?: () => ImageAttachmentEntry[];
|
|
312
320
|
}
|
|
313
321
|
export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
314
322
|
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
|
@@ -25,6 +25,16 @@ export interface LoadImageInputOptions {
|
|
|
25
25
|
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
|
|
26
26
|
excludeWebP?: boolean;
|
|
27
27
|
}
|
|
28
|
+
/** Options for loading an in-memory chat image attachment as a vision-model input. */
|
|
29
|
+
export interface LoadImageAttachmentInputOptions {
|
|
30
|
+
image: ImageContent;
|
|
31
|
+
label: string;
|
|
32
|
+
uri: string;
|
|
33
|
+
autoResize: boolean;
|
|
34
|
+
maxBytes?: number;
|
|
35
|
+
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
|
|
36
|
+
excludeWebP?: boolean;
|
|
37
|
+
}
|
|
28
38
|
export interface LoadedImageInput {
|
|
29
39
|
resolvedPath: string;
|
|
30
40
|
mimeType: string;
|
|
@@ -53,3 +63,5 @@ export interface NormalizeModelContextImagesOptions {
|
|
|
53
63
|
*/
|
|
54
64
|
export declare function normalizeModelContextImages(images: ImageContent[] | undefined, options?: NormalizeModelContextImagesOptions): Promise<ImageContent[] | undefined>;
|
|
55
65
|
export declare function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null>;
|
|
66
|
+
/** Loads a chat attachment image through the same size and encoder policy as file-backed image inputs. */
|
|
67
|
+
export declare function loadImageAttachmentInput(options: LoadImageAttachmentInputOptions): Promise<LoadedImageInput | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained QR Code generator (byte mode, versions 1-40, EC levels
|
|
3
|
+
* L/M/Q/H) with a half-block ANSI terminal renderer.
|
|
4
|
+
*
|
|
5
|
+
* Pure TypeScript, zero dependencies: the collab `/collab qrcode` command uses
|
|
6
|
+
* it to print scannable browser-join codes without pulling a runtime QR
|
|
7
|
+
* package into the bundle. The algorithm follows ISO/IEC 18004; the two
|
|
8
|
+
* error-correction tables below are direct transcriptions of that spec.
|
|
9
|
+
*/
|
|
10
|
+
export type QrEcLevel = "L" | "M" | "Q" | "H";
|
|
11
|
+
export interface QrEncodeOptions {
|
|
12
|
+
/** Lowest version to consider (default 1). */
|
|
13
|
+
minVersion?: number;
|
|
14
|
+
/** Highest version to consider (default 40). */
|
|
15
|
+
maxVersion?: number;
|
|
16
|
+
/** Force a mask 0-7; -1 (default) auto-selects the lowest-penalty mask. */
|
|
17
|
+
mask?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A finished QR symbol: a square grid of dark/light modules plus the chosen
|
|
21
|
+
* version, EC level, and mask. `module(x, y)` is the only access path the
|
|
22
|
+
* renderers need.
|
|
23
|
+
*/
|
|
24
|
+
export declare class QrCode {
|
|
25
|
+
#private;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly ecLevel: QrEcLevel;
|
|
28
|
+
readonly size: number;
|
|
29
|
+
/** Selected mask pattern (0-7). */
|
|
30
|
+
readonly mask: number;
|
|
31
|
+
private constructor();
|
|
32
|
+
module(x: number, y: number): boolean;
|
|
33
|
+
/** Encode a string in byte mode (UTF-8). Throws if it exceeds version 40. */
|
|
34
|
+
static encodeText(text: string, ecLevel?: QrEcLevel, options?: QrEncodeOptions): QrCode;
|
|
35
|
+
/** Encode raw bytes in byte mode. Throws if they exceed version 40 at this EC level. */
|
|
36
|
+
static encodeBytes(data: Uint8Array, ecLevel?: QrEcLevel, options?: QrEncodeOptions): QrCode;
|
|
37
|
+
}
|
|
38
|
+
export interface QrRenderOptions {
|
|
39
|
+
/** Quiet-zone width in modules on every side (default 4, per spec). */
|
|
40
|
+
margin?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Render a QR symbol as ANSI half-block rows: each text row packs two module
|
|
44
|
+
* rows via `▀`/`▄`/`█`, drawn black-on-white so a phone camera reads dark
|
|
45
|
+
* modules as data and the quiet zone as the light margin. The leading margin
|
|
46
|
+
* makes the symbol scannable regardless of the terminal's own background.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderQrHalfBlocks(qr: QrCode, options?: QrRenderOptions): string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.10",
|
|
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",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.0.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.0.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.0.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.0.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.0.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.0.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.0.
|
|
51
|
+
"@oh-my-pi/hashline": "16.0.10",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.0.10",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.0.10",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.0.10",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.0.10",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.0.10",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.0.10",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.0.10",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.0.10",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.0.10",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.0.10",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/cli/args.ts
CHANGED
|
@@ -214,7 +214,13 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
214
214
|
} else if (arg === "--auto-approve" || arg === "--yolo") {
|
|
215
215
|
result.autoApprove = true;
|
|
216
216
|
} else if (arg.startsWith("@")) {
|
|
217
|
-
|
|
217
|
+
let filePath = arg.slice(1);
|
|
218
|
+
if (filePath.startsWith('"') && filePath.endsWith('"') && filePath.length > 1) {
|
|
219
|
+
filePath = filePath.slice(1, -1);
|
|
220
|
+
} else if (filePath.startsWith("'") && filePath.endsWith("'") && filePath.length > 1) {
|
|
221
|
+
filePath = filePath.slice(1, -1);
|
|
222
|
+
}
|
|
223
|
+
result.fileArgs.push(filePath);
|
|
218
224
|
} else if (!arg.startsWith("-") || arg === "-") {
|
|
219
225
|
// Plain positional or lone `-` (stdin marker) — pass through as a
|
|
220
226
|
// message rather than flagging it.
|
package/src/collab/host.ts
CHANGED
|
@@ -133,7 +133,7 @@ export class CollabHost {
|
|
|
133
133
|
return this.#link;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** Browser deep link
|
|
136
|
+
/** Browser deep link for the configured collab web UI. */
|
|
137
137
|
get webLink(): string {
|
|
138
138
|
return this.#webLink;
|
|
139
139
|
}
|
|
@@ -156,15 +156,15 @@ export class CollabHost {
|
|
|
156
156
|
return list;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async start(relayUrl: string): Promise<void> {
|
|
159
|
+
async start(relayUrl: string, webUrl = ""): Promise<void> {
|
|
160
160
|
const rawKey = generateRoomKey();
|
|
161
161
|
const writeToken = generateWriteToken();
|
|
162
162
|
const roomId = generateRoomId();
|
|
163
163
|
this.#writeToken = writeToken;
|
|
164
164
|
this.#link = formatCollabLink(relayUrl, roomId, rawKey, writeToken);
|
|
165
|
-
this.#webLink = formatCollabWebLink(relayUrl, roomId, rawKey, writeToken);
|
|
165
|
+
this.#webLink = formatCollabWebLink(relayUrl, roomId, rawKey, writeToken, webUrl);
|
|
166
166
|
this.#viewLink = formatCollabLink(relayUrl, roomId, rawKey);
|
|
167
|
-
this.#webViewLink = formatCollabWebLink(relayUrl, roomId, rawKey);
|
|
167
|
+
this.#webViewLink = formatCollabWebLink(relayUrl, roomId, rawKey, undefined, webUrl);
|
|
168
168
|
const parsed = parseCollabLink(this.#link);
|
|
169
169
|
if ("error" in parsed) throw new Error(parsed.error);
|
|
170
170
|
const key = await importRoomKey(rawKey);
|
package/src/collab/protocol.ts
CHANGED
|
@@ -116,6 +116,10 @@ const BARE_LINK_RE = /^([A-Za-z0-9_-]{10,64})[#.]([A-Za-z0-9_-]+)$/;
|
|
|
116
116
|
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
117
117
|
const LOCAL_HOSTNAMES: Record<string, true> = { localhost: true, "127.0.0.1": true, "::1": true, "[::1]": true };
|
|
118
118
|
|
|
119
|
+
function isLocalHostname(hostname: string): boolean {
|
|
120
|
+
return LOCAL_HOSTNAMES[hostname] === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
119
123
|
export function generateRoomId(): string {
|
|
120
124
|
const bytes = new Uint8Array(ROOM_ID_BYTES);
|
|
121
125
|
crypto.getRandomValues(bytes);
|
|
@@ -143,7 +147,7 @@ function normalizeRelayOrigin(relayUrl: string): { origin: string } | { error: s
|
|
|
143
147
|
default:
|
|
144
148
|
return { error: `Unsupported relay URL scheme: ${url.protocol}` };
|
|
145
149
|
}
|
|
146
|
-
if (scheme === "ws:" && !
|
|
150
|
+
if (scheme === "ws:" && !isLocalHostname(url.hostname)) {
|
|
147
151
|
return { error: "relay link must be wss:// (plain ws:// is only allowed for localhost)" };
|
|
148
152
|
}
|
|
149
153
|
const port = url.port ? `:${url.port}` : "";
|
|
@@ -178,24 +182,48 @@ export function formatCollabLink(relayUrl: string, roomId: string, key: Uint8Arr
|
|
|
178
182
|
return `${compact}/r/${roomId}.${keyText}`;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
function normalizeCollabWebBaseUrl(relayUrl: string, webUrl?: string): string {
|
|
186
|
+
const explicitWebUrl = webUrl?.trim();
|
|
187
|
+
if (!explicitWebUrl) {
|
|
188
|
+
const normalized = normalizeRelayOrigin(relayUrl);
|
|
189
|
+
if ("error" in normalized) throw new Error(normalized.error);
|
|
190
|
+
return normalized.origin.startsWith("wss://")
|
|
191
|
+
? `https://${normalized.origin.slice("wss://".length)}`
|
|
192
|
+
: `http://${normalized.origin.slice("ws://".length)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let url: URL;
|
|
196
|
+
try {
|
|
197
|
+
url = new URL(explicitWebUrl);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new Error("collab.webUrl must start with http:// or https://");
|
|
200
|
+
}
|
|
201
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
202
|
+
throw new Error("collab.webUrl must start with http:// or https://");
|
|
203
|
+
}
|
|
204
|
+
if (url.protocol === "http:" && !isLocalHostname(url.hostname)) {
|
|
205
|
+
throw new Error("collab.webUrl must use https:// unless it targets localhost");
|
|
206
|
+
}
|
|
207
|
+
if (url.search || url.hash) {
|
|
208
|
+
throw new Error("collab.webUrl must not include a query string or fragment");
|
|
209
|
+
}
|
|
210
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
211
|
+
return `${url.origin}${path}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
181
214
|
/**
|
|
182
|
-
* Render the browser deep link
|
|
183
|
-
* relay
|
|
184
|
-
* room
|
|
185
|
-
* Terminals auto-link the https form, making it click-to-join.
|
|
215
|
+
* Render the browser deep link. The browser UI may be hosted separately from
|
|
216
|
+
* the relay; the fragment always carries the relay-specific collab link, so
|
|
217
|
+
* room secrets stay out of HTTP path and query bytes.
|
|
186
218
|
*/
|
|
187
219
|
export function formatCollabWebLink(
|
|
188
220
|
relayUrl: string,
|
|
189
221
|
roomId: string,
|
|
190
222
|
key: Uint8Array,
|
|
191
223
|
writeToken?: Uint8Array,
|
|
224
|
+
webUrl?: string,
|
|
192
225
|
): string {
|
|
193
|
-
|
|
194
|
-
if ("error" in normalized) throw new Error(normalized.error);
|
|
195
|
-
const httpOrigin = normalized.origin.startsWith("wss://")
|
|
196
|
-
? `https://${normalized.origin.slice("wss://".length)}`
|
|
197
|
-
: `http://${normalized.origin.slice("ws://".length)}`;
|
|
198
|
-
return `${httpOrigin}/#${formatCollabLink(relayUrl, roomId, key, writeToken)}`;
|
|
226
|
+
return `${normalizeCollabWebBaseUrl(relayUrl, webUrl)}/#${formatCollabLink(relayUrl, roomId, key, writeToken)}`;
|
|
199
227
|
}
|
|
200
228
|
|
|
201
229
|
export function parseCollabLink(link: string): ParsedCollabLink | { error: string } {
|
|
@@ -213,15 +241,20 @@ export function parseCollabLink(link: string): ParsedCollabLink | { error: strin
|
|
|
213
241
|
} catch {
|
|
214
242
|
return { error: `Invalid collab link: ${link}` };
|
|
215
243
|
}
|
|
244
|
+
if ((url.protocol === "http:" || url.protocol === "https:") && url.hash) {
|
|
245
|
+
const inner = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
246
|
+
const parsed = parseCollabLink(inner);
|
|
247
|
+
if (!("error" in parsed)) return parsed;
|
|
248
|
+
}
|
|
216
249
|
const normalized = normalizeRelayOrigin(url.origin);
|
|
217
250
|
if ("error" in normalized) return normalized;
|
|
218
251
|
const match = ROOM_PATH_RE.exec(url.pathname);
|
|
219
252
|
if (!match) {
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
253
|
+
// Non-http(s) deep links may also carry a complete collab link in the
|
|
254
|
+
// fragment. http(s) links are handled once above so invalid fragments
|
|
255
|
+
// fall through to direct relay validation instead of double-recursing.
|
|
223
256
|
const inner = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
224
|
-
if (inner) return parseCollabLink(inner);
|
|
257
|
+
if (inner && url.protocol !== "http:" && url.protocol !== "https:") return parseCollabLink(inner);
|
|
225
258
|
return { error: "Collab link must contain a /r/<roomId> path" };
|
|
226
259
|
}
|
|
227
260
|
const roomId = match[1]!;
|
|
@@ -40,7 +40,7 @@ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const content = fs.readFileSync(jsonPath, "utf-8");
|
|
43
|
-
const parsed =
|
|
43
|
+
const parsed = JSONC.parse(content);
|
|
44
44
|
if (!parsed) {
|
|
45
45
|
logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
|
|
46
46
|
migratedPaths.add(key);
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
KeybindingsManager as TuiKeybindingsManager,
|
|
11
11
|
} from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
-
import { YAML } from "bun";
|
|
13
|
+
import { JSONC, YAML } from "bun";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Application-level keybindings (coding agent specific).
|
|
@@ -381,7 +381,7 @@ function loadRawConfig(filePath: string): unknown {
|
|
|
381
381
|
try {
|
|
382
382
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
383
383
|
if (filePath.endsWith(".json")) {
|
|
384
|
-
return
|
|
384
|
+
return JSONC.parse(content);
|
|
385
385
|
}
|
|
386
386
|
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
|
|
387
387
|
return YAML.parse(content);
|
|
@@ -563,10 +563,16 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
563
563
|
} as ModelSpec<Api>);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
-
function normalizeSuppressedSelector(
|
|
566
|
+
function normalizeSuppressedSelector(
|
|
567
|
+
selector: string,
|
|
568
|
+
hasLiveModel?: (provider: string, id: string) => boolean,
|
|
569
|
+
): string {
|
|
567
570
|
const trimmed = selector.trim();
|
|
568
571
|
if (!trimmed) return trimmed;
|
|
569
|
-
const parsed = parseModelString(trimmed
|
|
572
|
+
const parsed = parseModelString(trimmed, {
|
|
573
|
+
allowMaxAlias: true,
|
|
574
|
+
isLiteralModelId: (provider, id) => hasLiveModel?.(provider, id) === true,
|
|
575
|
+
});
|
|
570
576
|
if (!parsed) return trimmed;
|
|
571
577
|
// Retired effort-tier variant ids normalize to their collapsed logical id
|
|
572
578
|
// so persisted suppressions keyed by raw member ids still bind.
|
|
@@ -2155,14 +2161,20 @@ export class ModelRegistry {
|
|
|
2155
2161
|
* Suppress a specific model selector (e.g., "provider/id") until a specific timestamp.
|
|
2156
2162
|
*/
|
|
2157
2163
|
suppressSelector(selector: string, untilMs: number): void {
|
|
2158
|
-
this.#suppressedSelectors.set(
|
|
2164
|
+
this.#suppressedSelectors.set(
|
|
2165
|
+
normalizeSuppressedSelector(selector, (provider, id) => this.find(provider, id) !== undefined),
|
|
2166
|
+
untilMs,
|
|
2167
|
+
);
|
|
2159
2168
|
}
|
|
2160
2169
|
|
|
2161
2170
|
/**
|
|
2162
2171
|
* Check if a model selector is currently suppressed due to rate limits.
|
|
2163
2172
|
*/
|
|
2164
2173
|
isSelectorSuppressed(selector: string): boolean {
|
|
2165
|
-
const normalizedSelector = normalizeSuppressedSelector(
|
|
2174
|
+
const normalizedSelector = normalizeSuppressedSelector(
|
|
2175
|
+
selector,
|
|
2176
|
+
(provider, id) => this.find(provider, id) !== undefined,
|
|
2177
|
+
);
|
|
2166
2178
|
const suppressedUntil = this.#suppressedSelectors.get(normalizedSelector);
|
|
2167
2179
|
if (!suppressedUntil) return false;
|
|
2168
2180
|
if (suppressedUntil <= Date.now()) {
|