@oh-my-pi/pi-coding-agent 11.7.2 → 11.8.0

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 CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [11.8.0] - 2026-02-10
6
+ ### Added
7
+
8
+ - Added `ctx.reload()` method to extension command context to reload extensions, skills, prompts, and themes from disk
9
+ - Added `ctx.ui.pasteToEditor()` method to paste text into the editor with proper handling (e.g., large paste markers in interactive mode)
10
+ - Added extension UI sub-protocol for RPC mode enabling dialog methods (`select`, `confirm`, `input`, `editor`) and fire-and-forget UI methods via client communication
11
+ - Added support for tilde (`~`) expansion in custom skill directory paths
12
+ - Added example extension demonstrating `ctx.reload()` usage with both command and LLM-callable tool patterns
13
+
14
+ ### Changed
15
+
16
+ - Changed `ctx.hasUI` behavior: now `true` in RPC mode (previously `false`), with dialog methods working via extension UI sub-protocol
17
+ - Changed warning output for invalid CLI arguments to use structured logging instead of console.error
18
+ - Changed help text to indicate command-specific help is available via `<command> --help`
19
+ - Changed tool result event handlers to chain like middleware, allowing each handler to see and modify results from previous handlers with partial patch support
20
+
21
+ ### Fixed
22
+
23
+ - Fixed archive extraction security vulnerability by validating that extracted paths do not escape the extraction directory
24
+ - Fixed archive format validation to reject unsupported formats before extraction attempt
25
+ - Fixed archive extraction error handling to provide clear error messages on failure
26
+
5
27
  ## [11.7.0] - 2026-02-07
6
28
  ### Changed
7
29
 
