@oh-my-pi/pi-coding-agent 15.4.2 → 15.5.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +80 -4
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/auth-broker-cli.d.ts +1 -1
  4. package/dist/types/commands/launch.d.ts +8 -0
  5. package/dist/types/config/settings-schema.d.ts +42 -1
  6. package/dist/types/edit/index.d.ts +2 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
  8. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  9. package/dist/types/lsp/index.d.ts +9 -1
  10. package/dist/types/mcp/client.d.ts +2 -1
  11. package/dist/types/mcp/oauth-discovery.d.ts +4 -3
  12. package/dist/types/mcp/timeout.d.ts +9 -0
  13. package/dist/types/mcp/types.d.ts +1 -1
  14. package/dist/types/sdk.d.ts +2 -0
  15. package/dist/types/session/streaming-output.d.ts +1 -1
  16. package/dist/types/task/index.d.ts +2 -0
  17. package/dist/types/task/types.d.ts +4 -0
  18. package/dist/types/tools/approval.d.ts +46 -0
  19. package/dist/types/tools/ask.d.ts +1 -0
  20. package/dist/types/tools/ast-edit.d.ts +2 -0
  21. package/dist/types/tools/ast-grep.d.ts +1 -0
  22. package/dist/types/tools/bash.d.ts +11 -1
  23. package/dist/types/tools/browser.d.ts +2 -0
  24. package/dist/types/tools/calculator.d.ts +1 -0
  25. package/dist/types/tools/checkpoint.d.ts +2 -0
  26. package/dist/types/tools/debug.d.ts +9 -1
  27. package/dist/types/tools/eval.d.ts +2 -0
  28. package/dist/types/tools/find.d.ts +10 -0
  29. package/dist/types/tools/gh.d.ts +2 -1
  30. package/dist/types/tools/hindsight-recall.d.ts +1 -0
  31. package/dist/types/tools/hindsight-reflect.d.ts +1 -0
  32. package/dist/types/tools/hindsight-retain.d.ts +1 -0
  33. package/dist/types/tools/inspect-image.d.ts +1 -0
  34. package/dist/types/tools/irc.d.ts +1 -0
  35. package/dist/types/tools/job.d.ts +1 -0
  36. package/dist/types/tools/read.d.ts +1 -0
  37. package/dist/types/tools/recipe/index.d.ts +1 -0
  38. package/dist/types/tools/render-mermaid.d.ts +1 -0
  39. package/dist/types/tools/resolve.d.ts +1 -0
  40. package/dist/types/tools/search-tool-bm25.d.ts +1 -0
  41. package/dist/types/tools/search.d.ts +1 -0
  42. package/dist/types/tools/ssh.d.ts +2 -0
  43. package/dist/types/tools/todo-write.d.ts +1 -0
  44. package/dist/types/tools/write.d.ts +2 -0
  45. package/dist/types/tools/yield.d.ts +1 -0
  46. package/dist/types/web/search/index.d.ts +1 -0
  47. package/package.json +7 -7
  48. package/src/cli/args.ts +14 -0
  49. package/src/cli/auth-broker-cli.ts +171 -22
  50. package/src/commands/auth-broker.ts +3 -0
  51. package/src/commands/launch.ts +16 -0
  52. package/src/config/mcp-schema.json +2 -2
  53. package/src/config/model-registry.ts +49 -5
  54. package/src/config/settings-schema.ts +59 -1
  55. package/src/config/settings.ts +2 -1
  56. package/src/dap/session.ts +35 -2
  57. package/src/discovery/builtin.ts +2 -2
  58. package/src/discovery/mcp-json.ts +1 -1
  59. package/src/edit/index.ts +26 -0
  60. package/src/edit/modes/patch.ts +1 -1
  61. package/src/edit/streaming.ts +12 -2
  62. package/src/exec/bash-executor.ts +6 -2
  63. package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
  64. package/src/extensibility/custom-tools/types.ts +16 -2
  65. package/src/extensibility/extensions/wrapper.ts +36 -1
  66. package/src/extensibility/hooks/types.ts +8 -1
  67. package/src/hashline/apply.ts +47 -2
  68. package/src/internal-urls/docs-index.generated.ts +8 -7
  69. package/src/lsp/edits.ts +82 -29
  70. package/src/lsp/index.ts +38 -1
  71. package/src/lsp/utils.ts +1 -1
  72. package/src/main.ts +6 -0
  73. package/src/mcp/client.ts +8 -6
  74. package/src/mcp/oauth-discovery.ts +120 -32
  75. package/src/mcp/oauth-flow.ts +34 -6
  76. package/src/mcp/timeout.ts +59 -0
  77. package/src/mcp/transports/http.ts +42 -44
  78. package/src/mcp/transports/stdio.ts +8 -5
  79. package/src/mcp/types.ts +1 -1
  80. package/src/modes/components/hook-editor.ts +11 -3
  81. package/src/modes/components/mcp-add-wizard.ts +6 -2
  82. package/src/modes/components/model-selector.ts +33 -11
  83. package/src/modes/controllers/command-controller.ts +6 -4
  84. package/src/modes/controllers/mcp-command-controller.ts +8 -4
  85. package/src/prompts/review-custom-request.md +22 -0
  86. package/src/prompts/review-headless-request.md +16 -0
  87. package/src/prompts/review-request.md +2 -3
  88. package/src/prompts/system/project-prompt.md +4 -0
  89. package/src/prompts/tools/debug.md +1 -0
  90. package/src/prompts/tools/find.md +4 -2
  91. package/src/prompts/tools/hashline.md +1 -0
  92. package/src/sdk.ts +47 -73
  93. package/src/session/agent-session.ts +93 -27
  94. package/src/session/streaming-output.ts +1 -1
  95. package/src/slash-commands/helpers/usage-report.ts +3 -1
  96. package/src/task/executor.ts +11 -0
  97. package/src/task/index.ts +19 -0
  98. package/src/task/render.ts +12 -2
  99. package/src/task/types.ts +4 -0
  100. package/src/tools/approval.ts +185 -0
  101. package/src/tools/ask.ts +1 -0
  102. package/src/tools/ast-edit.ts +25 -1
  103. package/src/tools/ast-grep.ts +1 -0
  104. package/src/tools/bash.ts +69 -1
  105. package/src/tools/browser/tab-supervisor.ts +1 -1
  106. package/src/tools/browser.ts +15 -0
  107. package/src/tools/calculator.ts +1 -0
  108. package/src/tools/checkpoint.ts +2 -0
  109. package/src/tools/debug.ts +38 -0
  110. package/src/tools/eval.ts +15 -0
  111. package/src/tools/find.ts +17 -8
  112. package/src/tools/gh.ts +28 -5
  113. package/src/tools/hindsight-recall.ts +1 -0
  114. package/src/tools/hindsight-reflect.ts +1 -0
  115. package/src/tools/hindsight-retain.ts +1 -0
  116. package/src/tools/image-gen.ts +1 -0
  117. package/src/tools/inspect-image.ts +1 -0
  118. package/src/tools/irc.ts +1 -0
  119. package/src/tools/job.ts +1 -0
  120. package/src/tools/path-utils.ts +14 -1
  121. package/src/tools/read.ts +1 -0
  122. package/src/tools/recipe/index.ts +1 -0
  123. package/src/tools/render-mermaid.ts +1 -0
  124. package/src/tools/report-tool-issue.ts +1 -0
  125. package/src/tools/resolve.ts +1 -0
  126. package/src/tools/review.ts +1 -0
  127. package/src/tools/search-tool-bm25.ts +1 -0
  128. package/src/tools/search.ts +1 -0
  129. package/src/tools/ssh.ts +8 -0
  130. package/src/tools/todo-write.ts +1 -0
  131. package/src/tools/write.ts +12 -1
  132. package/src/tools/yield.ts +1 -0
  133. package/src/web/search/index.ts +2 -0
