@oh-my-pi/pi-coding-agent 17.0.5 → 17.0.6
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 +166 -0
- package/dist/cli.js +2838 -2917
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/modes/components/status-line/component.d.ts +8 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
- package/dist/types/modes/setup-wizard/index.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +23 -5
- package/dist/types/session/session-paths.d.ts +21 -4
- package/dist/types/tools/xdev.d.ts +5 -3
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
- package/dist/types/vibe/runtime.d.ts +23 -0
- package/package.json +12 -12
- package/src/exec/bash-executor.ts +68 -8
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
- package/src/main.ts +8 -0
- package/src/modes/components/chat-transcript-builder.ts +10 -1
- package/src/modes/components/status-line/component.ts +55 -1
- package/src/modes/components/usage-row.ts +17 -1
- package/src/modes/controllers/command-controller.ts +41 -1
- package/src/modes/controllers/event-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +62 -52
- package/src/modes/noninteractive-dispose.test.ts +2 -0
- package/src/modes/print-mode.ts +60 -0
- package/src/modes/rpc/rpc-client.ts +76 -35
- package/src/modes/rpc/rpc-frame.ts +156 -0
- package/src/modes/rpc/rpc-mode.ts +4 -2
- package/src/modes/setup-wizard/index.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +134 -0
- package/src/modes/utils/ui-helpers.ts +6 -1
- package/src/prompts/system/workflow-notice.md +89 -74
- package/src/session/agent-session.ts +89 -26
- package/src/session/session-manager.ts +34 -3
- package/src/session/session-paths.ts +38 -9
- package/src/task/executor.ts +45 -0
- package/src/tools/xdev.ts +11 -4
- package/src/tools/yield.ts +4 -1
- package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
- package/src/vibe/__tests__/token-rate.test.ts +96 -0
- package/src/vibe/runtime.ts +50 -0
- /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
|
@@ -45,4 +45,5 @@ export interface BashResult {
|
|
|
45
45
|
}
|
|
46
46
|
/** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */
|
|
47
47
|
export declare function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined;
|
|
48
|
+
export declare function isPersistentShellCdCommand(command: string): boolean;
|
|
48
49
|
export declare function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult>;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
declare const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
|
|
3
|
+
interface BundledVirtualResolveResult {
|
|
4
|
+
path: string;
|
|
5
|
+
namespace: typeof BUNDLED_VIRTUAL_NAMESPACE;
|
|
6
|
+
}
|
|
7
|
+
/** Maps a bundled virtual specifier or registry key to Bun's plugin namespace shape. */
|
|
8
|
+
export declare function resolveBundledVirtualSpecifier(specifier: string): BundledVirtualResolveResult;
|
|
2
9
|
/** Test seam for the virtual module's named/default export forwarding. */
|
|
3
10
|
export declare function __synthesizeLegacyPiBundledSourceWithModules(moduleKey: string, modules: Readonly<Record<string, Readonly<Record<string, unknown>>>>): string;
|
|
4
11
|
/** Test seam for the global bridge key shared with synthetic module source. */
|
|
@@ -65,3 +72,4 @@ export declare function loadLegacyPiModule(resolvedPath: string): Promise<unknow
|
|
|
65
72
|
export declare function installLegacyPiSpecifierShim(): void;
|
|
66
73
|
/** Test seam: clears the memoized canonical specifier resolutions. */
|
|
67
74
|
export declare function __resetLegacyPiResolutionCache(): void;
|
|
75
|
+
export {};
|
|
@@ -64,6 +64,14 @@ export declare class StatusLineComponent implements Component {
|
|
|
64
64
|
setVibeModeStatus(status: {
|
|
65
65
|
enabled: boolean;
|
|
66
66
|
} | undefined): void;
|
|
67
|
+
/**
|
|
68
|
+
* Inject the aggregator that returns the aggregate tok/s of this session's
|
|
69
|
+
* live vibe worker sessions (null when no workers are streaming). Wired by
|
|
70
|
+
* interactive-mode, which owns the VibeSessionRegistry coupling, so the
|
|
71
|
+
* render layer stays off the heavy vibe/task dependency graph. Pass
|
|
72
|
+
* `undefined` to clear.
|
|
73
|
+
*/
|
|
74
|
+
setVibeWorkerTokenRateProvider(provider: (() => number | null) | undefined): void;
|
|
67
75
|
setCollabStatus(status: CollabStatus | null): void;
|
|
68
76
|
setHookStatus(key: string, text: string | undefined): void;
|
|
69
77
|
watchBranch(onBranchChange: () => void): void;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container } from "@oh-my-pi/pi-tui";
|
|
3
|
-
export declare function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number): Container;
|
|
3
|
+
export declare function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number): Container;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Maximum UTF-8 size of one newline-delimited RPC frame, including the newline. */
|
|
2
|
+
export declare const MAX_RPC_FRAME_BYTES: number;
|
|
3
|
+
/** Serialize a complete JSONL frame while enforcing the transport byte ceiling. */
|
|
4
|
+
export declare function encodeRpcFrame(frame: object, streamedMessageCount?: number, streamedMessages?: readonly unknown[]): string;
|
|
5
|
+
/** Stateful encoder that tracks which messages a client has already received. */
|
|
6
|
+
export declare class RpcFrameEncoder {
|
|
7
|
+
#private;
|
|
8
|
+
encode(frame: object): string;
|
|
9
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { SetupScene } from "./scenes/types.js";
|
|
|
5
5
|
export type { SetupScene, SetupSceneController, SetupSceneHost, SetupSceneResult } from "./scenes/types.js";
|
|
6
6
|
export { runStartupSplash } from "./startup-splash.js";
|
|
7
7
|
export { CURRENT_SETUP_VERSION };
|
|
8
|
-
export declare const ALL_SCENES: readonly [SetupScene, SetupScene, SetupScene];
|
|
8
|
+
export declare const ALL_SCENES: readonly [SetupScene, SetupScene, SetupScene, SetupScene];
|
|
9
9
|
export interface SetupSceneSelectionOptions {
|
|
10
10
|
resuming?: boolean;
|
|
11
11
|
isTTY?: boolean;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
16
|
-
import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type StreamFn, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
|
|
16
|
+
import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type AgentToolResult, type StreamFn, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
|
|
17
17
|
import { type CompactionResult, type ShakeConfig } from "@oh-my-pi/pi-agent-core/compaction";
|
|
18
18
|
import type { AssistantMessage, Context, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget, ServiceTier, ServiceTierByFamily, ServiceTierFamily, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
|
|
19
19
|
import { Effort } from "@oh-my-pi/pi-ai";
|
|
@@ -41,6 +41,7 @@ import type { Goal, GoalModeState } from "../goals/state.js";
|
|
|
41
41
|
import type { HindsightSessionState } from "../hindsight/state.js";
|
|
42
42
|
import { type IrcMessage } from "../irc/bus.js";
|
|
43
43
|
import { type MnemopiSessionState } from "../mnemopi/state.js";
|
|
44
|
+
import { type PlanApprovalDetails } from "../plan-mode/approved-plan.js";
|
|
44
45
|
import type { PlanModeState } from "../plan-mode/state.js";
|
|
45
46
|
import { type SecretObfuscator } from "../secrets/obfuscator.js";
|
|
46
47
|
import { type ConfiguredThinkingLevel } from "../thinking.js";
|
|
@@ -542,6 +543,8 @@ export declare class AgentSession {
|
|
|
542
543
|
* is already armed and waiting.
|
|
543
544
|
*/
|
|
544
545
|
armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): void;
|
|
546
|
+
/** Validate the active plan artifact and shape an `xd://propose` result for review-mode hosts. */
|
|
547
|
+
preparePlanForReview(title: string): Promise<AgentToolResult<PlanApprovalDetails>>;
|
|
545
548
|
constructor(config: AgentSessionConfig);
|
|
546
549
|
/** Model registry for API key resolution and model discovery */
|
|
547
550
|
get modelRegistry(): ModelRegistry;
|
|
@@ -730,6 +733,21 @@ export declare class AgentSession {
|
|
|
730
733
|
* Changes take effect before the next model call.
|
|
731
734
|
*/
|
|
732
735
|
setActiveToolsByName(toolNames: string[]): Promise<void>;
|
|
736
|
+
/**
|
|
737
|
+
* Restore an enabled tool set with its exact top-level versus `xd://` partition.
|
|
738
|
+
*
|
|
739
|
+
* Both inputs are required because {@link setActiveToolsByName} only receives the
|
|
740
|
+
* enabled name list and classifies mounts from the *current* `#mountedXdevToolNames`.
|
|
741
|
+
* Rollback/restore callers must pass the snapshotted mounted subset so names that
|
|
742
|
+
* were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
|
|
743
|
+
* `xd://` remain mount-eligible, even when the live mount set has drifted.
|
|
744
|
+
*
|
|
745
|
+
* Names outside `mountedToolNames` are pinned top-level for this application;
|
|
746
|
+
* names in the mounted subset remain eligible for xdev mounting. Delegates the
|
|
747
|
+
* actual apply through `#applyActiveToolsByName` and restores the prior runtime
|
|
748
|
+
* selection if that apply throws.
|
|
749
|
+
*/
|
|
750
|
+
setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void>;
|
|
733
751
|
/** Rebuild the base system prompt using the current active tool set. */
|
|
734
752
|
refreshBaseSystemPrompt(): Promise<void>;
|
|
735
753
|
/**
|
|
@@ -1054,10 +1072,10 @@ export declare class AgentSession {
|
|
|
1054
1072
|
*/
|
|
1055
1073
|
getAvailableModels(): Model[];
|
|
1056
1074
|
/**
|
|
1057
|
-
* Set the thinking level. `auto` enables per-turn classification
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1075
|
+
* Set the thinking level. `auto` enables per-turn classification. Entering
|
|
1076
|
+
* auto writes its provisional level plus `configured: "auto"` immediately,
|
|
1077
|
+
* giving external readers an authoritative selection receipt before the next
|
|
1078
|
+
* user turn. Later classifications persist only changed concrete resolutions.
|
|
1061
1079
|
*/
|
|
1062
1080
|
setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist?: boolean): void;
|
|
1063
1081
|
/**
|
|
@@ -10,16 +10,33 @@ export declare function computeDefaultSessionDir(cwd: string, storage: SessionSt
|
|
|
10
10
|
* Write a breadcrumb linking the current terminal to a session file.
|
|
11
11
|
* The breadcrumb contains the cwd and session path so --continue can
|
|
12
12
|
* find "this terminal's last session" even when running concurrent instances.
|
|
13
|
+
*
|
|
14
|
+
* `fresh` marks a `/new` (or freshly-minted) session boundary whose JSONL is
|
|
15
|
+
* not yet materialized (new-session persistence is lazy until assistant output
|
|
16
|
+
* exists). A fresh breadcrumb is honored by {@link readTerminalBreadcrumbEntry}
|
|
17
|
+
* even when its target file is still absent, so relaunch/auto-resume reopens the
|
|
18
|
+
* post-`/new` session instead of falling back to the pre-`/new` transcript. Once
|
|
19
|
+
* the session materializes the caller rewrites the breadcrumb with `fresh:false`
|
|
20
|
+
* so a later external delete is still treated as a genuinely stale crumb.
|
|
13
21
|
*/
|
|
14
|
-
export declare function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void;
|
|
22
|
+
export declare function writeTerminalBreadcrumb(cwd: string, sessionFile: string, fresh?: boolean): void;
|
|
15
23
|
export interface TerminalBreadcrumb {
|
|
16
24
|
cwd: string;
|
|
17
25
|
sessionFile: string;
|
|
26
|
+
/** The recorded session file exists on disk right now. */
|
|
27
|
+
exists: boolean;
|
|
28
|
+
/** Recorded as a `/new` fresh-session boundary whose JSONL may not exist yet. */
|
|
29
|
+
fresh: boolean;
|
|
18
30
|
}
|
|
19
31
|
/**
|
|
20
32
|
* Read the raw terminal breadcrumb for the current terminal.
|
|
21
|
-
* Returns the recorded cwd + session file
|
|
22
|
-
*
|
|
23
|
-
*
|
|
33
|
+
* Returns the recorded cwd + session file regardless of whether the recorded
|
|
34
|
+
* cwd still matches the current one. Callers decide how to interpret a cwd
|
|
35
|
+
* mismatch (e.g. a moved/renamed worktree).
|
|
36
|
+
*
|
|
37
|
+
* A missing target file yields `null` UNLESS the breadcrumb is a `fresh`
|
|
38
|
+
* boundary — a lazy `/new` session whose JSONL was never written — in which case
|
|
39
|
+
* the entry is returned with `exists:false` so the caller can distinguish it
|
|
40
|
+
* from a genuinely stale/deleted breadcrumb.
|
|
24
41
|
*/
|
|
25
42
|
export declare function readTerminalBreadcrumbEntry(): Promise<TerminalBreadcrumb | null>;
|
|
@@ -32,9 +32,11 @@ import type { ToolRenderer } from "./renderers.js";
|
|
|
32
32
|
/**
|
|
33
33
|
* Discoverable built-ins that must stay top-level even when xdev mounting is
|
|
34
34
|
* active: `todo` feeds the todo prelude/prewalk machinery, `ask` is the
|
|
35
|
-
* model's user-interaction affordance,
|
|
36
|
-
*
|
|
37
|
-
* behind dispatch
|
|
35
|
+
* model's user-interaction affordance, `grep` is the redirect target of the
|
|
36
|
+
* bash interceptor rules, and `web_search` is invoked directly by most models
|
|
37
|
+
* (which have no notion of the `xd://` protocol) so hiding it behind dispatch
|
|
38
|
+
* makes it unreachable in practice (issue #5973) — each loses its harness
|
|
39
|
+
* integration or usability if hidden behind dispatch.
|
|
38
40
|
*/
|
|
39
41
|
export declare const XDEV_KEEP_TOP_LEVEL: Record<string, true>;
|
|
40
42
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -83,6 +83,17 @@ export declare class VibeSessionRegistry {
|
|
|
83
83
|
static global(): VibeSessionRegistry;
|
|
84
84
|
/** Reset the global registry. Test-only. */
|
|
85
85
|
static resetGlobalForTests(): void;
|
|
86
|
+
/**
|
|
87
|
+
* Insert a bare worker record without the spawn/job machinery. Test-only —
|
|
88
|
+
* lets {@link aggregateVibeWorkerTokensPerSecond} be exercised against a
|
|
89
|
+
* fake roster + AgentRegistry session without driving a real turn.
|
|
90
|
+
*/
|
|
91
|
+
registerRecordForTests(record: {
|
|
92
|
+
id: string;
|
|
93
|
+
cli?: VibeCli;
|
|
94
|
+
ownerId: string;
|
|
95
|
+
state?: VibeSessionState;
|
|
96
|
+
}): void;
|
|
86
97
|
listIds(owner: string): string[];
|
|
87
98
|
/**
|
|
88
99
|
* Live screen snapshots for rich rendering (the "TV wall"): one entry per
|
|
@@ -122,3 +133,15 @@ export declare class VibeSessionRegistry {
|
|
|
122
133
|
/** Kill every session belonging to `owner` (vibe-mode exit / teardown). Returns the number killed. */
|
|
123
134
|
killAll(owner: string, manager?: AsyncJobManager): Promise<number>;
|
|
124
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Aggregate tok/s across every live vibe worker session owned by `ownerId`.
|
|
138
|
+
* Returns null when no workers are streaming (so callers can fall back to
|
|
139
|
+
* their own rate unchanged). The director is often idle while workers stream,
|
|
140
|
+
* so without this aggregation the status-line tok/s badge would show a stale
|
|
141
|
+
* value while parallel work is actively generating tokens.
|
|
142
|
+
*
|
|
143
|
+
* Reads each worker's last assistant message via {@link calculateTokensPerSecond}
|
|
144
|
+
* — the same leaf calculator the main status line uses — so worker rates are
|
|
145
|
+
* computed identically to the main session's rate.
|
|
146
|
+
*/
|
|
147
|
+
export declare function aggregateVibeWorkerTokensPerSecond(ownerId: string): number | null;
|
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": "17.0.
|
|
4
|
+
"version": "17.0.6",
|
|
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",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.0.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.6",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.6",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.6",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.6",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.6",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.6",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.6",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.6",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.6",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.6",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.6",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -150,9 +150,56 @@ function isBashShell(shell: string): boolean {
|
|
|
150
150
|
return basename.includes("bash");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const UNSUPPORTED_UNQUOTED_CD_CHARS = "\\$`;&|<>(){}*?[]!#\"'";
|
|
154
|
+
|
|
155
|
+
function hasUnsupportedUnquotedCdSyntax(value: string): boolean {
|
|
156
|
+
for (const char of value) {
|
|
157
|
+
if (/\s/.test(char) || UNSUPPORTED_UNQUOTED_CD_CHARS.includes(char)) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isPersistentShellCdCommand(command: string): boolean {
|
|
163
|
+
if (/[\r\n]/.test(command)) return false;
|
|
164
|
+
|
|
165
|
+
const trimmed = command.trim();
|
|
166
|
+
if (trimmed === "cd") return true;
|
|
167
|
+
if (!trimmed.startsWith("cd") || !/[ \t]/.test(trimmed[2] ?? "")) return false;
|
|
168
|
+
|
|
169
|
+
let rest = trimmed.slice(2).trim();
|
|
170
|
+
if (rest === "" || rest === "--") return true;
|
|
171
|
+
|
|
172
|
+
let hasOptionTerminator = false;
|
|
173
|
+
if (/^--[ \t]/.test(rest)) {
|
|
174
|
+
hasOptionTerminator = true;
|
|
175
|
+
rest = rest.slice(2).trimStart();
|
|
176
|
+
}
|
|
177
|
+
if (rest === "") return true;
|
|
178
|
+
|
|
179
|
+
const quote = rest[0];
|
|
180
|
+
let target: string;
|
|
181
|
+
let quoted = false;
|
|
182
|
+
if (quote === `"` || quote === "'") {
|
|
183
|
+
if (rest.length < 2 || rest[rest.length - 1] !== quote) return false;
|
|
184
|
+
target = rest.slice(1, -1);
|
|
185
|
+
if (target.includes(quote)) return false;
|
|
186
|
+
if (quote === `"` && /[\\$`\r\n]/.test(target)) return false;
|
|
187
|
+
quoted = true;
|
|
188
|
+
} else {
|
|
189
|
+
if (hasUnsupportedUnquotedCdSyntax(rest)) return false;
|
|
190
|
+
target = rest;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (target === "") return false;
|
|
194
|
+
if (/^[+-]\d+$/.test(target)) return false;
|
|
195
|
+
if (!hasOptionTerminator && target.startsWith("-") && target !== "-") return false;
|
|
196
|
+
if (!quoted && target.startsWith("~") && target !== "~" && !target.startsWith("~/")) return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
153
200
|
function needsInteractiveShellArg(shell: string): boolean {
|
|
154
201
|
const basename = shellBasename(shell);
|
|
155
|
-
return basename.includes("zsh");
|
|
202
|
+
return basename.includes("zsh") || basename.includes("fish");
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
function supportsAutoUserShell(shell: string): boolean {
|
|
@@ -165,19 +212,31 @@ function hasInteractiveShellArg(args: string[]): boolean {
|
|
|
165
212
|
}
|
|
166
213
|
|
|
167
214
|
function ensureInteractiveShellArgs(shell: string, args: string[]): string[] {
|
|
168
|
-
if (!needsInteractiveShellArg(shell)
|
|
215
|
+
if (!needsInteractiveShellArg(shell)) return args;
|
|
216
|
+
|
|
217
|
+
// fish sources the same config files (config.fish + conf.d) for interactive
|
|
218
|
+
// shells as for login shells, so the inherited `-l` adds nothing — it only
|
|
219
|
+
// marks the shell as login, firing `status is-login` blocks in user config
|
|
220
|
+
// (agent/keychain setup, path mutation) on every `!` command. zsh keeps `-l`
|
|
221
|
+
// because .zprofile is login-only. Args originate from procmgr's
|
|
222
|
+
// getShellArgs(), so login only ever appears as a standalone `-l`/`--login`.
|
|
223
|
+
const effectiveArgs = shellBasename(shell).includes("fish")
|
|
224
|
+
? args.filter(arg => arg !== "-l" && arg !== "--login")
|
|
225
|
+
: args;
|
|
226
|
+
|
|
227
|
+
if (hasInteractiveShellArg(effectiveArgs)) return effectiveArgs;
|
|
169
228
|
|
|
170
|
-
const commandIndex =
|
|
229
|
+
const commandIndex = effectiveArgs.findIndex(arg => arg === "-c" || arg === "--command");
|
|
171
230
|
if (commandIndex !== -1) {
|
|
172
|
-
return [...
|
|
231
|
+
return [...effectiveArgs.slice(0, commandIndex), "-i", ...effectiveArgs.slice(commandIndex)];
|
|
173
232
|
}
|
|
174
233
|
|
|
175
|
-
const compactCommandIndex =
|
|
234
|
+
const compactCommandIndex = effectiveArgs.findIndex(arg => /^-[^-]*c[^-]*$/.test(arg));
|
|
176
235
|
if (compactCommandIndex !== -1) {
|
|
177
|
-
return
|
|
236
|
+
return effectiveArgs.map((arg, index) => (index === compactCommandIndex ? arg.replace("c", "ic") : arg));
|
|
178
237
|
}
|
|
179
238
|
|
|
180
|
-
return [...
|
|
239
|
+
return [...effectiveArgs, "-i"];
|
|
181
240
|
}
|
|
182
241
|
|
|
183
242
|
function quoteShellArg(value: string): string {
|
|
@@ -224,8 +283,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
224
283
|
|
|
225
284
|
// Apply command prefix if configured
|
|
226
285
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
286
|
+
const runCdInPersistentShell = options?.useUserShell === true && !prefix && isPersistentShellCdCommand(command);
|
|
227
287
|
const finalCommand =
|
|
228
|
-
options?.useUserShell === true && !bashShell
|
|
288
|
+
options?.useUserShell === true && !bashShell && !runCdInPersistentShell
|
|
229
289
|
? buildUserShellCommand(shell, args, prefixedCommand)
|
|
230
290
|
: prefixedCommand;
|
|
231
291
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { replaceTabs, shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
2
|
+
import type { LoadExtensionsResult } from "./types";
|
|
3
|
+
|
|
4
|
+
/** Formats extension load failures for user-visible startup diagnostics. */
|
|
5
|
+
export function formatExtensionLoadNotifications(errors: LoadExtensionsResult["errors"]): string[] {
|
|
6
|
+
const messages: string[] = [];
|
|
7
|
+
for (const { path, error } of errors) {
|
|
8
|
+
const displayPath = truncateToWidth(replaceTabs(shortenPath(path)), TRUNCATE_LENGTHS.CONTENT);
|
|
9
|
+
const displayError = truncateToWidth(replaceTabs(error.replace(/\s+/g, " ").trim()), TRUNCATE_LENGTHS.LONG);
|
|
10
|
+
messages.push(`Failed to load extension ${displayPath}: ${displayError}`);
|
|
11
|
+
}
|
|
12
|
+
return messages;
|
|
13
|
+
}
|
|
@@ -32,6 +32,16 @@ type BundledModule = Readonly<Record<string, unknown>>;
|
|
|
32
32
|
type BundledModules = Readonly<Record<string, BundledModule>>;
|
|
33
33
|
type BundledModuleLoaders = Readonly<Record<string, () => Promise<BundledModule>>>;
|
|
34
34
|
|
|
35
|
+
interface LegacyPiResolveResult {
|
|
36
|
+
path: string;
|
|
37
|
+
namespace?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface BundledVirtualResolveResult {
|
|
41
|
+
path: string;
|
|
42
|
+
namespace: typeof BUNDLED_VIRTUAL_NAMESPACE;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
const loadedBundledModules: Record<string, BundledModule> = {};
|
|
36
46
|
let bundledModuleLoadersPromise: Promise<BundledModuleLoaders> | null = null;
|
|
37
47
|
|
|
@@ -72,6 +82,25 @@ function isBundledVirtualSpecifier(value: string): boolean {
|
|
|
72
82
|
return value.startsWith(BUNDLED_VIRTUAL_SCHEME);
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
function toLegacyPiResolveResult(resolvedPath: string): LegacyPiResolveResult {
|
|
86
|
+
if (isBundledVirtualSpecifier(resolvedPath)) {
|
|
87
|
+
const registryKey = resolvedPath.slice(BUNDLED_VIRTUAL_SCHEME.length);
|
|
88
|
+
return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
|
|
89
|
+
}
|
|
90
|
+
return { path: resolvedPath };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Maps a bundled virtual specifier or registry key to Bun's plugin namespace shape. */
|
|
94
|
+
export function resolveBundledVirtualSpecifier(specifier: string): BundledVirtualResolveResult {
|
|
95
|
+
const registryKey = isBundledVirtualSpecifier(specifier)
|
|
96
|
+
? specifier.slice(BUNDLED_VIRTUAL_SCHEME.length)
|
|
97
|
+
: specifier;
|
|
98
|
+
if (!registryKey) {
|
|
99
|
+
throw new Error("omp:legacy-pi-shim: bundled virtual specifier has no registry key");
|
|
100
|
+
}
|
|
101
|
+
return { path: registryKey, namespace: BUNDLED_VIRTUAL_NAMESPACE };
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/**
|
|
76
105
|
* Build a synthetic ES module for one live bundled namespace. Every export
|
|
77
106
|
* reads through the global bridge; no bunfs path or copied package is involved.
|
|
@@ -1611,7 +1640,7 @@ function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
|
|
|
1611
1640
|
return "js";
|
|
1612
1641
|
}
|
|
1613
1642
|
|
|
1614
|
-
function resolveLegacyPiSpecifier(args: { path: string; importer: string }):
|
|
1643
|
+
function resolveLegacyPiSpecifier(args: { path: string; importer: string }): LegacyPiResolveResult | undefined {
|
|
1615
1644
|
const remappedSpecifier = remapLegacyPiSpecifier(args.path);
|
|
1616
1645
|
if (!remappedSpecifier) {
|
|
1617
1646
|
return undefined;
|
|
@@ -1620,7 +1649,7 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
|
|
|
1620
1649
|
// Primary: resolve the canonical @oh-my-pi/* specifier from the host binary
|
|
1621
1650
|
// location. Works in dev mode and in source-link installs.
|
|
1622
1651
|
try {
|
|
1623
|
-
return
|
|
1652
|
+
return toLegacyPiResolveResult(resolveCanonicalPiSpecifier(remappedSpecifier));
|
|
1624
1653
|
} catch {
|
|
1625
1654
|
// Fallback for compiled binary mode: the bundled packages live inside
|
|
1626
1655
|
// /$bunfs/root and aren't reachable by filesystem resolution. Prefer the
|
|
@@ -1630,10 +1659,10 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
|
|
|
1630
1659
|
// @earendil-works peer deps.
|
|
1631
1660
|
const importerDir = path.dirname(args.importer);
|
|
1632
1661
|
try {
|
|
1633
|
-
return
|
|
1662
|
+
return toLegacyPiResolveResult(Bun.resolveSync(remappedSpecifier, importerDir));
|
|
1634
1663
|
} catch {
|
|
1635
1664
|
try {
|
|
1636
|
-
return
|
|
1665
|
+
return toLegacyPiResolveResult(Bun.resolveSync(args.path, importerDir));
|
|
1637
1666
|
} catch {
|
|
1638
1667
|
return undefined;
|
|
1639
1668
|
}
|
|
@@ -1641,8 +1670,8 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
|
|
|
1641
1670
|
}
|
|
1642
1671
|
}
|
|
1643
1672
|
|
|
1644
|
-
function resolveTypeBoxSpecifier():
|
|
1645
|
-
return TYPEBOX_SHIM_PATH ?
|
|
1673
|
+
function resolveTypeBoxSpecifier(): LegacyPiResolveResult | undefined {
|
|
1674
|
+
return TYPEBOX_SHIM_PATH ? toLegacyPiResolveResult(TYPEBOX_SHIM_PATH) : undefined;
|
|
1646
1675
|
}
|
|
1647
1676
|
|
|
1648
1677
|
export function installLegacyPiSpecifierShim(): void {
|
|
@@ -1656,6 +1685,12 @@ export function installLegacyPiSpecifierShim(): void {
|
|
|
1656
1685
|
setup(build) {
|
|
1657
1686
|
build.onResolve({ filter: LEGACY_PI_SPECIFIER_FILTER, namespace: "file" }, resolveLegacyPiSpecifier);
|
|
1658
1687
|
build.onResolve({ filter: TYPEBOX_SPECIFIER_FILTER, namespace: "file" }, resolveTypeBoxSpecifier);
|
|
1688
|
+
build.onResolve({ filter: /^omp-legacy-pi-bundled:.+$/, namespace: "file" }, args =>
|
|
1689
|
+
resolveBundledVirtualSpecifier(args.path),
|
|
1690
|
+
);
|
|
1691
|
+
build.onResolve({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, args =>
|
|
1692
|
+
resolveBundledVirtualSpecifier(args.path),
|
|
1693
|
+
);
|
|
1659
1694
|
// Compiled mode serves `omp-legacy-pi-bundled:<key>` imports from
|
|
1660
1695
|
// live host module references. No bunfs path leaves this loader.
|
|
1661
1696
|
build.onLoad({ filter: /.*/, namespace: BUNDLED_VIRTUAL_NAMESPACE }, async args => {
|
package/src/main.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
resolveActiveProjectRegistryPath,
|
|
51
51
|
} from "./discovery/helpers";
|
|
52
52
|
import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
|
|
53
|
+
import { formatExtensionLoadNotifications } from "./extensibility/extensions/load-errors";
|
|
53
54
|
import { ExtensionRunner } from "./extensibility/extensions/runner";
|
|
54
55
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
55
56
|
import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
|
|
@@ -1433,6 +1434,13 @@ export async function runRootCommand(
|
|
|
1433
1434
|
};
|
|
1434
1435
|
const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
|
|
1435
1436
|
normalizeContinueSessionArgs(initialArgs, rawArgs);
|
|
1437
|
+
for (const message of formatExtensionLoadNotifications(extensionsResult.errors)) {
|
|
1438
|
+
if (isInteractive) {
|
|
1439
|
+
notifs.push({ kind: "warn", message });
|
|
1440
|
+
} else {
|
|
1441
|
+
process.stderr.write(`${chalk.yellow(`${message}\n`)}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1436
1444
|
// Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
|
|
1437
1445
|
// know the real extension flag set. Without this check the unrecognized
|
|
1438
1446
|
// token gets silently consumed and any following positional leaks as the
|
|
@@ -85,6 +85,7 @@ export class ChatTranscriptBuilder {
|
|
|
85
85
|
#pendingUsage: Usage | undefined;
|
|
86
86
|
#pendingUsageDuration: number | undefined;
|
|
87
87
|
#pendingUsageTtft: number | undefined;
|
|
88
|
+
#pendingUsageTimestamp: number | undefined;
|
|
88
89
|
#lastAssistantUsage: Usage | undefined;
|
|
89
90
|
#waitingPoll: ToolExecutionComponent | null = null;
|
|
90
91
|
#todoSnapshot: ToolExecutionComponent | null = null;
|
|
@@ -133,6 +134,7 @@ export class ChatTranscriptBuilder {
|
|
|
133
134
|
this.#pendingUsage = undefined;
|
|
134
135
|
this.#pendingUsageDuration = undefined;
|
|
135
136
|
this.#pendingUsageTtft = undefined;
|
|
137
|
+
this.#pendingUsageTimestamp = undefined;
|
|
136
138
|
this.#lastAssistantUsage = undefined;
|
|
137
139
|
this.#waitingPoll = null;
|
|
138
140
|
this.#todoSnapshot = null;
|
|
@@ -201,11 +203,17 @@ export class ChatTranscriptBuilder {
|
|
|
201
203
|
this.#readGroup?.seal();
|
|
202
204
|
this.#readGroup = null;
|
|
203
205
|
this.container.addChild(
|
|
204
|
-
createUsageRowBlock(
|
|
206
|
+
createUsageRowBlock(
|
|
207
|
+
this.#pendingUsage,
|
|
208
|
+
this.#pendingUsageDuration,
|
|
209
|
+
this.#pendingUsageTtft,
|
|
210
|
+
this.#pendingUsageTimestamp,
|
|
211
|
+
),
|
|
205
212
|
);
|
|
206
213
|
this.#pendingUsage = undefined;
|
|
207
214
|
this.#pendingUsageDuration = undefined;
|
|
208
215
|
this.#pendingUsageTtft = undefined;
|
|
216
|
+
this.#pendingUsageTimestamp = undefined;
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
#appendChatMessage(message: AgentMessage): void {
|
|
@@ -381,6 +389,7 @@ export class ChatTranscriptBuilder {
|
|
|
381
389
|
settings.get("display.showTokenUsage") && assistantUsageIsBilled(message.usage) ? message.usage : undefined;
|
|
382
390
|
this.#pendingUsageDuration = message.duration;
|
|
383
391
|
this.#pendingUsageTtft = message.ttft;
|
|
392
|
+
this.#pendingUsageTimestamp = message.timestamp;
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|