@@ -583,6 +583,11 @@ pi.on("tool_call", async (event, ctx) => {
583
583
 
584
584
  Fired after tool executes. **Can modify result.**
585
585
 
586
+ `tool_result` handlers chain like middleware:
587
+ - Handlers run in extension load order
588
+ - Each handler sees the latest result after previous handler changes
589
+ - Handlers can return partial patches (`content`, `details`, or `isError`); omitted fields keep their current values
590
+
586
591
  ```typescript
587
592
  pi.on("tool_result", async (event, ctx) => {
588
593
  // event.toolName, event.toolCallId, event.input
@@ -609,7 +614,7 @@ UI methods for user interaction. See [Custom UI](#custom-ui) for full details.
609
614
 
610
615
  ### ctx.hasUI
611
616
 
612
- `false` in print mode (`-p`), JSON mode, and RPC mode. UI methods become no-ops, so check before prompting.
617
+ `false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. In RPC mode, dialog methods (`select`, `confirm`, `input`, `editor`) work via the extension UI sub-protocol, and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) emit requests to the client. Some TUI-specific methods are no-ops or return defaults (see [rpc.md](rpc.md#extension-ui-protocol)).
613
618
 
614
619
  ### ctx.cwd
615
620
 
@@ -704,6 +709,62 @@ const result = await ctx.navigateTree("entry-id-456", {
704
709
  });
705
710
  ```
706
711
 
712
+ ### ctx.reload()
713
+
714
+ Run the same reload flow as `/reload`.
715
+
716
+ ```typescript
717
+ pi.registerCommand("reload-runtime", {
718
+ description: "Reload extensions, skills, prompts, and themes",
719
+ handler: async (_args, ctx) => {
720
+ await ctx.reload();
721
+ return;
722
+ },
723
+ });
724
+ ```
725
+
726
+ Important behavior:
727
+ - `await ctx.reload()` emits `session_shutdown` for the current extension runtime
728
+ - It then reloads resources and emits `session_start` (and `resources_discover` with reason `"reload"`) for the new runtime
729
+ - The currently running command handler still continues in the old call frame
730
+ - Code after `await ctx.reload()` still runs from the pre-reload version
731
+ - Code after `await ctx.reload()` must not assume old in-memory extension state is still valid
732
+ - After the handler returns, future commands/events/tool calls use the new extension version
733
+
734
+ For predictable behavior, treat reload as terminal for that handler (`await ctx.reload(); return;`).
735
+
736
+ Tools run with `ExtensionContext`, so they cannot call `ctx.reload()` directly. Use a command as the reload entrypoint, then expose a tool that queues that command as a follow-up user message.
737
+
738
+ Example tool the LLM can call to trigger reload:
739
+
740
+ ```typescript
741
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
742
+ import { Type } from "@sinclair/typebox";
743
+
744
+ export default function (pi: ExtensionAPI) {
745
+ pi.registerCommand("reload-runtime", {
746
+ description: "Reload extensions, skills, prompts, and themes",
747
+ handler: async (_args, ctx) => {
748
+ await ctx.reload();
749
+ return;
750
+ },
751
+ });
752
+
753
+ pi.registerTool({
754
+ name: "reload_runtime",
755
+ label: "Reload Runtime",
756
+ description: "Reload extensions, skills, prompts, and themes",
757
+ parameters: Type.Object({}),
758
+ async execute() {
759
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
760
+ return {
761
+ content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
762
+ };
763
+ },
764
+ });
765
+ }
766
+ ```
767
+
707
768
  ## ExtensionAPI Methods
708
769
 
709
770
  ### pi.on(event, handler)
@@ -1111,6 +1172,9 @@ ctx.ui.setTitle("omp - my-project");
1111
1172
  ctx.ui.setEditorText("Prefill text");
1112
1173
  const current = ctx.ui.getEditorText();
1113
1174
 
1175
+ // Paste into editor (triggers paste handling, including collapse for large content)
1176
+ ctx.ui.pasteToEditor("pasted content");
1177
+
1114
1178
  // Custom editor component
1115
1179
  ctx.ui.setEditorComponent((tui, theme, keybindings) => new MyEditor(tui, theme, keybindings)); // EditorComponent
1116
1180
  ctx.ui.setEditorComponent(undefined); // Restore default
@@ -1147,7 +1211,7 @@ The callback receives:
1147
1211
  - `keybindings` - Keybindings manager for resolving bindings
1148
1212
  - `done(value)` - Call to close component and return value
1149
1213
 
1150
- See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (todo.ts, tools.ts).
1214
+ See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (todo.ts, tools.ts, reload-runtime.ts).
1151
1215
 
1152
1216
  ### Message Rendering
1153
1217
 
package/docs/rpc.md CHANGED
@@ -686,6 +686,42 @@ Response:
686
686
 
687
687
  Returns an error if the name is empty.
688
688
 
689
+ ## Extension UI (stdout)
690
+
691
+ In RPC mode, extensions receive an [`ExtensionUIContext`](./extensions.md#custom-ui) backed by an extension UI sub-protocol.
692
+ When an extension calls a dialog or UI method, the agent emits an `extension_ui_request` JSON line on stdout. The host must
693
+ respond by writing an `extension_ui_response` JSON line on stdin.
694
+
695
+ If a dialog request includes a `timeout` field, the agent auto-resolves it with a default value when the timeout expires.
696
+ The host does not need to track or enforce timeouts.
697
+
698
+ Example request (stdout):
699
+
700
+ ```json
701
+ { "type": "extension_ui_request", "id": "req-123", "method": "confirm", "title": "Confirm", "message": "Continue?", "timeout": 30000 }
702
+ ```
703
+
704
+ Example response (stdin):
705
+
706
+ ```json
707
+ { "type": "extension_ui_response", "id": "req-123", "confirmed": true }
708
+ ```
709
+
710
+ ### Unsupported / degraded UI methods
711
+
712
+ Some `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct TUI access:
713
+
714
+ - `custom()` returns `undefined`
715
+ - `setWorkingMessage()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` are no-ops
716
+ - `getEditorText()` returns `""`
717
+ - `getToolsExpanded()` returns `false`
718
+ - `setWidget()` only supports `string[]` (factory functions/components are ignored)
719
+ - `getAllThemes()` returns `[]`
720
+ - `getTheme()` returns `undefined`
721
+ - `setTheme()` returns `{ success: false, error: "Theme switching not supported in RPC mode" }`
722
+
723
+ Note: `ctx.hasUI` is `true` in RPC mode because dialog and fire-and-forget UI methods are functional via this sub-protocol.
724
+
689
725
  ## Events
690
726
 
691
727
  Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do).
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Reload Runtime Extension
3
+ *
4
+ * Demonstrates ctx.reload() from ExtensionCommandContext and an LLM-callable
5
+ * tool that queues a follow-up command to trigger reload.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "@sinclair/typebox";
10
+
11
+ export default function (pi: ExtensionAPI) {
12
+ // Command entrypoint for reload.
13
+ // Treat reload as terminal for this handler.
14
+ pi.registerCommand("reload-runtime", {
15
+ description: "Reload extensions, skills, prompts, and themes",
16
+ handler: async (_args, ctx) => {
17
+ await ctx.reload();
18
+ return;
19
+ },
20
+ });
21
+
22
+ // LLM-callable tool. Tools get ExtensionContext, so they cannot call ctx.reload() directly.
23
+ // Instead, queue a follow-up user command that executes the command above.
24
+ pi.registerTool({
25
+ name: "reload_runtime",
26
+ label: "Reload Runtime",
27
+ description: "Reload extensions, skills, prompts, and themes",
28
+ parameters: Type.Object({}),
29
+ async execute() {
30
+ pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
31
+ return {
32
+ content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
33
+ details: {},
34
+ };
35
+ },
36
+ });
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "11.7.2",
3
+ "version": "11.8.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -90,12 +90,12 @@
90
90
  "@mozilla/readability": "0.6.0",
91
91
  "@oclif/core": "^4.8.0",
92
92
  "@oclif/plugin-autocomplete": "^3.2.40",
93
- "@oh-my-pi/omp-stats": "11.7.2",
94
- "@oh-my-pi/pi-agent-core": "11.7.2",
95
- "@oh-my-pi/pi-ai": "11.7.2",
96
- "@oh-my-pi/pi-natives": "11.7.2",
97
- "@oh-my-pi/pi-tui": "11.7.2",
98
- "@oh-my-pi/pi-utils": "11.7.2",
93
+ "@oh-my-pi/omp-stats": "11.8.0",
94
+ "@oh-my-pi/pi-agent-core": "11.8.0",
95
+ "@oh-my-pi/pi-ai": "11.8.0",
96
+ "@oh-my-pi/pi-natives": "11.8.0",
97
+ "@oh-my-pi/pi-tui": "11.8.0",
98
+ "@oh-my-pi/pi-utils": "11.8.0",
99
99
  "@sinclair/typebox": "^0.34.48",
100
100
  "ajv": "^8.17.1",
101
101
  "chalk": "^5.6.2",
package/src/cli/args.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * CLI argument parsing and help display
3
3
  */
4
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
+ import { logger } from "@oh-my-pi/pi-utils";
5
6
  import chalk from "chalk";
6
7
  import { APP_NAME, CONFIG_DIR_NAME } from "../config";
7
8
  import { BUILTIN_TOOLS } from "../tools";
@@ -115,11 +116,10 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
115
116
  if (name in BUILTIN_TOOLS) {
116
117
  validTools.push(name);
117
118
  } else {
118
- console.error(
119
- chalk.yellow(
120
- `Warning: Unknown tool "${name}". Valid tools: ${Object.keys(BUILTIN_TOOLS).join(", ")}`,
121
- ),
122
- );
119
+ logger.warn("Unknown tool passed to --tools", {
120
+ tool: name,
121
+ validTools: Object.keys(BUILTIN_TOOLS),
122
+ });
123
123
  }
124
124
  }
125
125
  result.tools = validTools;
@@ -128,11 +128,10 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
128
128
  if (isValidThinkingLevel(level)) {
129
129
  result.thinking = level;
130
130
  } else {
131
- console.error(
132
- chalk.yellow(
133
- `Warning: Invalid thinking level "${level}". Valid values: ${VALID_THINKING_LEVELS.join(", ")}`,
134
- ),
135
- );
131
+ logger.warn("Invalid thinking level passed to --thinking", {
132
+ level,
133
+ validThinkingLevels: [...VALID_THINKING_LEVELS],
134
+ });
136
135
  }
137
136
  } else if (arg === "--print" || arg === "-p") {
138
137
  result.print = true;
@@ -245,7 +244,8 @@ ${chalk.bold("Available Tools (all enabled by default):")}
245
244
  export function printHelp(): void {
246
245
  process.stdout.write(
247
246
  `${chalk.bold(APP_NAME)} - AI coding assistant\n\n` +
248
- `Run ${APP_NAME} --help for full command and option details.\n\n` +
247
+ `Run ${APP_NAME} --help for full command and option details.\n` +
248
+ `Run ${APP_NAME} <command> --help for command-specific help.\n\n` +
249
249
  `${getExtraHelpText()}\n`,
250
250
  );
251
251
  }
@@ -138,6 +138,7 @@ const noOpUIContext: ExtensionUIContext = {
138
138
  setTitle: () => {},
139
139
  custom: async () => undefined as never,
140
140
  setEditorText: () => {},
141
+ pasteToEditor: () => {},
141
142
  getEditorText: () => "",
142
143
  editor: async () => undefined,
143
144
  setEditorComponent: () => {},
@@ -166,6 +167,7 @@ export class ExtensionRunner {
166
167
  private branchHandler: BranchHandler = async () => ({ cancelled: false });
167
168
  private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
168
169
  private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
170
+ private reloadHandler: () => Promise<void> = async () => {};
169
171
  private shutdownHandler: ShutdownHandler = () => {};
170
172
  private commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
171
173
 
@@ -212,6 +214,7 @@ export class ExtensionRunner {
212
214
  this.branchHandler = commandContextActions.branch;
213
215
  this.navigateTreeHandler = commandContextActions.navigateTree;
214
216
  this.switchSessionHandler = commandContextActions.switchSession;
217
+ this.reloadHandler = commandContextActions.reload;
215
218
  this.getContextUsageFn = commandContextActions.getContextUsage;
216
219
  this.compactFn = commandContextActions.compact;
217
220
  }
@@ -405,6 +408,7 @@ export class ExtensionRunner {
405
408
  branch: entryId => this.branchHandler(entryId),
406
409
  navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
407
410
  switchSession: sessionPath => this.switchSessionHandler(sessionPath),
411
+ reload: () => this.reloadHandler(),
408
412
  compact: instructionsOrOptions => this.compactFn(instructionsOrOptions),
409
413
  };
410
414
  }
@@ -112,6 +112,14 @@ export interface ExtensionUIContext {
112
112
  /** Set the text in the core input editor. */
113
113
  setEditorText(text: string): void;
114
114
 
115
+ /**
116
+ * Paste text into the core input editor.
117
+ *
118
+ * Interactive mode should route through the editor's paste handling (e.g. large paste markers).
119
+ * Non-interactive modes may fall back to replacing the editor text.
120
+ */
121
+ pasteToEditor(text: string): void;
122
+
115
123
  /** Get the current text from the core input editor. */
116
124
  getEditorText(): string;
117
125
 
@@ -220,6 +228,9 @@ export interface ExtensionCommandContext extends ExtensionContext {
220
228
  /** Switch to a different session file. */
221
229
  switchSession(sessionPath: string): Promise<{ cancelled: boolean }>;
222
230
 
231
+ /** Reload the current session/runtime state. */
232
+ reload(): Promise<void>;
233
+
223
234
  /** Compact the session context (interactive mode shows UI). */
224
235
  compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
225
236
  }
@@ -1008,6 +1019,7 @@ export interface ExtensionCommandContextActions {
1008
1019
  navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
1009
1020
  compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
1010
1021
  switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>;
1022
+ reload: () => Promise<void>;
1011
1023
  }
1012
1024
 
1013
1025
  /** Full runtime = state + actions. */
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
4
5
  import { skillCapability } from "../capability/skill";
@@ -316,7 +317,17 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
316
317
 
317
318
  // Process custom directories - scan directly without using full provider system
318
319
  const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
319
- const customScanResults = await Promise.all(customDirectories.map(dir => scanDirectoryForSkills(dir)));
320
+ const customScanResults = await Promise.all(
321
+ customDirectories.map(dir => {
322
+ let resolved = dir;
323
+ if (resolved.startsWith("~/")) {
324
+ resolved = path.join(os.homedir(), resolved.slice(2));
325
+ } else if (resolved === "~") {
326
+ resolved = os.homedir();
327
+ }
328
+ return scanDirectoryForSkills(resolved);
329
+ }),
330
+ );
320
331
  for (const customSkills of customScanResults) {
321
332
  for (const s of customSkills.skills) {
322
333
  if (matchesIgnorePatterns(s.name)) continue;
@@ -47,6 +47,9 @@ export class ExtensionUiController {
47
47
  setTitle: title => setTerminalTitle(title),
48
48
  custom: (factory, _options) => this.showHookCustom(factory),
49
49
  setEditorText: text => this.ctx.editor.setText(text),
50
+ pasteToEditor: text => {
51
+ this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
52
+ },
50
53
  getEditorText: () => this.ctx.editor.getText(),
51
54
  editor: (title, prefill) => this.showHookEditor(title, prefill),
52
55
  get theme() {
@@ -138,6 +141,13 @@ export class ExtensionUiController {
138
141
  const commandActions: ExtensionCommandContextActions = {
139
142
  getContextUsage: () => this.ctx.session.getContextUsage(),
140
143
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
144
+ reload: async () => {
145
+ await this.ctx.session.reload();
146
+ this.ctx.chatContainer.clear();
147
+ this.ctx.renderInitialMessages();
148
+ await this.ctx.reloadTodos();
149
+ this.ctx.showStatus("Reloaded session");
150
+ },
141
151
  newSession: async options => {
142
152
  // Stop any loading animation
143
153
  if (this.ctx.loadingAnimation) {
@@ -319,6 +329,16 @@ export class ExtensionUiController {
319
329
  const commandActions: ExtensionCommandContextActions = {
320
330
  getContextUsage: () => this.ctx.session.getContextUsage(),
321
331
  waitForIdle: () => this.ctx.session.agent.waitForIdle(),
332
+ reload: async () => {
333
+ if (this.ctx.isBackgrounded) {
334
+ return;
335
+ }
336
+ await this.ctx.session.reload();
337
+ this.ctx.chatContainer.clear();
338
+ this.ctx.renderInitialMessages();
339
+ await this.ctx.reloadTodos();
340
+ this.ctx.showStatus("Reloaded session");
341
+ },
322
342
  newSession: async options => {
323
343
  if (this.ctx.isBackgrounded) {
324
344
  return { cancelled: true };
@@ -436,6 +456,7 @@ export class ExtensionUiController {
436
456
  setTitle: () => {},
437
457
  custom: async () => undefined as never,
438
458
  setEditorText: () => {},
459
+ pasteToEditor: () => {},
439
460
  getEditorText: () => "",
440
461
  editor: async () => undefined,
441
462
  get theme() {
@@ -114,6 +114,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
114
114
  const success = await session.switchSession(sessionPath);
115
115
  return { cancelled: !success };
116
116
  },
117
+ reload: async () => {
118
+ await session.reload();
119
+ },
117
120
  compact: async instructionsOrOptions => {
118
121
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
119
122
  const options =
@@ -227,6 +227,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
227
227
  return undefined as never;
228
228
  }
229
229
 
230
+ pasteToEditor(text: string): void {
231
+ // Paste handling not supported in RPC mode - falls back to setEditorText
232
+ this.setEditorText(text);
233
+ }
234
+
230
235
  setEditorText(text: string): void {
231
236
  // Fire and forget - host can implement editor control
232
237
  this.output({
@@ -379,6 +384,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
379
384
  const success = await session.switchSession(sessionPath);
380
385
  return { cancelled: !success };
381
386
  },
387
+ reload: async () => {
388
+ await session.reload();
389
+ },
382
390
  compact: async instructionsOrOptions => {
383
391
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
384
392
  const options =
@@ -166,12 +166,11 @@ Continue non-destructively; someone's work may live there.
166
166
  {{#if rules.length}}
167
167
  - Rule applies? Read first.
168
168
  {{/if}}
169
- Skip only when single-file, ≤3 edits, requirements explicit.
169
+ - Skip only when single-file, ≤3 edits, requirements explicit.
170
170
  1. Plan if task has weight: 3–7 bullets, no more.
171
- 2. Before each tool call: state intent in one sentence.
172
- 3. After each tool call: interpret, decide, move; no echo.
173
- 4. Requirements conflict/unclear: if genuinely blocked **ONLY AFTER** exhausting your exploration with tools/context/files, ask.
174
- 5. If requested change includes refactor: remove now-unused elements; note removals.
171
+ 2. After each tool call: interpret, decide, move; no echo.
172
+ 3. Requirements conflict/unclear: if genuinely blocked **ONLY AFTER** exhausting your exploration with tools/context/files, ask.
173
+ 4. If requested change includes refactor: remove now-unused elements; note removals.
175
174
 
176
175
  ## Verification
177
176
  - Prefer external proof: tests, linters, type checks, repro steps.
@@ -225,6 +225,7 @@ const noOpUIContext: ExtensionUIContext = {
225
225
  setTitle: () => {},
226
226
  custom: async () => undefined as never,
227
227
  setEditorText: () => {},
228
+ pasteToEditor: () => {},
228
229
  getEditorText: () => "",
229
230
  editor: async () => undefined,
230
231
  get theme() {
@@ -1436,6 +1437,9 @@ export class AgentSession {
1436
1437
  const success = await this.switchSession(sessionPath);
1437
1438
  return { cancelled: !success };
1438
1439
  },
1440
+ reload: async () => {
1441
+ await this.reload();
1442
+ },
1439
1443
  getSystemPrompt: () => this.systemPrompt,
1440
1444
  };
1441
1445
  }
@@ -3334,6 +3338,18 @@ Be thorough - include exact file paths, function names, error messages, and tech
3334
3338
  // Session Management
3335
3339
  // =========================================================================
3336
3340
 
3341
+ /**
3342
+ * Reload the current session from disk.
3343
+ *
3344
+ * Intended for extension commands and headless modes to re-read the current session
3345
+ * file and re-emit session_switch hooks.
3346
+ */
3347
+ async reload(): Promise<void> {
3348
+ const sessionFile = this.sessionFile;
3349
+ if (!sessionFile) return;
3350
+ await this.switchSession(sessionFile);
3351
+ }
3352
+
3337
3353
  /**
3338
3354
  * Switch to a different session file.
3339
3355
  * Aborts current operation, loads messages, restores model/thinking.
@@ -203,12 +203,24 @@ async function downloadTool(tool: ToolName, signal?: AbortSignal): Promise<strin
203
203
  const tmp = await TempDir.create("@omp-tools-extract-");
204
204
 
205
205
  try {
206
- if (assetName.endsWith(".tar.gz") || assetName.endsWith(".zip")) {
206
+ if (!assetName.endsWith(".tar.gz") && !assetName.endsWith(".zip")) {
207
+ throw new Error(`Unsupported archive format: ${assetName}`);
208
+ }
209
+
210
+ try {
207
211
  const archive = new Bun.Archive(await Bun.file(archivePath).arrayBuffer());
208
212
  const files = await archive.files();
213
+ const extractRoot = path.resolve(tmp.path());
214
+
209
215
  for (const [filePath, file] of files) {
210
- await Bun.write(path.join(tmp.path(), filePath), file);
216
+ const outputPath = path.resolve(extractRoot, filePath);
217
+ if (!outputPath.startsWith(extractRoot + path.sep)) {
218
+ throw new Error(`Archive entry escapes extraction dir: ${filePath}`);
219
+ }
220
+ await Bun.write(outputPath, file);
211
221
  }
222
+ } catch (err) {
223
+ throw new Error(`Failed to extract ${assetName}: ${err instanceof Error ? err.message : String(err)}`);
212
224
  }
213
225
 
214
226
  // Find the binary in extracted files