@@ -1,10 +1,16 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback, RenderResultOptions, ToolApprovalDecision } from "@oh-my-pi/pi-agent-core";
2
2
  import { type Component } from "@oh-my-pi/pi-tui";
3
3
  import * as z from "zod/v4";
4
4
  import { type DapBreakpointRecord, type DapContinueOutcome, type DapDataBreakpointInfoResponse, type DapDataBreakpointRecord, type DapDisassembledInstruction, type DapEvaluateResponse, type DapFunctionBreakpointRecord, type DapInstructionBreakpointRecord, type DapModule, type DapScope, type DapSessionSummary, type DapSource, type DapStackFrame, type DapThread, type DapVariable } from "../dap";
5
5
  import type { Theme } from "../modes/theme/theme";
6
6
  import type { ToolSession } from ".";
7
7
  import type { OutputMeta } from "./output-meta";
8
+ /**
9
+ * DAP debug actions that only read program state (no mutation, no execution).
10
+ * Execution-side actions (`launch`, `attach`, `continue`, `step_*`, `pause`,
11
+ * `evaluate`, breakpoint mutations, memory writes) are exec-tier.
12
+ */
13
+ export declare const DEBUG_READONLY_ACTIONS: ReadonlySet<string>;
8
14
  declare const debugSchema: z.ZodObject<{
9
15
  action: z.ZodEnum<{
10
16
  attach: "attach";
@@ -125,6 +131,8 @@ export declare const debugToolRenderer: {
125
131
  export declare class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails> {
126
132
  private readonly session;
127
133
  readonly name = "debug";
134
+ readonly approval: (args: unknown) => ToolApprovalDecision;
135
+ readonly formatApprovalDetails: (args: unknown) => string[];
128
136
  readonly label = "Debug";
129
137
  readonly summary = "Debug a running process with DAP (debugger adapter protocol)";
130
138
  readonly description: string;
@@ -54,6 +54,8 @@ export declare class EvalTool implements AgentTool<typeof evalSchema> {
54
54
  #private;
55
55
  private readonly session;
56
56
  readonly name = "eval";
57
+ readonly approval: "exec";
58
+ readonly formatApprovalDetails: (args: unknown) => string[];
57
59
  readonly summary = "Execute Python or JavaScript code in an in-process eval backend";
58
60
  readonly loadMode = "discoverable";
59
61
  readonly label = "Eval";
@@ -14,6 +14,15 @@ declare const findSchema: z.ZodObject<{
14
14
  timeout: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
15
15
  }, z.core.$strict>;
16
16
  export type FindToolInput = z.infer<typeof findSchema>;
17
+ /**
18
+ * Reject comma-separated path lists packed into a single array element
19
+ * (`["a.py,b.py"]`). The schema is array-of-string; agents that pass a
20
+ * single comma-joined element get silent no-matches otherwise.
21
+ *
22
+ * Commas inside brace expansion (`{a,b}`) are legitimate glob syntax and
23
+ * must pass through.
24
+ */
25
+ export declare function validateFindPathInputs(paths: readonly string[]): void;
17
26
  export interface FindToolDetails {
18
27
  truncation?: TruncationResult;
19
28
  resultLimitReached?: number;
@@ -60,6 +69,7 @@ export declare class FindTool implements AgentTool<typeof findSchema, FindToolDe
60
69
  #private;
61
70
  private readonly session;
62
71
  readonly name = "find";
72
+ readonly approval: "read";
63
73
  readonly summary = "Find files and directories matching a glob pattern";
64
74
  readonly loadMode = "discoverable";
65
75
  readonly label = "Find";
@@ -1,4 +1,4 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback, ToolApprovalDecision } from "@oh-my-pi/pi-agent-core";
2
2
  import * as z from "zod/v4";
3
3
  import type { Settings } from "../config/settings";
4
4
  import type { ToolSession } from ".";
@@ -230,6 +230,7 @@ export declare function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSi
230
230
  export declare class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails> {
231
231
  private readonly session;
232
232
  readonly name = "github";
233
+ readonly approval: (args: unknown) => ToolApprovalDecision;
233
234
  readonly summary = "Interact with GitHub issues, pull requests, and repositories";
234
235
  readonly loadMode = "discoverable";
235
236
  readonly label = "GitHub";
@@ -8,6 +8,7 @@ export type HindsightRecallParams = z.infer<typeof hindsightRecallSchema>;
8
8
  export declare class HindsightRecallTool implements AgentTool<typeof hindsightRecallSchema> {
9
9
  private readonly session;
10
10
  readonly name = "recall";
11
+ readonly approval: "read";
11
12
  readonly label = "Recall";
12
13
  readonly description: string;
13
14
  readonly parameters: z.ZodObject<{
@@ -9,6 +9,7 @@ export type HindsightReflectParams = z.infer<typeof hindsightReflectSchema>;
9
9
  export declare class HindsightReflectTool implements AgentTool<typeof hindsightReflectSchema> {
10
10
  private readonly session;
11
11
  readonly name = "reflect";
12
+ readonly approval: "read";
12
13
  readonly label = "Reflect";
13
14
  readonly description: string;
14
15
  readonly parameters: z.ZodObject<{
@@ -11,6 +11,7 @@ export type HindsightRetainParams = z.infer<typeof hindsightRetainSchema>;
11
11
  export declare class HindsightRetainTool implements AgentTool<typeof hindsightRetainSchema> {
12
12
  private readonly session;
13
13
  readonly name = "retain";
14
+ readonly approval: "read";
14
15
  readonly label = "Retain";
15
16
  readonly description: string;
16
17
  readonly parameters: z.ZodObject<{
@@ -16,6 +16,7 @@ export declare class InspectImageTool implements AgentTool<typeof inspectImageSc
16
16
  private readonly session;
17
17
  private readonly completeImageRequest;
18
18
  readonly name = "inspect_image";
19
+ readonly approval: "read";
19
20
  readonly label = "InspectImage";
20
21
  readonly loadMode = "discoverable";
21
22
  readonly summary = "Describe or analyze an image file";
@@ -58,6 +58,7 @@ export declare class IrcTool implements AgentTool<typeof ircSchema, IrcDetails>
58
58
  #private;
59
59
  private readonly session;
60
60
  readonly name = "irc";
61
+ readonly approval: "read";
61
62
  readonly label = "IRC";
62
63
  readonly summary = "Send and receive messages between agents over IRC-like channels";
63
64
  readonly description: string;
@@ -31,6 +31,7 @@ export declare class JobTool implements AgentTool<typeof jobSchema, JobToolDetai
31
31
  #private;
32
32
  private readonly session;
33
33
  readonly name = "job";
34
+ readonly approval: "read";
34
35
  readonly label = "Job";
35
36
  readonly summary = "Manage long-running background jobs (async bash/python)";
36
37
  readonly description: string;
@@ -51,6 +51,7 @@ export declare class ReadTool implements AgentTool<typeof readSchema, ReadToolDe
51
51
  #private;
52
52
  private readonly session;
53
53
  readonly name = "read";
54
+ readonly approval: "read";
54
55
  readonly label = "Read";
55
56
  readonly loadMode = "essential";
56
57
  readonly description: string;
@@ -23,6 +23,7 @@ export declare class RecipeTool implements AgentTool<typeof recipeSchema, BashTo
23
23
  #private;
24
24
  readonly name = "recipe";
25
25
  readonly label = "Run";
26
+ readonly approval: "exec";
26
27
  readonly description: string;
27
28
  readonly parameters: z.ZodObject<{
28
29
  op: z.ZodString;
@@ -17,6 +17,7 @@ export interface RenderMermaidToolDetails {
17
17
  export declare class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
18
18
  private readonly session;
19
19
  readonly name = "render_mermaid";
20
+ readonly approval: "read";
20
21
  readonly label = "RenderMermaid";
21
22
  readonly summary = "Render a Mermaid diagram to an image";
22
23
  readonly description: string;
@@ -58,6 +58,7 @@ export declare function runResolveInvocation(params: ResolveParams, options: {
58
58
  export declare class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolDetails> {
59
59
  private readonly session;
60
60
  readonly name = "resolve";
61
+ readonly approval: "read";
61
62
  readonly label = "Resolve";
62
63
  readonly hidden = true;
63
64
  readonly description: string;
@@ -37,6 +37,7 @@ export declare function renderSearchToolBm25Description(discoverableTools?: Disc
37
37
  export declare class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
38
38
  private readonly session;
39
39
  readonly name = "search_tool_bm25";
40
+ readonly approval: "read";
40
41
  readonly label = "SearchTools";
41
42
  readonly loadMode = "essential";
42
43
  get description(): string;
@@ -56,6 +56,7 @@ type SearchParams = z.infer<typeof searchSchema>;
56
56
  export declare class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
57
57
  private readonly session;
58
58
  readonly name = "search";
59
+ readonly approval: "read";
59
60
  readonly label = "Search";
60
61
  readonly loadMode = "discoverable";
61
62
  readonly summary = "Search file contents using ripgrep (fast text search)";
@@ -23,6 +23,8 @@ export declare class SshTool implements AgentTool<typeof sshSchema, SSHToolDetai
23
23
  private readonly hostsByName;
24
24
  readonly description: string;
25
25
  readonly name = "ssh";
26
+ readonly approval: "exec";
27
+ readonly formatApprovalDetails: (args: unknown) => string[];
26
28
  readonly summary = "Execute a command on a remote host over SSH";
27
29
  readonly loadMode = "discoverable";
28
30
  readonly label = "SSH";
@@ -64,6 +64,7 @@ export declare function markdownToPhases(md: string): {
64
64
  export declare class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
65
65
  private readonly session;
66
66
  readonly name = "todo_write";
67
+ readonly approval: "read";
67
68
  readonly label = "Todo Write";
68
69
  readonly summary = "Write a structured todo list to track progress within a session";
69
70
  readonly description: string;
@@ -26,6 +26,8 @@ export declare class WriteTool implements AgentTool<typeof writeSchema, WriteToo
26
26
  #private;
27
27
  private readonly session;
28
28
  readonly name = "write";
29
+ readonly approval: (args: unknown) => "read" | "write";
30
+ readonly formatApprovalDetails: (args: unknown) => string[];
29
31
  readonly label = "Write";
30
32
  readonly description: string;
31
33
  readonly parameters: z.ZodObject<{
@@ -14,6 +14,7 @@ export interface YieldDetails {
14
14
  export declare class YieldTool implements AgentTool<TSchema, YieldDetails> {
15
15
  #private;
16
16
  readonly name = "yield";
17
+ readonly approval: "read";
17
18
  readonly label = "Submit Result";
18
19
  readonly description: string;
19
20
  readonly parameters: TSchema;
@@ -55,6 +55,7 @@ export declare function runSearchQuery(params: SearchQueryParams, options?: {
55
55
  export declare class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
56
56
  #private;
57
57
  readonly name = "web_search";
58
+ readonly approval: "read";
58
59
  readonly label = "Web Search";
59
60
  readonly description: string;
60
61
  readonly parameters: z.ZodObject<{
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.4.2",
4
+ "version": "15.5.0",
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.4.2",
51
- "@oh-my-pi/pi-agent-core": "15.4.2",
52
- "@oh-my-pi/pi-ai": "15.4.2",
53
- "@oh-my-pi/pi-natives": "15.4.2",
54
- "@oh-my-pi/pi-tui": "15.4.2",
55
- "@oh-my-pi/pi-utils": "15.4.2",
50
+ "@oh-my-pi/omp-stats": "15.5.0",
51
+ "@oh-my-pi/pi-agent-core": "15.5.0",
52
+ "@oh-my-pi/pi-ai": "15.5.0",
53
+ "@oh-my-pi/pi-natives": "15.5.0",
54
+ "@oh-my-pi/pi-tui": "15.5.0",
55
+ "@oh-my-pi/pi-utils": "15.5.0",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@types/turndown": "5.0.6",
58
58
  "@xterm/headless": "^6.0.0",
package/src/cli/args.ts CHANGED
@@ -46,6 +46,8 @@ export interface Args {
46
46
  noRules?: boolean;
47
47
  listModels?: string | true;
48
48
  noTitle?: boolean;
49
+ autoApprove?: boolean;
50
+ approvalMode?: "always-ask" | "write" | "yolo";
49
51
  messages: string[];
50
52
  fileArgs: string[];
51
53
  /** Unknown flags (potentially extension flags) - map of flag name to value */
@@ -172,6 +174,18 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
172
174
  result.noRules = true;
173
175
  } else if (arg === "--no-title") {
174
176
  result.noTitle = true;
177
+ } else if (arg === "--auto-approve" || arg === "--yolo") {
178
+ result.autoApprove = true;
179
+ } else if (arg === "--approval-mode" && i + 1 < args.length) {
180
+ const mode = args[++i];
181
+ if (mode === "always-ask" || mode === "write" || mode === "yolo") {
182
+ result.approvalMode = mode;
183
+ } else {
184
+ logger.warn("Invalid value passed to --approval-mode", {
185
+ value: mode,
186
+ validValues: ["always-ask", "write", "yolo"],
187
+ });
188
+ }
175
189
  } else if (arg === "--skills" && i + 1 < args.length) {
176
190
  // Comma-separated glob patterns for skill filtering
177
191
  result.skills = args[++i].split(",").map(s => s.trim());
@@ -17,6 +17,7 @@ import * as crypto from "node:crypto";
17
17
  import * as fs from "node:fs/promises";
18
18
  import * as os from "node:os";
19
19
  import * as path from "node:path";
20
+ import * as readline from "node:readline";
20
21
  import {
21
22
  AuthBrokerClient,
22
23
  type AuthCredential,
@@ -28,6 +29,7 @@ import {
28
29
  listProvidersWithEnvKey,
29
30
  type OAuthCredential,
30
31
  type OAuthProvider,
32
+ type OAuthProviderInfo,
31
33
  SqliteAuthCredentialStore,
32
34
  startAuthBroker,
33
35
  } from "@oh-my-pi/pi-ai";
@@ -36,7 +38,7 @@ import { $ } from "bun";
36
38
  import chalk from "chalk";
37
39
  import { resolveAuthBrokerConfig } from "../session/auth-broker-config";
38
40
 
39
- export type AuthBrokerAction = "serve" | "token" | "login" | "logout" | "status" | "import" | "migrate";
41
+ export type AuthBrokerAction = "serve" | "token" | "login" | "logout" | "status" | "import" | "migrate" | "list";
40
42
 
41
43
  export interface AuthBrokerCommandArgs {
42
44
  action: AuthBrokerAction;
@@ -60,7 +62,16 @@ export interface AuthBrokerCommandArgs {
60
62
  };
61
63
  }
62
64
 
63
- const ACTIONS: readonly AuthBrokerAction[] = ["serve", "token", "login", "logout", "import", "migrate", "status"];
65
+ const ACTIONS: readonly AuthBrokerAction[] = [
66
+ "serve",
67
+ "token",
68
+ "login",
69
+ "logout",
70
+ "import",
71
+ "migrate",
72
+ "status",
73
+ "list",
74
+ ];
64
75
 
65
76
  /** Callback ports baked from the per-provider OAuth flow modules. */
66
77
  const CALLBACK_PORTS: Record<string, number> = {
@@ -168,13 +179,23 @@ async function runToken(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
168
179
  }
169
180
 
170
181
  async function runLogin(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
171
- const providerArg = flags.provider;
182
+ const providers = getOAuthProviders();
183
+ let providerArg = flags.provider;
172
184
  if (!providerArg) {
173
- throw new Error("Usage: omp auth-broker login <provider> [--via=user@host]");
185
+ if (flags.via) {
186
+ throw new Error(
187
+ "Usage: omp auth-broker login <provider> --via=user@host (provider required for remote login)",
188
+ );
189
+ }
190
+ providerArg = await pickProviderInteractively(providers);
174
191
  }
175
- const oauthProviders = new Set<string>(getOAuthProviders().map(p => p.id));
176
- if (!oauthProviders.has(providerArg)) {
177
- throw new Error(`Unknown OAuth provider '${providerArg}'. Known: ${[...oauthProviders].sort().join(", ")}`);
192
+ if (!providers.some(p => p.id === providerArg)) {
193
+ throw new Error(
194
+ `Unknown OAuth provider '${providerArg}'. Known: ${providers
195
+ .map(p => p.id)
196
+ .sort()
197
+ .join(", ")}`,
198
+ );
178
199
  }
179
200
  if (flags.via) {
180
201
  await runRemoteLogin(providerArg, flags.via, flags.dryRun ?? false);
@@ -184,18 +205,107 @@ async function runLogin(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
184
205
  }
185
206
 
186
207
  async function runLocalLogin(provider: OAuthProvider): Promise<void> {
187
- // Spawn the pi-ai CLI in-process it handles the per-provider OAuth dance
188
- // and persists into the same SQLite store the broker uses.
189
- const piAiCli = Bun.fileURLToPath(import.meta.resolve("@oh-my-pi/pi-ai/cli"));
190
- const proc = Bun.spawn({
191
- cmd: [process.execPath, piAiCli, "login", provider],
192
- stdin: "inherit",
193
- stdout: "inherit",
194
- stderr: "inherit",
208
+ // Drive the per-provider OAuth dance in-process. Persists into the same
209
+ // SQLite store the broker uses.
210
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
211
+ const ask = (msg: string) => promptLine(rl, `${msg} `);
212
+ const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
213
+ const storage = new AuthStorage(store);
214
+ await storage.reload();
215
+ try {
216
+ await storage.login(provider, {
217
+ onAuth({ url, instructions }) {
218
+ process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
219
+ if (instructions) process.stdout.write(`${instructions}\n`);
220
+ process.stdout.write("\n");
221
+ },
222
+ onProgress(message) {
223
+ process.stdout.write(`${message}\n`);
224
+ },
225
+ onPrompt(p) {
226
+ return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
227
+ },
228
+ });
229
+ process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
230
+ } finally {
231
+ store.close();
232
+ rl.close();
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Interactive `readline` prompt that cleanly tears down on Ctrl-C / Escape so
238
+ * cancelling a half-finished login flow doesn't leave the terminal in raw mode.
239
+ */
240
+ function promptLine(rl: readline.Interface, question: string): Promise<string> {
241
+ const { promise, resolve, reject } = Promise.withResolvers<string>();
242
+ const input = process.stdin as NodeJS.ReadStream;
243
+ const supportsRawMode = input.isTTY && typeof input.setRawMode === "function";
244
+ const wasRaw = supportsRawMode ? input.isRaw : false;
245
+ let settled = false;
246
+
247
+ const cleanup = () => {
248
+ rl.off("SIGINT", onSigint);
249
+ if (supportsRawMode) {
250
+ input.off("keypress", onKeypress);
251
+ input.setRawMode?.(wasRaw);
252
+ }
253
+ };
254
+
255
+ const finish = (result: () => void) => {
256
+ if (settled) return;
257
+ settled = true;
258
+ cleanup();
259
+ result();
260
+ };
261
+
262
+ const cancel = () => {
263
+ finish(() => reject(new Error("Login cancelled")));
264
+ };
265
+
266
+ const onSigint = () => {
267
+ cancel();
268
+ };
269
+
270
+ const onKeypress = (_str: string, key: readline.Key) => {
271
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
272
+ cancel();
273
+ rl.close();
274
+ }
275
+ };
276
+
277
+ if (supportsRawMode) {
278
+ readline.emitKeypressEvents(input, rl);
279
+ input.setRawMode(true);
280
+ input.on("keypress", onKeypress);
281
+ }
282
+
283
+ rl.once("SIGINT", onSigint);
284
+ rl.question(question, answer => {
285
+ finish(() => resolve(answer));
195
286
  });
196
- const exitCode = await proc.exited;
197
- if (exitCode !== 0) {
198
- throw new Error(`pi-ai login exited with code ${exitCode}`);
287
+ return promise;
288
+ }
289
+
290
+ async function pickProviderInteractively(providers: readonly OAuthProviderInfo[]): Promise<string> {
291
+ if (providers.length === 0) {
292
+ throw new Error("No OAuth providers registered");
293
+ }
294
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
295
+ try {
296
+ process.stdout.write("Select a provider:\n\n");
297
+ for (let i = 0; i < providers.length; i++) {
298
+ process.stdout.write(` ${i + 1}. ${providers[i].name}\n`);
299
+ }
300
+ process.stdout.write("\n");
301
+ const choice = await promptLine(rl, `Enter number (1-${providers.length}): `);
302
+ const index = Number.parseInt(choice, 10) - 1;
303
+ if (Number.isNaN(index) || index < 0 || index >= providers.length) {
304
+ throw new Error(`Invalid selection: ${choice}`);
305
+ }
306
+ return providers[index].id;
307
+ } finally {
308
+ rl.close();
199
309
  }
200
310
  }
201
311
 
@@ -235,12 +345,17 @@ async function runRemoteLogin(provider: string, via: string, dryRun: boolean): P
235
345
  }
236
346
 
237
347
  async function runLogout(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
238
- const providerArg = flags.provider;
239
- if (!providerArg) {
240
- throw new Error("Usage: omp auth-broker logout <provider>");
241
- }
348
+ let providerArg = flags.provider;
242
349
  const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
243
350
  try {
351
+ if (!providerArg) {
352
+ const stored = store.listProviders();
353
+ if (stored.length === 0) {
354
+ process.stdout.write("No credentials stored.\n");
355
+ return;
356
+ }
357
+ providerArg = await pickStoredProviderInteractively(stored);
358
+ }
244
359
  store.deleteAuthCredentialsForProvider(providerArg, "logged out by user");
245
360
  process.stdout.write(`Logged out of ${providerArg}\n`);
246
361
  } finally {
@@ -248,6 +363,37 @@ async function runLogout(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
248
363
  }
249
364
  }
250
365
 
366
+ async function pickStoredProviderInteractively(providers: string[]): Promise<string> {
367
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
368
+ try {
369
+ process.stdout.write("Select a provider to logout:\n\n");
370
+ for (let i = 0; i < providers.length; i++) {
371
+ process.stdout.write(` ${i + 1}. ${providers[i]}\n`);
372
+ }
373
+ process.stdout.write("\n");
374
+ const choice = await promptLine(rl, `Enter number (1-${providers.length}): `);
375
+ const index = Number.parseInt(choice, 10) - 1;
376
+ if (Number.isNaN(index) || index < 0 || index >= providers.length) {
377
+ throw new Error(`Invalid selection: ${choice}`);
378
+ }
379
+ return providers[index];
380
+ } finally {
381
+ rl.close();
382
+ }
383
+ }
384
+
385
+ async function runList(flags: AuthBrokerCommandArgs["flags"]): Promise<void> {
386
+ const providers = getOAuthProviders();
387
+ if (flags.json) {
388
+ process.stdout.write(`${JSON.stringify(providers.map(p => ({ id: p.id, name: p.name })))}\n`);
389
+ return;
390
+ }
391
+ process.stdout.write("Available providers:\n\n");
392
+ for (const p of providers) {
393
+ process.stdout.write(` ${p.id.padEnd(20)} ${p.name}\n`);
394
+ }
395
+ }
396
+
251
397
  // ─── CLIProxyAPI import ─────────────────────────────────────────────────
252
398
 
253
399
  /**
@@ -732,6 +878,9 @@ export async function runAuthBrokerCommand(cmd: AuthBrokerCommandArgs): Promise<
732
878
  case "status":
733
879
  await runStatus(cmd.flags);
734
880
  return;
881
+ case "list":
882
+ await runList(cmd.flags);
883
+ return;
735
884
  default: {
736
885
  // Exhaustive check.
737
886
  const _exhaustive: never = cmd.action;
@@ -56,8 +56,11 @@ export default class AuthBroker extends Command {
56
56
  "# Boot on a non-default port\n omp auth-broker serve --bind=127.0.0.1:9000",
57
57
  "# Print the bearer token\n omp auth-broker token",
58
58
  "# Rotate the bearer token\n omp auth-broker token --regenerate",
59
+ "# List supported OAuth providers\n omp auth-broker list",
59
60
  "# Local login (run on the broker host)\n omp auth-broker login anthropic",
61
+ "# Interactive provider selection\n omp auth-broker login",
60
62
  "# Remote login over SSH tunnel\n omp auth-broker login anthropic --via=user@broker",
63
+ "# Log out of a provider (interactive without provider arg)\n omp auth-broker logout anthropic",
61
64
  "# Import a CLIProxyAPI auth dump\n omp auth-broker import ~/.cliproxy/auth",
62
65
  "# Import a single CLIProxyAPI JSON, overriding the provider mapping\n omp auth-broker import ~/.cliproxy/auth/claude-foo.json --provider anthropic",
63
66
  "# Preview a migration from local store + env vars to the configured broker\n omp auth-broker migrate --from-local --include-env --dry-run",
@@ -120,6 +120,22 @@ export default class Index extends Command {
120
120
  "no-title": Flags.boolean({
121
121
  description: "Disable title auto-generation",
122
122
  }),
123
+ // `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
124
+ // Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
125
+ // consumes the manual-parser output, not these oclif flag values. If you rename or remove
126
+ // either form, update both call sites in lockstep.
127
+ "auto-approve": Flags.boolean({
128
+ aliases: ["yolo"],
129
+ description: "Auto-approve all tool calls (skip approval prompts)",
130
+ }),
131
+ // `--approval-mode`: declared here so oclif's auto-generated `--help` lists it; runtime parsing
132
+ // happens in `cli/args.ts parseArgs`. The value is applied via `Settings.override("tools.approvalMode", …)`
133
+ // in `main.ts` after the `Settings` instance is constructed, so every `settings.get("tools.approvalMode")`
134
+ // site (wrapper, `/settings` UI) observes the same value.
135
+ "approval-mode": Flags.string({
136
+ options: ["always-ask", "write", "yolo"],
137
+ description: "Override tools.approvalMode for this session (always-ask|write|yolo)",
138
+ }),
123
139
  };
124
140
 
125
141
  static examples = [
@@ -98,8 +98,8 @@
98
98
  },
99
99
  "timeout": {
100
100
  "type": "number",
101
- "exclusiveMinimum": 0,
102
- "description": "Connection timeout in milliseconds."
101
+ "minimum": 0,
102
+ "description": "MCP request timeout in milliseconds. Set to 0 to disable client-side MCP timeouts."
103
103
  },
104
104
  "auth": {
105
105
  "$ref": "#/$defs/authConfig"